具有多个选择值的动态Linq查询

本文关键字:动态 Linq 查询 选择 | 更新日期: 2023-09-27 18:04:53

我想在多个列上实现一个过滤器,但是我不想写对于每个列一个新的查询。所以我实现了一个GetDistinctProperty函数,看起来像这样:

public ActionResult GetDistinctProperty(string propertyName)
{
    var selector = CreateExpression<TDomain>(propertyName);
    var query = this.InventoryService.GetAll(Deal);
    var results = query.Select(selector).Distinct().ToList();
    return Json(results, JsonRequestBehavior.AllowGet);
}
private static Expression<Func<T, object>> CreateExpression<T>(string propertyName)
{
    // Specify the type we are accessing the member from
    var param = Expression.Parameter(typeof(T), "x");
    Expression body = param;
    // Loop through members in specified property name
    foreach (var member in propertyName.Split('.'))
    {
        // Access each individual property
        body = Expression.Property(body, member);
    }
    var conversion = Expression.Convert(body, typeof(object));
    // Create a lambda of this MemberExpression 
    return Expression.Lambda<Func<T, object>>(conversion, param);
}

让我们以我的propertyName SiteIdentifier为例。

选择器给我的值

{x => Convert(x.SiteIdentifier)}

,当我想看到结果时,它给了我以下错误:

Unable to cast the type 'System.String' to type 'System.Object'.
LINQ to Entities only supports casting EDM primitive or enumeration types.

当我尝试如下选择时:

var results = query.Select(x=>x.SiteIdentifier).Distinct().ToList();

它的工作原理。

有人知道吗?

具有多个选择值的动态Linq查询

在静态类型语言中使用Linq时,没有理由将属性名作为字符串传递。泛型和委托(Func)的引入就是为了淘汰这种逻辑。

你可以简单地传递表达式,而不是传递属性名:

public ActionResult GetDistinctProperty(Expression<Func<TDomain, TProp> selector)
{
    var query = this.InventoryService.GetAll(Deal);
    var results = query.Select(selector).Distinct().ToList();
    return Json(results, JsonRequestBehavior.AllowGet);
}

用法:

GetDistinctProperty(x=> x.SiteIdentifier);

问题是,虽然IQueryable<T>接口是协变的,但值类型不支持协变,因此不能将IQueryable<int>视为IQueryable<object>。另一方面,EF不喜欢将值类型强制转换为object

所以为了使它工作,你需要求助于非通用的IQueryable接口。不幸的是,几乎所有Queryable扩展方法都是围绕IQueryable<T>构建的,因此您必须手动编写相应的调用。

例如,为了按名称(路径)选择属性,您需要这样做:
public static partial class QueryableExtensions()
{
    public static IQueryable SelectProperty(this IQueryable source, string path)
    {
        var parameter = Expression.Parameter(source.ElementType, "x");
        var property = path.Split(',')
            .Aggregate((Expression)parameter, Expression.PropertyOrField);
        var selector = Expression.Lambda(property, parameter);
        var selectCall = Expression.Call(
            typeof(Queryable), "Select", new[] { parameter.Type, property.Type },
            source.Expression, Expression.Quote(selector));
        return source.Provider.CreateQuery(selectCall);
    }
}

但是你需要一个Distinct方法来处理IQueryable:

public static partial class QueryableExtensions()
{
    public static IQueryable Distinct(this IQueryable source)
    {
        var distinctCall = Expression.Call(
            typeof(Queryable), "Distinct", new[] { source.ElementType },
            source.Expression);
        return source.Provider.CreateQuery(distinctCall);
    }
}

现在您已经拥有了实现所讨论的方法所需的所有部分。但是还有一个重要的细节。为了能够创建List<object>,您需要调用Cast<object>。但是如果您使用IQueryable.Cast扩展方法,您将从EF中获得相同的不支持异常。所以你需要显式地调用IEnumerable.Cast:

public ActionResult GetDistinctProperty(string propertyName)
{
    var query = this.InventoryService.GetAll(Deal);
    var results = Enumerable.Cast<object>(
        query.SelectProperty(propertyName).Distinct()).ToList();
    return Json(results, JsonRequestBehavior.AllowGet);
}