不同的存储库(Repository)模式实现
目录
介绍
命名、术语和实践
Repo
数据存储
测试工厂和上下文
经典存储库模式实现
实现
请求和结果
命令
项请求
列出查询
处理程序
命令处理程序
项请求处理程序
列表请求处理程序
存储库类替换
测试数据代理
总结
附录
数据存储
介绍
经典存储库模式是在任何应用程序中实现数据库访问的简单方法。它满足小型应用的许多正常设计目标。另一方面,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 datavar testProvider = WeatherTestDataProvider.Instance();// Build our services containervar services = new ServiceCollection();// Define the DbSet and Server Type for the DbContext Factoryservices.AddDbContextFactory<InMemoryWeatherDbContext>(options=> options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));var rootProvider = services.BuildServiceProvider();//define a scoped containervar providerScope = rootProvider.CreateScope();var provider = providerScope.ServiceProvider;// get the DbContext factory and add the test datavar factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();if (factory is not null)WeatherTestDataProvider.Instance().LoadDbContext<InMemoryWeatherDbContext>(factory);// Check the data has been loadedvar dbContext = factory!.CreateDbContext();Assert.NotNull(dbContext);var count = dbContext.Set<WeatherForecast>().Count();Assert.Equal(testProvider.WeatherForecasts.Count(), count);// Test an arbitrary recordvar testRecord = testProvider.GetRandomRecord()!;var record = await dbContext.Set<WeatherForecast>().SingleOrDefaultAsync(item => item.Uid.Equals(testRecord.Uid));Assert.Equal(testRecord, record);// Dispose of the resources correctlyproviderScope.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>: ICreateRequestHandlerwhere 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>: IItemRequestHandlerwhere 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 implementationif (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 Factoryservices.AddDbContextFactory<InMemoryWeatherDbContext>(options=> options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));// Define the Broker and Handlersservices.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 containerreturn services.BuildServiceProvider();
}private IDbContextFactory<InMemoryWeatherDbContext> GetPopulatedFactory(IServiceProvider provider)
{// get the DbContext factory and add the test datavar 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 controlvar testProvider = WeatherTestDataProvider.Instance();// Build the root DI Containervar rootProvider = this.BuildRootContainer();//define a scoped containervar providerScope = rootProvider.CreateScope();var provider = providerScope.ServiceProvider;// get the DbContext factory and add the test datavar factory = this.GetPopulatedFactory(provider);// Check we can retrieve the first 1000 recordsvar 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 controlvar testProvider = WeatherTestDataProvider.Instance();// Build the root DI Containervar rootProvider = this.BuildRootContainer();//define a scoped containervar providerScope = rootProvider.CreateScope();var provider = providerScope.ServiceProvider;// get the DbContext factory and add the test datavar factory = this.GetPopulatedFactory(provider);// Check we can retrieve the first 1000 recordsvar dbContext = factory!.CreateDbContext();Assert.NotNull(dbContext);var databroker = provider.GetRequiredService<IDataBroker>();// Create a Test recordvar 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 againif (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