通过泛型类型参数访问属性

5

我正在尝试为我的模型创建一个通用仓库。目前,我有三个不相关的不同模型(联系人、笔记、提醒)。

class Repository<T> where T:class
{
    public IQueryable<T> SearchExact(string keyword)
    {
        //Is there a way i can make the below line generic
        //return db.ContactModels.Where(i => i.Name == keyword)        
        //I also tried db.GetTable<T>().Where(i => i.Name == keyword)
        //But the variable i doesn't have the Name property since it would know it only in the runtime
        //db also has a method ITable GetTable(Type modelType) but don't think if that would help me
    }
}

在 MainViewModel 中,我这样调用 Search 方法:
Repository<ContactModel> _contactRepository = new Repository<ContactModel>();

public void Search(string keyword)
{
    var filteredList = _contactRepository.SearchExact(keyword).ToList();
}

解决方案:

最终选择了Ray的动态表达式解决方案:

public IQueryable<TModel> SearchExact(string searchKeyword, string columnName)
{
    ParameterExpression param = Expression.Parameter(typeof(TModel), "i");
    Expression left = Expression.Property(param, typeof(TModel).GetProperty(columnName));
    Expression right = Expression.Constant(searchKeyword);
    Expression expr = Expression.Equal(left, right);
}

query = db.GetTable<TModel>().Where(Expression.Lambda<Func<TModel, bool>>(expr, param));
2个回答

12

接口解决方案

如果您可以为对象添加一个接口,那么您可以使用它。例如,您可以定义:

 public interface IName
 {
   string Name { get; }
 }

那么你的代码库可以声明为:

然后您的代码库可以声明为:

class Repository<T> where T:class, IName
{
  public IQueryable<T> SearchExact(string keyword)  
  {  
    return db.GetTable<T>().Where(i => i.Name == keyword);
  }
}  

备选接口解决方案

或者,您可以通过使用第二个通用参数在SearchExact方法中添加“where”:

class Repository<T> where T:class
{  
  public IQueryable<T> SearchExact<U>(string keyword) where U: T,IName
  {  
    return db.GetTable<U>().Where(i => i.Name == keyword);
  }
}  

这使得Repository类可以与不实现IName接口的对象一起使用,而SearchExact方法只能与实现IName接口的对象一起使用。

反射解决方案

如果您无法向对象添加类似于IName的接口,则可以改用反射:

class Repository<T> where T:class
{
  static PropertyInfo _nameProperty = typeof(T).GetProperty("Name");

  public IQueryable<T> SearchExact(string keyword)
  {
    return db.GetTable<T>().Where(i => (string)_nameProperty.GetValue(i) == keyword);
  }
}

这种方法比使用接口慢,但有时是唯一的方法。

关于接口解决方案的更多说明和为什么要使用它

在您的评论中提到您不能使用接口,但没有解释原因。您说:“三个模型中没有共同点。所以我认为无法从它们中制作接口。” 从您的问题中,我了解到所有三个模型都有一个“Name”属性。在这种情况下,可以在所有三个模型上实现接口。只需按照所示实现接口并将", IName"添加到您的三个类定义中即可。这将为本地查询和SQL生成提供最佳性能。

即使问题中涉及的属性并非全部称为“Name”,您仍然可以通过为每个属性添加一个“Name”属性并使其getter和setter访问其他属性来使用接口解决方案。

表达式解决方案

如果IName解决方案行不通,而您需要进行SQL转换,则可以通过使用表达式构建LINQ查询来实现。这需要更多的工作量,并且对本地使用效率显著降低,但可以很好地转换为SQL。代码应该像这样:

class Repository<T> where T:Class
{
  public IQueryable<T> SearchExact(string keyword,
                                   Expression<Func<T,string>> getNameExpression)
  {
    var param = Expression.Parameter(typeof(T), "i");
    return db.GetTable<T>().Where(
                Expression.Lambda<Func<T,bool>>(
                  Expression.Equal(
                    Expression.Invoke(
                      Expression.Constant(getNameExpression),
                      param),
                    Expression.Constant(keyword),
                  param));
  }
}

并且它将被称为这样:
repository.SearchExact("Text To Find", i => i.Name)

侧重于思考,我喜欢它。Linq2Sql意味着他们可能需要使用一些后处理来将接口应用于类。 - Spence
@Ray Burns:感谢您的快速回复。这三个模型没有共同点,所以我认为无法将它们制作成接口。相反,我可以像这样给出范围 where U : T, ContactModel, NoteModel, ReminderModel 来实现我的目标吗?我在反射方面真的很弱,请您能否解释一下这部分内容? - Amsakanna
where子句需要满足所有指定的条件,因此该语句要求它必须是或继承自指定的所有4种类型(T、ContactModel、NoteModel和ReminderModel),因此如果没有接口,这种方法就根本行不通。另外,作为一个旁注,反射解决方案目前说它需要接口,但只需删除“where U:T,IName”并将U替换为T即可(我假设这只是复制粘贴)。 - fyjham
不,where U : T, ContactModel, NoteModel, ReminderModel 是行不通的,因为U必须同时是这三个类。你有没有注意到我给了你反射解决方案的实际代码?我不确定有什么需要解释的。var prop = typeof(T).GetProperty("Name") 在目标对象上找到一个名为“Name”的属性,并返回一个可以快速访问Name值的对象。在Where内部,prop.GetValue(i) 实际检索名称。这很简单,而且相对快速。 - Ray Burns
@Tim:谢谢你指出来。你说得对——那是复制/粘贴错误。我已经更正了我的回答。 - Ray Burns
显示剩余5条评论

3

Ray的方法非常好,如果你有能力添加一个接口,那么它肯定更优秀。但是,如果由于某种原因你无法向这些类添加接口(例如它们是类库的一部分,不能编辑),那么你也可以考虑传递一个Func来告诉它如何获取名称。

例如:

class Repository<T>
{  
  public IQueryable<T> SearchExact(string keyword, Func<T, string> getSearchField)  
  {  
    return db.GetTable<T>().Where(i => getSearchField(i) == keyword);
  }
}

然后您需要这样调用它:

var filteredList = _contactRepository.SearchExact(keyword, cr => cr.Name).ToList();

除了这两个选项,您还可以考虑使用反射来访问Name属性,而无需使用任何接口。但是这种方法的缺点在于,没有编译时检查来确保您所传递的类实际上具有一个Name属性,并且它会对LINQ进行副作用将不会被转换为SQL,过滤将在.NET中发生(这意味着SQL服务器可能会受到比所需更多的请求)。
您还可以使用动态LINQ查询来实现此SQL-side效果,但它具有上面列出的同样的非类型安全问题。

使用替代的函数式方法而不是Ray解决方案中的面向对象方法,可以获得额外的加分。 - David_001
是的,Func也是另一种不错的方法。请注意,它与反射在翻译方面具有相同的缺点:因为getSearchField已经是一个委托(指向实际机器代码的指针),LINQ2SQL无法解析它,所以过滤将再次发生在.NET中。接口方法可以成功地转换为SQL,但要使其转换,您必须安排传递表达式,但这会使本地执行变慢。 - Ray Burns
我在我的答案中添加了几个段落和另一个解决方案来帮助回答这个问题。我解释说,接口解决方案似乎与你到目前为止所说的任何内容都不相冲突,并提供了一个新的表达式解决方案,虽然效率更低、代码更多,但如果接口解决方案实际上不可行,它也可以工作。 - Ray Burns
@Ray/@Tim:如你所见,这只是为了使存储库通用化。只有一个类来管理所有模型。通常我们会有ContactRepository、NoteRepository和ReminderRepository。如果我将来添加另一个模型,就必须再添加另一个存储库。此外,通常的CRUD操作在所有这些存储库中都被重复执行。仅在搜索操作和其他需要访问特定字段的任务中存在问题,因为它们无法通用。我想你更好地理解了这个问题。 - Amsakanna
使用动态关键字怎么样?dynamic dTable = db.GetTable<T>(); dTable.Where(i => i.Name == keyword); - Amsakanna
显示剩余6条评论

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