我们正在开发ASP.NET MVC应用程序,现在正在构建存储库/服务类。我想知道创建一个所有存储库实现的通用IRepository接口是否有任何重大优势,而每个存储库都有自己独特的接口和一组方法。
例如:通用的IRepository接口可能看起来像(取自this answer):
public interface IRepository : IDisposable
{
T[] GetAll<T>();
T[] GetAll<T>(Expression<Func<T, bool>> filter);
T GetSingle<T>(Expression<Func<T, bool>> filter);
T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
void Delete<T>(T entity);
void Add<T>(T entity);
int SaveChanges();
DbTransaction BeginTransaction();
}
每个存储库都将实现此接口,例如:
我们之前项目中遵循的备选方案是:
public interface IInvoiceRepository : IDisposable
{
EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
InvoiceEntity CreateInvoice();
InvoiceLineEntity CreateInvoiceLine();
void SaveChanges(InvoiceEntity); //handles inserts or updates
void DeleteInvoice(InvoiceEntity);
void DeleteInvoiceLine(InvoiceLineEntity);
}
在第二种情况下,表达式(LINQ或其他)将完全包含在Repository实现中,无论谁实现该服务,只需要知道要调用哪个存储库函数。
我想我没有看到在服务类中编写所有表达式语法并传递给存储库的优势。在许多情况下,这是否意味着容易混乱的LINQ代码?
例如,在我们的旧发票系统中,我们调用
InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)
来自几个不同的服务(客户,发票,账户等)。这似乎比在多个地方写下以下内容要清晰得多:
rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);
我看到使用特定方法的唯一缺点是我们最终可能会得到许多Get *函数的排列,但这似乎仍然可以将表达式逻辑推送到Service类中。
我错过了什么?
答案 0 :(得分:164)
这是一个与Repository模式本身一样古老的问题。最近引入的LINQ IQueryable
是查询的统一表示,引发了很多关于这个主题的讨论。
在努力构建通用存储库框架之后,我自己更喜欢特定的存储库。无论我尝试过哪种聪明的机制,我总是遇到同样的问题:存储库是被建模的域的一部分,并且该域不是通用的。并非每个实体都可以删除,不是每个实体都可以添加,并非每个实体都有一个存储库。查询变化很大;存储库API变得与实体本身一样唯一。
我经常使用的模式是具有特定的存储库接口,但是实现的基类。例如,使用LINQ to SQL,您可以执行以下操作:
public abstract class Repository<TEntity>
{
private DataContext _dataContext;
protected Repository(DataContext dataContext)
{
_dataContext = dataContext;
}
protected IQueryable<TEntity> Query
{
get { return _dataContext.GetTable<TEntity>(); }
}
protected void InsertOnCommit(TEntity entity)
{
_dataContext.GetTable<TEntity>().InsertOnCommit(entity);
}
protected void DeleteOnCommit(TEntity entity)
{
_dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
}
}
将DataContext
替换为您选择的工作单元。示例实现可能是:
public interface IUserRepository
{
User GetById(int id);
IQueryable<User> GetLockedOutUsers();
void Insert(User user);
}
public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(DataContext dataContext) : base(dataContext)
{}
public User GetById(int id)
{
return Query.Where(user => user.Id == id).SingleOrDefault();
}
public IQueryable<User> GetLockedOutUsers()
{
return Query.Where(user => user.IsLockedOut);
}
public void Insert(User user)
{
InsertOnCommit(user);
}
}
请注意,存储库的公共API不允许删除用户。此外,暴露IQueryable
是另一种蠕虫 - 在该主题上有与肚脐一样多的意见。
答案 1 :(得分:26)
我实际上与布莱恩的帖子略有不同意见。我认为他是对的,最终一切都非常独特等等。但与此同时,大部分内容都是在你设计的时候出现的,而且我发现在开发我的模型的同时获得一个通用的存储库并使用它,我可以很快得到一个应用程序,然后重新调整到更高的特异性,因为我发现需要这样做。
因此,在这种情况下,我经常创建一个具有完整CRUD堆栈的通用IRepository,这让我可以快速使用API并让人们玩UI并进行集成和放大。用户验收并行测试。然后,当我发现我需要对repo等进行特定查询时,如果需要,我会从那里开始替换那个特定的依赖关系。一个潜在的影响。很容易创建和使用(可能挂钩到内存中的db或静态对象或模拟对象或其他)。
那就是说,我最近开始做的就是打破这种行为。因此,如果您为IDataFetcher,IDataUpdater,IDataInserter和IDataDeleter(例如)执行接口,您可以通过接口混合匹配来定义您的需求,然后实现处理其中的部分或全部,并且我可以在我正在构建应用程序时,仍然会注入“全部实现”实现。
保
答案 2 :(得分:12)
我更喜欢使用可覆盖方法签名从通用存储库(或通用存储库列表中指定确切行为)派生的特定存储库。
答案 3 :(得分:5)
拥有一个由特定存储库包装的通用存储库。这样您就可以控制公共接口,但仍然具有代码重用的优势,而代码重用来自于具有通用存储库。
答案 4 :(得分:1)
public class UserRepository:Repository,IUserRepository
不应该注入IUserRepository以避免暴露接口。正如人们所说,你可能不需要完整的CRUD堆栈等。