From 54db7a6e8ea04e5ed2c70a8d62305a887aa5843c Mon Sep 17 00:00:00 2001 From: Behdad Kardgar Date: Wed, 25 Dec 2024 11:57:00 +0100 Subject: [PATCH] add outbox table to order command api (#72) --- .../Endpoints/CreateOrder/Endpoint.cs | 44 +-- .../Endpoints/CreateOrder/Models.cs | 21 +- .../Endpoints/DeleteOrder/Endpoint.cs | 22 +- .../Endpoints/DeleteOrder/Models.cs | 9 +- .../Endpoints/UpdateOrder/Endpoint.cs | 54 +++- .../Endpoints/UpdateOrder/Models.cs | 58 +++- .../IApplicationDbContext\342\200\216.cs" | 5 +- .../Dtos/UpdateOrderDto.cs | 5 +- .../Order.Command.Application/Extensions.cs | 80 +++++ .../Order.Command.Application.csproj | 6 +- .../CreateOrder/CreateOrderCommandHandler.cs | 40 ++- .../Orders/Commands/CreateOrder/Models.cs | 23 +- .../Orders/Commands/CreateOrder/Validator.cs | 45 ++- .../DeleteOrder/DeleteOrderCommandHandler.cs | 55 +++- .../Orders/Commands/DeleteOrder/Validator.cs | 5 +- .../UpdateOrder/UpdateOrderCommandHandler.cs | 64 +++- .../Orders/Commands/UpdateOrder/Validator.cs | 6 +- .../GetOrderByCustomerQueryHandler.cs | 2 +- .../Queries/GetOrderById/GetOrderByIdDto.cs | 9 +- .../GetOrders/GetOrdersQueryHandler.cs | 1 + .../GetOrdersByNameQueryHandler.cs | 2 +- .../Events/OrderDeletedEvent.cs | 5 + .../Events/OrderItemAddedEvent.cs | 5 - .../Events/OrderItemDeletedEvent.cs | 5 - .../Order.Command.Domain/Models/Order.cs | 35 +-- .../Order.Command.Domain/Models/Outbox.cs | 45 +++ .../Models/ValueObjects/ValueObjects.cs | 54 +++- .../Order.Command.Domain.csproj | 10 +- .../Data/ApplicationDbContext.cs | 1 + .../Data/Configuration/OrderConfiguration.cs | 9 +- .../Data/Configuration/OutboxConfiguration.cs | 52 +++ .../Data/Extensions/DatabaseExtensions.cs | 2 + .../AuditableEntityInterceptor.cs | 8 +- .../DispatchDomainEventsInterceptor.cs | 28 +- .../20241220065009_add outbox.Designer.cs | 297 ++++++++++++++++++ .../Migrations/20241220065009_add outbox.cs | 61 ++++ .../ApplicationDbContextModelSnapshot.cs | 58 ++++ 37 files changed, 1076 insertions(+), 155 deletions(-) create mode 100644 src/Services/Order.Command/Order.Command.Application/Extensions.cs create mode 100644 src/Services/Order.Command/Order.Command.Domain/Events/OrderDeletedEvent.cs delete mode 100644 src/Services/Order.Command/Order.Command.Domain/Events/OrderItemAddedEvent.cs delete mode 100644 src/Services/Order.Command/Order.Command.Domain/Events/OrderItemDeletedEvent.cs create mode 100644 src/Services/Order.Command/Order.Command.Domain/Models/Outbox.cs create mode 100644 src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OutboxConfiguration.cs create mode 100644 src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.Designer.cs create mode 100644 src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.cs diff --git a/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Endpoint.cs b/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Endpoint.cs index c0ed727..9f703b6 100644 --- a/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Endpoint.cs +++ b/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Endpoint.cs @@ -38,31 +38,26 @@ public override async Task HandleAsync(Request request) ShippingAddress: MapAddress(request.ShippingAddress), BillingAddress: MapAddress(request.BillingAddress), OrderPayment: MapPayment(request.Payment), - OrderItems: request.OrderItems.Select(x => - new OrderDto.OrderItem( - x.Id, - x.OrderId, - x.ProductId, - x.Quantity, - x.Price - )).ToList())); + OrderItems: MapOrderItems(request.OrderItems))); } - private static OrderDto.Address MapAddress(AddressDto addressDto) + private static OrderDto.Address? MapAddress(AddressDto? addressDto) { - return new OrderDto.Address( - Firstname: addressDto.Firstname, - Lastname: addressDto.Lastname, - EmailAddress: addressDto.EmailAddress, - AddressLine: addressDto.AddressLine, - Country: addressDto.Country, - State: addressDto.State, - ZipCode: addressDto.ZipCode); + return addressDto is null + ? null + : new OrderDto.Address( + Firstname: addressDto.Firstname, + Lastname: addressDto.Lastname, + EmailAddress: addressDto.EmailAddress, + AddressLine: addressDto.AddressLine, + Country: addressDto.Country, + State: addressDto.State, + ZipCode: addressDto.ZipCode); } - private static OrderDto.Payment MapPayment(PaymentDto paymentDto) + private static OrderDto.Payment? MapPayment(PaymentDto? paymentDto) { - return new OrderDto.Payment( + return paymentDto is null ? null : new OrderDto.Payment( paymentDto.CardName, paymentDto.CardNumber, paymentDto.Expiration, @@ -70,6 +65,17 @@ private static OrderDto.Payment MapPayment(PaymentDto paymentDto) paymentDto.PaymentMethod); } + private static List? MapOrderItems(List? orderItems) + { + return orderItems?.Select(x => + new OrderDto.OrderItem( + x.Id, + x.OrderId, + x.ProductId, + x.Quantity, + x.Price + )).ToList(); + } private static Response MapResult(CreateOrderResult result) { diff --git a/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Models.cs b/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Models.cs index ec6b4f7..ead52f9 100644 --- a/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Models.cs +++ b/src/Services/Order.Command/Order.Command.API/Endpoints/CreateOrder/Models.cs @@ -2,25 +2,26 @@ namespace Order.Command.API.Endpoints.CreateOrder; public record Request { - [property: JsonPropertyName("id")] public string Id { get; set; } + [property: JsonPropertyName("id")] + public string? Id { get; set; } [property: JsonPropertyName("customer_id")] - public string CustomerId { get; set; } + public string? CustomerId { get; set; } [property: JsonPropertyName("order_name")] - public string OrderName { get; set; } + public string? OrderName { get; set; } [property: JsonPropertyName("shipping_Address")] - public AddressDto ShippingAddress { get; set; } + public AddressDto? ShippingAddress { get; set; } [property: JsonPropertyName("billing_address")] - public AddressDto BillingAddress { get; set; } + public AddressDto? BillingAddress { get; set; } [property: JsonPropertyName("payment")] - public PaymentDto Payment { get; set; } + public PaymentDto? Payment { get; set; } [property: JsonPropertyName("order_items")] - public List OrderItems { get; set; } + public List? OrderItems { get; set; } }; public record AddressDto( @@ -34,7 +35,8 @@ public record AddressDto( string AddressLine, [property: JsonPropertyName("country")] string Country, - [property: JsonPropertyName("state")] string State, + [property: JsonPropertyName("state")] + string State, [property: JsonPropertyName("zip_code")] string ZipCode); @@ -57,7 +59,8 @@ public record OrderItemsDto( string ProductId, [property: JsonPropertyName("quantity")] int? Quantity, - [property: JsonPropertyName("price")] decimal? Price); + [property: JsonPropertyName("price")] + decimal? Price); public record Response( [property: JsonPropertyName("order_id")] diff --git a/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Endpoint.cs b/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Endpoint.cs index 0dd6359..ee34dae 100644 --- a/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Endpoint.cs +++ b/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Endpoint.cs @@ -1,35 +1,33 @@ using Order.Command.Application.Orders.Commands.DeleteOrder; +using Order.Command.Domain.Models.ValueObjects; namespace Order.Command.API.Endpoints.DeleteOrder; -public class Endpoint : EndpointBase +public class Endpoint : EndpointBase { public override void MapEndpoint() { Delete("/orders/{order_id}", HandleAsync); Name("DeleteOrder"); - Produces(StatusCodes.Status202Accepted); + Produces(StatusCodes.Status204NoContent); ProducesProblem(StatusCodes.Status400BadRequest); ProducesProblem(StatusCodes.Status404NotFound); + ProducesProblem(StatusCodes.Status412PreconditionFailed); Summary("Delete an existing order."); Description("Delete an existing order"); } public override async Task HandleAsync(Request request) { - var command = MapToCommand(request); - var result = await SendAsync(command).ConfigureAwait(false); - var response = MapToResponse(result); - return TypedResults.Ok(response); - } + var eTag = Context.Request.Headers.IfMatch; - private static DeleteOrderCommand MapToCommand(Request request) - { - return new DeleteOrderCommand(request.OrderId); + var command = MapToCommand(request, eTag); + await SendAsync(command).ConfigureAwait(false); + return TypedResults.NoContent(); } - private static Response MapToResponse(DeleteOrderResult result) + private static DeleteOrderCommand MapToCommand(Request request, string? etag) { - return new Response(result.IsSuccess); + return new DeleteOrderCommand(request.OrderId, etag); } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Models.cs b/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Models.cs index 2ee2a22..9b07a93 100644 --- a/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Models.cs +++ b/src/Services/Order.Command/Order.Command.API/Endpoints/DeleteOrder/Models.cs @@ -4,10 +4,5 @@ namespace Order.Command.API.Endpoints.DeleteOrder; public record Request { - [FromRoute(Name = "order_id")] - public string OrderId { get; set; } -} - -public record Response( - [property: JsonPropertyName("is_success")] - bool IsSuccess); \ No newline at end of file + [FromRoute(Name = "order_id")] public string? OrderId { get; set; } +} \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Endpoint.cs b/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Endpoint.cs index 4d25537..11f2ac9 100644 --- a/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Endpoint.cs +++ b/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Endpoint.cs @@ -1,3 +1,4 @@ +using Order.Command.Application.Dtos; using Order.Command.Application.Orders.Commands.UpdateOrder; namespace Order.Command.API.Endpoints.UpdateOrder; @@ -11,23 +12,64 @@ public override void MapEndpoint() Produces(StatusCodes.Status204NoContent); ProducesProblem(StatusCodes.Status400BadRequest); ProducesProblem(StatusCodes.Status404NotFound); + ProducesProblem(StatusCodes.Status412PreconditionFailed); Summary("Update an existing order."); Description("Update an existing order"); } public override async Task HandleAsync(Request request) { - var command = MapToCommand(request); - var result = await SendAsync(command).ConfigureAwait(false); - var response = MapToResponse(result); - return TypedResults.Ok(response); + var eTag = Context.Request.Headers.IfMatch; + + var command = MapToCommand(request, eTag); + await SendAsync(command).ConfigureAwait(false); + + return TypedResults.NoContent(); + } + + private static UpdateOrderCommand MapToCommand(Request request, string? eTag) + { + return new UpdateOrderCommand(new UpdateOrderDto( + request.Id, + request.CustomerId, + request.OrderName, + MapAddress(request.ShippingAddress), + MapAddress(request.BillingAddress), + MapPayment(request.Payment), + request.Status, + eTag, + request.OrderItems.Select(x => new OrderItems( + x.Id, + x.OrderId, + x.ProductId, + x.Quantity, + x.Price)).ToList() + )); } - private static UpdateOrderCommand MapToCommand(Request request) + private static AddressDto MapAddress(Address addressDto) { - return new UpdateOrderCommand(request.Order); + return new AddressDto( + Firstname: addressDto.Firstname, + Lastname: addressDto.Lastname, + EmailAddress: addressDto.EmailAddress, + AddressLine: addressDto.AddressLine, + Country: addressDto.Country, + State: addressDto.State, + ZipCode: addressDto.ZipCode); } + private static PaymentDto MapPayment(Payment paymentDto) + { + return new PaymentDto( + paymentDto.CardName, + paymentDto.CardNumber, + paymentDto.Expiration, + paymentDto.Cvv, + paymentDto.PaymentMethod); + } + + private static Response MapToResponse(UpdateOrderResult result) { return new Response(result.IsSuccess); diff --git a/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Models.cs b/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Models.cs index f988ce5..930bef9 100644 --- a/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Models.cs +++ b/src/Services/Order.Command/Order.Command.API/Endpoints/UpdateOrder/Models.cs @@ -5,9 +5,65 @@ namespace Order.Command.API.Endpoints.UpdateOrder; public record Request { - [FromBody] public UpdateOrderDto Order { get; set; } + [property: JsonPropertyName("id")] public string Id { get; set; } + + [property: JsonPropertyName("customer_id")] + public string? CustomerId { get; set; } + + [property: JsonPropertyName("order_name")] + public string OrderName { get; set; } + + [property: JsonPropertyName("shipping_Address")] + public Address ShippingAddress { get; set; } + + [property: JsonPropertyName("billing_address")] + public Address BillingAddress { get; set; } + + [property: JsonPropertyName("payment")] + public Payment Payment { get; set; } + + [property: JsonPropertyName("status")] public string Status { get; set; } + + [property: JsonPropertyName("order_items")] + public List OrderItems { get; set; } } +public record Address( + [property: JsonPropertyName("firstname")] + string Firstname, + [property: JsonPropertyName("lastname")] + string Lastname, + [property: JsonPropertyName("email_address")] + string EmailAddress, + [property: JsonPropertyName("address_line")] + string AddressLine, + [property: JsonPropertyName("country")] + string Country, + [property: JsonPropertyName("state")] string State, + [property: JsonPropertyName("zip_code")] + string ZipCode); + +public record Payment( + [property: JsonPropertyName("card_name")] + string CardName, + [property: JsonPropertyName("card_number")] + string CardNumber, + [property: JsonPropertyName("expiration")] + string Expiration, + [property: JsonPropertyName("cvv")] string Cvv, + [property: JsonPropertyName("payment_method")] + int PaymentMethod); + +public record OrderItem( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("order_id")] + string OrderId, + [property: JsonPropertyName("product_id")] + string ProductId, + [property: JsonPropertyName("quantity")] + int? Quantity, + [property: JsonPropertyName("price")] decimal? Price); + public record Response( [property: JsonPropertyName("success")] bool IsSuccess); \ No newline at end of file diff --git "a/src/Services/Order.Command/Order.Command.Application/Data/IApplicationDbContext\342\200\216.cs" "b/src/Services/Order.Command/Order.Command.Application/Data/IApplicationDbContext\342\200\216.cs" index 17b17ab..dcb2b4d 100644 --- "a/src/Services/Order.Command/Order.Command.Application/Data/IApplicationDbContext\342\200\216.cs" +++ "b/src/Services/Order.Command/Order.Command.Application/Data/IApplicationDbContext\342\200\216.cs" @@ -8,8 +8,9 @@ public interface IApplicationDbContext { DbSet Orders { get; } DbSet OrderItems { get; } - + DbSet Outboxes { get; } + DatabaseFacade Database { get; } - + Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Dtos/UpdateOrderDto.cs b/src/Services/Order.Command/Order.Command.Application/Dtos/UpdateOrderDto.cs index 3e61dc0..ba1e5b4 100644 --- a/src/Services/Order.Command/Order.Command.Application/Dtos/UpdateOrderDto.cs +++ b/src/Services/Order.Command/Order.Command.Application/Dtos/UpdateOrderDto.cs @@ -3,9 +3,9 @@ namespace Order.Command.Application.Dtos; public record UpdateOrderDto( - [property: JsonPropertyName("id")] Ulid Id, + [property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("customer_id")] - Guid? CustomerId, + string? CustomerId, [property: JsonPropertyName("order_name")] string OrderName, [property: JsonPropertyName("shipping_Address")] @@ -15,5 +15,6 @@ public record UpdateOrderDto( [property: JsonPropertyName("payment")] PaymentDto Payment, [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("etag")] string? Version, [property: JsonPropertyName("order_items")] List OrderItems); \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Extensions.cs b/src/Services/Order.Command/Order.Command.Application/Extensions.cs new file mode 100644 index 0000000..018bac9 --- /dev/null +++ b/src/Services/Order.Command/Order.Command.Application/Extensions.cs @@ -0,0 +1,80 @@ +using System.Text.RegularExpressions; +using FluentValidation; +using Microsoft.Net.Http.Headers; + +namespace Order.Command.Application; + +public static partial class ValidationExtensions +{ + public static void MustBeValidCardNumber( + this IRuleBuilder ruleBuilder + ) => ruleBuilder.Must(x => x is not null && CardNumberRegex().IsMatch(x)) + .WithMessage("card_number is not valid."); + + public static void MustBeValidExpiryDate(this IRuleBuilder ruleBuilder) + => ruleBuilder.Must(x => x is not null && ExpiryDateRegex().IsMatch(x)) + .WithMessage("payment expiration is not valid."); + + public static void MustBeValidEtag( + this IRuleBuilder ruleBuilder + ) => ruleBuilder.Must(x => x is not null && EtagRegex().IsMatch(x)) + .WithMessage($"{HeaderNames.IfMatch} header is not valid."); + + public static void MustBeValidCountryName( + this IRuleBuilder ruleBuilder + ) => ruleBuilder.Must(x => x is not null && CountryNames.Contains(x)) + .WithMessage($"country header is not valid."); + + public static void MustBeValidUlid(this IRuleBuilder ruleBuilder) => + ruleBuilder.NotEmpty().Must(x => Ulid.TryParse(x, out _)) + .WithMessage((_, propertyName)=> $"{propertyName} is not a invalid Ulid."); + + public static IRuleBuilderOptions MustBeValidGuid(this IRuleBuilder ruleBuilder) => + ruleBuilder.NotEmpty().Must(x => Guid.TryParse(x, out _)) + .WithMessage((_, propertyName) => $"{propertyName} is not a valid UUID"); + + [GeneratedRegex( + @"^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})$")] + private static partial Regex CardNumberRegex(); + + [GeneratedRegex(@"^(0[1-9]|1[0-2])\/\d{4}$")] + private static partial Regex ExpiryDateRegex(); + + [GeneratedRegex("""^W\/"\d+"$""")] + private static partial Regex EtagRegex(); + + private static readonly HashSet CountryNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + // The list includes the official country names based on the ISO 3166-1 standard. + "Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", + "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", + "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", + "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", + "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", + "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", + "Congo (Congo-Brazzaville)", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czechia (Czech Republic)", + "Denmark", "Djibouti", "Dominica", "Dominican Republic", "Ecuador", "Egypt", + "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini (fmr. Swaziland)", + "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", + "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", + "Haiti", "Holy See", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", + "Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", + "Kenya", "Kiribati", "Korea (North)", "Korea (South)", "Kuwait", "Kyrgyzstan", "Laos", + "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", + "Luxembourg", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", + "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", + "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar (formerly Burma)", + "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", + "Nigeria", "North Macedonia (formerly Macedonia)", "Norway", "Oman", "Pakistan", + "Palau", "Palestine State", "Panama", "Papua New Guinea", "Paraguay", "Peru", + "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", + "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", + "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", + "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", + "Somalia", "South Africa", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", + "Sweden", "Switzerland", "Syria", "Tajikistan", "Tanzania", "Thailand", "Timor-Leste", + "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", + "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States of America", + "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe" + }; +} \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Order.Command.Application.csproj b/src/Services/Order.Command/Order.Command.Application/Order.Command.Application.csproj index a3e265e..afc2a51 100644 --- a/src/Services/Order.Command/Order.Command.Application/Order.Command.Application.csproj +++ b/src/Services/Order.Command/Order.Command.Application/Order.Command.Application.csproj @@ -7,12 +7,12 @@ - - + + - + diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs index b2c9689..7f364d5 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/CreateOrderCommandHandler.cs @@ -1,3 +1,5 @@ +using System.Text.Json; + namespace Order.Command.Application.Orders.Commands.CreateOrder; public record CreateOrderCommand(OrderDto OrderDto) : ICommand; @@ -10,8 +12,18 @@ public class CreateOrderCommandHandler(IApplicationDbContext dbContext) public async Task Handle(CreateOrderCommand command, CancellationToken cancellationToken) { var order = MapOrder(command.OrderDto); + var outbox = MapOutbox(order); + + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); + dbContext.Orders.Add(order); + dbContext.Outboxes.Add(outbox); + + AddOrderCreatedEvent(order); + await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + await transaction.CommitAsync(cancellationToken); return new CreateOrderResult(order.Id.Value); } @@ -20,11 +32,11 @@ private static Domain.Models.Order MapOrder(OrderDto orderDtoDto) var order = new Domain.Models.Order().Create( id: OrderId.From(Ulid.Parse(orderDtoDto.Id)), customerId: CustomerId.From(Guid.Parse(orderDtoDto.CustomerId!)), - orderName: OrderName.From(orderDtoDto.OrderName), - shippingAddress: MapAddress(orderDtoDto.ShippingAddress), - billingAddress: MapAddress(orderDtoDto.BillingAddress), - payment: MapPayment(orderDtoDto.OrderPayment), - orderItems: orderDtoDto.OrderItems.Select(x => + orderName: OrderName.From(orderDtoDto.OrderName!), + shippingAddress: MapAddress(orderDtoDto.ShippingAddress!), + billingAddress: MapAddress(orderDtoDto.BillingAddress!), + payment: MapPayment(orderDtoDto.OrderPayment!), + orderItems: orderDtoDto.OrderItems!.Select(x => new OrderItem( orderId: OrderId.From(Ulid.Parse(orderDtoDto.Id)), productId: ProductId.From(Ulid.Parse(x.ProductId!)), @@ -51,4 +63,22 @@ private static Payment MapPayment(OrderDto.Payment paymentDto) => expiration: paymentDto.Expiration, cvv: paymentDto.Cvv, paymentMethod: paymentDto.PaymentMethod); + + private static void AddOrderCreatedEvent(Domain.Models.Order order) + { + order.AddDomainEvent(new OrderCreatedEvent(order)); + } + + private static Outbox MapOutbox(Domain.Models.Order order) + { + var outbox = new Outbox().Create( + aggregateId: AggregateId.From(order.Id.Value), + aggregateType: AggregateType.From(order.GetType().Name), + versionId: VersionId.From(order.RowVersion.Value), + dispatchDateTime: DispatchDateTime.ToIso8601UtcFormat(DateTimeOffset.UtcNow.AddMinutes(2)), + eventType: EventType.From(nameof(OrderCreatedEvent)), + payload: Payload.From(JsonSerializer.Serialize(order))); + + return outbox; + } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Models.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Models.cs index 6bc1c7c..0c6ea29 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Models.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Models.cs @@ -1,13 +1,13 @@ namespace Order.Command.Application.Orders.Commands.CreateOrder; public record OrderDto( - string Id, + string? Id, string? CustomerId, - string OrderName, - OrderDto.Address ShippingAddress, - OrderDto.Address BillingAddress, - OrderDto.Payment OrderPayment, - List OrderItems) + string? OrderName, + OrderDto.Address? ShippingAddress, + OrderDto.Address? BillingAddress, + OrderDto.Payment? OrderPayment, + List? OrderItems) { public record Address( string Firstname, @@ -17,23 +17,18 @@ public record Address( string Country, string State, string ZipCode); - + public record Payment( string CardName, string CardNumber, string Expiration, string Cvv, int PaymentMethod); - + public record OrderItem( string Id, string? OrderId, string? ProductId, int? Quantity, decimal? Price); -} - - - - - +} \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Validator.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Validator.cs index 4db9fb1..6bb239c 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Validator.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/CreateOrder/Validator.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using FluentValidation; namespace Order.Command.Application.Orders.Commands.CreateOrder; @@ -6,21 +7,57 @@ public class CreateOrderCommandValidator : AbstractValidator { public CreateOrderCommandValidator() { + RuleFor(x => x.OrderDto.Id).MustBeValidUlid(); RuleFor(x => x.OrderDto.OrderName).NotEmpty().WithMessage("Name is required."); - RuleFor(x => x.OrderDto.CustomerId).NotNull().WithMessage("CustomerId is required."); + RuleFor(x => x.OrderDto.CustomerId).MustBeValidGuid(); RuleFor(x => x.OrderDto.OrderItems).NotEmpty().WithMessage("OrderItems should not be empty."); RuleForEach(x => x.OrderDto.OrderItems).SetValidator(new OrderItemValidator()); + RuleFor(x => x.OrderDto.BillingAddress!).NotNull().SetValidator(new AddressValidator()); + RuleFor(x => x.OrderDto.ShippingAddress!).NotNull().SetValidator(new AddressValidator()); + RuleFor(x => x.OrderDto.OrderPayment!).NotNull().SetValidator(new PaymentValidator()); } - + + private class AddressValidator : AbstractValidator + { + public AddressValidator() + { + RuleFor(x => x.Firstname).NotNull().NotEmpty().WithMessage("firstname is required."); + RuleFor(x => x.Lastname).NotNull().NotEmpty().WithMessage("lastname is required."); + RuleFor(x => x.EmailAddress).NotNull().NotEmpty().WithMessage("email_address is required."); + RuleFor(x => x.EmailAddress) + .EmailAddress() + .WithMessage("Email is required.") + .When(x => !string.IsNullOrWhiteSpace(x.EmailAddress)); + + RuleFor(x => x.AddressLine).NotNull().NotEmpty().WithMessage("address_line is required."); + RuleFor(x => x.Country) + .MustBeValidCountryName(); + RuleFor(x => x.State).NotNull().NotEmpty().WithMessage("state is required."); + RuleFor(x => x.ZipCode).NotNull().NotEmpty().Length(5).WithMessage("zip_code is not valid."); + } + } + private class OrderItemValidator : AbstractValidator { public OrderItemValidator() { - RuleFor(x => x.OrderId).NotNull(); - RuleFor(x => x.ProductId).NotNull(); + RuleFor(x => x.OrderId).MustBeValidUlid(); + + RuleFor(x => x.ProductId).MustBeValidUlid(); RuleFor(x => x.Price).NotNull(); RuleFor(x => x.Quantity).NotNull(); } } + + private class PaymentValidator : AbstractValidator + { + public PaymentValidator() + { + RuleFor(x => x.Cvv).NotNull().NotEmpty().Length(3).WithMessage("CVV is not valid."); + RuleFor(x => x.CardName).NotNull().NotEmpty().WithMessage("card_name is required."); + RuleFor(x => x.CardNumber).NotNull().MustBeValidCardNumber(); + RuleFor(x => x.Expiration).NotNull().MustBeValidExpiryDate(); + } + } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs index b9bb904..7d7c948 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs @@ -1,8 +1,10 @@ +using System.Text.Json; +using BuildingBlocks.Exceptions; using Order.Command.Application.Exceptions; namespace Order.Command.Application.Orders.Commands.DeleteOrder; -public record DeleteOrderCommand(string OrderId) : ICommand; +public record DeleteOrderCommand(string OrderId, string? Version) : ICommand; public record DeleteOrderResult(bool IsSuccess); @@ -12,14 +14,59 @@ public class DeleteOrderCommandHandler(IApplicationDbContext dbContext) public async Task Handle(DeleteOrderCommand command, CancellationToken cancellationToken) { var orderId = OrderId.From(Ulid.Parse(command.OrderId)); + var version = VersionId.FromWeakEtag(command.Version!).Value; + + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); + var order = await dbContext.Orders.FindAsync([orderId], cancellationToken).ConfigureAwait(false); - if (order is null) - throw new OrderNotFoundExceptions(orderId.Value); + AssertOrder(order, command.OrderId, version); + var outbox = MapOutbox(order!); + + order!.Delete(order.RowVersion.Increment()); + + dbContext.Orders.Update(order); + dbContext.Outboxes.Add(outbox); + + AddOrderDeletedEvent(order); - dbContext.Orders.Remove(order); await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); return new DeleteOrderResult(true); } + + private static void AssertOrder(Domain.Models.Order? orderDb, string orderId, int version) + { + if (orderDb is null) + throw new OrderNotFoundExceptions(Ulid.Parse(orderId)); + + if (version != orderDb.RowVersion.Value) + throw new InvalidEtagException(version); + } + + private static void AddOrderDeletedEvent(Domain.Models.Order order) + { + var @event = MapOrderDeletedEvent(order); + order.AddDomainEvent(@event); + } + + private static Outbox MapOutbox(Domain.Models.Order order) + { + var payload = Payload.From(JsonSerializer.Serialize(MapOrderDeletedEvent(order))); + var outbox = new Outbox().Create( + aggregateId: AggregateId.From(order.Id.Value), + aggregateType: AggregateType.From(order.GetType().Name), + versionId: VersionId.From(order.RowVersion.Value).Increment(), + dispatchDateTime: DispatchDateTime.ToIso8601UtcFormat(DateTimeOffset.UtcNow.AddMinutes(2)), + eventType: EventType.From(nameof(OrderDeletedEvent)), + payload: payload); + + return outbox; + } + + private static OrderDeletedEvent MapOrderDeletedEvent(Domain.Models.Order order) + { + return new OrderDeletedEvent(order.Id, order.DeleteDate!, order.RowVersion); + } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/Validator.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/Validator.cs index df7dd00..f895386 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/Validator.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/DeleteOrder/Validator.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using FluentValidation; namespace Order.Command.Application.Orders.Commands.DeleteOrder; @@ -8,6 +9,8 @@ public Validator() { RuleFor(x => x.OrderId) .NotEmpty() - .Must(x => Guid.TryParse(x, out _)).WithMessage("Valid OrderId is required."); + .MustBeValidUlid(); + + RuleFor(x => x.Version).NotEmpty().MustBeValidEtag(); } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/UpdateOrderCommandHandler.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/UpdateOrderCommandHandler.cs index 9d5be63..a4d23b2 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/UpdateOrderCommandHandler.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/UpdateOrderCommandHandler.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using BuildingBlocks.Exceptions; using Order.Command.Application.Exceptions; namespace Order.Command.Application.Orders.Commands.UpdateOrder; @@ -11,30 +13,65 @@ public class UpdateOrderCommandHandler(IApplicationDbContext dbContext) { public async Task Handle(UpdateOrderCommand command, CancellationToken cancellationToken) { - var orderId = OrderId.From(command.Order.Id); - var orderDb = await dbContext.Orders.FindAsync(orderId, cancellationToken).ConfigureAwait(false); + var orderId = OrderId.From(Ulid.Parse(command.Order.Id)); + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); + var orderDb = await dbContext.Orders.FindAsync([orderId], cancellationToken: cancellationToken) + .ConfigureAwait(false); - if (orderDb is null) - throw new OrderNotFoundExceptions(orderId.Value); + AssertOrder(orderDb, command.Order); + UpdateOrderWithNewValues(command.Order, orderDb!); + AddOrderUpdatedEvent(orderDb!); + var outbox = MapOutbox(orderDb!); + + dbContext.Orders.Update(orderDb!); + dbContext.Outboxes.Add(outbox); - UpdateOrderWithNewValues(command.Order, orderDb); - dbContext.Orders.Update(orderDb); await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + return new UpdateOrderResult(true); } + private static void AssertOrder(Domain.Models.Order? orderDb, UpdateOrderDto orderDto) + { + if (orderDb is null) + throw new OrderNotFoundExceptions(Ulid.Parse(orderDto.Id)); + + var version = VersionId.FromWeakEtag(orderDto.Version!).Value; + if (version != orderDb.RowVersion.Value) + throw new InvalidEtagException(orderDto.Version!); + } + + private static void AddOrderUpdatedEvent(Domain.Models.Order order) + { + order.AddDomainEvent(new OrderUpdatedEvent(order)); + } + private static void UpdateOrderWithNewValues(UpdateOrderDto orderDto, Domain.Models.Order orderDb) { var shippingAddress = MapAddress(orderDto.ShippingAddress); var billingAddress = MapAddress(orderDto.BillingAddress); var payment = MapPayment(orderDto.Payment); + var orderItems = GetOrderItems(orderDto); orderDb.Update( OrderName.From(orderDto.OrderName), shippingAddress, billingAddress, payment, - MapOrderStatus(orderDto.Status)); + MapOrderStatus(orderDto.Status), + versionId: orderDb.RowVersion.Increment(), + orderItems: orderItems); + } + + private static List GetOrderItems(UpdateOrderDto orderDto) + { + return orderDto.OrderItems.Select(x => + new OrderItem( + orderId: OrderId.From(Ulid.Parse(orderDto.Id)), + productId: ProductId.From(Ulid.Parse(x.ProductId!)), + quantity: x.Quantity!.Value, + price: Price.From(x.Price!.Value))).ToList(); } private static Address MapAddress(AddressDto addressDto) @@ -69,4 +106,17 @@ private static OrderStatus MapOrderStatus(string status) _ => throw new ArgumentOutOfRangeException(nameof(status)) }; } + + private static Outbox MapOutbox(Domain.Models.Order order) + { + var outbox = new Outbox().Create( + aggregateId: AggregateId.From(order.Id.Value), + aggregateType: AggregateType.From(order.GetType().Name), + versionId: VersionId.From(order.RowVersion.Value), + dispatchDateTime: DispatchDateTime.ToIso8601UtcFormat(DateTimeOffset.UtcNow.AddMinutes(2)), + eventType: EventType.From(nameof(OrderUpdatedEvent)), + payload: Payload.From(JsonSerializer.Serialize(order))); + + return outbox; + } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/Validator.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/Validator.cs index e6c1405..915f38f 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/Validator.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Commands/UpdateOrder/Validator.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using FluentValidation; namespace Order.Command.Application.Orders.Commands.UpdateOrder; @@ -6,9 +7,12 @@ public class Validator : AbstractValidator { public Validator() { + RuleFor(x => x.Order.Id).MustBeValidUlid(); RuleFor(x => x.Order.OrderName).NotEmpty().WithMessage("Name is required."); - RuleFor(x => x.Order.CustomerId).NotNull().WithMessage("CustomerId is required."); + RuleFor(x => x.Order.CustomerId).MustBeValidGuid(); RuleFor(x => x.Order.OrderItems).NotEmpty().WithMessage("OrderItems should not be empty."); + RuleFor(x => x.Order.Version).MustBeValidEtag(); + RuleFor(x => x.Order.Status).NotEmpty().Must(value => value is OrderStatusDto.Cancelled or diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderByCustomer/GetOrderByCustomerQueryHandler.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderByCustomer/GetOrderByCustomerQueryHandler.cs index 35ee283..68b4ccd 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderByCustomer/GetOrderByCustomerQueryHandler.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderByCustomer/GetOrderByCustomerQueryHandler.cs @@ -14,7 +14,7 @@ public async Task Handle(GetOrderByCustomerQuery query var orders = await dbContext.Orders .Include(x => x.OrderItems) .AsNoTracking() - .Where(x => x.CustomerId!.Equals(CustomerId.From(customerId))) + .Where(x => x.CustomerId.Equals(CustomerId.From(customerId)) && x.DeleteDate == null) .OrderBy(x => x.OrderName) .ToListAsync(cancellationToken); diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderById/GetOrderByIdDto.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderById/GetOrderByIdDto.cs index 48d7f3a8..4b21559 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderById/GetOrderByIdDto.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrderById/GetOrderByIdDto.cs @@ -3,8 +3,7 @@ namespace Order.Command.Application.Orders.Queries.GetOrderById; public record GetOrderByIdDto( - [property: JsonPropertyName("id")] - Ulid Id, + [property: JsonPropertyName("id")] Ulid Id, [property: JsonPropertyName("customer_id")] Guid? CustomerId, [property: JsonPropertyName("order_name")] @@ -15,9 +14,7 @@ public record GetOrderByIdDto( AddressDto BillingAddress, [property: JsonPropertyName("payment")] PaymentDto Payment, - [property: JsonPropertyName("status")] - string Status, - [property: JsonPropertyName("version")] - int Version, + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("etag")] int Version, [property: JsonPropertyName("order_items")] List OrderItems); \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrders/GetOrdersQueryHandler.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrders/GetOrdersQueryHandler.cs index c834e3d..5a69ba6 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrders/GetOrdersQueryHandler.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrders/GetOrdersQueryHandler.cs @@ -18,6 +18,7 @@ public async Task Handle(GetOrdersQuery request, CancellationTo var orders = await dbContext.Orders .Include(x => x.OrderItems) + .Where(x => x.DeleteDate == null) .OrderBy(x => x.OrderName) .Skip(pageSize * pageIndex) .Take(pageSize) diff --git a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrdersByName/GetOrdersByNameQueryHandler.cs b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrdersByName/GetOrdersByNameQueryHandler.cs index 7c05741..10fcb3e 100644 --- a/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrdersByName/GetOrdersByNameQueryHandler.cs +++ b/src/Services/Order.Command/Order.Command.Application/Orders/Queries/GetOrdersByName/GetOrdersByNameQueryHandler.cs @@ -12,7 +12,7 @@ public async Task Handle(GetOrdersByNameQuery query, Canc var orders = await dbContext.Orders .Include(x => x.OrderItems) .AsNoTracking() - .Where(x => x.OrderName.Equals(OrderName.From(query.Name))) + .Where(x => x.OrderName.Equals(OrderName.From(query.Name)) && x.DeleteDate == null) .OrderBy(x => x.OrderName) .ToListAsync(cancellationToken); diff --git a/src/Services/Order.Command/Order.Command.Domain/Events/OrderDeletedEvent.cs b/src/Services/Order.Command/Order.Command.Domain/Events/OrderDeletedEvent.cs new file mode 100644 index 0000000..d449c8a --- /dev/null +++ b/src/Services/Order.Command/Order.Command.Domain/Events/OrderDeletedEvent.cs @@ -0,0 +1,5 @@ +using Order.Command.Domain.Models; + +namespace Order.Command.Domain.Events; + +public record OrderDeletedEvent(OrderId OrderId, DeleteDate DeleteDate, VersionId VersionId) : IDomainEvent; \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Domain/Events/OrderItemAddedEvent.cs b/src/Services/Order.Command/Order.Command.Domain/Events/OrderItemAddedEvent.cs deleted file mode 100644 index 60fe391..0000000 --- a/src/Services/Order.Command/Order.Command.Domain/Events/OrderItemAddedEvent.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Order.Command.Domain.Models; - -namespace Order.Command.Domain.Events; - -public record OrderItemAddedEvent(OrderItem OrderItem) : IDomainEvent; \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Domain/Events/OrderItemDeletedEvent.cs b/src/Services/Order.Command/Order.Command.Domain/Events/OrderItemDeletedEvent.cs deleted file mode 100644 index 3436d92..0000000 --- a/src/Services/Order.Command/Order.Command.Domain/Events/OrderItemDeletedEvent.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Order.Command.Domain.Models; - -namespace Order.Command.Domain.Events; - -public record OrderItemDeletedEvent(OrderItem OrderItem) : IDomainEvent; \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Domain/Models/Order.cs b/src/Services/Order.Command/Order.Command.Domain/Models/Order.cs index 209df6c..0a7075c 100644 --- a/src/Services/Order.Command/Order.Command.Domain/Models/Order.cs +++ b/src/Services/Order.Command/Order.Command.Domain/Models/Order.cs @@ -13,14 +13,14 @@ public class Order : Aggregate public Address BillingAddress { get; private set; } = default!; public Payment Payment { get; private set; } = default!; public OrderStatus Status { get; private set; } = default!; + public DeleteDate? DeleteDate { get; private set; } + + [ConcurrencyCheck] public VersionId RowVersion { get; set; } = default!; - [ConcurrencyCheck] - public VersionId RowVersion { get; set; } = default!; - public Price TotalPrice { get { return Price.From(OrderItems.Sum(x => x.Price.Value * x.Quantity)); } - set{ } + set { } } public Order Create( @@ -43,26 +43,27 @@ public Order Create( Status = OrderStatus.Pending, RowVersion = VersionId.InitialVersion }; - order._orderItems.AddRange(orderItems?.Select(x => new OrderItem(id, x.ProductId, x.Quantity, x.Price)).ToArray() ?? []); - order.AddDomainEvent(new OrderCreatedEvent(order)); - + order._orderItems.AddRange(orderItems?.Select(x => new OrderItem(id, x.ProductId, x.Quantity, x.Price)) + .ToArray() ?? []); return order; } - + public void Update( OrderName orderName, Address shippingAddress, Address billingAddress, Payment payment, - OrderStatus orderStatus) + OrderStatus orderStatus, + VersionId versionId, + List? orderItems = null) { OrderName = orderName; ShippingAddress = shippingAddress; BillingAddress = billingAddress; Payment = payment; Status = orderStatus; - - AddDomainEvent(new OrderUpdatedEvent(this)); + RowVersion = versionId; + _orderItems.AddRange(orderItems?.Select(x => new OrderItem(Id, x.ProductId, x.Quantity, x.Price)).ToArray() ?? []); } public void Add(ProductId productId, int quantity, Price price) @@ -72,17 +73,11 @@ public void Add(ProductId productId, int quantity, Price price) var orderItem = new OrderItem(Id, productId, quantity, price); _orderItems.Add(orderItem); - - AddDomainEvent(new OrderItemAddedEvent(orderItem)); - } - public void Remove(ProductId productId) + public void Delete(VersionId versionId) { - var orderItem = _orderItems.FirstOrDefault(x => x.ProductId == productId); - if (orderItem is null) return; - - _orderItems.Remove(orderItem); - AddDomainEvent(new OrderItemDeletedEvent(orderItem)); + DeleteDate = DeleteDate.ToIso8601UtcFormat(DateTimeOffset.UtcNow); + RowVersion = versionId; } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Domain/Models/Outbox.cs b/src/Services/Order.Command/Order.Command.Domain/Models/Outbox.cs new file mode 100644 index 0000000..70f77c5 --- /dev/null +++ b/src/Services/Order.Command/Order.Command.Domain/Models/Outbox.cs @@ -0,0 +1,45 @@ +namespace Order.Command.Domain.Models; + +public class Outbox : Aggregate +{ + public AggregateId AggregateId { get; private set; } = default!; + public AggregateType AggregateType { get; private set; } = default!; + public DispatchDateTime DispatchDateTime { get; private set; } = default!; + public VersionId VersionId { get; private set; } = default!; + public IsDispatched IsDispatched { get; private set; } = default!; + public NumberOfDispatchTry NumberOfDispatchTry { get; private set; } = default!; + public EventType EventType { get; private set; } = default!; + public Payload Payload { get; private set; } = default!; + + public Outbox Create( + AggregateId aggregateId, + AggregateType aggregateType, + VersionId versionId, + DispatchDateTime dispatchDateTime, + EventType eventType, + Payload payload) + { + var outbox = new Outbox + { + Id = OutboxId.From(Ulid.NewUlid()), + AggregateId = aggregateId, + AggregateType = aggregateType, + DispatchDateTime = dispatchDateTime, + IsDispatched = IsDispatched.No, + EventType = eventType, + VersionId = versionId, + NumberOfDispatchTry = NumberOfDispatchTry.InitialValue, + Payload = payload + }; + + return outbox; + } + + public void Update( + IsDispatched isDispatched, + NumberOfDispatchTry numberOfDispatchTry) + { + NumberOfDispatchTry = numberOfDispatchTry; + IsDispatched = isDispatched; + } +} \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Domain/Models/ValueObjects/ValueObjects.cs b/src/Services/Order.Command/Order.Command.Domain/Models/ValueObjects/ValueObjects.cs index 964e891..69a4bd9 100644 --- a/src/Services/Order.Command/Order.Command.Domain/Models/ValueObjects/ValueObjects.cs +++ b/src/Services/Order.Command/Order.Command.Domain/Models/ValueObjects/ValueObjects.cs @@ -1,4 +1,6 @@ +using System.Globalization; using System.Text.RegularExpressions; +using BuildingBlocks.Exceptions; using ValueOf; namespace Order.Command.Domain.Models.ValueObjects; @@ -15,10 +17,58 @@ public class OrderId : ValueOf { } +public class OutboxId : ValueOf +{ +} + +public class AggregateId : ValueOf +{ +} + +public class EventType : ValueOf +{ +} + +public class NumberOfDispatchTry : ValueOf +{ + public static NumberOfDispatchTry InitialValue => From(0); + public NumberOfDispatchTry Increment() => From(Value + 1); +} + +public class Payload : ValueOf +{ +} + +public class DeleteDate : ValueOf +{ + public static DeleteDate ToIso8601UtcFormat(DateTimeOffset dateTimeOffset) + { + return From(dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)); + } +} + +public class DispatchDateTime : ValueOf +{ + public static DispatchDateTime ToIso8601UtcFormat(DateTimeOffset dateTimeOffset) + { + return From(dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)); + } +} + +public class IsDispatched : ValueOf +{ + public static IsDispatched Yes => From(true); + public static IsDispatched No => From(false); +} + public class OrderItemId : ValueOf { } +public class AggregateType : ValueOf +{ +} + public partial class VersionId : ValueOf { public static VersionId InitialVersion => From(1); @@ -27,14 +77,14 @@ public static VersionId FromWeakEtag(string etag) { if (string.IsNullOrWhiteSpace(etag) || !EtagRegex().IsMatch(etag)) { - throw new InvalidOperationException($"Invalid Etag value: {etag}."); + throw new InvalidEtagException($"Invalid Etag value: {etag}."); } return From(int.Parse(etag[3..^1])); } public VersionId Increment() => From(Value + 1); - + [GeneratedRegex("""^W\/"\d+"$""")] private static partial Regex EtagRegex(); } diff --git a/src/Services/Order.Command/Order.Command.Domain/Order.Command.Domain.csproj b/src/Services/Order.Command/Order.Command.Domain/Order.Command.Domain.csproj index 1207db0..045a1a9 100644 --- a/src/Services/Order.Command/Order.Command.Domain/Order.Command.Domain.csproj +++ b/src/Services/Order.Command/Order.Command.Domain/Order.Command.Domain.csproj @@ -7,9 +7,13 @@ - - - + + + + + + + diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/ApplicationDbContext.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/ApplicationDbContext.cs index 29a7564..bd6e9e1 100644 --- a/src/Services/Order.Command/Order.Command.Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/ApplicationDbContext.cs @@ -10,6 +10,7 @@ public class ApplicationDbContext(DbContextOptions options { public DbSet Orders => Set(); public DbSet OrderItems => Set(); + public DbSet Outboxes => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OrderConfiguration.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OrderConfiguration.cs index 8e973ac..3287348 100644 --- a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OrderConfiguration.cs +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OrderConfiguration.cs @@ -15,10 +15,15 @@ public void Configure(EntityTypeBuilder builder) dbId => OrderId.From(Ulid.Parse(dbId))); builder.Property(p => p.RowVersion).HasConversion( - versionId => versionId.Value, - dbVersionId => VersionId.From(dbVersionId)) + versionId => versionId.Value, + dbVersionId => VersionId.From(dbVersionId)) .IsConcurrencyToken(); + + builder.Property(x => x.DeleteDate).HasConversion( + deleteDate => (deleteDate == null) ? null : deleteDate.Value, + dbDeleteDate => DeleteDate.From(dbDeleteDate ?? string.Empty)); + builder.Property(x => x.CustomerId).HasConversion( customerId => customerId.Value, dbCustomerId => CustomerId.From(dbCustomerId)) diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OutboxConfiguration.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OutboxConfiguration.cs new file mode 100644 index 0000000..a77807a --- /dev/null +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Configuration/OutboxConfiguration.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Order.Command.Domain.Models; +using Order.Command.Domain.Models.ValueObjects; + +namespace Order.Command.Infrastructure.Data.Configuration; + +public class OutboxConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.HasIndex(x => new { x.AggregateId, x.VersionId }).IsUnique(); + + builder.Property(x => x.Id).HasConversion( + id => id.ToString(), + dbId => OutboxId.From(Ulid.Parse(dbId))); + + builder.Property(x => x.AggregateId).HasConversion( + aggregateId => aggregateId.ToString(), + dbAggregateId => AggregateId.From(Ulid.Parse(dbAggregateId))); + + builder.Property(p => p.VersionId).HasConversion( + versionId => versionId.Value, + dbVersionId => VersionId.From(dbVersionId)) + .IsConcurrencyToken(); + + builder.Property(x => x.AggregateType).HasConversion( + aggregateType => aggregateType.Value, + dbAggregateType => AggregateType.From(dbAggregateType)); + + builder.Property(x => x.DispatchDateTime).HasConversion( + dispatchDateTime => dispatchDateTime.Value, + dbDispatchDateTime => DispatchDateTime.From(dbDispatchDateTime)); + + builder.Property(x => x.IsDispatched).HasConversion( + isDispatched => isDispatched.Value, + dbIsDispatched => IsDispatched.From(dbIsDispatched)); + + builder.Property(x => x.NumberOfDispatchTry).HasConversion( + numberOfDispatchTry => numberOfDispatchTry.Value, + dbNumberOfDispatchTry => NumberOfDispatchTry.From(dbNumberOfDispatchTry)); + + builder.Property(x => x.EventType).HasConversion( + eventType => eventType.Value, + dbEventType => EventType.From(dbEventType)); + + builder.Property(x => x.Payload).HasConversion( + payload => payload.Value, + dbPayload => Payload.From(dbPayload)); + } +} \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Extensions/DatabaseExtensions.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Extensions/DatabaseExtensions.cs index 58a0c3d..6fb50a2 100644 --- a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Extensions/DatabaseExtensions.cs +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Extensions/DatabaseExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Order.Command.Domain.Models; +using Order.Command.Domain.Models.ValueObjects; namespace Order.Command.Infrastructure.Data.Extensions; diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs index c2e0c7e..ac18724 100644 --- a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs @@ -21,7 +21,7 @@ public override ValueTask> SavingChangesAsync(DbContextE return base.SavingChangesAsync(eventData, result, cancellationToken); } - private static void UpdateEntities(DbContext? context) + private static void UpdateEntities(DbContext? context, string username = "test user") { if (context is null) return; @@ -30,14 +30,14 @@ private static void UpdateEntities(DbContext? context) if (entry.State == EntityState.Added) { entry.Entity.CreatedAt = DateTime.Now; - entry.Entity.CreatedBy = "test user"; + entry.Entity.CreatedBy = username; } if (entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.HasChangedOwnedEntities()) { entry.Entity.LastModified = DateTime.Now; - entry.Entity.LastModifiedBy = "test user"; + entry.Entity.LastModifiedBy = username; } } } @@ -50,6 +50,6 @@ public static bool HasChangedOwnedEntities(this EntityEntry entry) return entry.References.Any(x => x.TargetEntry != null && x.TargetEntry.Metadata.IsOwned() && - (x.TargetEntry.State == EntityState.Added || x.TargetEntry.State == EntityState.Modified)); + x.TargetEntry.State is EntityState.Added or EntityState.Modified); } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/DispatchDomainEventsInterceptor.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/DispatchDomainEventsInterceptor.cs index afb5686..891d188 100644 --- a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/DispatchDomainEventsInterceptor.cs +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Interceptors/DispatchDomainEventsInterceptor.cs @@ -9,21 +9,33 @@ public class DispatchDomainEventsInterceptor(IMediator mediator) : SaveChangesIn { public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) { - DispatchDomainEvents(eventData.Context).GetAwaiter().GetResult(); - return base.SavingChanges(eventData, result); + var eventToDispatch = GetDomainEvents(eventData.Context); + var interceptionResult = base.SavingChanges(eventData, result); + DispatchDomainEvents(eventToDispatch).GetAwaiter().GetResult(); + return interceptionResult; } public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = new()) { - DispatchDomainEvents(eventData.Context).GetAwaiter().GetResult(); - return base.SavingChangesAsync(eventData, result, cancellationToken); + var eventToDispatch = GetDomainEvents(eventData.Context); + var interceptionResult = base.SavingChangesAsync(eventData, result, cancellationToken); + DispatchDomainEvents(eventToDispatch).GetAwaiter().GetResult(); + return interceptionResult; } - private async Task DispatchDomainEvents(DbContext? context) + private async Task DispatchDomainEvents(List? domainEvents) { - if (context is null) return; + if (domainEvents is null) return; + + foreach (var domainEvent in domainEvents) + await mediator.Publish(domainEvent).ConfigureAwait(false); + } + + private static List? GetDomainEvents(DbContext? context) + { + if (context is null) return null; var entities = context.ChangeTracker .Entries() @@ -35,8 +47,6 @@ private async Task DispatchDomainEvents(DbContext? context) .ToList(); entities.ToList().ForEach(e => e.ClearDomainEvents()); - - foreach (var domainEvent in domainEvents) - await mediator.Publish(domainEvent).ConfigureAwait(false); + return domainEvents; } } \ No newline at end of file diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.Designer.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.Designer.cs new file mode 100644 index 0000000..7d686f6 --- /dev/null +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.Designer.cs @@ -0,0 +1,297 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Order.Command.Infrastructure.Data; + +#nullable disable + +namespace Order.Command.Infrastructure.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20241220065009_add outbox")] + partial class addoutbox + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Order.Command.Domain.Models.Order", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("uniqueidentifier"); + + b.Property("DeleteDate") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotalPrice") + .HasColumnType("decimal(18,2)"); + + b.ComplexProperty>("BillingAddress", "Order.Command.Domain.Models.Order.BillingAddress#Address", b1 => + { + b1.IsRequired(); + + b1.Property("AddressLine") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("nvarchar(180)"); + + b1.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("EmailAddress") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b1.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + }); + + b.ComplexProperty>("Payment", "Order.Command.Domain.Models.Order.Payment#Payment", b1 => + { + b1.IsRequired(); + + b1.Property("CVV") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b1.Property("CardName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("CardNumber") + .IsRequired() + .HasMaxLength(24) + .HasColumnType("nvarchar(24)"); + + b1.Property("Expiration") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b1.Property("PaymentMethod") + .HasColumnType("int"); + }); + + b.ComplexProperty>("ShippingAddress", "Order.Command.Domain.Models.Order.ShippingAddress#Address", b1 => + { + b1.IsRequired(); + + b1.Property("AddressLine") + .IsRequired() + .HasMaxLength(180) + .HasColumnType("nvarchar(180)"); + + b1.Property("Country") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("EmailAddress") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b1.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("State") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + }); + + b.HasKey("Id"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Order.Command.Domain.Models.OrderItem", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("OrderId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("OrderId1") + .HasColumnType("nvarchar(450)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("ProductId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("OrderId1"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Order.Command.Domain.Models.Outbox", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AggregateId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("AggregateType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DispatchDateTime") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDispatched") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfDispatchTry") + .HasColumnType("int"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionId") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregateId", "VersionId") + .IsUnique(); + + b.ToTable("Outboxes"); + }); + + modelBuilder.Entity("Order.Command.Domain.Models.OrderItem", b => + { + b.HasOne("Order.Command.Domain.Models.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Order.Command.Domain.Models.Order", null) + .WithMany("OrderItems") + .HasForeignKey("OrderId1"); + }); + + modelBuilder.Entity("Order.Command.Domain.Models.Order", b => + { + b.Navigation("OrderItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.cs new file mode 100644 index 0000000..24d8311 --- /dev/null +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/20241220065009_add outbox.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Order.Command.Infrastructure.Data.Migrations +{ + /// + public partial class addoutbox : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeleteDate", + table: "Orders", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.CreateTable( + name: "Outboxes", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + AggregateId = table.Column(type: "nvarchar(450)", nullable: false), + AggregateType = table.Column(type: "nvarchar(max)", nullable: false), + DispatchDateTime = table.Column(type: "nvarchar(max)", nullable: false), + VersionId = table.Column(type: "int", nullable: false), + IsDispatched = table.Column(type: "bit", nullable: false), + NumberOfDispatchTry = table.Column(type: "int", nullable: false), + EventType = table.Column(type: "nvarchar(max)", nullable: false), + Payload = table.Column(type: "nvarchar(max)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: true), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + LastModified = table.Column(type: "datetime2", nullable: true), + LastModifiedBy = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Outboxes", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Outboxes_AggregateId_VersionId", + table: "Outboxes", + columns: new[] { "AggregateId", "VersionId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Outboxes"); + + migrationBuilder.DropColumn( + name: "DeleteDate", + table: "Orders"); + } + } +} diff --git a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 6ae0fa4..7413a53 100644 --- a/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Services/Order.Command/Order.Command.Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -37,6 +37,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CustomerId") .HasColumnType("uniqueidentifier"); + b.Property("DeleteDate") + .HasColumnType("nvarchar(max)"); + b.Property("LastModified") .HasColumnType("datetime2"); @@ -213,6 +216,61 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OrderItems"); }); + modelBuilder.Entity("Order.Command.Domain.Models.Outbox", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AggregateId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("AggregateType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DispatchDateTime") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDispatched") + .HasColumnType("bit"); + + b.Property("LastModified") + .HasColumnType("datetime2"); + + b.Property("LastModifiedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("NumberOfDispatchTry") + .HasColumnType("int"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("VersionId") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AggregateId", "VersionId") + .IsUnique(); + + b.ToTable("Outboxes"); + }); + modelBuilder.Entity("Order.Command.Domain.Models.OrderItem", b => { b.HasOne("Order.Command.Domain.Models.Order", null)