Skip to content

Commit

Permalink
order command api - add ConcurrencyToken
Browse files Browse the repository at this point in the history
  • Loading branch information
behdad088 committed Dec 17, 2024
1 parent 6c5d038 commit 5eaf875
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Order.Command.Application.Orders.Queries.GetOrderById;

namespace Order.Command.API.Endpoints.GetOrderById;

public class Endpoint : EndpointBase<Request, Response>
{
public override void MapEndpoint()
{
Get("/orders/{id}", HandleAsync);
Name("GetOrdersById");
Produces();
ProducesProblem(StatusCodes.Status400BadRequest);
ProducesProblem(StatusCodes.Status404NotFound);
Summary("Gets orders by Id.");
Description("Gets orders by Id");
}

public override async Task<IResult> HandleAsync(Request request)
{
var query = MapToQuery(request);
var result = await SendAsync(query).ConfigureAwait(false);
Context.Response.Headers.ETag = $"W/\"{result.Order.Version}\"";

var response = MapToResponse(result);
return TypedResults.Ok(response);
}

private static GetOrdersByIdQuery MapToQuery(Request request)
{
return new GetOrdersByIdQuery(request.Id);
}

private static Response MapToResponse(GetOrdersByIdResult result)
{
return new Response(result.Order);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Order.Command.Application.Orders.Queries.GetOrderById;

namespace Order.Command.API.Endpoints.GetOrderById;

public record Request
{
[FromRoute(Name = "id")] public string? Id { get; set; }
}

public record Response(GetOrderByIdDto Order);
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
namespace Order.Command.Application.Dtos;

public record OrderDto(
[property: JsonPropertyName("id")] Ulid Id,
[property: JsonPropertyName("id")]
Ulid Id,
[property: JsonPropertyName("customer_id")]
Guid? CustomerId,
[property: JsonPropertyName("order_name")]
Expand All @@ -14,6 +15,7 @@ public record OrderDto(
AddressDto BillingAddress,
[property: JsonPropertyName("payment")]
PaymentDto Payment,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("status")]
string Status,
[property: JsonPropertyName("order_items")]
List<OrderItems> OrderItems);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;

namespace Order.Command.Application.Orders.Queries.GetOrderById;

public record GetOrderByIdDto(
[property: JsonPropertyName("id")]
Ulid Id,
[property: JsonPropertyName("customer_id")]
Guid? CustomerId,
[property: JsonPropertyName("order_name")]
string OrderName,
[property: JsonPropertyName("shipping_Address")]
AddressDto ShippingAddress,
[property: JsonPropertyName("billing_address")]
AddressDto BillingAddress,
[property: JsonPropertyName("payment")]
PaymentDto Payment,
[property: JsonPropertyName("status")]
string Status,
[property: JsonPropertyName("version")]
int Version,
[property: JsonPropertyName("order_items")]
List<OrderItems> OrderItems);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using BuildingBlocks.Exceptions;
using Order.Command.Application.Exceptions;

namespace Order.Command.Application.Orders.Queries.GetOrderById;

public record GetOrdersByIdQuery(string? Id) : IQuery<GetOrdersByIdResult>;

public record GetOrdersByIdResult(GetOrderByIdDto Order);

public class GetOrderByIdHandler(IApplicationDbContext dbContext) : IQueryHandler<GetOrdersByIdQuery, GetOrdersByIdResult>
{
public async Task<GetOrdersByIdResult> Handle(GetOrdersByIdQuery request, CancellationToken cancellationToken)
{
var orderId = Ulid.Parse(request.Id);
var order = await dbContext.Orders
.Include(x => x.OrderItems)
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id.Equals(OrderId.From(orderId)), cancellationToken);

if (order is null)
throw new OrderNotFoundExceptions(orderId);

var result = MapResult(order);
return new GetOrdersByIdResult(result);
}

private static GetOrderByIdDto MapResult(Domain.Models.Order order)
{
var result = new GetOrderByIdDto(
order.Id.Value,
order.CustomerId.Value,
order.OrderName.Value,
MapAddress(order.ShippingAddress),
MapAddress(order.BillingAddress),
MapPayment(order.Payment),
order.Status.Value,
order.RowVersion.Value,
MapOrderItems(order.OrderItems));

return result;
}

private static AddressDto MapAddress(Address address)
{
return new AddressDto(address.FirstName, address.LastName, address.EmailAddress, address.AddressLine,
address.Country,
address.State, address.ZipCode);
}

private static PaymentDto MapPayment(Payment payment)
{
return new PaymentDto(payment.CardName, payment.CardNumber, payment.Expiration, payment.CVV,
payment.PaymentMethod);
}

private static List<OrderItems> MapOrderItems(IReadOnlyCollection<OrderItem> orderItems)
{
return orderItems.Select(x =>
new OrderItems(x.Id.Value.ToString(), x.OrderId.Value.ToString(), x.ProductId.Value.ToString(), x.Quantity,
x.Price.Value)).ToList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentValidation;

namespace Order.Command.Application.Orders.Queries.GetOrderById;

public class Validator : AbstractValidator<GetOrdersByIdQuery>
{
public Validator()
{
RuleFor(x => x.Id).NotEmpty().Must(x => Ulid.TryParse(x, out _)).WithMessage("Invalid order id");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public class Order : Aggregate<OrderId>
public Payment Payment { get; private set; } = default!;
public OrderStatus Status { get; private set; } = default!;

[Timestamp]
public byte[] RowVersion { get; set; } = default!;
[ConcurrencyCheck]
public VersionId RowVersion { get; set; } = default!;

public Price TotalPrice
{
Expand All @@ -25,7 +25,7 @@ public Price TotalPrice

public Order Create(
OrderId id,
CustomerId? customerId,
CustomerId customerId,
OrderName orderName,
Address shippingAddress,
Address billingAddress,
Expand All @@ -41,6 +41,7 @@ public Order Create(
BillingAddress = billingAddress,
Payment = payment,
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using ValueOf;

namespace Order.Command.Domain.Models.ValueObjects;
Expand All @@ -18,6 +19,26 @@ public class OrderItemId : ValueOf<Ulid, OrderItemId>
{
}

public partial class VersionId : ValueOf<int, VersionId>
{
public static VersionId InitialVersion => From(1);

public static VersionId FromWeakEtag(string etag)
{
if (string.IsNullOrWhiteSpace(etag) || !EtagRegex().IsMatch(etag))
{
throw new InvalidOperationException($"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();
}

public class ProductName : ValueOf<string, ProductName>
{
public static ProductName? FromNullable(string? value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ public void Configure(EntityTypeBuilder<Domain.Models.Order> builder)
id => id.ToString(),
dbId => OrderId.From(Ulid.Parse(dbId)));

builder.Property(p => p.RowVersion)
.IsRowVersion();
builder.Property(p => p.RowVersion).HasConversion(
versionId => versionId.Value,
dbVersionId => VersionId.From(dbVersionId))
.IsConcurrencyToken();

builder.Property(x => x.CustomerId).HasConversion(
customerId => customerId.Value,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
CustomerId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
OrderName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Status = table.Column<string>(type: "nvarchar(max)", nullable: false),
RowVersion = table.Column<byte[]>(type: "rowversion", rowVersion: true, nullable: false),
RowVersion = table.Column<int>(type: "int", nullable: false),
TotalPrice = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
BillingAddress_AddressLine = table.Column<string>(type: "nvarchar(180)", maxLength: 180, nullable: false),
BillingAddress_Country = table.Column<string>(type: "nvarchar(max)", nullable: false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");

b.Property<byte[]>("RowVersion")
b.Property<int>("RowVersion")
.IsConcurrencyToken()
.IsRequired()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
.HasColumnType("int");

b.Property<string>("Status")
.IsRequired()
Expand Down

0 comments on commit 5eaf875

Please sign in to comment.