在我们公司,我们将包含必须执行的用例的对象(工作单元)与存储数据的概念(存储库)与如何存储数据的方法(在使用实体框架的数据库中)分开.
将 DbContext 与 Repository 分离的优点
通过使用这种分离,可以将数据库更改为存储表的任何其他内容。例如,您可以使用一系列 CSV 文件,或使用 Dapper 访问数据库。
Entity Framework 和 Repository 分离的另一个优点是,您可以提供不授予访问您不希望用户访问的项目的接口。例如,一些用户可能只查询数据,另一些用户可能会添加或更新数据,而只有少数用户可能会删除对象。
一个非常好的副作用是我们可以对使用带有Test Lists 集合而不是真实数据库表的存储库的代码进行单元测试。
仅当新用例需要新数据时,我们才需要更改所有这三个。我们工作单元中不需要新数据的用户不会注意到差异
数据库上下文
DbContext 中的 DbSet 代表我们数据库的表。每个表至少有一个 Id 作为主键,以及一个可以为空的 DateTime 对象,用于标记对象被声明为过时的日期。后台进程会定期删除所有过时一段时间的对象。
后一部分是为了防止用户 A 正在更新一条记录,而用户 B 正在删除同一条记录。用户只能将记录标记为过时,不能删除。
interface IDbItem
{
int Id {get; } // no need to ever change the primary key
DateTime? Obsolete {get; set;}
}
例如客户:
class Customer : IDbItem
{
public int Id {get; set;}
public DateTime? ObsoleteDate {get; set;}
public string Name {get; set;}
... // other properties
}
DbContext 尽可能简单:它只表示表和表之间的关系
存储库
存储库隐藏了用于存储数据的存储方法。它可以是一个数据库,一系列 CSV 文件,数据可以分布在多个数据库中。
存储库通常有几个接口:
- 仅查询接口,返回您要公开的每个表的
IQueryable<...>。这个界面的用户可以做他们想做的每一个查询。他们无法更改数据。这样做的好处是您可以隐藏不想公开的属性和表。用户不能意外更改项目。
- 创建/更新项目以及查询的界面。对于真正添加或更新数据库的少数表单。他们还可以将项目标记为已过时。
- 删除标记为
Obsolete 的数据的接口。由后台进程用于定期删除过时的数据。
就像实体框架有代表实体的类(表:客户、订单、订单行等)和代表实体集合的类(IDbSet<Customer>)一样,存储库也有类似的类和接口。它们中的大多数都是可重复使用的,并且是单行的
存储库实体类
interface IId
{
int Id {get;}
}
interface IRepositoryEntity : IId
{
bool IsObsolete {get;}
void MarkObsolete();
}
每个存储库项目都可以标记为过时。一个通用的基类:
class RepositoryEntity<TSource> : IId, IRepositoryEntity
where TSource : IDbItem
{
public TSource DbItem {get; set;}
// Interface IId
public int Id => this.DbItem.Id;
// Interface IRepositoryEntity
public bool IsObsolete => this.DbItem.ObsoleteDate != null;
public void MarkObsolete()
{
this.DbItem.ObsoleteDate = DateTime.UtcNow;
}
}
例如客户:
interface IReadOnlyCustomer : IId
{
string Name {get;}
...
}
interface ICustomer : IRepositoryItem
{
string Name {get; set;}
}
class Customer : RepositoryEntity<Customer>, IReadOnlyCustomer, ICustomer
{
// Interfaces IId and IRepositoryItem implemented by base class
// Interface ICustomer
public string Name {get; set;}
...
// Interface IReadOnlyCustomer
string IReadOnlyCustomer.Name => this.Name;
...
}
您会看到存储库客户只需要实现您实际想要向外部世界公开的客户属性。存储库不需要代表您的数据库表。
例如,如果您的数据库有客户FirstName、MiddleName、FamilyName 的拆分值,那么您可以在 get Name 函数中将它们连接起来。
存储库集合
存储库集合类似于IDbSet<...>。 Query only 有一个接口,Query, Update, Mark Obsolete 有一个接口。当然,我们也有完全的访问权限,只授予少数快乐的人。
对于 ReadOnly 来说,IQueryable<TEntity> where TEntity : Iid 就足够了
要查询/添加/更新/废弃,我需要一个 ISet 和一个 Set:
interface ISet<TEntity> : IQueryable<TEntity> where TEntity: IRepositoryEntity
{
TEntity Add(TEntity item);
}
class Set<TEntity, TDbEntity> : ISet<TEntity>
where TEntity: IRepositoryEntity,
where TDbEntity: IDbItem
{
public IDbSet<TEntity> DbSet {get; set;}
// implement the interfaces via DbSet
public TEntity Add(TEntity item)
{
// TODO: convert item to a dbItem
return this.DbSet.Add(dbItem);
}
// Similar for IQueryable<TEntity> and IQueryable
}
ReadOnly 访问和 CRUD 访问的接口:
interface IReadOnlyRepository : IDisposable
{
IQueryable<IReadOnlyCustomer> Customers {get;}
IQueryable<IReadOnlyOrders> Orders {get;}
}
interface IRepository : IDisposable
{
ISet<ICustomer> Customers {get;}
ISet<IOrder> Orders {get;}
void SaveChanges();
}
有权访问 ReadOnlyRepository 的人只能查询数据。他们无法做出任何改变。有权访问 IRepository 的用户可以添加项目、更新项目和保存更改。
类存储库实现所有接口:
class Repository : IReadOnlyRepository, // Query Only
IRepository, // Query, Add and Update
IDisposable
{
private readonly dbContext = new CustomerDbContext();
// TODO: Dispose() will Dispose dbContext
// Used by the other interfaces
protected IDbSet<Customer> Customers => this.dbContext.Customers;
protected IDbSet<Orders> Orders => this.dbContext.Orders;
void SaveChanges() {this.dbContext.SaveChanges();}
// IRepository:
ISet<ICustomer> IRepository.Customers => new Set<Customer>{DbSet = this.Customers};
ISet<IOrder> IRepository.Orders => new Set<Order>{DbSet = this.Orders};
void IRepository.SaveChanges() {this.DbContext.SaveChanges();}
// IReadOnlyRepository
IQueryable<IReadOnlyCustomer> IReadOnlyRepository.Customers => this.Customers;
IQueryable<IReadOnlyOrders> IReadOnlyRepository.Orders => this.Orders;
}
看起来代码很多,但是大部分函数都是单行代码,调用对应的entity-framework函数。
最后,我们需要一个创建存储库的工厂。如果您想为多个存储库重新使用它,请创建一个通用工厂类。为简单起见,我为 Ordering 数据库创建它:
class OrdersRepository
{
public IReadOnlyRepository CreateReadOnly()
{
// TODO: if desired check rights: can this user access this database?
return new Repository();
}
public IRepository CreateUpdateAccess()
{
// TODO: if desired check rights: can this user access this database?
return new Repository();
}
public Repository CreateFullControl()
{
// TODO: if desired check rights: can this user access this database?
return new Repository();
}
事实上:对于删除所有过时项目的后台进程,我们有一个特殊的界面,可以删除所有过时一段时间的项目。此处不再提及。
用法:
var repositoryFactory = new RepositoryFactory() {AccessRights = ...}
// I need to query only:
using (var repository = repositoryFactory.CreateUpdatAccess())
{
// you can query, change value and save changes, for instance after a Brexit:
var customersToRemove = repository.Customers.Where(customer => customer.State == "United Kingdom")
foreach (var customerToRemove in customersToRemove);
{
customerToRemove.MarkObsolete();
}
repository.SaveChanges();
}
// I need to change data:
using (var repository = repositoryFactory.CreateReadOnly())
{
// do some queries. Compiler error if you try to change
}