目录
测试数据
附录提供了数据类和测试数据提供程序的摘要。存储库的文档部分有完整的描述。
存储库
与本文相关的数据存储库位于此处:Blazr.Demo.DataPipeline。存储库中有一些关于数据类、测试数据提供程序和数据库上下文的设计和详细信息的额外文档。
介绍
CQS——不要与CQRS混淆——基本上是一种编程风格。每个操作都是:
- Command——请求进行数据更改
- Query——获取一些数据的请求
Command返回状态信息或不返回任何内容。命令从不返回数据。
Query返回一个数据集。查询从不更改数据的状态。进行查询没有副作用。
这是一个很好的模式,可以普遍应用于你的代码。
每个操作都有一个定义操作的Command/Query类和一个用于执行已定义操作的Handler类。通常,一对一关系:每个请求的唯一处理程序。
基本上:
- Request对象定义执行请求所需的信息Handler以及它期望返回的内容——Result。
- Handler对象执行必要的代码,并使用Request提供的数据返回已定义的Result。
从概念上讲,它非常简单,并且相对容易实现。问题是每个数据库操作都需要一个请求和一个处理程序对象。许多类定义和重复相同的旧代码。
解决方案布局和设计
该解决方案由一组根据清洁设计原则组织的库组成。它旨在在任何DotNetCore环境中工作。Blazr.Core和Blazr.Data是可用于任何实现的两个基本库。 Blazr.Demo.Core和Blazr.Demo.Data是两个特定于应用程序的库。
前端应用程序是一个XUnit测试项目,用于演示和测试代码。
我在Blazor项目中使用它。
接口和基类
基本方法可以通过两个泛型接口定义。
ICQSRequest定义任何请求:
- 它表示请求生成定义为TResult。
- 它具有唯一的TransactionId以跟踪事务(如果需要并实现)。
public interface ICQSRequest<out TResult>
{
public Guid TransactionId { get;}
}
ICQSHandler定义执行ICQSRequest实例的任何处理程序:
- 处理程序获取实现ICQSRequest接口的TRequest。
- 处理程序输出ICQSRequest接口中定义的TResult。
- 它有一个ExecuteAsync方法,它接受TRequest并返回TResult。
public interface ICQSHandler<in TRequest, out TResult>
where TRequest : ICQSRequest<TResult>
{
TResult ExecuteAsync(TRequest request);
}
要构建更简洁的实现,请执行以下操作:
- 我们必须接受80/20规则。并非每个请求都可以通过我们的标准实现来满足,但80%需要节省大量的精力和类。
- 我们需要一个针对20%的方法论。
- 我们需要一个基于“兼容”泛型的ORM来与我们的数据存储接口。此实现使用提供此功能的实体框架。
- 在基类中编写一些相当复杂的泛型,以将功能抽象为样板代码。
结果
该解决方案定义了一组要返回的标准结果:TResult请求。它们被定义为带有静态构造函数的record,并包含状态信息,如果是查询,则包含数据。它们必须是可序列化的,才能在API中使用。每个如下所示:
public record ListProviderResult<TRecord>
{
public IEnumerable<TRecord> Items { get; init; }
public int TotalItemCount { get; init; }
public bool Success { get; init; }
public string? Message { get; init; }
//....Constructors
}
public record RecordProviderResult<TRecord>
{
public TRecord? Record { get; init; }
public bool Success { get; init; }
public string? Message { get; init; }
//....Constructors
}
public record CommandResult
{
public Guid NewId { get; init; }
public bool Success { get; init; }
public string Message { get; init; }
//....Constructors
}
public record FKListProviderResult
{
public IEnumerable<IFkListItem> Items { get; init; }
public bool Success { get; init; }
public string? Message { get; init; }
//....Constructors
}
所有实现static构造函数来严格控制内容。
基类
TRecord表示使用ORM从数据存储中检索的数据类。它被限定为实现空构造函数new()的class。
Request接口/类结构如下所示:

Handler接口/类结构如下所示:

命令
所有命令:
- 取一个我们定义为TRecord的记录。
- 修复TResult为异步ValueTask<CommandResult>。
实现ICQSRequest和此功能的接口。
public interface IRecordCommand<TRecord>
: ICQSRequest<ValueTask<CommandResult>>
{
public TRecord Record { get;}
}
和一个抽象的实现。
public abstract class RecordCommandBase<TRecord>
: IRecordCommand<TRecord>
{
public Guid TransactionId { get; } = Guid.NewGuid();
public TRecord Record { get; protected set; } = default!;
protected RecordCommandBase() { }
}
我们现在可以定义Add/Delete/Update特定命令。所有这些都使用静态构造函数来控制和验证内容。需要有一对一的关系(请求->处理程序),因此我们为每种类型的命令定义一个处理程序。
public class AddRecordCommand<TRecord>
: RecordCommandBase<TRecord>
{
private AddRecordCommand() { }
public static AddRecordCommand<TRecord> GetCommand(TRecord record)
=> new AddRecordCommand<TRecord> { Record = record };
}
public class DeleteRecordCommand<TRecord>
: RecordCommandBase<TRecord>
{
private DeleteRecordCommand() { }
public static DeleteRecordCommand<TRecord> GetCommand(TRecord record)
=> new DeleteRecordCommand<TRecord> { Record = record };
}
public class UpdateRecordCommand<TRecord>
: RecordCommandBase<TRecord>
{
private UpdateRecordCommand() { }
public static UpdateRecordCommand<TRecord> GetCommand(TRecord record)
=> new UpdateRecordCommand<TRecord> { Record = record };
}
命令处理程序
为处理程序创建接口或基类没有任何好处,因此我们将Create/Update/Delete命令实现为三个单独的类。TRecord定义记录类,TDbContext定义了DI DbContextFactory中使用的DbContext。
我们在DbContext中使用内置的泛型方法,所以不需要特定的DbContext。Set<TRecord>方法通过SaveChangesAsync查找TRecord和Update<TRecord>,Add<TRecord>以及Delete<TRecord>方法的DbSet实例来实现命令。
所有处理程序都遵循相同的模式。
- 构造函数传入DbContext工厂并执行命令请求。
- Execute:
- 获取DbContext。
- 在记录中传递的上下文上调用泛型Add/Update/Delete。在内部,EF查找记录集和特定记录,并将其替换为提供的记录。
- 调用将更改提交到数据存储的DbContext上的SaveChanges。
- 检查我们有一个更改并返回一个CommandResult。
这是添加处理程序:
public class AddRecordCommandHandler<TRecord, TDbContext>
: ICQSHandler<AddRecordCommand<TRecord>, ValueTask<CommandResult>>
where TDbContext : DbContext
where TRecord : class, new()
{
protected IDbContextFactory<TDbContext> factory;
public AddRecordCommandHandler(IDbContextFactory<TDbContext> factory)
=> this.factory = factory;
public async ValueTask<CommandResult> ExecuteAsync(AddRecordCommand<TRecord> command)
{
using var dbContext = factory.CreateDbContext();
dbContext.Add<TRecord>(command.Record);
return await dbContext.SaveChangesAsync() == 1
? CommandResult.Successful("Record Saved")
: CommandResult.Failure("Error saving Record");
}
}
查询
查询不是那么统一。
- 有各种类型的TResult。
- 它们具有特定的操作,例如Where和OrderBy。
为了处理这些要求,我们定义了三个查询请求:
RecordQuery
这将返回包含基于提供Uid的单个记录的RecordProviderResult。
public record RecordQuery<TRecord>
: ICQSRequest<ValueTask<RecordProviderResult<TRecord>>>
{
public Guid TransactionId { get; } = Guid.NewGuid();
public Guid GuidId { get; init; }
protected RecordQuery() { }
public static RecordQuery<TRecord> GetQuery(Guid recordId)
=> new RecordQuery<TRecord> { GuidId = recordId };
}
ListQuery
这将返回一个ListProviderResult,其中包含TRecord的分页IEnumerable。
我们定义一个接口:
public interface IListQuery<TRecord>
: ICQSRequest<ValueTask<ListProviderResult<TRecord>>>
where TRecord : class, new()
{
public int StartIndex { get; }
public int PageSize { get; }
public string? SortExpressionString { get; }
public string? FilterExpressionString { get; }
}
abstract基类实现:
public abstract record ListQueryBase<TRecord>
:IListQuery<TRecord>
where TRecord : class, new()
{
public int StartIndex { get; init; }
public int PageSize { get; init; }
public string? SortExpressionString { get; init; }
public string? FilterExpressionString { get; init; }
public Guid TransactionId { get; init; } = Guid.NewGuid();
protected ListQueryBase() { }
}
最后是一个泛型查询:
public record ListQuery<TRecord>
:ListQueryBase<TRecord>
where TRecord : class, new()
{
public static ListQuery<TRecord> GetQuery(ListProviderRequest<TRecord> request)
=> new ListQuery<TRecord>
{
StartIndex = request.StartIndex,
PageSize = request.PageSize,
SortExpressionString = request.SortExpressionString,
FilterExpressionString = request.FilterExpressionString
};
}
我们将代码分离到接口/abstract基类模式中,以便可以实现自定义列表查询。如果这些继承自ListQuery,我们会遇到工厂和模式匹配方法的问题。使用基类实现样板代码可以解决此问题。
FKListQuery
这将返回一个FkListProviderResult,其包含IFkListItem的IEnumerable。FkListItem是一个包含Guid/Name对的简单对象。它的主要用途是UI中的外键Select控件中。
public record FKListQuery<TRecord>
: ICQSRequest<ValueTask<FKListProviderResult>>
{
public Guid TransactionId { get; } = Guid.NewGuid();
}
查询处理程序
相应的查询处理程序是:
RecordQueryHandler
创建“泛型”版本可能具有挑战性,具体取决于ORM。
需要注意的关键概念是:
- 从IDbContextFactory中使用DbContexts工作单元。
- _dbContext.Set<TRecord>()为TRecord获取DbSet。
- 使用两种方法来应用查询。
public class RecordQueryHandler<TRecord, TDbContext>
: ICQSHandler<RecordQuery<TRecord>,
ValueTask<RecordProviderResult<TRecord>>>
where TRecord : class, new()
where TDbContext : DbContext
{
private IDbContextFactory<TDbContext> _factory;
public RecordQueryHandler(IDbContextFactory<TDbContext> factory)
=> _factory = factory;
public async ValueTask<RecordProviderResult<TRecord>>
ExecuteAsync(RecordQuery<TRecord> query)
{
var dbContext = _factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
TRecord? record = null;
// first check if the record implements IRecord.
// If so we can do a cast and then do the query via the Uid property directly
if ((new TRecord()) is IRecord)
record = await dbContext.Set<TRecord>().SingleOrDefaultAsync
(item => ((IRecord)item).Uid == query.GuidId);
// Try and use the EF FindAsync implementation
if (record is null)
record = await dbContext.FindAsync<TRecord>(query.GuidId);
if (record is null)
return RecordProviderResult<TRecord>.Failure
(No record retrieved");
return RecordProviderResult<TRecord>.Successful(record);
}
}
ListQueryHandler
这里要注意的关键概念是:
- 从IDbContextFactory中使用DbContexts工作单元。
- _dbContext.Set<TRecord>()为TRecord获取DbSet。
- IQueryable用于生成查询。
- 需要两个查询。一个用于获取“分页”recordset,一个用于获取总记录计数。
- 使用System.Linq.Dynamic来做排序和过滤。这将在后面讨论。
public class ListQueryHandler<TRecord, TDbContext>
: IListQueryHandler<TRecord>
where TDbContext : DbContext
where TRecord : class, new()
{
protected IEnumerable<TRecord> items = Enumerable.Empty<TRecord>();
protected int count = 0;
protected IDbContextFactory<TDbContext> factory;
protected IListQuery<TRecord> listQuery = default!;
public ListQueryHandler(IDbContextFactory<TDbContext> factory)
=> this.factory = factory;
public async ValueTask<ListProviderResult<TRecord>>
ExecuteAsync(IListQuery<TRecord> query)
{
if (query is null)
return ListProviderResult<TRecord>.Failure
("No Query Defined");
listQuery = query;
if (await this.GetItemsAsync())
await this.GetCountAsync();
return ListProviderResult<TRecord>.Successful(this.items, this.count);
}
protected virtual async ValueTask<bool> GetItemsAsync()
{
var dbContext = this.factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
IQueryable<TRecord> query = dbContext.Set<TRecord>();
if (listQuery.FilterExpressionString is not null)
query = query
.Where(listQuery.FilterExpressionString)
.AsQueryable();
if (listQuery.SortExpressionString is not null)
query = query.OrderBy(listQuery.SortExpressionString);
if (listQuery.PageSize > 0)
query = query
.Skip(listQuery.StartIndex)
.Take(listQuery.PageSize);
if (query is IAsyncEnumerable<TRecord>)
this.items = await query.ToListAsync();
else
this.items = query.ToList();
return true;
}
protected virtual async ValueTask<bool> GetCountAsync()
{
var dbContext = this.factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior =
QueryTrackingBehavior.NoTracking;
IQueryable<TRecord> query = dbContext.Set<TRecord>();
if (listQuery.FilterExpressionString is not null)
query = query
.Where(listQuery.FilterExpressionString)
.AsQueryable();
if (query is IAsyncEnumerable<TRecord>)
count = await query.CountAsync();
else
count = query.Count();
return true;
}
}
FKListQueryHandler
public class FKListQueryHandler<TRecord, TDbContext>
: ICQSHandler<FKListQuery<TRecord>, ValueTask<FKListProviderResult>>
where TDbContext : DbContext
where TRecord : class, IFkListItem, new()
{
protected IEnumerable<TRecord> items = Enumerable.Empty<TRecord>();
protected IDbContextFactory<TDbContext> factory;
public FKListQueryHandler(IDbContextFactory<TDbContext> factory)
=> this.factory = factory;
public async ValueTask<FKListProviderResult>
ExecuteAsync(FKListQuery<TRecord> listQuery)
{
var dbContext = this.factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior =
QueryTrackingBehavior.NoTracking;
if (listQuery is null)
return FKListProviderResult.Failure
("No Query defined");
IEnumerable<TRecord> dbSet = await dbContext.Set<TRecord>().ToListAsync();
return FKListProviderResult.Successful(dbSet);
}
}
实现处理程序
处理程序设计为以两种方式使用:
- 单独作为依赖注入服务
- 依赖注入工厂
我们将看到两者都用于测试。
泛型工厂代理
代理使用单一方法ExecuteAsync(Request),每个请求的实现映射正确的处理程序,执行请求并提供预期的结果。
var TResult = await DataBrokerInstance.ExecuteAsync<TRecord>(TRequest);
用于在DI中定义服务的接口:
public interface ICQSDataBroker
{
public ValueTask<CommandResult> ExecuteAsync<TRecord>
(AddRecordCommand<TRecord> command) where TRecord : class, new();
public ValueTask<CommandResult> ExecuteAsync<TRecord>
(UpdateRecordCommand<TRecord> command) where TRecord : class, new();
public ValueTask<CommandResult> ExecuteAsync<TRecord>
(DeleteRecordCommand<TRecord> command) where TRecord : class, new();
public ValueTask<ListProviderResult<TRecord>> ExecuteAsync<TRecord>
(ListQuery<TRecord> query) where TRecord : class, new();
public ValueTask<RecordProviderResult<TRecord>> ExecuteAsync<TRecord>
(RecordQuery<TRecord> query) where TRecord : class, new();
public ValueTask<FKListProviderResult> ExecuteAsync<TRecord>
(FKListQuery<TRecord> query) where TRecord : class, IFkListItem, new();
}
和服务器代理实现:
public class CQSDataBroker<TDbContext>
:ICQSDataBroker
where TDbContext : DbContext
{
private readonly IDbContextFactory<TDbContext> _factory;
private readonly IServiceProvider _serviceProvider;
public CQSDataBroker(IDbContextFactory<TDbContext> factory,
IServiceProvider serviceProvider)
{
_factory = factory;
_serviceProvider = serviceProvider;
}
public async ValueTask<CommandResult> ExecuteAsync<TRecord>
(AddRecordCommand<TRecord> command) where TRecord : class, new()
{
var handler = new AddRecordCommandHandler<TRecord, TDbContext>
(_factory, command);
return await handler.ExecuteAsync();
}
//.... Update and Delete ExecuteAsyncs
public async ValueTask<ListProviderResult<TRecord>> ExecuteAsync<TRecord>
(ListQuery<TRecord> query) where TRecord : class, new()
{
var handler = new ListQueryHandler<TRecord, TDbContext>(_factory, query);
return await handler.ExecuteAsync();
}
public async ValueTask<RecordProviderResult<TRecord>>
ExecuteAsync<TRecord>(RecordQuery<TRecord> query) where TRecord : class, new()
{
var handler = new RecordQueryHandler<TRecord, TDbContext>(_factory, query);
return await handler.ExecuteAsync();
}
public async ValueTask<FKListProviderResult> ExecuteAsync<TRecord>
(FKListQuery<TRecord> query) where TRecord : class, IFkListItem, new()
{
var handler = new FKListQueryHandler<TRecord, TDbContext>(_factory, query);
return await handler.ExecuteAsync();
}
public ValueTask<object> ExecuteAsync<TRecord>(object query)
=> throw new NotImplementedException();
}
请注意引发异常的catch all方法。
测试代理
设置
以下是代理演示测试的设置。它设置DI服务容器并将实例传递给测试。
public CQSBrokerTests()
// Creates an instance of the Test Data provider
=> _weatherTestDataProvider = WeatherTestDataProvider.Instance();
private ServiceProvider GetServiceProvider()
{
// Creates a Service Collection
var services = new ServiceCollection();
// Adds the application services to the collection
Action<DbContextOptionsBuilder> dbOptions = options => options.UseInMemoryDatabase
($"WeatherDatabase-{Guid.NewGuid().ToString()}");
services.AddDbContextFactory<TDbContext>(options);
services.AddSingleton<ICQSDataBroker, CQSDataBroker<InMemoryWeatherDbContext>>();
// Creates a Service Provider from the Services collection
// This is our DI container
var provider = services.BuildServiceProvider();
// Adds the test data to the in memory database
var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
WeatherTestDataProvider.Instance().LoadDbContext<InMemoryWeatherDbContext>(factory);
return provider!;
}
测试
获取天气位置列表的典型测试:
[Fact]
public async void TestWeatherLocationListCQSDataBroker()
{
// Build our DI container
var provider = GetServiceProvider();
//Get the Data Broker
var broker = provider.GetService<ICQSDataBroker>()!;
// Get the control record count from the Test Data Provider
var testRecordCount = _weatherTestDataProvider.WeatherLocations.Count();
int pageSize = 10;
// Get the expected recordset count.
// It should be either the page size
//or the total record count if that's smaller
var testCount = testRecordCount > pageSize ? pageSize : testRecordCount ;
// Create a list request
var listRequest = new ListProviderRequest<DboWeatherLocation>(0, pageSize);
// Create a ListQuery and execute the query
//on the Data Broker against the DboWeatherLocation recordset
var query = new ListQuery<DboWeatherLocation>(listRequest);
var result = await broker.ExecuteAsync<DboWeatherLocation>(query);
// Check we have success
Assert.True(result.Success);
// Check the recordset count
Assert.Equal(testCount, result.Items.Count());
// Check the total count os correct against the test provider
Assert.True(result.TotalItemCount == testRecordCount);
}
和一个添加命令测试:
[Fact]
public async void TestAddCQSDataBroker()
{
var provider = GetServiceProvider();
var broker = provider.GetService<ICQSDataBroker>()!;
var newRecord = _weatherTestDataProvider.GetForecast();
var id = newRecord!.Uid;
var command = new AddRecordCommand<DboWeatherForecast>(newRecord);
var result = await broker.ExecuteAsync(command);
var query = new RecordQuery<DboWeatherForecast>(id);
var checkResult = await broker.ExecuteAsync(query);
Assert.True(result.Success);
Assert.Equal(newRecord, checkResult.Record);
}
自定义请求
筛选列表
这可能是最常见的自定义请求。该ListQuery标准使用动态Linq,因此可以将查询生成为字符串以传入查询。但是,Dynamic Linq效率不高,因此我更喜欢在经常使用自定义查询的地方定义自定义查询。
所有此类查询都可以使用自定义的BaseListQuery。
我们的示例自定义查询基于Location过滤WeatherForecast。
查询
- 继承ListQueryBase将TRecord固定为DvoWeatherForecast
- 定义WeatherLocationId属性
- 定义一个static creator方法:
public record WeatherForecastListQuery
: ListQueryBase<DvoWeatherForecast>
{
public Guid WeatherLocationId { get; init; }
private WeatherForecastListQuery() { }
public static WeatherForecastListQuery GetQuery
(Guid weatherLocationId, ListProviderRequest<DvoWeatherForecast> request)
=> new WeatherForecastListQuery
{
StartIndex = request.StartIndex,
PageSize = request.PageSize,
SortExpressionString = request.SortExpressionString,
FilterExpressionString = request.FilterExpressionString,
WeatherLocationId = weatherLocationId,
};
}
处理程序
这是基于与泛型处理程序相同的模式构建的。
public class WeatherForecastListQueryHandler<TDbContext>
: IListQueryHandler<DvoWeatherForecast>
where TDbContext : DbContext
{
protected IEnumerable<DvoWeatherForecast> items =
Enumerable.Empty<DvoWeatherForecast>();
protected int count = 0;
protected IDbContextFactory<TDbContext> factory;
protected WeatherForecastListQuery listQuery = default!;
public WeatherForecastListQueryHandler(IDbContextFactory<TDbContext> factory)
{
this.factory = factory;
}
public async ValueTask<ListProviderResult<DvoWeatherForecast>>
ExecuteAsync(IListQuery<DvoWeatherForecast> query)
{
if (query is null || query is not WeatherForecastListQuery)
return new ListProviderResult<DvoWeatherForecast>
(new List<DvoWeatherForecast>(), 0, false, "No Query Defined");
listQuery = (WeatherForecastListQuery)query;
if (await this.GetItemsAsync())
await this.GetCountAsync();
return new ListProviderResult<DvoWeatherForecast>(this.items, this.count);
}
protected virtual async ValueTask<bool> GetItemsAsync()
{
var dbContext = this.factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
IQueryable<DvoWeatherForecast> query = dbContext.Set<DvoWeatherForecast>();
if (listQuery.WeatherLocationId != Guid.Empty)
query = query
.Where(item => item.WeatherLocationId == listQuery.WeatherLocationId)
.AsQueryable();
if (listQuery.SortExpressionString is not null)
query = query.OrderBy(listQuery.SortExpressionString);
if (listQuery.PageSize > 0)
query = query
.Skip(listQuery.StartIndex)
.Take(listQuery.PageSize);
if (query is IAsyncEnumerable<DvoWeatherForecast>)
this.items = await query.ToListAsync();
else
this.items = query.ToList();
return true;
}
protected virtual async ValueTask<bool> GetCountAsync()
{
var dbContext = this.factory.CreateDbContext();
dbContext.ChangeTracker.QueryTrackingBehavior =
QueryTrackingBehavior.NoTracking;
IQueryable<DvoWeatherForecast> query = dbContext.Set<DvoWeatherForecast>();
if (listQuery.WeatherLocationId != Guid.Empty)
query = query
.Where(item => item.WeatherLocationId == listQuery.WeatherLocationId)
.AsQueryable();
if (query is IAsyncEnumerable<DvoWeatherForecast>)
count = await query.CountAsync();
else
count = query.Count();
return true;
}
}
处理程序可以在DI中定义:
services.AddScoped<IListQueryHandler<DvoWeatherForecast>,
WeatherForecastListQueryHandler<InMemoryWeatherDbContext>>();
代理
我们可以在标准代理中添加一种方法来处理IListQueryHandler<TRecord>。请注意,使用此方法,我们只能为每个数据类定义一个IListQueryHandler。
ICQSDataBroker定义:
public interface ICQSDataBroker
{
public ValueTask<ListProviderResult<TRecord>>
ExecuteAsync<TRecord>(IListQuery<TRecord> query) where TRecord : class, new();
}
和在CQSDataBroker中实现:
public async ValueTask<ListProviderResult<TRecord>>
ExecuteAsync<TRecord>(IListQuery<TRecord> query) where TRecord : class, new()
{
var queryType = query.GetType();
var handler = _serviceProvider.GetService<IListQueryHandler<TRecord>>();
if (handler == null)
throw new NullReferenceException
("No Handler service registered for the List Query");
return await handler.ExecuteAsync(query);
}
测试
更新CQSBrokerTests添加自定义处理程序:
private ServiceProvider GetServiceProvider()
{
// Creates a Service Collection
var services = new ServiceCollection();
// Adds the application services to the collection
Action<DbContextOptionsBuilder> dbOptions = options => options.UseInMemoryDatabase
($"WeatherDatabase-{Guid.NewGuid().ToString()}");
services.AddWeatherAppServerDataServices<InMemoryWeatherDbContext>(dbOptions);
services.AddSingleton<ICQSDataBroker, CQSDataBroker<InMemoryWeatherDbContext>>();
services.AddScoped<IListQueryHandler<DvoWeatherForecast>,
WeatherForecastListQueryHandler<InMemoryWeatherDbContext>>();
// Creates a Service Provider from the Services collection
// This is our DI container
var provider = services.BuildServiceProvider();
// Adds the test data to the in memory database
var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
WeatherTestDataProvider.Instance().LoadDbContext<InMemoryWeatherDbContext>(factory);
return provider!;
}
并添加一个测试:
[Fact]
public async void TestCustomDvoWeatherForecastListCQSDataBroker()
{
var provider = GetServiceProvider();
var broker = provider.GetService<ICQSDataBroker>()!;
var locationId = _weatherTestDataProvider.WeatherLocations.First().Uid;
var testRecordCount = _weatherTestDataProvider.WeatherForecasts.Where
(item => item.WeatherLocationId == locationId).Count();
int pageSize = 10;
var testCount = testRecordCount > pageSize ? pageSize : testRecordCount;
var listRequest = new ListProviderRequest<DvoWeatherForecast>(0, pageSize);
var query = WeatherForecastListQuery.GetQuery(locationId, listRequest);
var result = await broker.ExecuteAsync<DvoWeatherForecast>(query);
Assert.True(result.Success);
Assert.Equal(testCount, result.Items.Count());
Assert.True(result.TotalItemCount == testRecordCount);
}
标识提供程序
这演示了一个完整的自定义实现。它从数据库标识表中获取包含ClaimsIdentity(身份验证系统的一部分)的结果。
作为参考,数据库记录为:
public record DboIdentity
{
[Key] public Guid Id { get; init; } = Guid.Empty;
public string Name { get; init; } = String.Empty;
public string Role { get; init; } = String.Empty;
}
结果:
public class IdentityRequestResult
{
public ClaimsIdentity? Identity { get; init; } = null;
public bool Success { get; init; } = false;
public string Message { get; init; } = string.Empty;
public static IdentityRequestResult Failure(string message)
=> new IdentityRequestResult {Message = message };
public static IdentityRequestResult Successful
(ClaimsIdentity identity, string? message = null)
=> new IdentityRequestResult
{Identity = identity, Success=true, Message = message ?? string.Empty };
}
查询请求:
public record IdentityQuery
: ICQSRequest<ValueTask<IdentityRequestResult>>
{
public Guid TransactionId { get; } = Guid.NewGuid();
public Guid IdentityId { get; init; } = Guid.Empty;
public static IdentityQuery Query(Guid Uid)
=> new IdentityQuery { IdentityId = Uid };
}
处理程序接口:我们可能需要服务器和API版本。
public interface IIdentityQueryHandler
: ICQSHandler<IdentityQuery, ValueTask<IdentityRequestResult>>
{}
和处理程序:
public class IdentityQueryHandler<TDbContext>
: ICQSHandler<IdentityQuery, ValueTask<IdentityRequestResult>>
where TDbContext : DbContext
{
protected IDbContextFactory<TDbContext> factory;
public IdentityQueryHandler(IDbContextFactory<TDbContext> factory)
=> this.factory = factory;
public async ValueTask<IdentityRequestResult> ExecuteAsync(IdentityQuery query)
{
var dbContext = this.factory.CreateDbContext();
IQueryable<DboIdentity> queryable = dbContext.Set<DboIdentity>();
if (queryable is not null)
{
var record = await queryable.SingleOrDefaultAsync
(item => item.Id == query.IdentityId);
if (record is not null)
{
var identity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Sid, record.Id.ToString()),
new Claim(ClaimTypes.Name, record.Name),
new Claim(ClaimTypes.Role, record.Role)
});
return IdentityRequestResult.Successful(identity);
}
return IdentityRequestResult.Failure("No Identity exists.");
}
return IdentityRequestResult.Failure("No Identity Records Found.");
}
}
和演示测试:
public class CQSCustomTests
{
private WeatherTestDataProvider _weatherTestDataProvider;
public CQSCustomTests()
// Creates an instance of the Test Data provider
=> _weatherTestDataProvider = WeatherTestDataProvider.Instance();
private ServiceProvider GetServiceProvider()
{
// Creates a Service Collection
var services = new ServiceCollection();
// Adds the application services to the collection
Action<DbContextOptionsBuilder> dbOptions = options =>
options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}");
services.AddWeatherAppServerDataServices<InMemoryWeatherDbContext>(dbOptions);
services.AddScoped<IIdentityQueryHandler,
IdentityQueryHandler<InMemoryWeatherDbContext>>();
// Creates a Service Provider from the Services collection
// This is our DI container
var provider = services.BuildServiceProvider();
// Adds the test data to the in memory database
var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
WeatherTestDataProvider.Instance().LoadDbContext
<InMemoryWeatherDbContext>(factory);
return provider!;
}
[Fact]
public async void TestIdentityCQSDataBroker()
{
var provider = GetServiceProvider();
var broker = provider.GetService<IIdentityQueryHandler>()!;
var testRecord = _weatherTestDataProvider.Identities.Skip(1).First();
var query = IdentityQuery.GetQuery(testRecord.Id);
var result = await broker.ExecuteAsync(query);
Assert.True(result.Success);
Assert.NotNull(result.Identity);
Assert.Equal(testRecord.Name, result.Identity.Name);
}
}
总结
希望我演示了一种不同的、更简洁的实现CQS模式的方法。我现在是一名皈依者。
我故意没有实现事务日志记录或集中式异常处理。
附录
数据存储
本文和存储库的后端数据库是内存中实体框架数据库。与其他模拟数据存储的方法相比,它的主要优势在于它适用于DbContext工厂并支持多个上下文。我使用内存中查询来模拟视图。
TestDataProvider有一个将其数据填充到DbContext中。
完整版DbContext如下所示:
public class InMemoryWeatherDbContext
: DbContext
{
public DbSet<DboWeatherForecast> DboWeatherForecast { get; set; } = default!;
public DbSet<DvoWeatherForecast> DvoWeatherForecast { get; set; } = default!;
public DbSet<DboWeatherSummary> DboWeatherSummary { get; set; } = default!;
public DbSet<DboWeatherLocation> DboWeatherLocation { get; set; } = default!;
public DbSet<FkWeatherSummary> FkWeatherSummary { get; set; } = default!;
public DbSet<FkWeatherLocation> FkWeatherLocation { get; set; } = default!;
public DbSet<DboIdentity> DboIdentity { get; set; } = default!;
public InMemoryWeatherDbContext
(DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<DboWeatherForecast>().ToTable("WeatherForecast");
modelBuilder.Entity<DboWeatherSummary>().ToTable("WeatherSummary");
modelBuilder.Entity<DboWeatherLocation>().ToTable("WeatherLocation");
modelBuilder.Entity<DboIdentity>().ToTable("Identity");
modelBuilder.Entity<DvoWeatherForecast>()
.ToInMemoryQuery(()
=> from f in this.DboWeatherForecast
join s in this.DboWeatherSummary! on
f.WeatherSummaryId equals s.Uid into fs
from fsjoin in fs
join l in this.DboWeatherLocation! on
f.WeatherLocationId equals l.Uid into fl
from fljoin in fl
select new DvoWeatherForecast
{
Uid = f.Uid,
WeatherSummaryId = f.WeatherSummaryId,
WeatherLocationId = f.WeatherLocationId,
Date = f.Date,
Summary = fsjoin.Summary,
Location = fljoin.Location,
TemperatureC = f.TemperatureC,
})
.HasKey(x => x.Uid);
modelBuilder.Entity<FkWeatherSummary>()
.ToInMemoryQuery(()
=> from s in this.DboWeatherSummary!
select new FkWeatherSummary
{
Id =s.Uid,
Name = s.Summary
})
.HasKey(x => x.Id);
modelBuilder.Entity<FkWeatherLocation>()
.ToInMemoryQuery(()
=> from l in this.DboWeatherLocation!
select new FkWeatherLocation
{
Id = l.Uid,
Name = l.Location
})
.HasKey(x => x.Id);
}
}
存储库中有一个自述文件,其中提供了测试数据设置的完整说明。
https://www.codeproject.com/Articles/5340253/Building-a-Succinct-DotNetCore-CQS-Data-Pipeline
文章介绍了使用命令查询职责分离(CQS)模式在DotNetCore中构建数据处理管道的方法,包括命令和查询的接口、基类定义,以及如何通过泛型和抽象基类减少重复代码。文章还展示了如何实现命令和查询的处理程序,以及在内存中数据库的测试示例。

385

被折叠的 条评论
为什么被折叠?



