使用 C# 简单介绍通用存储库模式
C# 中的通用存储库模式是一种抽象应用程序数据层的设计模式,使跨不同数据源和项目的数据访问逻辑管理更加便捷。它旨在通过在单个通用存储库中实现常见的数据操作(而不是为每种实体类型设置单独的存储库)来减少冗余。项目类型可以是桌面或 Web。当项目类型为 Web 时,需要考虑使用依赖注入来实现。在提供的示例中,我们使用控制台项目来覆盖更广泛的受众,而没有使用依赖注入,以便专注于如何编写通用存储库。
- 通用存储库始于一个通用接口,该接口定义了一些常用操作,例如 CRUD 操作。这些操作以通用的方式定义,适用于任何实体类型。
- 然后,通用接口在一个具体类中实现。该类处理数据源交互,例如使用 EF Core 或 Dapper 等 ORM 查询数据库。
- 该实现通常利用 Entity Framework Core DbContext 或带有 IDbConnection 的 Dapper 与数据库进行交互。## 创建通用存储库
通用存储库将提供 CRUD 功能、选择所有记录、单个记录、插入新记录(可选择返回新主键)、更新和删除单个或批量记录集。
🟢 无论代码是由单个开发人员还是开发人员团队使用,考虑应该包含什么以及命名约定都至关重要,因为这里的想法是在多个项目中使用该想法时的一致性。
首先定义一个基本接口,以便如果需要使用主键迭代数据,则将使用 Id 来保持一致性,并且它不属于通用接口的一部分。
public interface IBase
{
public int Id { get; }
}
通用接口。如果您不介意方法名称,可以随意更改它们以及添加或删除方法。
public interface IGenericRepository<T> where T : class
{
IEnumerable<T> GetAll();
Task<List<T>> GetAllAsync();
T GetById(int id);
T GetByIdWithIncludes(int id);
Task<T> GetByIdAsync(int id);
Task<T> GetByIdWithIncludesAsync(int id);
bool Remove(int id);
void Add(in T sender);
void Update(in T sender);
int Save();
Task<int> SaveAsync();
public T Select(Expression<Func<T, bool>> predicate);
public Task<T> SelectAsync(Expression<Func<T, bool>> predicate);
}
将使用以下模型,一个将使用下面接口的所有方法创建,另外两个供读者练习。
public class Category : IBase
{
public int Id => CategoryId;
public int CategoryId { get; set; }
public string? Name { get; set; }
public virtual ICollection<Product>? Products { get; set; }
public override string? ToString() => Name;
}
public partial class Countries : IBase
{
public int Id => CountryId;
[Key]
public int CountryId { get; set; }
public string Name { get; set; }
public override string ToString() => Name;
}
public class Product : IBase
{
public int Id => ProductId;
public int ProductId { get; set; }
public string? Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
public override string? ToString() => Name;
}
要实现生产操作的接口,请创建一个名为 ProductsRepository 的新类。按如下所示更改类签名。
public class ProductsRepository : IGenericRepository<Product>
此时,Visual Studio 将提示您创建缺少的方法,请允许 Visual Studio 创建这些方法。每个方法都会被存根,以便您使用代码
public IEnumerable<Countries> GetAll()
{
throw new NotImplementedException();
}
由于此示例在同一个类项目中使用带有 DbContext 和 DbSets 的 EF Core,我们可以在此处初始化名为 Context 的 DbContext,如下所示,以便每次需要存储库时,DbContext 都已准备就绪。
public class ProductsRepository : IGenericRepository<Product>
{
private Context _context;
public ProductsRepository(Context context)
{
_context = context;
}
事后清理也是很好的,所以我们实现了 IDisposable。
public class ProductsRepository : IGenericRepository<Product>,
IDisposable
{
...
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.Dispose();
}
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
从这里开始,为每种方法编写代码,以便完成后得到类似这样的结果。
public class ProductsRepository : IGenericRepository<Product>,
IDisposable
{
private Context _context;
public ProductsRepository(Context context)
{
_context = context;
}
public IEnumerable<Product> GetAll()
{
return _context.Products.ToList();
}
public Task<List<Product>> GetAllAsync()
{
return _context.Products.ToListAsync();
}
public Product GetById(int id)
{
return _context.Products.Find(id);
}
public Product GetByIdWithIncludes(int id)
{
return _context.Products.Include(x => x.Category)
.FirstOrDefault(x => x.ProductId == id);
}
public async Task<Product> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
public async Task<Product> GetByIdWithIncludesAsync(int id)
{
return await _context.Products.Include(x => x.Category)
.FirstOrDefaultAsync(x => x.ProductId == id);
}
public bool Remove(int id)
{
var product = _context.Products.Find(id);
if (product is { })
{
_context.Products.Remove(product);
return true;
}
return false;
}
public void Add(in Product sender)
{
_context.Add(sender).State = EntityState.Added;
}
public void Update(in Product sender)
{
_context.Entry(sender).State = EntityState.Modified;
}
public int Save()
{
return _context.SaveChanges();
}
public Task<int> SaveAsync()
{
return _context.SaveChangesAsync();
}
public Product Select(
Expression<Func<Product, bool>> predicate)
{
return _context.Products
.WhereNullSafe(predicate).FirstOrDefault()!;
}
public async Task<Product> SelectAsync(
Expression<Func<Product, bool>> predicate)
{
return
(
await _context.Products
.WhereNullSafe(predicate).FirstOrDefaultAsync())!;
}
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_context.Dispose();
}
}
_disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
上面缺少什么?您可以添加更多方法,例如,您的数据库模型包含订单和订单详情,并且只需要特定订单的产品,只需添加该方法即可。
把十个开发人员放在一个房间里,问他们如何使用通用存储库,我们很可能会得到至少三个不同的版本。
以下内容来自一个随机的Stackoverflow 帖子。请注意,它们的界面与上面显示的界面类似。
public interface IRepositoryBase<T> where T : class
{
void Add(T objModel);
void AddRange(IEnumerable<T> objModel);
T? GetId(int id);
Task<T?> GetIdAsync(int id);
T? Get(Expression<Func<T, bool>> predicate);
Task<T?> GetAsync(Expression<Func<T, bool>> predicate);
IEnumerable<T> GetList(Expression<Func<T, bool>> predicate);
Task<IEnumerable<T>> GetListAsync(Expression<Func<T, bool>> predicate);
IEnumerable<T> GetAll();
Task<IEnumerable<T>> GetAllAsync();
int Count();
Task<int> CountAsync();
void Update(T objModel);
void Remove(T objModel);
void Dispose();
}
然后有一个基础存储库,与上面显示的不同,它用于 ORM 中的所有实体。
public class RepositoryBase<TEntity> : IRepositoryBase<TEntity> where TEntity : class
{
protected readonly Context _context = new();
public void Add(TEntity model)
{
_context.Set<TEntity>().Add(model);
_context.SaveChanges();
}
public void AddRange(IEnumerable<TEntity> model)
{
_context.Set<TEntity>().AddRange(model);
_context.SaveChanges();
}
public TEntity? GetId(int id)
{
return _context.Set<TEntity>().Find(id);
}
public async Task<TEntity?> GetIdAsync(int id)
{
return await _context.Set<TEntity>().FindAsync(id);
}
public TEntity? Get(Expression<Func<TEntity, bool>> predicate)
{
return _context.Set<TEntity>().FirstOrDefault(predicate);
}
public async Task<TEntity?> GetAsync(Expression<Func<TEntity, bool>> predicate)
{
return await _context.Set<TEntity>().FirstOrDefaultAsync(predicate);
}
public IEnumerable<TEntity> GetList(Expression<Func<TEntity, bool>> predicate)
{
return _context.Set<TEntity>().Where<TEntity>(predicate).ToList();
}
public async Task<IEnumerable<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate)
{
return await Task.Run(() => _context.Set<TEntity>().Where<TEntity>(predicate));
}
public IEnumerable<TEntity> GetAll()
{
return _context.Set<TEntity>().ToList();
}
public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await Task.Run(() => _context.Set<TEntity>());
}
public int Count()
{
return _context.Set<TEntity>().Count();
}
public async Task<int> CountAsync()
{
return await _context.Set<TEntity>().CountAsync();
}
public void Update(TEntity objModel)
{
_context.Entry(objModel).State = EntityState.Modified;
_context.SaveChanges();
}
public void Remove(TEntity objModel)
{
_context.Set<TEntity>().Remove(objModel);
_context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
使用哪种模式?这取决于个人喜好和经验。
运行提供的代码
项目 WorkingWithInterfacesApp1 在 appsettings.json 中定义了使用 .\SQLEXPRESS 作为服务器的连接字符串。如果此服务器可用,请保留原样,否则请将其更改为其他可用的服务器。
- DbContext 中的代码将创建数据库并填充 DbContext 中定义的表。
- 使用 ProductsRepository
- 使用 GetAll 方法获取所有产品
- 使用 GetByIdWithIncludeAsync 获取产品
- 删除id为4的商品
- 使用 Add 方法添加新产品
- 保存更改并检查保存更改返回的 2 个结果,一个用于删除,一个用于添加。
- 编辑产品
- 保存更改。
展翅高飞
研究产品代码,然后尝试使用其他模型之一或采用这两个接口并在您的项目中尝试。
不使用 EF Core
通用存储库模式将与 Dapper 一起使用实例或使用 DataTable 的连接和命令对象。
源代码
克隆以下GitHub 存储库。