构建简洁的DotNetCore CQS数据管道

文章介绍了使用命令查询职责分离(CQS)模式在DotNetCore中构建数据处理管道的方法,包括命令和查询的接口、基类定义,以及如何通过泛型和抽象基类减少重复代码。文章还展示了如何实现命令和查询的处理程序,以及在内存中数据库的测试示例。

目录

测试数据

存储库

介绍

解决方案布局和设计

接口和基类

结果

基类

命令

命令处理程序

查询

RecordQuery

ListQuery

FKListQuery

查询处理程序

RecordQueryHandler

ListQueryHandler

FKListQueryHandler

实现处理程序

泛型工厂代理

测试代理

设置

测试

自定义请求

筛选列表

查询

处理程序

代理

测试

标识提供程序

总结

附录

数据存储


测试数据

附录提供了数据类和测试数据提供程序的摘要。存储库的文档部分有完整的描述。

存储库

与本文相关的数据存储库位于此处:Blazr.Demo.DataPipeline。存储库中有一些关于数据类、测试数据提供程序和数据库上下文的设计和详细信息的额外文档。

介绍

CQS——不要与CQRS混淆——基本上是一种编程风格。每个操作都是:

  1. Command——请求进行数据更改
  2. Query——获取一些数据的请求

Command返回状态信息或不返回任何内容。命令从不返回数据。

Query返回一个数据集。查询从不更改数据的状态。进行查询没有副作用

这是一个很好的模式,可以普遍应用于你的代码。

每个操作都有一个定义操作的Command/Query类和一个用于执行已定义操作的Handler类。通常,一对一关系:每个请求的唯一处理程序。

基本上:

  • Request对象定义执行请求所需的信息Handler以及它期望返回的内容——Result
  • Handler对象执行必要的代码,并使用Request提供的数据返回已定义的Result

从概念上讲,它非常简单,并且相对容易实现。问题是每个数据库操作都需要一个请求和一个处理程序对象。许多类定义和重复相同的旧代码。

解决方案布局和设计

该解决方案由一组根据清洁设计原则组织的库组成。它旨在在任何DotNetCore环境中工作。Blazr.CoreBlazr.Data是可用于任何实现的两个基本库。 Blazr.Demo.CoreBlazr.Demo.Data是两个特定于应用程序的库。

前端应用程序是一个XUnit测试项目,用于演示和测试代码。

我在Blazor项目中使用它。

接口和基类

基本方法可以通过两个泛型接口定义。

ICQSRequest定义任何请求:

  1. 它表示请求生成定义为TResult
  2. 它具有唯一的TransactionId以跟踪事务(如果需要并实现)。

public interface ICQSRequest<out TResult>
{
    public Guid TransactionId { get;}
}

ICQSHandler定义执行ICQSRequest实例的任何处理程序:

  1. 处理程序获取实现ICQSRequest接口的TRequest
  2. 处理程序输出ICQSRequest接口中定义的TResult
  3. 它有一个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接口/类结构如下所示:

命令

所有命令:

  1. 取一个我们定义为TRecord的记录。
  2. 修复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中使用内置的泛型方法,所以不需要特定的DbContextSet<TRecord>方法通过SaveChangesAsync查找TRecordUpdate<TRecord>Add<TRecord>以及Delete<TRecord>方法的DbSet实例来实现命令。

所有处理程序都遵循相同的模式。

  1. 构造函数传入DbContext工厂并执行命令请求。
  2. Execute:
    1. 获取DbContext
    2. 在记录中传递的上下文上调用泛型Add/Update/Delete。在内部,EF查找记录集和特定记录,并将其替换为提供的记录。
    3. 调用将更改提交到数据存储的DbContext上的SaveChanges
    4. 检查我们有一个更改并返回一个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");
    }
}

查询

查询不是那么统一。

  1. 有各种类型的TResult
  2. 它们具有特定的操作,例如WhereOrderBy

为了处理这些要求,我们定义了三个查询请求:

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,其包含IFkListItemIEnumerableFkListItem是一个包含Guid/Name对的简单对象。它的主要用途是UI中的外键Select控件中。

public record FKListQuery<TRecord>
    : ICQSRequest<ValueTask<FKListProviderResult>>
{
    public Guid TransactionId { get; } = Guid.NewGuid();
}

查询处理程序

相应的查询处理程序是:

RecordQueryHandler

创建泛型版本可能具有挑战性,具体取决于ORM

需要注意的关键概念是:

  1. IDbContextFactory中使用DbContexts工作单元
  2. _dbContext.Set<TRecord>()TRecord获取DbSet
  3. 使用两种方法来应用查询。

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

这里要注意的关键概念是:

  1. IDbContextFactory中使用DbContexts工作单元
  2. _dbContext.Set<TRecord>()TRecord获取DbSet
  3. IQueryable用于生成查询。
  4. 需要两个查询。一个用于获取“分页”recordset,一个用于获取总记录计数。
  5. 使用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);
    }
}

实现处理程序

处理程序设计为以两种方式使用:

  1. 单独作为依赖注入服务
  2. 依赖注入工厂

我们将看到两者都用于测试。

泛型工厂代理

代理使用单一方法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

查询

  1. 继承ListQueryBaseTRecord固定为DvoWeatherForecast
  2. 定义WeatherLocationId属性
  3. 定义一个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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值