注意:这是一篇很长的文章,请滚动到底部查看问题 - 希望这会让我更容易理解我的问题。谢谢!
我有“成员(member)”模型,其定义如下:
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;
}
但我从提供商处收到以下错误:
The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.
(2) 在 DisplayName 属性中使用 Expression.Compile() 是一个好方法吗?有什么问题吗?
(3) 如何在 GetDisplayNameExpression() 中移动 RealName 逻辑?我想我必须创建另一个表达式和另一个编译表达式,但我不明白如何从 GetDisplayNameExpression() 内部调用 RealNameExpression。
谢谢。
最佳答案
我可以修复你的表达式生成器,并且我可以编写你的 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,并且使用三元表达式等技巧将无法使其发挥作用。
关于c# - 表达式——如何重用业务逻辑?如何将它们结合起来?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18488184/