表达式 - 如何重用业务逻辑?如何结合它们?

时间:2013-08-28 12:40:01

标签: c# linq entity-framework lambda expression

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


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

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;
}
这是有效的。唯一的问题是生成显示名称的业务逻辑是在两个不同的地方复制/粘贴的。我想避免这种情况。但我不明白该怎么做。我带来的最好的是以下内容:

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

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

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

谢谢。

2 个答案:

答案 0 :(得分:2)

我可以修复你的表达式生成器,我可以撰写你的GetDisplayNameExpression(所以 1 3

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(),两者都需要作为参数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);
        }
    }
}
  • 它是如何工作的?魔术,反射,仙尘......
  • 它是否支持引用其他属性的属性?的
  • 它需要什么?

需要名称为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,并且使用三元表达式等技巧将无法使用它。

答案 1 :(得分: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");
}

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

因此我们使用其他表达式描述表达式。要获得完整表达,需要扩展Wrap方法的所有用法,因此您需要在Unwrap(或Expression)上调用方法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 =&gt; (Person.SearchFieldExpression.Wrap(p)==   value(QueryMapper.Exampl es.Person +&lt;&gt; c__DisplayClass0).q)

     

解开:p =&gt; (IIF(IsNullOrEmpty(p.FirstName),p.LastName,   ((p.FirstName +“”)+ p.LastName))==   值(QueryMapper.Examples.Person + LT;&GT; 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]为空)或((   CAST(LEN([Extent1]。[Firs tName])AS int))= 0))那么   [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]。[公司] AS [Extent1] WHERE EXISTS(SELECT           1 AS [C1]           来自[dbo]。[人物] 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()方法将表达式世界转换为非表达式的主要思想是提供重用表达式的简便方法。

如果您需要更多解释,请与我们联系。