假设我有一个简单的模型:
public class Movie
{
public int ID { get; set; }
public string Name { get; set; }
}
和一个DbContext:
public class MoviesContext : DbContext
{
...
public DbSet<Movie> Movies { get; set; }
}
我也有一个方法在MoviesContext类过滤电影的子字符串像这样:
return Movies.Where(m => m.Name.Contains(filterString)).Select(m => m);
现在假设我想添加一个新模型,例如:
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string FullName { get { return FirstName + (MiddleName?.Length > 0 ? $" {MiddleName}" : "") + $" {LastName}"; } }
}
我还想通过名称(即FullName)过滤人员(DbSet persons)。我喜欢DRY,所以最好泛化MoviesContext的筛选方法。重要的是,我想在数据库级别进行过滤。所以我必须处理LINQ for Entities
如果没有这个,任务是相当简单的。我可以使用一个抽象类并添加一个执行"包含子字符串"逻辑的虚拟方法。或者,我可以使用接口。不幸的是,由于LINQ for Entities,我不能使用FullName属性(这是不方便的,但可以忍受),我不能写这样的东西:
return dbset.Where(ent => ent.NameContains(filterString)).Select(ent => ent);
那么,如何解决这个问题呢?我已经找到了一些解决方案(几乎让我的头破了),但我不是很满意。我将单独发布我的解决方案,但我希望有一个更优雅的。
仔细阅读你的代码,而不是你的NameFilterable
抽象类,你能不做这样的事情吗:
public interface IHasPredicateGetter<T> {
[NotNull] Expression<Func<T, bool>> GetPredicateFromString([NotNull] string pValue);
}
public class Movie : IHasPredicateGetter<Movie> {
public int ID { get; set; }
public string Name { get; set; }
public Expression<Func<Movie, bool>> GetPredicateFromString(string pValue) {
return m => m.Name.Contains(pValue);
}
}
例如,这可以防止您需要强制转换。很难理解你在这里想做什么,所以我不确定这是不是全部。你仍然被一个实例方法困住了,它应该是一个静态方法,但它不能实现一个接口。
我的解决方案是这样的。
[1]基类:
public abstract class NameFilterable
{
protected static Expression<Func<T, bool>> False<T>() { return f => false; }
public virtual Expression<Func<T, bool>> GetNameContainsPredicate<T>(string filterString)
{
return False<T>();
}
}
[2] Person类(为了更简单,我将省略Movie类):
public class Person : NameFilterable
{
...
public override Expression<Func<T, bool>> GetNameContainsPredicate<T>(string filterString)
{
return entity =>
String.IsNullOrEmpty(filterString) ||
(entity as Person).LastName.Contains(filterString) ||
(entity as Person).FirstName.Contains(filterString) ||
(((entity as Person).MiddleName != null) && (entity as Person).MiddleName.Contains(filterString))
;
}
}
[3] MoviesContext中的过滤器方法:
private static IQueryable<T> _filterDbSet<T>(DbSet<T> set, Expression<Func<T, bool>> filterPredicate) where T : class
{
return set
.Where(filterPredicate)
.Select(ent => ent);
}
private static IQueryable<T> _filterDbSet<T>(DbSet<T> set, string search = null) where T : NameFilterable, new()
{
T ent = new T();
return _filterDbSet<T>(set, (ent as NameFilterable).GetNameContainsPredicate<T>(search));
}
public static ICollection<T> Filter<T>(DbSet<T> set, string search = null) where T : NameFilterable, new()
{
return _filterDbSet(set, search).ToList();
}
似乎所有这些都运行得很好。但我不能说它很优雅。
[1]我必须使用泛型T,尽管在Person级别上我总是使用Person对象(或后代)。所以我必须将T转换为Person(作为Person)。
[2]在GetNameContainsPredicate方法中,我不能写(因为LINQ for Entities):
return entity =>
{
Person p = entity as Person;
String.IsNullOrEmpty(filterString) ||
p.LastName.Contains(filterString) ||
p.FirstName.Contains(filterString) ||
((p.MiddleName != null) && p.MiddleName.Contains(filterString))
};
[3]我不能使用静态方法(静态不能被覆盖),所以我必须创建一个虚拟T对象(T = new T();)。
[4]我仍然不能使用FullName.Contains(filterString)
所以,问题仍然存在:也许我错过了一些东西,有一个更优雅的解决方案来解决这个问题?您可以创建一个方法,如果类型具有特定属性,则该方法负责搜索该属性,如果对象没有该属性,则该方法负责过滤该属性,如果对象没有该属性,则该方法只返回null。有了这个,你可以创建一个表达式来过滤这个属性
//gets the property info of the property with the giving name
public static PropertyInfo GetPropetyInfo<T>(string name)
{
var type = typeof(T);
var property = type.GetProperty(name);
return property;
}
//Creates an expression thats represents the query
public static Func<T, bool> GetFilterExpression<T>( string propertyName, object propertyValue)
{
var prop = GetPropetyInfo<T>(propertyName);
if(prop==null)return t=>false;
var parameter = Expression.Parameter(typeof(T), "t");
Expression expression = parameter;
var left = Expression.Property(expression, prop);
if (prop.PropertyType == typeof(string))
{
var toLower = typeof(string).GetMethods().FirstOrDefault(t => t.Name.Equals("ToLower"));
var tlCall = Expression.Call(left, toLower);
var right = Expression.Constant(propertyValue.ToString().ToLower());
var contains = Expression.Call(tlCall, typeof(string).GetMethod("Contains"), right);
var containsCall = Expression.IsTrue(contains);
expression = Expression.AndAlso(Expression.NotEqual(left, Expression.Constant(null)), containsCall);
}
else
{
if (prop.PropertyType.ToString().ToLower().Contains("nullable"))
{
var getValue = prop.PropertyType.GetMethods().FirstOrDefault(t => t.Name.Equals("GetValueOrDefault"));
var getValueCall = Expression.Call(left, getValue);
var right = Expression.Constant(propertyValue);
expression = Expression.Equal(getValueCall, right);
}
else
{
var value = Convert.ChangeType(propertyValue,prop.PropertyType);
var right = Expression.Constant(value);
expression = Expression.Equal(left, right);
}
}
return Expression.Lambda<Func<T, bool>>(expression, new ParameterExpression[] { parameter }).Compile();
}
你可以这样使用
var expression = YOURCLASS.GetFilterExpression<Person>("LastName", "Jhon");
var result=dbset.Where(expression);
为了使用EF获得多态性,我做了一些事情,但是在您需要可重用过滤器的特定情况下,我不确定是否值得这样做。我基本上试着做同样的事情,但每次我最终都意识到没有意义。问问你自己:这样做到底有什么好处,它如何比Where
条款提供的更灵活?
有两个问题。一个是很难或几乎不可能通过使用共享接口(例如INamedObject
)在两个单独的类之间使用过滤器。这是因为您需要一个强类型表达式。您可以创建一个返回强类型表达式的函数,但是为什么不首先编写这个表达式呢?另一个问题是,您需要为每个搜索值创建一个新的过滤器表达式,这与我们现在所处的位置非常接近。
如果你完善这个,你会有什么?推断类型、指定搜索值和获得可以使用的表达式的能力?这不就是我们已经拥有的吗?Where
子句的方式是,它们已经具有强类型,和使用动态搜索值的能力。虽然在多个地方说x => x.Name == value
可能有点多余,但是能够指定这样一个简洁而强大的过滤器语句已经是一个非常了不起的地方了。