LINQ通用IQueryable<T> WHERE子句

3
假设我有一个包含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,返回提供的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:

无法翻译 LINQ 表达式 'DbSet<A>.Where(m => (string)__Prop_0.GetValue(m) == __code_1)'。

请将查询重写为可翻译的形式,或者通过插入调用 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync() 中的任何一个来明确地切换到客户端评估。

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

3个回答

3
最简单(并且类型安全)的方法就是引入一个名为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();
}

如果不适合您,那么您需要自己构建一个 表达式树


1
你可以使用 表达式树 实现 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);
}

1
比 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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接