2

注意:这是一篇很长的帖子,请滚动到底部查看问题 - 希望这会让我更容易理解我的问题。谢谢!


我有“成员”模型,定义如下:

public class Member
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string ScreenName { get; set; }

    [NotMapped]
    public string RealName
    {
         get { return (FirstName + " " + LastName).TrimEnd(); }
    }

    [NotMapped]
    public string DisplayName
    {
        get
        {
            return string.IsNullOrEmpty(ScreenName) ? RealName : ScreenName;
        }
    }
}

这是现有的项目和模型,我不想更改它。现在我们收到了通过DisplayName启用配置文件检索的请求:

public Member GetMemberByDisplayName(string displayName)
{
     var member = this.memberRepository
                      .FirstOrDefault(m => m.DisplayName == displayName);
     return member;
}

此代码不起作用,因为DisplayName未映射到数据库中的字段。好的,那我就做一个表达:

public Member GetMemberByDisplayName(string displayName)
{
     Expression<Func<Member, bool>> displayNameSearchExpr = m => (
                string.IsNullOrEmpty(m.ScreenName) 
                    ? (m.Name + " " + m.LastName).TrimEnd() 
                    : m.ScreenName
            ) == displayName;

     var member = this.memberRepository
                      .FirstOrDefault(displayNameSearchExpr);

     return member;
}

这行得通。唯一的问题是生成显示名称的业务逻辑被复制/粘贴在 2 个不同的地方。我想避免这种情况。但我不明白该怎么做。我带来的最好的是以下内容:

  public class Member
    {

        public static Expression<Func<Member, string>> GetDisplayNameExpression()
        {
            return m => (
                            string.IsNullOrEmpty(m.ScreenName)
                                ? (m.Name + " " + m.LastName).TrimEnd()
                                : m.ScreenName
                        );
        }

        public static Expression<Func<Member, bool>> FilterMemberByDisplayNameExpression(string displayName)
        {
            return m => (
                string.IsNullOrEmpty(m.ScreenName)
                    ? (m.Name + " " + m.LastName).TrimEnd()
                    : m.ScreenName
            ) == displayName;
        }

        private static readonly Func<Member, string> GetDisplayNameExpressionCompiled = GetDisplayNameExpression().Compile();

        [NotMapped]
        public string DisplayName
        {
            get
            {
                return GetDisplayNameExpressionCompiled(this);
            }
        }

        [NotMapped]
        public string RealName
        {
             get { return (FirstName + " " + LastName).TrimEnd(); }
        }

   }

问题:

(1)如何在FilterMemberByDisplayNameExpression ()中复用GetDisplayNameExpression ( )?我试过Expression.Invoke

public static Expression<Func<Member, bool>> FilterMemberByDisplayNameExpression(string displayName)
{
    Expression<Func<string, bool>> e0 = s => s == displayName;
    var e1 = GetDisplayNameExpression();

    var combinedExpression = Expression.Lambda<Func<Member, bool>>(
           Expression.Invoke(e0, e1.Body), e1.Parameters);

    return combinedExpression;
}

但我从提供商那里得到以下错误:

LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”。

(2)DisplayName属性中使用Expression.Compile( )是一种好方法吗?有什么问题吗?

(3)如何在GetDisplayNameExpression ()中移动RealName逻辑?我想我必须创建另一个表达式和另一个编译表达式,但我不明白如何从GetDisplayNameExpression ()内部调用RealNameExpression 。

谢谢你。

4

2 回答 2

2

最近,我需要在表达式中保留一些业务逻辑,以便在 SQL 查询和 .net 代码中使用它。我已将一些有助于此的代码移至github repo。我已经实现了组合和重用表达式的简单方法。看我的例子:

public class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public int Age { get; set; }

    public Company Company { get; set; }

    public static Expression<Func<Person, string>> FirstNameExpression
    {
        get { return x => x.FirstName; }
    }

    public static Expression<Func<Person, string>> LastNameExpression
    {
        get { return x => x.LastName; }
    }

    public static Expression<Func<Person, string>> FullNameExpression
    {
        //get { return FirstNameExpression.Plus(" ").Plus(LastNameExpression); }
        // or
        get { return x => FirstNameExpression.Wrap(x) + " " + LastNameExpression.Wrap(x); }
    }

    public static Expression<Func<Person, string>> SearchFieldExpression
    {
        get
        {
            return
                p => string.IsNullOrEmpty(FirstNameExpression.Wrap(p)) ? LastNameExpression.Wrap(p) : FullNameExpression.Wrap(p);
        }
    }

    public static Expression<Func<Person, bool>> GetFilterExpression(string q)
    {
        return p => SearchFieldExpression.Wrap(p) == q;
    }
}

扩展方法 .Wrap() 只是标记:

public static TDest Wrap<TSource, TDest>(this Expression<Func<TSource, TDest>> expr, TSource val)
{
    throw new NotImplementedException("Used only as expression transform marker");
}

什么是全名?这是FirstName + " " + LastNamewhereFirstNameLastName- 字符串。但是我们有表达式,它不是一个真正的值,我们需要组合这些表达式。方法.Wrap(val)帮助我们转移到一个简单的代码。我们不需要写任何作曲家或其他访问者来表达。所有这些魔法都已经通过方法 .Wrap(val) 完成了,其中val- 参数将被传递给调用的 lambda 表达式。

因此,我们使用其他表达式来描述表达式。要获得完整的表达式需要扩展方法的所有用法Wrap,因此您需要UnwrapExpression(或IQueryable)上调用方法。见样本:

using (var context = new Entities())
{
    var originalExpr = Person.GetFilterExpression("ivan");
    Console.WriteLine("Original: " + originalExpr);
    Console.WriteLine();

    var expr = Person.GetFilterExpression("ivan").Unwrap();
    Console.WriteLine("Unwrapped: " + expr);
    Console.WriteLine();

    var persons = context.Persons.Where(Person.GetFilterExpression("ivan").Unwrap());
    Console.WriteLine("SQL Query 1: " + persons);
    Console.WriteLine();

    var companies = context.Companies.Where(x => x.Persons.Any(Person.GetFilterExpression("abc").Wrap())).Unwrap(); // here we use .Wrap method without parameters, because .Persons is the ICollection (not IQueryable) and we can't pass Expression<Func<T, bool>> as Func<T, bool>, so we need it for successful compilation. Unwrap method expand Wrap method usage and convert Expression to lambda function.
    Console.WriteLine("SQL Query 2: " + companies);
    Console.WriteLine();

    var traceSql = persons.ToString();
}

控制台输出:

原文:p => (Person.SearchFieldExpression.Wrap(p) == value(QueryMapper.Exampl es.Person+<>c__DisplayClass0).q)

展开: p => (IIF(IsNullOrEmpty(p.FirstName), p.LastName, ((p.FirstName + " " ) + p.LastName)) == value(QueryMapper.Examples.Person+<>c__DisplayClass0).q)

SQL 查询 1: SELECT [Extent1].[Id] AS [Id], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], [Extent1].[Age] AS [ Age], [Extent1].[Company_Id] AS [Company_Id] FROM [dbo].[People] AS [Extent1] WHERE (CASE WHEN (([Extent1].[FirstName] IS NULL) OR (( CAST(LEN([ Extent1].[FirstName]) AS int)) = 0)) THEN [Extent1].[LastName] ELSE [Extent1].[FirstName] + N' ' + [Extent1].[LastName] END) = @p_ linq _0

SQL 查询 2: SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[Companies] AS [Extent1] WHERE EXISTS (SELECT 1 AS [C1] FROM [dbo].[People] AS [Extent2] WHERE ([Extent1].[Id] = [Extent2].[Company_Id]) AND ((CASE WHEN (([Exten t2].[FirstName] IS NULL) OR (( CAST(LEN([Extent2].[FirstName]) AS int)) = 0)) TH EN [Extent2].[LastName] ELSE [Extent2].[FirstName] + N' ' + [Extent2].[LastName] END ) = @p_ linq _0) )

因此,主要思想是使用 .Wrap() 方法将表达式世界转换为非表达式,这提供了重用表达式的简便方法。

如果您需要更多解释,请告诉我。

于 2013-08-31T19:59:09.693 回答
2

我可以修复你的表达式生成器,我可以编写你的GetDisplayNameExpression(所以13

public class Member
{
    public string ScreenName { get; set; }
    public string Name { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public static Expression<Func<Member, string>> GetRealNameExpression()
    {
        return m => (m.Name + " " + m.LastName).TrimEnd();
    }

    public static Expression<Func<Member, string>> GetDisplayNameExpression()
    {
        var isNullOrEmpty = typeof(string).GetMethod("IsNullOrEmpty", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(string) }, null);

        var e0 = GetRealNameExpression();
        var par1 = e0.Parameters[0];

        // Done in this way, refactoring will correctly rename m.ScreenName
        // We could have used a similar trick for string.IsNullOrEmpty,
        // but it would have been useless, because its name and signature won't
        // ever change.
        Expression<Func<Member, string>> e1 = m => m.ScreenName;

        var screenName = (MemberExpression)e1.Body;
        var prop = Expression.Property(par1, (PropertyInfo)screenName.Member);
        var condition = Expression.Condition(Expression.Call(null, isNullOrEmpty, prop), e0.Body, prop);

        var combinedExpression = Expression.Lambda<Func<Member, string>>(condition, par1);
        return combinedExpression;
    }

    private static readonly Func<Member, string> GetDisplayNameExpressionCompiled = GetDisplayNameExpression().Compile();

    private static readonly Func<Member, string> GetRealNameExpressionCompiled = GetRealNameExpression().Compile();

    public string DisplayName
    {
        get
        {
            return GetDisplayNameExpressionCompiled(this);
        }
    }

    public string RealName
    {
        get
        {
            return GetRealNameExpressionCompiled(this);
        }
    }

    public static Expression<Func<Member, bool>> FilterMemberByDisplayNameExpression(string displayName)
    {
        var e0 = GetDisplayNameExpression();
        var par1 = e0.Parameters[0];

        var combinedExpression = Expression.Lambda<Func<Member, bool>>(
            Expression.Equal(e0.Body, Expression.Constant(displayName)), par1);

        return combinedExpression;
    }

请注意我如何重用GetDisplayNameExpression表达式的相同参数e1.Parameters[0](放入par1),这样我就不必重写表达式(否则我需要使用表达式重写器)。

我们可以使用这个技巧,因为我们只有一个表达式要处理,我们必须附加一些新代码。完全不同(我们需要一个表达式重写器)将是尝试组合两个表达式的情况(例如做 a GetRealNameExpression() + " " + GetDisplayNameExpression(),都需要作为参数 a Member,但它们的参数是分开的......可能这个https://stackoverflow .com/a/5431309/613130可以工作...

对于2,我认为没有任何问题。您正确使用static readonly. 但是,请看看GetDisplayNameExpression并思考“一些业务代码重复支付更好还是那个更好?”

通用解决方案

现在......我很确定它是可行的......事实上它可行的:一个表达式“扩展器”,它“自动”“扩展”“特殊属性”到它们的表达式。

public static class QueryableEx
{
    private static readonly ConcurrentDictionary<Type, Dictionary<PropertyInfo, LambdaExpression>> expressions = new ConcurrentDictionary<Type, Dictionary<PropertyInfo, LambdaExpression>>();

    public static IQueryable<T> Expand<T>(this IQueryable<T> query)
    {
        var visitor = new QueryableVisitor();
        Expression expression2 = visitor.Visit(query.Expression);

        return query.Expression != expression2 ? query.Provider.CreateQuery<T>(expression2) : query;
    }

    private static Dictionary<PropertyInfo, LambdaExpression> Get(Type type)
    {
        Dictionary<PropertyInfo, LambdaExpression> dict;

        if (expressions.TryGetValue(type, out dict))
        {
            return dict;
        }

        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        dict = new Dictionary<PropertyInfo, LambdaExpression>();

        foreach (var prop in props)
        {
            var exp = type.GetMember(prop.Name + "Expression", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).Where(p => p.MemberType == MemberTypes.Field || p.MemberType == MemberTypes.Property).SingleOrDefault();

            if (exp == null)
            {
                continue;
            }

            if (!typeof(LambdaExpression).IsAssignableFrom(exp.MemberType == MemberTypes.Field ? ((FieldInfo)exp).FieldType : ((PropertyInfo)exp).PropertyType))
            {
                continue;
            }

            var lambda = (LambdaExpression)(exp.MemberType == MemberTypes.Field ? ((FieldInfo)exp).GetValue(null) : ((PropertyInfo)exp).GetValue(null, null));

            if (prop.PropertyType != lambda.ReturnType)
            {
                throw new Exception(string.Format("Mismatched return type of Expression of {0}.{1}, {0}.{2}", type.Name, prop.Name, exp.Name));
            }

            dict[prop] = lambda;
        }

        // We try to save some memory, removing empty dictionaries
        if (dict.Count == 0)
        {
            dict = null;
        }

        // There is no problem if multiple threads generate their "versions"
        // of the dict at the same time. They are all equivalent, so the worst
        // case is that some CPU cycles are wasted.
        dict = expressions.GetOrAdd(type, dict);

        return dict;
    }

    private class SingleParameterReplacer : ExpressionVisitor
    {
        public readonly ParameterExpression From;
        public readonly Expression To;

        public SingleParameterReplacer(ParameterExpression from, Expression to)
        {
            this.From = from;
            this.To = to;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node != this.From ? base.VisitParameter(node) : this.Visit(this.To);
        }
    }

    private class QueryableVisitor : ExpressionVisitor
    {
        protected static readonly Assembly MsCorLib = typeof(int).Assembly;
        protected static readonly Assembly Core = typeof(IQueryable).Assembly;

        // Used to check for recursion
        protected readonly List<MemberInfo> MembersBeingVisited = new List<MemberInfo>();

        protected override Expression VisitMember(MemberExpression node)
        {
            var declaringType = node.Member.DeclaringType;
            var assembly = declaringType.Assembly;

            if (assembly != MsCorLib && assembly != Core && node.Member.MemberType == MemberTypes.Property)
            {
                var dict = QueryableEx.Get(declaringType);

                LambdaExpression lambda;

                if (dict != null && dict.TryGetValue((PropertyInfo)node.Member, out lambda))
                {
                    // Anti recursion check
                    if (this.MembersBeingVisited.Contains(node.Member))
                    {
                        throw new Exception(string.Format("Recursively visited member. Chain: {0}", string.Join("->", this.MembersBeingVisited.Concat(new[] { node.Member }).Select(p => p.DeclaringType.Name + "." + p.Name))));
                    }

                    this.MembersBeingVisited.Add(node.Member);

                    // Replace the parameters of the expression with "our" reference
                    var body = new SingleParameterReplacer(lambda.Parameters[0], node.Expression).Visit(lambda.Body);

                    Expression exp = this.Visit(body);

                    this.MembersBeingVisited.RemoveAt(this.MembersBeingVisited.Count - 1);

                    return exp;
                }
            }

            return base.VisitMember(node);
        }
    }
}
  • 它是如何工作的?魔法、倒影、仙尘……
  • 它是否支持引用其他属性的属性?是的
  • 它需要什么?

它需要 name 的每个“特殊”属性Foo都有一个相应的静态字段/静态属性,命名为FooExpression返回一个Expression<Func<Class, something>>

Expand()它需要在具体化/枚举之前的某个时刻通过扩展方法“转换”查询。所以:

public class Member
{
    // can be private/protected/internal
    public static readonly Expression<Func<Member, string>> RealNameExpression =
        m => (m.Name + " " + m.LastName).TrimEnd();

    // Here we are referencing another "special" property, and it just works!
    public static readonly Expression<Func<Member, string>> DisplayNameExpression =
        m => string.IsNullOrEmpty(m.ScreenName) ? m.RealName : m.ScreenName;

    public string RealName
    {
        get 
        { 
            // return the real name however you want, probably reusing
            // the expression through a compiled readonly 
            // RealNameExpressionCompiled as you had done
        }  
    }

    public string DisplayName
    {
        get
        {
        }
    }
}

// Note the use of .Expand();
var res = (from p in ctx.Member 
          where p.RealName == "Something" || p.RealName.Contains("Anything") ||
                p.DisplayName == "Foo"
          select new { p.RealName, p.DisplayName, p.Name }).Expand();

// now you can use res normally.
  • 限制 1:一个问题是使用 , 和类似的方法Single(Expression)First(Expression)它们Any(Expression)不返回IQueryable. 通过使用第一个更改Where(Expression).Expand().Single()

  • 限制 2:“特殊”属性不能在循环中引用自己。因此,如果 A 使用 B,B 就不能使用 A,并且使用三元表达式之类的技巧不会使它起作用。

于 2013-08-28T13:04:17.777 回答