LINQ 泛型 IQueryable<T> WHERE 子句

LINQ Generic IQueryable<T> WHERE Clause

假设我有一个包含 3 个 DBSet 的 DBContext:

public class DatabaseContext : DbContext
{
    public DbSet<A> As { get; set; }
    public DbSet<B> Bs { get; set; }
    public DbSet<C> Cs { get; set; }
}

class A
{
    public string Text { get; set; }
    public string Code { get; set; }
}

class B
{
    public string Name { get; set; }
    public string Code { get; set; }
}

class C
{
    public string Book { get; set; }
    public string Code { get; set; }
}

我想编写一个通用方法:

  1. 一般取三个DbSet中的任何一个作为第一个参数
  2. 将字符串值作为第二个参数
  3. 不转换为 IEnumerable,returns 提供的 DbSet 中的第一条记录,其中 Code 字段与提供的字符串匹配

到目前为止我有这个方法:

public static T GetCode<T>(IQueryable<T> set, string code) where T : class
{
    var Prop = typeof(T).GetProperty("Code");
    return set.Where(x => (string)Prop.GetValue(x) == code).FirstOrDefault();
}

当我尝试使用此行调用它时:

var _A = GetCode(TheDB.As, "123");
var _B = GetCode(TheDB.Bs, "123");
var _C = GetCode(TheDB.Cs, "123");

我收到这个错误:

InvalidOperationException:

The LINQ expression 'DbSet<A>.Where(m => (string)__Prop_0.GetValue(m) == __code_1)' could not be translated.

Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().

如何在 DbSet 上编写能够为 IQueryable 正确翻译的 WHERE 子句?如果我将 IQueryable 转换为 IEnumerable,我的方法会起作用,但我不想这样做,因为集合可能非常大,我希望数据库(而不是我的应用程序)这样做记录搜索。

最简单(且类型安全)的方法是创建一个类似 IHaveCode 的接口并将通用参数限制为它:

interface IHaveCode
{
     string Code { get; set; }
}
class A: IHaveCode
{
    public string Text { get; set; }
    public string Code { get; set; }
}

class B: IHaveCode
{
    public string Name { get; set; }
    public string Code { get; set; }
}

class C: IHaveCode
{
    public string Book { get; set; }
    public string Code { get; set; }
}

public static T GetCode<T>(IQueryable<T> set, string code) where T : IHaveCode
{
    return set.Where(x => x.Code == code).FirstOrDefault();
}

如果出于某种原因它不适合您,那么您需要自己构建一个 expression tree

一种比 Guru Stron 的方法更通用的方法,也可用于没有 属性 Code 的 类 DbSets 的方法是提供 属性 你想在你的比较中使用。

public static T GetFirstPropertyMatchOrDefault<T>(
    this IQueryable<T> source,
    Expression<Func<T, string>> propertySelector,
    string comparisonValue)
{
    return source.Where(sourceElement => propertySelector(sourceElement) == comparisonValue)
                 .FirstOrDefault();
}

用法:

string comparisonValue = ...
A fetchedA = dbContext.As.GetFirstPropertyMachOrDefault(a => a.Code, comparisonValue);

但是既然你已经把它变成了通用的,你也可以用它来获取其他属性;

// Get a C by Book title:
string bookTitle = ...
C fetchedC = dbContext.Cs.GetFirstPropertyOrDefault(c => c.Book);

您可以使用 Expression Trees 实现 GetCode 方法。

public static T GetCode<T>(IQueryable<T> set, string code) where T : class {
    PropertyInfo codeProp = typeof(T).GetProperty("Code");
    if(codeProp == null)
        throw new ArgumentException($"{typeof(T).FullName} does not have the Code property");
    ParameterExpression param = Expression.Parameter(typeof(T));
    Expression getCode = Expression.MakeMemberAccess(param, codeProp);
    Expression codeVal = Expression.Constant(code);
    Expression body = Expression.Equal(getCode, codeVal);
    Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(body, param);
    return set.FirstOrDefault(predicate);
}