C# 最佳实践:实体框架:如果使用存储库和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中进行连接?还是服务?就我在何处进行这些连接而言,最佳实践是什么 聚合根: 在你的集合中,根将是你的回购协议的名称,因为 来自聚合外部的任何引用只应转到 聚合根 在我们公司

我有一个用户存储库和合作伙伴存储库。我的存储库不返回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
}