命令模式和消息幂等去重

This commit is contained in:
hello 2024-09-06 22:42:27 +08:00
parent 2201f36422
commit 780c489e02
10 changed files with 208 additions and 0 deletions

View File

@ -0,0 +1,14 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using MediatR;
namespace HelloShop.OrderingService.Commands
{
public class IdentifiedCommand<TRequest, TResponse>(TRequest command, Guid id) : IRequest<TResponse> where TRequest : IRequest<TResponse>
{
public TRequest Command { get; } = command;
public Guid Id { get; } = id;
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.OrderingService.Services;
using MediatR;
namespace HelloShop.OrderingService.Commands
{
public abstract class IdentifiedCommandHandler<TRequest, TResponse>(IMediator mediator, IClientRequestManager requestManager) : IRequestHandler<IdentifiedCommand<TRequest, TResponse>, TResponse> where TRequest : IRequest<TResponse>
{
protected virtual TResponse? CreateResultForDuplicateRequest() => default;
public async Task<TResponse> Handle(IdentifiedCommand<TRequest, TResponse> request, CancellationToken cancellationToken)
{
if (await requestManager.ExistAsync(request.Id))
{
return CreateResultForDuplicateRequest() ?? throw new NotImplementedException();
}
await requestManager.CreateRequestForCommandAsync<TRequest>(request.Id);
return await mediator.Send(request.Command, cancellationToken);
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using MediatR;
namespace HelloShop.OrderingService.Commands.Orders
{
public class CreateOrderCommand : IRequest<bool>
{
public int UserId { get; set; }
public required string UserName { get; set; }
#region Order Items
public required IEnumerable<CreateOrderCommandItem> OrderItems { get; set; }
#endregion
#region PaymentMethod
public required string CardAlias { get; set; }
public required string CardNumber { get; set; }
public required string CardHolderName { get; set; }
public string? CardSecurityNumber { get; set; }
public DateTimeOffset? CardExpiration { get; set; }
#endregion
#region Address
public required string Country { get; set; }
public required string State { get; set; }
public required string City { get; set; }
public required string Street { get; set; }
public required string ZipCode { get; set; }
#endregion
public class CreateOrderCommandItem
{
public int ProductId { get; set; }
public required string ProductName { get; set; }
public required string PictureUrl { get; set; }
public decimal UnitPrice { get; set; }
public int Units { get; set; }
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.OrderingService.Services;
using MediatR;
namespace HelloShop.OrderingService.Commands.Orders
{
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, bool>
{
public Task<bool> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
public class CreateOrderIdentifiedCommandHandler(IMediator mediator, IClientRequestManager requestManager) : IdentifiedCommandHandler<CreateOrderCommand, bool>(mediator, requestManager)
{
protected override bool CreateResultForDuplicateRequest() => default;
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
namespace HelloShop.OrderingService.Entities.Idempotency
{
public class ClientRequest
{
public Guid Id { get; set; }
public required string Name { get; set; }
public DateTimeOffset Time { get; set; }
}
}

View File

@ -3,8 +3,10 @@
using HelloShop.OrderingService.Constants; using HelloShop.OrderingService.Constants;
using HelloShop.OrderingService.Infrastructure; using HelloShop.OrderingService.Infrastructure;
using HelloShop.OrderingService.Services;
using HelloShop.ServiceDefaults.Extensions; using HelloShop.ServiceDefaults.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Reflection;
namespace HelloShop.OrderingService.Extensions namespace HelloShop.OrderingService.Extensions
{ {
@ -18,6 +20,13 @@ namespace HelloShop.OrderingService.Extensions
{ {
options.UseNpgsql(builder.Configuration.GetConnectionString(DbConstants.MasterConnectionStringName)); options.UseNpgsql(builder.Configuration.GetConnectionString(DbConstants.MasterConnectionStringName));
}); });
builder.Services.AddScoped<IClientRequestManager, ClientRequestManager>();
builder.Services.AddMediatR(options =>
{
options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
});
} }
public static WebApplication MapApplicationEndpoints(this WebApplication app) public static WebApplication MapApplicationEndpoints(this WebApplication app)

View File

@ -8,6 +8,7 @@
<ProjectReference Include="..\HelloShop.ServiceDefaults\HelloShop.ServiceDefaults.csproj" /> <ProjectReference Include="..\HelloShop.ServiceDefaults\HelloShop.ServiceDefaults.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -0,0 +1,21 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.OrderingService.Entities.Idempotency;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace HelloShop.OrderingService.Infrastructure.EntityConfigurations.Idempotency
{
public class ClientRequestEntityTypeConfiguration : IEntityTypeConfiguration<ClientRequest>
{
public void Configure(EntityTypeBuilder<ClientRequest> builder)
{
builder.ToTable("ClientRequests");
builder.HasKey(t => t.Id);
builder.Property(t => t.Name).HasMaxLength(64);
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
using HelloShop.OrderingService.Entities.Idempotency;
using HelloShop.OrderingService.Infrastructure;
namespace HelloShop.OrderingService.Services
{
public class ClientRequestManager(OrderingServiceDbContext dbContext) : IClientRequestManager
{
public async Task<bool> ExistAsync(Guid id) => await dbContext.FindAsync<ClientRequest>(id) is not null;
public async Task CreateRequestForCommandAsync<T>(Guid id)
{
if (await ExistAsync(id))
{
throw new Exception($"Request with {id} already exists");
}
await dbContext.AddAsync(new ClientRequest
{
Id = id,
Name = typeof(T).Name,
Time = DateTime.UtcNow
});
await dbContext.SaveChangesAsync();
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) HelloShop Corporation. All rights reserved.
// See the license file in the project root for more information.
namespace HelloShop.OrderingService.Services
{
public interface IClientRequestManager
{
Task<bool> ExistAsync(Guid id);
Task CreateRequestForCommandAsync<T>(Guid id);
}
}