使用ASP.NET核心应用程序实现存储库模式和工作单元的指南
目录
介绍
在学习之后实现存储库模式时,我遇到了许多正确实现的问题。但是我没有找到适合实施的完整解决方案。这促使我写了这篇文章。
本文将指导您使用存储库模式创建一个小应用程序,其中包含ASP.NET Core中的工作单元。本文主要针对初级到中级程序员。在本文中,我想提供实现的总体情况。
在这里,我不想提供通用存储库模式实现的详细信息。每当我搜索存储库模式实现时,我都会遇到许多带有通用存储库模式的示例。
完成本文后,您将正确理解特定存储库模式的实现。
存储库模式
存储库在域和数据映射层之间进行调解,其作用类似于内存中的域对象集合。
当我们想要封装逻辑以访问数据源时,存储库模式很有用。这里,存储库描述了访问数据源的类或组件。
存储库充当数据源和应用程序的业务层之间的中介。它在数据源中查询数据,将数据从数据源映射到业务实体,并将业务实体中的更改持久保存到数据源。
我们为什么要封装?
当我们想要从业务层分离数据访问功能时,我们将转移到存储库模式。
当通用存储库为最常见的数据操作类型(如更新、提取和删除)定义通用方法时,它非常有用。
在某些情况下,我们可能不需要对所有类型的存储库执行常见操作。
所以我们需要特定的存储库。这是基于我们将要实施的项目。
在存储库模式实现中,业务逻辑到数据访问逻辑和API到业务逻辑使用接口相互通信。数据访问层隐藏了业务逻辑的数据访问细节。详细说明,业务逻辑可以在不了解数据源的情况下访问数据访问层。
例如,业务层不知道数据访问层是使用LINQ to SQL还是ADO.NET等。
优点
以下是存储库模式的主要优点。
隔离数据访问逻辑
数据访问功能是集中的。因此,业务层将不知道数据来自何处。它可能来自任何数据源或缓存或模拟数据。
单元测试
基于前面的内容,这将理解业务层不知道数据来自何处。模拟数据访问层很容易。所以这将有助于我们为业务逻辑编写单元测试。
我们不能为数据访问层编写任何测试吗?为什么不?我们可以为这一层编写集成测试。
高速缓存
由于数据访问功能是集中的,我们可以为该层实现缓存。
数据源迁移
我们可以轻松地从一个数据源迁移到另一个数据源。迁移时,这不会影响我们的业务逻辑。
复杂查询被封装
复杂查询被封装并移动到该层。因此,可以从业务层重用查询。
当任何开发人员在编写查询方面都很强时,他/她可以独立地处理查询,而另一个开发人员可以专注于业务逻辑。
实现的经验法则
- 每个存储库都应该基于Domain实现,而不是基于数据库实体。
- 每个存储库都不应该相互联系。
- IQueryable不应该是存储库模式实现的返回类型。他们应该只返回IEnumerable。
- 它们不应该保存/删除/添加任何数据到数据库。所有细节都应该在内存中。我们可能会考虑如何进行粗暴操作。在这里,工作单元扮演着这个角色。工作单元将详细信息保存到数据库或回滚。这有什么好处?这将一次性保存存储库中发生的多个事务。
- 数据层不应该实现业务逻辑。业务逻辑应该在业务层中实现。它们应该返回数据的表示,业务层应该封装返回或解封装请求。
项目结构
以下是我们要实现的项目结构。请从链接下载样本。这里PL使用Angular应用程序。ASP.NET Core已应用于API和业务层,然后用于数据访问层。
业务层和数据访问层将具有单独的约定(接口)。业务层和数据访问层将取决于抽象而不是具体实现。
这是因为依赖注入。因此,任何层都不会有关于另一层的知识。当我们进行模拟和测试时,这很容易。
- 表示层(PL)
- API
- 业务层(BL)
- 数据访问层(DAL)
有关应用程序流程,请参阅下图。PL将联系API。API将联系BL。BL将联系DAL。
我们将进行一个松散耦合的实现。业务层将不知道数据访问层。API不会知道BL。
对于此实现,我们将实现依赖注入(DI)。
依赖注入(DI)
什么是依赖注入?
较高级别的模块不应该依赖于较低级别的模块。依赖注入主要用于将具体实现注入到使用抽象的类中,即内部接口。这使得能够开发松散耦合的代码。
详细地说,如果你的ClassA需要使用 ClassB,请让我们的ClassA知道一个IClassB接口而不是一个ClassB。通过这次执行,我们可以在ClassB不破坏主机代码的情况下多次更改实现。
DI的优点
- 干净、更易读的代码
- 类或对象松散耦合
- 模拟对象很容易
使用代码
请考虑以下示例以实现此实现。
- 用户的CRUD操作
- 产品的CRUD操作
- 向/从用户添加或删除产品。只能为用户分配一个产品。
数据访问层
现在我们必须确定问题的领域。基于上面的示例,我们确定了两个领域。
- 用户领域
- 产品领域
基于经验法则,我们需要基于领域创建存储库。因此,在此示例中,我们将为上述两个领域创建两个存储库:
- 用户存储库
- 产品存储库
要创建UserRepository和ProductRepository,分别创建将实现仓库接口IUserRepository,IProductRepository。
IUserRepository
public interface IUserRepository
{
void AddUser(User user);
IEnumerable<User> GetUsers();
bool DeleteUser(long userId);
User GetUser(long Id);
}
IProductRepository
public interface IProductRepository
{
void AddProduct(Product product);
Product GetProduct(long id);
IEnumerable<Product> GetProducts();
bool DeleteProduct(long productId);
IEnumerable<Product> GetUserProducts(long userId);
void AddProductToUser(long userId, long productId);
}
现在创建将实现抽象的具体类,即接口。
这些具体的类将具有实际的实现。在这里,我们可以注意到:
- 每个添加或删除都在内存中实现,而不是在数据源中实现
- 数据源没有更新。
UserRepository
public class UserRepository : IUserRepository
{
private readonly AppDbContext context;
public UserRepository(AppDbContext dbContext)
{
this.context = dbContext;
}
public void AddUser(User user)
{
context.Users.Add(user);
}
public bool DeleteUser(long userId)
{
var removed = false;
User user = GetUser(userId);
if (user != null)
{
removed = true;
context.Users.Remove(user);
}
return removed;
}
public User GetUser(long Id)
{
return context.Users.Where(u => u.Id == Id).FirstOrDefault();
}
public IEnumerable<User> GetUsers()
{
return context.Users;
}
}
ProductRepository
public class ProductRepository : IProductRepository
{
private readonly AppDbContext context;
public ProductRepository(AppDbContext dbContext)
{
this.context = dbContext;
}
public void AddProduct(Product product)
{
context.Products.Add(product);
}
public void AddProductToUser(long userId, long productId)
{
context.UserProducts.Add(new UserProduct()
{
ProductId = productId,
UserId = userId
});
}
public bool DeleteProduct(long productId)
{
var removed = false;
Product product = GetProduct(productId);
if (product != null)
{
removed = true;
context.Products.Remove(product);
}
return removed;
}
public Product GetProduct(long id)
{
return context.Products.Where(p => p.Id == id).FirstOrDefault();
}
public IEnumerable<Product> GetProducts()
{
return context.Products;
}
public IEnumerable<Product> GetUserProducts(long userId)
{
return context.UserProducts
.Include(up => up.Product)
.Where(up => up.UserId == userId)
.Select(p => p.Product)
.AsEnumerable();
}
}
工作单位(UOW)
从上面的实现中,我们可以理解应该使用存储库:
- 从数据源读取数据
- 在内存中添加/删除数据
那么添加/更新/删除将如何影响数据源?UOW扮演这个角色。UOW知道每个存储库。这有助于一次实现多个事务。
对于这种实现,需要实现如上所述。创建一个具体的UnitOfWork 将实现抽象,即接口IUnitOfWork。
IUnitOfWork
public interface IUnitOfWork
{
IUserRepository User { get; }
IProductRepository Product { get; }
Task<int> CompleteAsync();
int Complete();
}
UnitOfWork
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext dbContext;
public UnitOfWork(AppDbContext dbContext)
{
this.dbContext = dbContext;
}
private IUserRepository _User;
private IProductRepository _Product;
public IUserRepository User
{
get
{
if (this._User == null)
{
this._User = new UserRepository(dbContext);
}
return this._User;
}
}
public IProductRepository Product
{
get
{
if (this._Product == null)
{
this._Product = new ProductRepository(dbContext);
}
return this._Product;
}
}
public async Task<int> CompleteAsync()
{
return await dbContext.SaveChangesAsync();
}
public int Complete()
{
return dbContext.SaveChanges();
}
public void Dispose() => dbContext.Dispose();
}
我们已经使用UOW为DAL完成了存储库模式实现。
以下是愚蠢的。执行此操作后,我对如何在保存数据之前需要从另一个存储库获取数据进行检查感到困惑。例如,将产品添加到用户时,请检查用户或产品是否存在。
此方案将违反规则,即存储库不应在其中进行交互。发生了什么?我现在应该怎么做?在这里,我的理解是错误的。业务逻辑不应出现在存储库模式中。这只是数据访问的封装。每个逻辑验证都应该移到业务层。业务层将了解负责验证的所有存储库。
业务层
现在我们需要专注于业务层。在这一层中,我们将注入UOW而不是所有必需的存储库。UOW知道所有的存储库,我们可以使用UOW访问。
例如,为了实现Product的BL,我们将创建一个接口IProduct ,并需要创建一个将实现的IProduct接口的具体类BLProduct 。
下面的BLProduct中,所有必要的验证和业务逻辑已经完成,我们可以在AddProductToUser 方法中注意作为多个存储库使用的示例。
IProduct
public interface IProduct
{
Product UpsertProduct(Product product);
IEnumerable<Product> GetProducts();
bool DeleteProduct(long productId);
IEnumerable<Product> GetUserProducts(long userId);
bool AddProductToUser(long userId, long productId);
}
BLProduct
public class BLProduct : IProduct
{
private readonly IUnitOfWork uow;
public BLProduct(IUnitOfWork uow)
{
this.uow = uow;
}
public bool AddProductToUser(long userId, long productId)
{
if (userId <= default(int))
throw new ArgumentException("Invalid user id");
if (productId <= default(int))
throw new ArgumentException("Invalid product id");
if (uow.Product.GetProduct(productId) == null)
throw new InvalidOperationException("Invalid product");
if (uow.User.GetUser(userId) == null)
throw new InvalidOperationException("Invalid user");
var userProducts = uow.Product.GetUserProducts(userId);
if (userProducts.Any(up => up.Id == productId))
throw new InvalidOperationException("Products are already mapped");
uow.Product.AddProductToUser(userId, productId);
uow.Complete();
return true;
}
public bool DeleteProduct(long productId)
{
if (productId <= default(int))
throw new ArgumentException("Invalid produt id");
var isremoved = uow.Product.DeleteProduct(productId);
if (isremoved)
uow.Complete();
return isremoved;
}
public IEnumerable<Product> GetProducts()
{
// May implement role based access
return uow.Product.GetProducts();
}
public IEnumerable<Product> GetUserProducts(long userId)
{
if (userId <= default(int))
throw new ArgumentException("Invalid user id");
return uow.Product.GetUserProducts(userId);
}
public Product UpsertProduct(Product product)
{
if (product == null)
throw new ArgumentException("Invalid product details");
if (string.IsNullOrWhiteSpace(product.Name))
throw new ArgumentException("Invalid product name");
var _product = uow.Product.GetProduct(product.Id);
if (_product == null)
{
_product = new Product
{
Name = product.Name
};
uow.Product.AddProduct(_product);
}
else
{
_product.Name = product.Name;
}
uow.Complete();
return _product;
}
}
在AddProductToUser方法中,我想向用户添加产品。因此,在向用户添加产品之前,我已在方法中进行了以下验证:
- 参数验证
- 检查产品是否已删除
- 检查用户是否存在
- 检查产品是否已添加到用户
- 最后,将产品添加到集合中
完成上述步骤后,最后保存用户产品。
在UpsertProduct方法中,我们将实现添加或更新。如果产品不可用,请添加。如果产品可用,则更新。为了这:
- 需要检查有效值
- 然后尝试获取产品并检查产品是否可用
- 如果它不可用,则添加到集合中
- 如果可用,则更新集合中的必要值
完成上述操作后,保存值。
这是什么意思?它有助于控制何时可以保存值。我们在添加或更新时没有立即保存。我们可以在这里做更多的操作,最后我们可以保存。
API
正如我们在流程中,我们可以看到我们已经完成了DAL和BL。现在我们在API中注入BL并执行必要的操作。
在这里,我使用的是ASP.NET CORE。我们需要在服务容器中注册依赖项,如下所示:
// Inject BL
services.AddScoped<IUser, BLUser>();
services.AddScoped<IProduct, BLProduct>();
// Inject unit of work
services.AddScoped<IUnitOfWork, UnitOfWork>();
注册后,我们需要在控制器中注入此依赖项。请参考以下代码。
ProductController
[Route("api/Product")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly IMapper mapper;
private readonly IProduct blProduct;
public ProductController(IMapper mapper, IProduct product)
{
this.mapper = mapper;
this.blProduct = product;
}
// GET: api/Product
[HttpGet]
public IEnumerable<ProductModel> Get()
{
var products = blProduct.GetProducts();
return mapper.Map<IEnumerable<Product>, IEnumerable<ProductModel>>(products);
}
// GET: api/Product/5
[HttpGet("{id}")]
public IEnumerable<ProductModel> Get(int userId)
{
var products = blProduct.GetUserProducts(userId);
return mapper.Map<IEnumerable<Product>, IEnumerable<ProductModel>>(products);
}
// POST: api/Product
[HttpPost]
public void Post([FromBody] ProductModel product)
{
}
// DELETE: api/ApiWithActions/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
争议
当我开始学习和实现存储库模式时,我发现许多文章我们不应该使用Entity Framework(EF)实现存储库模式。
为什么?
因为EF是使用存储库模式和工作单元实现的。为什么我们需要一个Layer到另一个使用相同模式实现的层?
是的,听起来不错。对吗?
我的结论
是的,以上是一个很好的点。在考虑以下内容后,我得出结论,在使用EF实现存储库模式时,我们没有错。
- 将来,如果我们要针对任何类型的问题迁移ORM,那么我们的实现部分为迁移提供了更好的解决方案。
- 我们可以在DAL中移动复杂和批量查询。
- 当我们要进行单元测试时,这个实现提供了一种模拟DAL的简单方法。
- 我们只能专注于DAL 对缓存的实现。
原文地址:https://www.codeproject.com/Articles/1275116/Guidance-for-the-Implementation-of-Repository-Patt