(二)毛子整洁架构(CQRS/Dapper/领域事件处理器/垂直切片)


文章目录

  • 项目地址
  • [一、Application 层](#一、Application 层)
    • [1.1 定义CQRS的接口以及其他服务](#1.1 定义CQRS的接口以及其他服务)
      • [1. Command](#1. Command)
      • [2. IQuery查询](#2. IQuery查询)
      • [3. 当前时间服务接口](#3. 当前时间服务接口)
      • [4. 邮件发送服务接口](#4. 邮件发送服务接口)
    • [1.2 ReserveBooking Command](#1.2 ReserveBooking Command)
      • [1. 处理传入的参数](#1. 处理传入的参数)
      • [2. ReserveBookingCommandHandler](#2. ReserveBookingCommandHandler)
      • [3. BookingReservedDomainEvent](#3. BookingReservedDomainEvent)
    • [1.3 Query使用Sql查询](#1.3 Query使用Sql查询)
      • [1. 创建Dapper的链接接口](#1. 创建Dapper的链接接口)
      • [2. GetBookingQuery](#2. GetBookingQuery)
      • [3. QueryHandler](#3. QueryHandler)
  • 二、垂直切片
    • [2.1 Validator中间件](#2.1 Validator中间件)
      • [1. 创建中间件](#1. 创建中间件)
      • [2. 创建ValidationError](#2. 创建ValidationError)
      • [3. 注册服务](#3. 注册服务)
    • [2.2 LoggingBehavior中间件](#2.2 LoggingBehavior中间件)
      • [1. 创建中间件](#1. 创建中间件)
      • [2. 注册服务](#2. 注册服务)

项目地址

  • 教程作者:
  • 教程地址:
复制代码
  • 代码仓库地址:
复制代码
  • 所用到的框架和插件:

    dbt
    airflow

一、Application 层

1.1 定义CQRS的接口以及其他服务

1. Command

  • 用于处理除查询以外的
  1. ICommand.cs
cs 复制代码
using Bookify.Domain.Abstractions;
using MediatR;
namespace Bookify.Application.Abstractions.Messaging;
//无返回值的命令
public interface ICommand : IRequest<Result>
{
}
//返回一个TReponse的命令
public interface ICommand<TReponse> : IRequest<Result<TReponse>>
{
}
  1. ICommandHandler.cs
cs 复制代码
namespace Bookify.Application.Abstractions.Messaging;
public interface ICommandHandler<TCommand> : IRequestHandler<TCommand, Result>
    where TCommand : ICommand
{
}

public interface ICommandHandler<TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
    where TCommand : ICommand<TResponse>
{
}

2. IQuery查询

  • 用于查询
    IQuery.cs
cs 复制代码
using Bookify.Domain.Abstractions;
using MediatR;
namespace Bookify.Application.Abstractions.Messaging;
public interface IQuery<TResponse> : IRequest<Result<TResponse>>
{
}
  • IQueryHandler.cs
cs 复制代码
using Bookify.Domain.Abstractions;
using MediatR;
namespace Bookify.Application.Abstractions.Messaging;
public interface IQueryHandler<TQuery, TResponse> : IRequestHandler<TQuery, Result<TResponse>>
    where TQuery : IQuery<TResponse>
{
}

3. 当前时间服务接口

  • 用于提供当前时间
cs 复制代码
namespace Bookify.Application.Abstractions.Clock;

public interface IDateTimeProvider
{
    DateTime UtcNow { get; }
}

4. 邮件发送服务接口

  • 发送邮件的服务
cs 复制代码
namespace Bookify.Application.Abstractions.Email;
public interface IEmailService
{
    Task SendAsync(Domain.Users.Email recipient, string subject, string body);
}

1.2 ReserveBooking Command

1. 处理传入的参数

  • ReserveBookingCommand.cs:返回值是Guid,参数时4个
cs 复制代码
using Bookify.Application.Abstractions.Messaging;
namespace Bookify.Application.Bookings.ReserveBooking;
public record ReserveBookingCommand(
    Guid ApartmentId,
    Guid UserId,
    DateOnly StartDate,
    DateOnly EndDate) : ICommand<Guid>;

2. ReserveBookingCommandHandler

  • 用于保存预定的处理方法
cs 复制代码
internal sealed class ReserveBookingCommandHandler : ICommandHandler<ReserveBookingCommand, Guid>
{
    private readonly IUserRepository _userRepository;
    private readonly IApartmentRepository _apartmentRepository;
    private readonly IBookingRepository _bookingRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly PricingService _pricingService;
    private readonly IDateTimeProvider _dateTimeProvider;

    public ReserveBookingCommandHandler(
        IUserRepository userRepository,
        IApartmentRepository apartmentRepository,
        IBookingRepository bookingRepository,
        IUnitOfWork unitOfWork,
        PricingService pricingService,
        IDateTimeProvider dateTimeProvider)
    {
        _userRepository = userRepository;
        _apartmentRepository = apartmentRepository;
        _bookingRepository = bookingRepository;
        _unitOfWork = unitOfWork;
        _pricingService = pricingService;
        _dateTimeProvider = dateTimeProvider;
    }

    public async Task<Result<Guid>> Handle(ReserveBookingCommand request, CancellationToken cancellationToken)
    {
        // Check if the user exists
        User? user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);

        if (user is null)
        {
            return Result.Failure<Guid>(UserErrors.NotFound);
        }
        // Check if the apartment exists
        Apartment? apartment = await _apartmentRepository.GetByIdAsync(request.ApartmentId, cancellationToken);

        if (apartment is null)
        {
            return Result.Failure<Guid>(ApartmentErrors.NotFound);
        }

        //创建预定时间段
        var duration = DateRange.Create(request.StartDate, request.EndDate);
        // Check if the booking duration is valid
        if (await _bookingRepository.IsOverlappingAsync(apartment, duration, cancellationToken))
        {
            return Result.Failure<Guid>(BookingErrors.Overlap);
        }

        var booking = Booking.Reserve(
            apartment,
            user.Id,
            duration,
            _dateTimeProvider.UtcNow,
            _pricingService);
        //添加booking
        _bookingRepository.Add(booking);
        //保存
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return booking.Id;
    }
}

3. BookingReservedDomainEvent

  • 处理 BookingReservedDomainEvent 事件的逻辑,这里只是一个处理的函数,并没有自动执行,只有当发布了事件之后,才会触发
cs 复制代码
namespace Bookify.Application.Bookings.ReserveBooking;

/// 处理 BookingReservedDomainEvent 事件
internal sealed class BookingReservedDomainEventHandler : INotificationHandler<BookingReservedDomainEvent>
{
    private readonly IBookingRepository _bookingRepository;
    private readonly IUserRepository _userRepository;
    private readonly IEmailService _emailService;

    public BookingReservedDomainEventHandler(
        IEmailService emailService, 
        IUserRepository userRepository, 
        IBookingRepository bookingRepository)
    {
        _emailService = emailService;
        _userRepository = userRepository;
        _bookingRepository = bookingRepository;
    }


    public async Task Handle(BookingReservedDomainEvent notification, CancellationToken cancellationToken)
    {
        //通过事件里传来的 BookingId 从数据库查出预订信息。
        Booking? booking = await _bookingRepository.GetByIdAsync(notification.BookingId, cancellationToken);

        if (booking is null)
        {
            return;
        }
        //根据 booking.UserId 查出用户信息。
        User? user = await _userRepository.GetByIdAsync(booking.UserId, cancellationToken);

        if (user is null)
        {
            return;
        }
        //通过用户信息发送邮件。
        await _emailService.SendAsync(
            user.Email,
            "Booking reserved!",
            "You have 10 minutes to confirm this booking");
    }
}

1.3 Query使用Sql查询

  • 所有查询,直接使用Dapper

1. 创建Dapper的链接接口

  • 用于连接数据库用
cs 复制代码
namespace Bookify.Application.Abstractions.Data;
//Dapper链接数据库的工厂接口
public interface ISqlConnectionFactory
{
    IDbConnection CreateConnection();
}

2. GetBookingQuery

  • GetBookingQuery:传入BookingID,返回BookingResponse
cs 复制代码
using Bookify.Application.Abstractions.Messaging;
namespace Bookify.Application.Bookings.GetBooking;
public sealed record GetBookingQuery(Guid BookingId) : IQuery<BookingResponse>;
  • BookingResponse.cs
cs 复制代码
namespace Bookify.Application.Bookings.GetBooking;
public sealed class BookingResponse
{
    public Guid Id { get; init; }
    public Guid UserId { get; init; }
    public Guid ApartmentId { get; init; }    
    public int Status { get; init; }
    public decimal PriceAmount { get; init; }
    public string PriceCurrency { get; init; }
    public decimal CleaningFeeAmount { get; init; }
    public string CleaningFeeCurrency { get; init; }
    public decimal AmenitiesUpChargeAmount { get; init; }
    public string AmenitiesUpChargeCurrency { get; init; }
    public decimal TotalPriceAmount { get; init; }
    public string TotalPriceCurrency { get; init; }
    public DateOnly DurationStart { get; init; }
    public DateOnly DurationEnd { get; init; }
    public DateTime CreatedOnUtc { get; init; }
}

3. QueryHandler

cs 复制代码
using System.Data;
using Bookify.Application.Abstractions.Data;
using Bookify.Application.Abstractions.Messaging;
using Bookify.Domain.Abstractions;
using Dapper;

namespace Bookify.Application.Bookings.GetBooking;
internal sealed class GetBookingQueryHandler : IQueryHandler<GetBookingQuery, BookingResponse>
{
    private readonly ISqlConnectionFactory _sqlConnectionFactory;

    public GetBookingQueryHandler(ISqlConnectionFactory sqlConnectionFactory)
    {
        _sqlConnectionFactory = sqlConnectionFactory;
    }

    public async Task<Result<BookingResponse>> Handle(GetBookingQuery request, CancellationToken cancellationToken)
    {
        //创建数据库连接
        using IDbConnection connection = _sqlConnectionFactory.CreateConnection();
        //执行的sql
        const string sql = """
                           SELECT
                               id AS Id,
                               apartment_id AS ApartmentId,
                               user_id AS UserId,
                               status AS Status,
                               price_for_period_amount AS PriceAmount,
                               price_for_period_currency AS PriceCurrency,
                               cleaning_fee_amount AS CleaningFeeAmount,
                               cleaning_fee_currency AS CleaningFeeCurrency,
                               amenities_up_charge_amount AS AmenitiesUpChargeAmount,
                               amenities_up_charge_currency AS AmenitiesUpChargeCurrency,
                               total_price_amount AS TotalPriceAmount,
                               total_price_currency AS TotalPriceCurrency,
                               duration_start AS DurationStart,
                               duration_end AS DurationEnd,
                               created_on_utc AS CreatedOnUtc
                           FROM bookings
                           WHERE id = @BookingId
                           """;
        //执行sql
        BookingResponse? booking = await connection.QueryFirstOrDefaultAsync<BookingResponse>(
            sql,
            new
            {
                request.BookingId
            });

        return booking;
    }
}

二、垂直切片

2.1 Validator中间件

  • 只Command进行验证,并且自动注入验证器

1. 创建中间件

  • 只作用于 Command 类型,进行验证,并且会自动注入验证器,不用每次手动添加validator对于command的请求
cs 复制代码
namespace Bookify.Application.Abstractions.Behaviors;

//只对继承IBaseCommand接口,进行处理
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse> //IPipelineBehavior 是 MediatR 提供的接口,在请求发送到 handler 之前或之后插入额外逻辑
    where TRequest : IBaseCommand  //指定 TRequest 必须实现 IBaseCommand 接口
{
    //1.获取所有的验证器
    private readonly IEnumerable<IValidator<TRequest>> _validators;
    //2.构造函数注入所有的验证器
    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }
    //3.IPipelineBehavior核心方法
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
        {
            return await next();
        }
        //4.创建一个验证上下文对象,将请求传入给 FluentValidation 使用
        var context = new ValidationContext<TRequest>(request);
        //5.使用 LINQ 查询验证器,获取验证结果
        var validationErrors = _validators
            .Select(validator => validator.Validate(context))
            .Where(validationResult => validationResult.Errors.Any())
            .SelectMany(validationResult => validationResult.Errors)
            .Select(validationFailure => new ValidationError(
                validationFailure.PropertyName,
                validationFailure.ErrorMessage))
            .ToList();
        //6.如果验证失败,抛出 ValidationException 异常
        if (validationErrors.Any())
        {
            throw new Exceptions.ValidationException(validationErrors);
        }

        return await next();
    }
}

2. 创建ValidationError

  • 用于存储验证错误的实例
  1. 创建一个ValidationError的类
cs 复制代码
namespace Bookify.Application.Exceptions;

public sealed record ValidationError(string PropertyName, string ErrorMessage);
  1. 实例化ValidationError,并且将错误存储到列表里
cs 复制代码
public sealed class ValidationException : Exception
{
    public ValidationException(IEnumerable<ValidationError> errors)
    {
        Errors = errors;
    }
    public IEnumerable<ValidationError> Errors { get; }

3. 注册服务

  1. 注册中间件,在MR服务里
  2. 注册所有的validator

2.2 LoggingBehavior中间件

  • 我们对Command 进行特殊单独的Logging逻辑,因为这里使用了Dapper直接写了sql,所以,没有必要使用日志记录Sql,这样提高了程序性能

1. 创建中间件

  • 只对IBaseCommand实现的类进行特殊化logging处理
cs 复制代码
public class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IBaseCommand
{
    private readonly ILogger<TRequest> _logger;

    public LoggingBehavior(ILogger<TRequest> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string name = request.GetType().Name;

        try
        {
            _logger.LogInformation("Executing command {Command}", name);

            TResponse result = await next();

            _logger.LogInformation("Command {Command} processed successfully", name);

            return result;
        }
        catch (Exception exception)
        {
            _logger.LogError(exception, "Command {Command} processing failed", name);

            throw;
        }
    }
}

2. 注册服务

cs 复制代码
configuration.AddOpenBehavior(typeof(LoggingBehavior<,>));
相关推荐
一只蒟蒻ovo8 分钟前
操作系统导论——第26章 并发:介绍
java·开发语言
TPBoreas4 小时前
Jenkins 改完端口号启动不起来了
java·开发语言
TE-茶叶蛋5 小时前
Vuerouter 的底层实现原理
开发语言·javascript·ecmascript
云闲不收5 小时前
设计模式原则
开发语言
秋名RG5 小时前
深入解析建造者模式(Builder Pattern)——以Java实现复杂对象构建的艺术
java·开发语言·建造者模式
技术求索者6 小时前
c++学习
开发语言·c++·学习
方博士AI机器人9 小时前
Python 3.x 内置装饰器 (4) - @dataclass
开发语言·python
weixin_376934639 小时前
JDK Version Manager (JVMS)
java·开发语言
Logintern099 小时前
【每天学习一点点】使用Python的pathlib模块分割文件路径
开发语言·python·学习