【问题标题】:Expressions - how to reuse business logic? How to combine them?表达式——如何重用业务逻辑?如何将它们结合起来?
【发布时间】:2013-08-28 12:40:00
【问题描述】:

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


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

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 不支持 LINQ 表达式节点类型“Invoke” 实体。

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

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

谢谢。

【问题讨论】:

  • A) 没有好的解决方案。 B) 你所做的是一场噩梦(不是因为你不知道自己在做什么,而是因为FilterMemberByDisplayNameExpression 是一场等待你做梦的噩梦)。 C) 没有好的解决方案。
  • @xanatos 为什么 (B) 是一场噩梦?你能解释一下最大的问题是什么吗?
  • 如果你真的想做某事,你可以像这样创建一个“过滤器”:static IQueryable&lt;T&gt; MakeMyExpressionWork(this IQueryable&lt;T&gt; exp)。这个魔法过滤器,通过表达式重写、反射、mumbo jumbo 等会修改Expression
  • @xanatos 您所说的对于 (A) 和 (C) 情况是正确的 - 我知道我需要以某种方式修改表达式,这是我的问题。但是我对(B)感到困惑-编译表达式并在属性中使用它有什么问题,只是为了避免业务逻辑复制/粘贴?
  • 如果你有 Sql Server 作为数据库,你介意使用计算列吗?这将使这类事情变得非常容易(我们不久前遇到过同样的问题)!

标签: c# linq entity-framework lambda expression


【解决方案1】:

我可以修复你的表达式生成器,我可以编写你的 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),这样我就不必重写表达式(否则我需要使用表达式重写器)。

我们可以使用这个技巧,因为我们只有一个表达式要处理,我们必须附加一些新代码。完全不同(我们需要一个表达式重写器)是尝试组合两个表达式的情况(例如做一个GetRealNameExpression() + " " + GetDisplayNameExpression(),两者都需要一个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);
        }
    }
}
  • 它是如何工作的?魔法、反射、仙尘……
  • 是否支持引用其他属性的属性? 是的
  • 它需要什么?

它需要名称 Foo 的每个“特殊”属性都有一个相应的静态字段/静态属性,名称为 FooExpression,它返回一个 Expression&lt;Func&lt;Class, something&gt;&gt;

它需要在具体化/枚举之前的某个时刻通过扩展方法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,并且使用三元表达式之类的技巧不会使其起作用。

【讨论】:

    【解决方案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 + " " + LastName,其中FirstNameLastName - 字符串。但是我们有表达式,它不是一个真正的值,我们需要组合这些表达式。方法.Wrap(val) 帮助我们转向一个简单的代码。我们不需要写任何作曲家或其他访问者来表达。所有这些魔法已经通过方法 .Wrap(val) 完成,其中val - 将传递给调用的 lambda 表达式的参数。

    所以我们使用其他表达式来描述表达式。要获得完整的表达式需要扩展Wrap 方法的所有用法,因此您需要在Expression(或IQueryable)上调用Unwrap 方法。 查看示例:

    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)) == 值(QueryMapper.Examples.Person+c__DisplayClass0).q)

    SQL 查询 1:SELECT [Extent1].[Id] AS [Id], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], [Extent1].[Age] AS [年龄],[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 作为 [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() 方法将表达式世界转换为非表达式,这提供了重用表达式的简单方法。

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

    【讨论】:

      猜你喜欢
      • 2011-06-19
      • 1970-01-01
      • 2011-08-29
      • 1970-01-01
      • 2018-11-10
      • 2011-05-23
      • 2021-09-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多