文章目录
- 项目地址
- [一、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
- 用于处理除查询以外的
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>>
{
}
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
- 用于存储验证错误的实例
- 创建一个
ValidationError
的类
cs
namespace Bookify.Application.Exceptions;
public sealed record ValidationError(string PropertyName, string ErrorMessage);
- 实例化ValidationError,并且将错误存储到列表里
cs
public sealed class ValidationException : Exception
{
public ValidationException(IEnumerable<ValidationError> errors)
{
Errors = errors;
}
public IEnumerable<ValidationError> Errors { get; }
3. 注册服务
- 注册中间件,在MR服务里
- 注册所有的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<,>));