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

本文关键字:组合 何组合 何重用 业务 表达式 | 更新日期: 2023-09-27 18:29:24

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


我有"成员"模型,其定义如下:

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()?我尝试了表达式。调用:

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

谢谢。

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

我可以修复你的表达式生成器,我可以编写你的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<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,而像使用三元表达式这样的技巧也不会起作用。

最近我遇到了在表达式中保留一些业务逻辑的需要,以便在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方法的所有用法,因此需要在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))==value(QueryMapper.Examples.Person+<>c_DisplayClass0).q)

SQL查询1:SELECT[Extent1]。[Id]AS[Id]、[Extent1]。[名字]AS[FirstName]、[Extent1]。[姓氏]AS[姓氏],[范围1]。[年龄]AS[年龄],[范围1]。[公司Id]作为[公司Id]FROM[dbo]。[人物]AS[Extent1]WHERE(CASE WHEN(([Extent1].[FirstName]为NULL)OR((CAST(LEN([Extent1].[Firs tName])AS int))=0)然后[延伸1]。[LastName]ELSE[Extent1]。[FirstName]+N''+[延伸1]。[LastName]END)=@p_linq_0

SQL查询2:SELECT[Extent1]。[Id]AS[Id]、[Extent1]。[姓名]AS[姓名]自[dbo]。[公司]AS[Extent1]哪里存在(选择1作为[C1]自[dbo]。[人员]AS[范围2]WHERE([Extent1].[Id]=[Extent2].[Company_Id])AND((CASE WHEN([Extent t2].[FirstName]为NULL)OR((CAST(LEN([Extent2].[FirstName])AS int))=0)TH EN[Extent2]。[LastName]ELSE[Extent2]。[FirstName]+N''+[Extent2]。[LastName]END)=@p_linq_0))

因此,使用.Rap()方法从表达式世界转换为非表达式世界的主要思想提供了重用表达式的简单方法。

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