目录
介绍
经典存储库模式是在任何应用程序中实现数据库访问的简单方法。它满足小型应用的许多正常设计目标。另一方面,CQS和CQRS为更大更复杂的应用程序提供了更复杂但结构良好的设计模式。
在本文中,我将应用CQS中使用的一些基本良好做法开发基本存储库模式,并实现完全泛型提供程序。
这不是DotNetCore中带有一些装饰的重复IRepository实现。
1、没有每个实体类的实现。你不会看到这个:
public class WeatherForecastRepository : GenericRepository<WeatherForecast>,
IWeatherForcastRepository
{
public WeatherForecastRepository(DbContextClass dbContext) : base(dbContext) {}
}
public interface IProductRepository : IGenericRepository<WeatherForecast> { }
2、没有单独的UnitOfWork类:它是内置的。
3、所有标准数据I/O都使用单个数据代理。
4、设计中使用CQS请求、结果和处理程序模式。
命名、术语和实践
- DI:依赖注入
- CQS:命令/查询分离
代码为:
- Net 7.0
- C# 10
- 启用Nullable
Repo
本文的存储库和最新版本在这里:Blazr.IRepository。
数据存储
该解决方案需要一个真实的数据存储进行测试:它实现实体框架内存中数据库。
我是Blazor开发人员,所以我的测试数据类是WeatherForecast。数据提供程序的代码在附录中。
这是DBContext工厂使用的DbContext。
public sealed class InMemoryWeatherDbContext : DbContext
{
public DbSet<WeatherForecast> WeatherForecast { get; set; } = default!;
public InMemoryWeatherDbContext
(DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<WeatherForecast>().ToTable("WeatherForecast");
}
测试工厂和上下文
以下XUnit测试演示了DI中的基本数据存储设置。它:
- 设置DI容器。
- 从测试提供程序加载数据。
- 测试记录计数是否正确。
- 测试任意记录是否正确。
[Fact]
public async Task DBContextTest()
{
// Gets the control test data
var testProvider = WeatherTestDataProvider.Instance();
// Build our services container
var services = new ServiceCollection();
// Define the DbSet and Server Type for the DbContext Factory
services.AddDbContextFactory<InMemoryWeatherDbContext>(options
=> options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));
var rootProvider = services.BuildServiceProvider();
//define a scoped container
var providerScope = rootProvider.CreateScope();
var provider = providerScope.ServiceProvider;
// get the DbContext factory and add the test data
var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
if (factory is not null)
WeatherTestDataProvider.Instance().LoadDbContext
<InMemoryWeatherDbContext>(factory);
// Check the data has been loaded
var dbContext = factory!.CreateDbContext();
Assert.NotNull(dbContext);
var count = dbContext.Set<WeatherForecast>().Count();
Assert.Equal(testProvider.WeatherForecasts.Count(), count);
// Test an arbitrary record
var testRecord = testProvider.GetRandomRecord()!;
var record = await dbContext.Set<WeatherForecast>().SingleOrDefaultAsync
(item => item.Uid.Equals(testRecord.Uid));
Assert.Equal(testRecord, record);
// Dispose of the resources correctly
providerScope.Dispose();
rootProvider.Dispose();
}
经典存储库模式实现
这是我在互联网上找到的一个很好的简洁实现。
public abstract class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContextClass _dbContext;
protected GenericRepository(DbContextClass context)
=> _dbContext = context;
public async Task<T> GetById(int id)
=> await _dbContext.Set<T>().FindAsync(id);
public async Task<IEnumerable<T>> GetAll()
=> await _dbContext.Set<T>().ToListAsync();
public async Task Add(T entity)
=> await _dbContext.Set<T>().AddAsync(entity);
public void Delete(T entity)
=> _dbContext.Set<T>().Remove(entity);
public void Update(T entity)
=> _dbContext.Set<T>().Update(entity);
}
}
把它拆开:
- 返回null时会发生什么,这意味着什么?
- 那add/update/delete真的成功了吗?我怎么知道?
- 您如何处理取消令牌?大多数async方法现在都接受取消令牌。
- 当您的DBSet包含一百万条记录时会发生什么(也许DBA昨晚出了点问题)?
- 应用程序中的每个数据存储实体都有一个我。
实现
请求和结果
请求对象封装我们请求的内容,结果对象封装我们期望返回的数据和状态信息。它们是records:定义一次,然后消费。
命令
命令是对数据存储进行更改的请求:Create/Update/Delete操作。我们可以这样定义一个:
public record CommandRequest<TRecord>
{
public required TRecord Item { get; init; }
public CancellationToken Cancellation { get; set; } = new ();
}
命令仅返回状态信息:不返回数据。我们可以定义这样的结果:
public record CommandResult
{
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
private CommandResult() { }
public static CommandResult Success(string? message = null)
=> new CommandResult { Successful = true, Message= message ?? string.Empty };
public static CommandResult Failure(string message)
=> new CommandResult { Message = message};
}
在这一点上,值得注意的是返回规则的一个小例外:Id对于插入的记录。如果不使用Guid为记录提供唯一标识符,则数据库生成的Id是状态信息。
项请求
查询是从数据存储中获取数据的请求:无突变。我们可以定义一个项查询,如下所示:
public sealed record ItemQueryRequest
{
public required Guid Uid { get; init; }
public CancellationToken Cancellation { get; set; } = new();
}
并返回结果:请求的数据和状态。
public sealed record ItemQueryResult<TRecord>
{
public TRecord? Item { get; init;}
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
private ItemQueryResult() { }
public static ItemQueryResult<TRecord>
Success(TRecord Item, string? message = null)
=> new ItemQueryResult<TRecord>
{ Successful=true, Item= Item, Message= message ?? string.Empty };
public static ItemQueryResult<TRecord> Failure(string message)
=> new ItemQueryResult<TRecord> { Message = message};
}
列出查询
列表查询带来了一些额外的挑战:
- 他们永远不应该要求一切。在边缘条件下,表中可能有1,000,000+行。每个请求都应该受到限制。该请求定义StartIndex和PageSize约束数据并提供分页。如果将页面大小设置为1,000,000,数据管道和前端是否会正常处理它?
- 他们需要处理排序和过滤。请求将这些表达式定义为Linq表达式。
public sealed record ListQueryRequest<TRecord>
{
public int StartIndex { get; init; } = 0;
public int PageSize { get; init; } = 1000;
public CancellationToken Cancellation { get; set; } = new ();
public bool SortDescending { get; } = false;
public Expression<Func<TRecord, bool>>? FilterExpression { get; init; }
public Expression<Func<TRecord, object>>? SortExpression { get; init; }
}
结果返回项、项总数(用于分页)和状态信息。Items始终返回为IEnumerable。
public sealed record ListQueryResult<TRecord>
{
public IEnumerable<TRecord> Items { get; init;} = Enumerable.Empty<TRecord>();
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public long TotalCount { get; init; }
private ListQueryResult() { }
public static ListQueryResult<TRecord> Success
(IEnumerable<TRecord> Items, long totalCount, string? message = null)
=> new ListQueryResult<TRecord> {Successful=true, Items= Items,
TotalCount = totalCount, Message= message ?? string.Empty };
public static ListQueryResult<TRecord> Failure(string message)
=> new ListQueryResult<TRecord> { Message = message};
}
处理程序
处理程序是处理请求和返回结果的小型单一用途类。他们从更高级别的数据代理中抽象出细节执行。
命令处理程序
接口提供抽象。
public interface ICreateRequestHandler
{
public ValueTask<CommandResult> ExecuteAsync<TRecord>
(CommandRequest<TRecord> request)
where TRecord : class, new();
}
并且实现完成实际工作。
- 注入DBContext工厂。
- 通过DbContext工厂实现工作单元Db上下文。
- 使用上下文上的Add方法将记录添加到EF。
- 调用SaveChangesAsync,传入取消令牌,并期望报告单个更改。
- 在出现问题时提供状态信息。
public sealed class CreateRequestHandler<TDbContext>
: ICreateRequestHandler
where TDbContext : DbContext
{
private readonly IDbContextFactory<TDbContext> _factory;
public CreateRequestHandler(IDbContextFactory<TDbContext> factory)
=> _factory = factory;
public async ValueTask<CommandResult> ExecuteAsync<TRecord>
(CommandRequest<TRecord> request)
where TRecord : class, new()
{
if (request == null)
throw new DataPipelineException
($"No CommandRequest defined in {this.GetType().FullName}");
using var dbContext = _factory.CreateDbContext();
dbContext.Add<TRecord>(request.Item);
return await dbContext.SaveChangesAsync(request.Cancellation) == 1
? CommandResult.Success("Record Updated")
: CommandResult.Failure("Error updating Record");
}
}
Update和Delete处理程序是相同的,但使用不同的dbContext方法:Update和Remove。
项请求处理程序
接口。
public interface IItemRequestHandler
{
public ValueTask<ItemQueryResult<TRecord>> ExecuteAsync<TRecord>
(ItemQueryRequest request)
where TRecord : class, new();
}
和服务器实现。注意:
- 注入DBContext工厂。
- 通过DbContext工厂实现工作单元Db上下文。
- 关闭跟踪。此事务不涉及任何突变。
- 检查它是否可以使用Id来获取项——记录实现IGuidIdentity。
- 如果没有,请尝试FindAsync,它使用内置的Key方法来获取记录。
- 在出现问题时提供状态信息。
public sealed class ItemRequestHandler<TDbContext>
: IItemRequestHandler
where TDbContext : DbContext
{
private readonly IDbContextFactory<TDbContext> _factory;
public ItemRequestHandler(IDbContextFactory<TDbContext> factory)
=> _factory = factory;
public async ValueTask<ItemQueryResult<TRecord>>
ExecuteAsync<TRecord>(ItemQueryRequest request)
where TRecord : class, new()
{
if (request == null)
throw new DataPipelineException
($"No ListQueryRequest defined in {this.GetType().FullName}");
using var dbContext = _factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior =
QueryTrackingBehavior.NoTracking;
TRecord? record = null;
// first check if the record implements IGuidIdentity.
// If so, we can do a cast and then do the query via the Uid property directly.
if ((new TRecord()) is IGuidIdentity)
record = await dbContext.Set<TRecord>().SingleOrDefaultAsync
(item => ((IGuidIdentity)item).Uid == request.Uid, request.Cancellation);
// Try and use the EF FindAsync implementation
if (record is null)
record = await dbContext.FindAsync<TRecord>(request.Uid);
if (record is null)
return ItemQueryResult<TRecord>.Failure
("No record retrieved");
return ItemQueryResult<TRecord>.Success(record);
}
}
列表请求处理程序
接口。
public interface IListRequestHandler
{
public ValueTask<ListQueryResult<TRecord>> ExecuteAsync<TRecord>
(ListQueryRequest<TRecord> request)
where TRecord : class, new();
}
和实现。
请注意,有两种内部方法:
- _getItemsAsync获取项。这将构建一个IQueryable对象并返回一个具体化的IEnumerable。必须先执行查询,然后工厂才会释放DbContext。
- _getCountAsync获取基于筛选器的所有记录的计数。
private async ValueTask<IEnumerable<TRecord>> _getItemsAsync<TRecord>
(ListQueryRequest<TRecord> request)
where TRecord : class, new()
{
if (request == null)
throw new DataPipelineException
($"No ListQueryRequest defined in {this.GetType().FullName}");
using var dbContext = _factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
IQueryable<TRecord> query = dbContext.Set<TRecord>();
if (request.FilterExpression is not null)
query = query
.Where(request.FilterExpression)
.AsQueryable();
if (request.SortExpression is not null)
query = request.SortDescending
? query.OrderByDescending(request.SortExpression)
: query.OrderBy(request.SortExpression);
if (request.PageSize > 0)
query = query
.Skip(request.StartIndex)
.Take(request.PageSize);
return query is IAsyncEnumerable<TRecord>
? await query.ToListAsync()
: query.ToList();
}
存储库类替换
首先是接口。
非常重要的一点是每个方法上的泛型TRecord定义,而不是接口上的通用定义。这消除了对实体特定实现的需求。
public interface IDataBroker
{
public ValueTask<ListQueryResult<TRecord>> GetItemsAsync<TRecord>
(ListQueryRequest<TRecord> request) where TRecord : class, new();
public ValueTask<ItemQueryResult<TRecord>> GetItemAsync<TRecord>
(ItemQueryRequest request) where TRecord : class, new();
public ValueTask<CommandResult> UpdateItemAsync<TRecord>
(CommandRequest<TRecord> request) where TRecord : class, new();
public ValueTask<CommandResult> CreateItemAsync<TRecord>
(CommandRequest<TRecord> request) where TRecord : class, new();
public ValueTask<CommandResult> DeleteItemAsync<TRecord>
(CommandRequest<TRecord> request) where TRecord : class, new();
}
和实现。每个处理程序都在DI中注册并注入到代理中。
public sealed class RepositoryDataBroker : IDataBroker
{
private readonly IListRequestHandler _listRequestHandler;
private readonly IItemRequestHandler _itemRequestHandler;
private readonly IUpdateRequestHandler _updateRequestHandler;
private readonly ICreateRequestHandler _createRequestHandler;
private readonly IDeleteRequestHandler _deleteRequestHandler;
public RepositoryDataBroker(
IListRequestHandler listRequestHandler,
IItemRequestHandler itemRequestHandler,
ICreateRequestHandler createRequestHandler,
IUpdateRequestHandler updateRequestHandler,
IDeleteRequestHandler deleteRequestHandler)
{
_listRequestHandler = listRequestHandler;
_itemRequestHandler = itemRequestHandler;
_createRequestHandler = createRequestHandler;
_updateRequestHandler = updateRequestHandler;
_deleteRequestHandler = deleteRequestHandler;
}
public ValueTask<ItemQueryResult<TRecord>> GetItemAsync<TRecord>
(ItemQueryRequest request) where TRecord : class, new()
=> _itemRequestHandler.ExecuteAsync<TRecord>(request);
public ValueTask<ListQueryResult<TRecord>> GetItemsAsync<TRecord>
(ListQueryRequest<TRecord> request) where TRecord : class, new()
=> _listRequestHandler.ExecuteAsync<TRecord>(request);
public ValueTask<CommandResult> CreateItemAsync<TRecord>
(CommandRequest<TRecord> request) where TRecord : class, new()
=> _createRequestHandler.ExecuteAsync<TRecord>(request);
public ValueTask<CommandResult> UpdateItemAsync<TRecord>
(CommandRequest<TRecord> request) where TRecord : class, new()
=> _updateRequestHandler.ExecuteAsync<TRecord>(request);
public ValueTask<CommandResult> DeleteItemAsync<TRecord>
(CommandRequest<TRecord> request) where TRecord : class, new()
=> _deleteRequestHandler.ExecuteAsync<TRecord>(request);
}
测试数据代理
现在,我们可以为数据代理定义一组测试。我在这里包括了两个。其余的都在存储库中。
创建根DI容器并填充数据库的前两种方法。
private ServiceProvider BuildRootContainer()
{
var services = new ServiceCollection();
// Define the DbSet and Server Type for the DbContext Factory
services.AddDbContextFactory<InMemoryWeatherDbContext>(options
=> options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));
// Define the Broker and Handlers
services.AddScoped<IDataBroker, RepositoryDataBroker>();
services.AddScoped<IListRequestHandler,
ListRequestHandler<InMemoryWeatherDbContext>>();
services.AddScoped<IItemRequestHandler,
ItemRequestHandler<InMemoryWeatherDbContext>>();
services.AddScoped<IUpdateRequestHandler,
UpdateRequestHandler<InMemoryWeatherDbContext>>();
services.AddScoped<ICreateRequestHandler,
CreateRequestHandler<InMemoryWeatherDbContext>>();
services.AddScoped<IDeleteRequestHandler,
DeleteRequestHandler<InMemoryWeatherDbContext>>();
// Create the container
return services.BuildServiceProvider();
}
private IDbContextFactory<InMemoryWeatherDbContext>
GetPopulatedFactory(IServiceProvider provider)
{
// get the DbContext factory and add the test data
var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
if (factory is not null)
WeatherTestDataProvider.Instance().LoadDbContext
<InMemoryWeatherDbContext>(factory);
return factory!;
}
GetItems测试:
[Fact]
public async Task GetItemsTest()
{
// Get our test provider to use as our control
var testProvider = WeatherTestDataProvider.Instance();
// Build the root DI Container
var rootProvider = this.BuildRootContainer();
//define a scoped container
var providerScope = rootProvider.CreateScope();
var provider = providerScope.ServiceProvider;
// get the DbContext factory and add the test data
var factory = this.GetPopulatedFactory(provider);
// Check we can retrieve the first 1000 records
var dbContext = factory!.CreateDbContext();
Assert.NotNull(dbContext);
var databroker = provider.GetRequiredService<IDataBroker>();
var request = new ListQueryRequest<WeatherForecast>();
var result = await databroker.GetItemsAsync<WeatherForecast>(request);
Assert.NotNull(result);
Assert.Equal(testProvider.WeatherForecasts.Count(), result.TotalCount);
providerScope.Dispose();
rootProvider.Dispose();
}
AddItem测试:
[Fact]
public async Task AddItemTest()
{
// Get our test provider to use as our control
var testProvider = WeatherTestDataProvider.Instance();
// Build the root DI Container
var rootProvider = this.BuildRootContainer();
//define a scoped container
var providerScope = rootProvider.CreateScope();
var provider = providerScope.ServiceProvider;
// get the DbContext factory and add the test data
var factory = this.GetPopulatedFactory(provider);
// Check we can retrieve the first 1000 records
var dbContext = factory!.CreateDbContext();
Assert.NotNull(dbContext);
var databroker = provider.GetRequiredService<IDataBroker>();
// Create a Test record
var newRecord = new WeatherForecast { Uid = Guid.NewGuid(),
Date = DateOnly.FromDateTime(DateTime.Now),
TemperatureC = 50, Summary = "Add Testing" };
// Add the Record
{
var request = new CommandRequest<WeatherForecast>() { Item = newRecord };
var result = await databroker.CreateItemAsync<WeatherForecast>(request);
Assert.NotNull(result);
Assert.True(result.Successful);
}
// Get the new record
{
var request = new ItemQueryRequest() { Uid = newRecord.Uid };
var result = await databroker.GetItemAsync<WeatherForecast>(request);
Assert.Equal(newRecord, result.Item);
}
// Check the record count has incremented
{
var request = new ListQueryRequest<WeatherForecast>();
var result = await databroker.GetItemsAsync<WeatherForecast>(request);
Assert.NotNull(result);
Assert.Equal(testProvider.WeatherForecasts.Count() + 1, result.TotalCount);
}
providerScope.Dispose();
rootProvider.Dispose();
}
总结
我在这里介绍的是一个混合存储库模式。它保持了存储库模式的简单性,并添加了一些最佳的CQS模式功能。
将细节EF和Linq代码抽象到各个处理程序可以使类保持小、简洁和单一用途。
单个数据代理简化了核心域和表示域的数据管道配置。
对于那些认为通过EF实现任何数据库管道都是一种反模式的人,我的答案是:我将EF用作另一个对象请求代理[ORB]。您可以将此管道插入Dapper、LinqToDb、... 。我从不在我的数据/基础架构域中构建核心业务逻辑代码(数据关系):[个人观点]疯狂的想法。
附录
数据存储
测试系统实现实体框架内存中数据库。
我是Blazor开发人员,所以我的演示数据类自然是WeatherForecast。这是我的数据类。请注意,这是不可变性的记录,我设置了一些任意默认值以进行测试。
public sealed record WeatherForecast : IGuidIdentity
{
[Key] public Guid Uid { get; init; } = Guid.Empty;
public DateOnly Date { get; init; } = DateOnly.FromDateTime(DateTime.Now);
public int TemperatureC { get; init; } = 60;
public string? Summary { get; init; } = <span class="pl-pds">"Testing";
}
首先是一个生成数据集的类。这是一个单一实例模式类(不是DI单一实例)。诸如测试之类的GetRandomRecord方法。
public sealed class WeatherTestDataProvider
{
private int RecordsToGenerate;
public IEnumerable<WeatherForecast> WeatherForecasts { get; private set; } =
Enumerable.Empty<WeatherForecast>();
private WeatherTestDataProvider()
=> this.Load();
public void LoadDbContext<TDbContext>(IDbContextFactory<TDbContext> factory)
where TDbContext : DbContext
{
using var dbContext = factory.CreateDbContext();
var weatherForcasts = dbContext.Set<WeatherForecast>();
// Check if we already have a full data set
// If not clear down any existing data and start again
if (weatherForcasts.Count() == 0)
{
dbContext.AddRange(this.WeatherForecasts);
dbContext.SaveChanges();
}
}
public void Load(int records = 100)
{
RecordsToGenerate = records;
if (WeatherForecasts.Count() == 0)
this.LoadForecasts();
}
private void LoadForecasts()
{
var forecasts = new List<WeatherForecast>();
for (var index = 0; index < RecordsToGenerate; index++)
{
var rec = new WeatherForecast
{
Uid = Guid.NewGuid(),
Summary = Summaries[Random.Shared.Next(Summaries.Length)],
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
};
forecasts.Add(rec);
}
this.WeatherForecasts = forecasts;
}
public WeatherForecast GetForecast()
{
return new WeatherForecast
{
Uid = Guid.NewGuid(),
Summary = Summaries[Random.Shared.Next(Summaries.Length)],
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(-1)),
TemperatureC = Random.Shared.Next(-20, 55),
};
}
public WeatherForecast? GetRandomRecord()
{
var record = new WeatherForecast();
if (this.WeatherForecasts.Count() > 0)
{
var ran = new Random().Next(0, WeatherForecasts.Count());
return this.WeatherForecasts.Skip(ran).FirstOrDefault();
}
return null;
}
private static WeatherTestDataProvider? _weatherTestData;
public static WeatherTestDataProvider Instance()
{
if (_weatherTestData is null)
_weatherTestData = new WeatherTestDataProvider();
return _weatherTestData;
}
public static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy",
"Hot", "Sweltering", "Scorching"
};
}
DbContext:
public sealed class InMemoryWeatherDbContext
: DbContext
{
public DbSet<WeatherForecast> WeatherForecast { get; set; } = default!;
public InMemoryWeatherDbContext
(DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<WeatherForecast>().ToTable("WeatherForecast");
}
https://www.codeproject.com/Articles/5350000/A-Different-Repository-Pattern-Implementation