C# 最佳实践:实体框架:如果使用存储库和UoW模式,在哪里进行连接
我有一个用户存储库和合作伙伴存储库。我的存储库不返回iquerable。用户实体具有partnerID。我想使用partnerID通过使用Linq的存储库连接两个表user和partner表。但是,我不确定在哪里进行这些连接。合作伙伴和用户上没有外键,因此我无法通过导航属性进行包含 我知道连接不应该进入存储库。是否应该在UoW中进行连接?还是服务?就我在何处进行这些连接而言,最佳实践是什么 聚合根: 在你的集合中,根将是你的回购协议的名称,因为 来自聚合外部的任何引用只应转到 聚合根C# 最佳实践:实体框架:如果使用存储库和UoW模式,在哪里进行连接,c#,.net,entity-framework,asp.net-core,design-patterns,C#,.net,Entity Framework,Asp.net Core,Design Patterns,我有一个用户存储库和合作伙伴存储库。我的存储库不返回iquerable。用户实体具有partnerID。我想使用partnerID通过使用Linq的存储库连接两个表user和partner表。但是,我不确定在哪里进行这些连接。合作伙伴和用户上没有外键,因此我无法通过导航属性进行包含 我知道连接不应该进入存储库。是否应该在UoW中进行连接?还是服务?就我在何处进行这些连接而言,最佳实践是什么 聚合根: 在你的集合中,根将是你的回购协议的名称,因为 来自聚合外部的任何引用只应转到 聚合根 在我们公司
在我们公司中,我们将保存必须执行的用例的对象(工作单元)与存储数据的概念(存储库)以及存储数据的方法(在使用实体框架的数据库中)分开 将DbContext与存储库分离的优点 通过使用这种分离,可以将数据库更改为存储表的任何其他内容。例如,您可以使用CSV文件序列,或者使用Dapper访问数据库 实体框架和存储库分离的另一个优点是,您可以提供不允许访问您不希望用户访问的项目的接口。例如,一些用户可能只查询数据,其他用户可能添加或更新数据,只有少数用户可能删除对象 一个非常好的副作用是,我们可以使用
测试列表的集合来单元测试使用存储库的代码,而不是真正的数据库表
只有当新的用例需要新的数据时,我们才需要改变这三种情况。不需要新数据的工作单元用户不会注意到差异
DbContext
DbContext中的数据库集表示数据库的表。每个表至少有一个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
}
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;
...
}
DbContext尽可能简单:它只表示表和表之间的关系
存储库
存储库隐藏用于存储数据的存储方法。它可以是一个数据库,一系列CSV文件,数据可以分为多个数据库
存储库通常有几个接口:
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;
}
- 仅查询接口,返回要公开的每个表的
IQueryable
。此界面的用户可以执行他们想要的任何查询。他们不能改变数据。这样做的优点是可以隐藏不想公开的属性和表。用户不能意外更改项目
- 用于创建/更新项目以及查询的界面。对于真正添加或更新数据库的少数表单。它们还可以将项目标记为过时
- 用于删除标记为
已过时的数据的接口。后台进程用于定期删除过时数据
正如实体框架有表示实体的类(表:客户、订单、订单行等)和表示实体集合的类(IDbSet
),存储库也有类似的类和接口。它们中的大多数是可重用的,只有一行程序
存储库实体类
每个存储库项都可以标记为已过时。公共基类:
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;
}
}
只读访问和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();
}
事实上:对于删除所有过时项的后台进程,我们有一个特殊的接口,可以删除所有过时一段时间的项。这里不再提了
用法:
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
}
最佳实践是不要在存储库/uow后面使用实体框架,这只会给您带来巨大的性能影响。如果您不打算使用实体框架的单一功能,请使用Dapper。此外,连接始终必须在单个数据库查询中运行,不要为了遵循与@CamiloTerevinto完全一致的完全过时的模式而影响性能。Repository模式用于低级数据访问,例如直接使用SQL(ADO.NET、Dapper等)。它不适用于像EF这样已经实现Repository/UoW模式的ORM。当您使用ORM时,您选择使用第三方DAL,而不是创建自己的DAL。就这么简单。如果您的目标是抽象数据层,那么您应该寻找更高层次的体系结构模式,例如微服务。我主要使用它来帮助单元测试,我已经阅读并看到了这种模拟方法的好处:。我应该不使用这种模式吗?我的存储库不会返回IQuerables-好吧,如果您坚持在冗余存储库中包装DbSet
s,至少要修复该部分。并确保两个repo具有相同的上下文实例。那么导航属性呢?列出的repository/UoW模式的“优点”与直接使用EF没有区别,特别是现在EF Core支持内存中的提供程序。唯一的一点好处是不与特定的ORM绑定,但是切换ORM有足够的摩擦,它将很好地旋转到您的存储库/UoW层中,导致应用程序代码无论如何都必须重写。int Id{get;}
然后当表需要复合主键甚至仅仅是一个GuidTotally时,所有这些都将转到sh*t。这就是ORM上的repository/UoW的问题。不可避免地,您的repo层将不支持ORM所做的事情,这使得从一个到另一个的内容转换实际上是即时消息
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;
}
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
}