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


文章目录

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


项目地址

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

一、Application 层

在这里插入图片描述

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

在这里插入图片描述

1. Command

  • 用于处理除查询以外的
  1. ICommand.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
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
using Bookify.Domain.Abstractions;
using MediatR;
namespace Bookify.Application.Abstractions.Messaging;
public interface IQuery<TResponse> : IRequest<Result<TResponse>>
{
}
  • IQueryHandler.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. 当前时间服务接口

  • 用于提供当前时间
namespace Bookify.Application.Abstractions.Clock;public interface IDateTimeProvider
{DateTime UtcNow { get; }
}

4. 邮件发送服务接口

  • 发送邮件的服务
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个
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

  • 用于保存预定的处理方法
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 existsUser? user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);if (user is null){return Result.Failure<Guid>(UserErrors.NotFound);}// Check if the apartment existsApartment? 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 validif (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 事件的逻辑,这里只是一个处理的函数,并没有自动执行,只有当发布了事件之后,才会触发
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的链接接口

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

2. GetBookingQuery

  • GetBookingQuery:传入BookingID,返回BookingResponse
using Bookify.Application.Abstractions.Messaging;
namespace Bookify.Application.Bookings.GetBooking;
public sealed record GetBookingQuery(Guid BookingId) : IQuery<BookingResponse>;
  • BookingResponse.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

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();//执行的sqlconst string sql = """SELECTid 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 CreatedOnUtcFROM bookingsWHERE id = @BookingId""";//执行sqlBookingResponse? booking = await connection.QueryFirstOrDefaultAsync<BookingResponse>(sql,new{request.BookingId});return booking;}
}

二、垂直切片

2.1 Validator中间件

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

1. 创建中间件

  • 只作用于 Command 类型,进行验证,并且会自动注入验证器,不用每次手动添加validator对于command的请求
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的类
namespace Bookify.Application.Exceptions;public sealed record ValidationError(string PropertyName, string ErrorMessage);
  1. 实例化ValidationError,并且将错误存储到列表里

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处理

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. 注册服务

configuration.AddOpenBehavior(typeof(LoggingBehavior<,>));

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/80018.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详解Redis

一.Redis的基本概念 首先&#xff0c;什么是Redis&#xff1f; Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的、高性能的键值对内存数据库&#xff0c;常被用作缓存、消息队列、分布式锁等。 二.Redis的基本数据类型 1. 字符串&#xff08;String&am…

智慧医院的可视化变革:可视化工具助力数字化转型

在科技飞速发展的当下&#xff0c;智慧医院已从概念逐步落地&#xff0c;深刻改变着传统医疗模式。它借助互联网、数字孪生及人工智能等前沿技术&#xff0c;在医疗服务领域掀起革新&#xff0c;涵盖面向医务人员的“智慧医疗”、面向患者的“智慧服务”以及面向医院的“智慧管…

Ubuntu Linux系统配置账号无密码sudo

在Linux系统中&#xff0c;配置无密码sudo可以通过修改sudoers文件来实现。以下是具体的配置步骤 一、编辑sudoers文件 输入sudo visudo命令来编辑sudo的配置文件。visudo是一个专门用于编辑sudoers文件的命令&#xff0c;它会在保存前检查语法错误&#xff0c;从而防止可能的…

graphviz和dot绘制流程图

graphviz和dot绘制流程图 指令 1.写后端需求文档 2.用中文输出结果 3.必须详细全面 4.必须搭配相关流程图step1:下载graphviz&#xff0c;https://graphviz.org/download/ step2&#xff1a;安装&#xff0c;记得添加环境变量 step3&#xff1a;验证是否安装成功 dot --versio…

MongoDB常用操作示例

以下是基于 MongoDB Shell 的完整操作示例&#xff0c;覆盖数据库管理、集合操作、文档处理、聚合分析、索引管理等核心功能&#xff0c;并结合实际场景说明。所有示例均结合搜索结果中的技术要点整理而成。 一、连接与配置管理 1. 启动 MongoDB Shell 并连接实例 # 默认连接…

C++ 模板方法模式详解与实例

模板方法模式概念​ 模板方法模式(Template Method Pattern)属于行为型设计模式,其核心思想是在一个抽象类中定义一个算法的骨架,而将一些步骤延迟到子类中实现。这样可以使得子类在不改变算法结构的情况下,重新定义算法中的某些步骤。它通过继承机制,实现代码复用和行为…

MySQL基础关键_012_事务

目 录 一、概述 二、ACID 四大特性 三、MySQL 事务 四、事务隔离级别 1.说明 2.现象 &#xff08;1&#xff09;脏读 &#xff08;2&#xff09;不可重复读 &#xff08;3&#xff09;幻读 3.查看隔离级别 4.设置隔离级别 5.隔离级别 &#xff08;1&#xff09;初始…

Hutool中的Pair类详解

1. Pair类概述 Hutool工具库中的Pair类是一个简单的键值对数据结构&#xff0c;用于存储两个相关联的对象。它类似于Map的Entry&#xff0c;但更加轻量级&#xff0c;适用于需要临时存储两个相关联数据的场景。 2. Pair类的主要特点 简单轻量&#xff1a;不依赖复杂的数据结…

02-GBase 8s 事务型数据库 客户端工具dbaccess

dbaccess概述 数据库产品通常会提供一个命令行客户端工具。 数据库厂商 命令行客户端 Oracle sqlplus MySQL mysql Marladb mysql GBase 8s dbaccess Kingbase ES ksql DM8 disql dbaccess 是 GBase 8s 数…

手撕基于AMQP协议的简易消息队列-8(单元测试的编写)

在MQTest中编写模块的单元测试 在MQTest中编写makefile文件来编译客户端模块 all:Test_FileHelper Test_Exchange Test_Queue Test_Binding Test_Message Test_VirtualHost Test_Route Test_Consumer Test_Channel Test_Connection Test_VirtualHost:Test_VirtualHost.cpp ..…

Spark 之 metrics

peak memory.//sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala: “peakMemory” -> SQLMetrics.createSizeMetric(sparkContext, “peak memory”), .//sql/core/src/main/scala/org/apache/spark/sql/execution/SortExec.scal…

HTTP/HTTPS协议(请求响应模型、状态码)

目录 HTTP/HTTPS协议简介 HTTP协议 HTTPS协议 请求 - 响应模型 HTTP请求 &#xff08;二&#xff09;HTTP响应 HTTPS协议与HTTP协议在请求 - 响应模型中的区别 HTTP/HTTPS协议简介 HTTP协议 定义 HTTP&#xff08;HyperText Transfer Protocol&#xff09;即超文本传输…

OpenHarmony 5.0 切换已连接过的wifi切换失败

目录 1.背景 2.流程分析 3.方案 1.背景 在OpenHarmony 5.0的设置中,输入密码进行wifi连接可以正常连接,然后多个已经连接过的wifi进行切换发现大概率切换失败 2.流程分析 wifi连接过程其实是先进行断开当前的wifi连接,然后再连接另一个wifi,如下: 虽然上述流程看起来没…

【Ollama】docker离线部署Ollama+deepseek

因为要做项目&#xff0c;实验室的服务器不联网&#xff0c;所以只能先打包一个基础的docker环境&#xff0c;然后再在实验室的服务器上进行解压和配置环境 参考&#xff1a;https://zhuanlan.zhihu.com/p/23377266873 1.打包基础的docker环境 这里最好用有cuda的&#xff0c…

如何使用极狐GitLab 软件包仓库功能托管 terraform?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 Terraform 模块库 (BASIC ALL) 基础设施仓库和 Terraform 模块仓库合并到单个 Terraform 模块仓库功能引入于极狐GitLab 15.1…

【QT】深入理解 Qt 中的对象树:机制、用途与最佳实践

深入理解 Qt 中的对象树&#xff1a;机制、用途与最佳实践 在使用 Qt 编程时&#xff0c;你是否注意到很多对象可以设置“父对象”&#xff1f;比如&#xff1a; QPushButton* btn new QPushButton(parentWidget);这不是简单的层级结构&#xff0c;而是 Qt 强大而优雅的 对象…

比较入站和出站防火墙规则

组织需要仔细配置防火墙规则&#xff0c;监控网络的传入和传出流量&#xff0c;从而最大限度降低遭受攻击的风险。在有效管理入站和出站防火墙规则前&#xff0c;了解入站与出站流量的区别至关重要。 一、什么是入站流量&#xff1f; 入站流量指的是并非源自网络内部&#xf…

Unity-Shader详解-其五

关于Unity的Shader部分的基础知识其实已经讲解得差不多了&#xff0c;今天我们来一些实例分享&#xff1a; 溶解 效果如下&#xff1a; 代码如下&#xff1a; Shader "Chapter8/chapter8_1" {Properties{// 定义属性[NoScaleOffset]_Albedo("Albedo", 2…

COLT_CMDB_linux_userInfo_20250508.sh修复历史脚本输出指标信息中userName与输出信息不一致问题

#!/bin/bash #IT_BEGIN #IT_TYPE3 #IT SYSTEM_LINUX_AGENTUSERDISCOVER|discovery.user[disc] #原型指标 #IT_RULE SYSTEM_LINUX_AGENTUSERGROUPID|groupId[{#USERNAME}] #IT_RULE SYSTEM_LINUX_AGENTUSERHOME|userHome[{#USERNAME}] #IT_RULE SYSTEM_LINUX_AGENTUSERNAME|user…

TCP 与 UDP报文

** TCP 与 UDP报文** 1. 引言 在网络通信中&#xff0c;TCP&#xff08;传输控制协议&#xff09; 和 UDP&#xff08;用户数据报协议&#xff09; 是两种最核心的传输层协议。它们各自适用于不同的场景&#xff0c;理解其工作原理对开发高性能网络应用至关重要。本文将详细解…