diff --git a/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs b/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs index 4d629146d..958d4ea1f 100644 --- a/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs +++ b/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Net; using System.Threading.Tasks; using Core.Testing; using FluentAssertions; using Microsoft.AspNetCore.Hosting; +using Warehouse.Api.Tests.Products.RegisteringProduct; using Warehouse.Products.GettingProductDetails; -using Warehouse.Products.RegisteringProduct; using Xunit; namespace Warehouse.Api.Tests.Products.GettingProductDetails diff --git a/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs b/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs index a6c154b97..a51d45e15 100644 --- a/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs +++ b/Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -6,8 +6,8 @@ using Core.Testing; using FluentAssertions; using Microsoft.AspNetCore.Hosting; +using Warehouse.Api.Tests.Products.RegisteringProduct; using Warehouse.Products.GettingProducts; -using Warehouse.Products.RegisteringProduct; using Xunit; namespace Warehouse.Api.Tests.Products.GettingProducts diff --git a/Sample/Warehouse/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductRequest.cs b/Sample/Warehouse/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductRequest.cs new file mode 100644 index 000000000..406ef84a9 --- /dev/null +++ b/Sample/Warehouse/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductRequest.cs @@ -0,0 +1,8 @@ +namespace Warehouse.Api.Tests.Products.RegisteringProduct +{ + public record RegisterProductRequest( + string? SKU, + string? Name, + string? Description + ); +} diff --git a/Sample/Warehouse/Warehouse/Core/Commands/ICommandHandler.cs b/Sample/Warehouse/Warehouse/Core/Commands/ICommandHandler.cs index e13d72275..c34c1c8cf 100644 --- a/Sample/Warehouse/Warehouse/Core/Commands/ICommandHandler.cs +++ b/Sample/Warehouse/Warehouse/Core/Commands/ICommandHandler.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -8,7 +8,19 @@ namespace Warehouse.Core.Commands { public interface ICommandHandler { - ValueTask Handle(T command, CancellationToken token); + ValueTask Handle(T command, CancellationToken token); + } + + public record CommandResult + { + public object? Result { get; } + + private CommandResult(object? result = null) + => Result = result; + + public static CommandResult None => new(); + + public static CommandResult Of(object result) => new(result); } public static class CommandHandlerConfiguration @@ -37,7 +49,7 @@ public static ICommandHandler GetCommandHandler(this HttpContext context) => context.RequestServices.GetRequiredService>(); - public static ValueTask SendCommand(this HttpContext context, T command) + public static ValueTask SendCommand(this HttpContext context, T command) => context.GetCommandHandler() .Handle(command, context.RequestAborted); } diff --git a/Sample/Warehouse/Warehouse/Core/Extensions/EndpointsExtensions.cs b/Sample/Warehouse/Warehouse/Core/Extensions/EndpointsExtensions.cs new file mode 100644 index 000000000..9bd0415ac --- /dev/null +++ b/Sample/Warehouse/Warehouse/Core/Extensions/EndpointsExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Warehouse.Core.Commands; + +namespace Warehouse.Core.Extensions +{ +internal static class EndpointsExtensions +{ + internal static IEndpointRouteBuilder MapCommand( + this IEndpointRouteBuilder endpoints, + HttpMethod httpMethod, + string url, + HttpStatusCode statusCode = HttpStatusCode.OK) + { + endpoints.MapMethods(url, new []{httpMethod.ToString()} , async context => + { + var command = await context.FromBody(); + + var commandResult = await context.SendCommand(command); + + if (commandResult == CommandResult.None) + { + context.Response.StatusCode = (int)statusCode; + return; + } + + await context.ReturnJSON(commandResult.Result, statusCode); + }); + + return endpoints; + } +} +} diff --git a/Sample/Warehouse/Warehouse/Core/Extensions/HttpExtensions.cs b/Sample/Warehouse/Warehouse/Core/Extensions/HttpExtensions.cs index c8dbf755f..dc5b8a798 100644 --- a/Sample/Warehouse/Warehouse/Core/Extensions/HttpExtensions.cs +++ b/Sample/Warehouse/Warehouse/Core/Extensions/HttpExtensions.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.ComponentModel; using System.Net; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; diff --git a/Sample/Warehouse/Warehouse/Products/Configuration.cs b/Sample/Warehouse/Warehouse/Products/Configuration.cs index abbe5a1c7..2c96ce490 100644 --- a/Sample/Warehouse/Warehouse/Products/Configuration.cs +++ b/Sample/Warehouse/Warehouse/Products/Configuration.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Net; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Warehouse.Core.Commands; using Warehouse.Core.Entities; +using Warehouse.Core.Extensions; using Warehouse.Core.Queries; using Warehouse.Products.GettingProductDetails; using Warehouse.Products.GettingProducts; @@ -35,7 +38,7 @@ public static IServiceCollection AddProductServices(this IServiceCollection serv public static IEndpointRouteBuilder UseProductsEndpoints(this IEndpointRouteBuilder endpoints) => endpoints - .UseRegisterProductEndpoint() + .MapCommand(HttpMethod.Post, "/api/products", HttpStatusCode.Created) .UseGetProductsEndpoint() .UseGetProductDetailsEndpoint(); diff --git a/Sample/Warehouse/Warehouse/Products/GettingProductDetails/GetProductDetails.cs b/Sample/Warehouse/Warehouse/Products/GettingProductDetails/GetProductDetails.cs index 3862fbe53..ff39be4e3 100644 --- a/Sample/Warehouse/Warehouse/Products/GettingProductDetails/GetProductDetails.cs +++ b/Sample/Warehouse/Warehouse/Products/GettingProductDetails/GetProductDetails.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -19,7 +19,8 @@ public HandleGetProductDetails(IQueryable products) public async ValueTask Handle(GetProductDetails query, CancellationToken ct) { - // await is needed because of https://github.com/dotnet/efcore/issues/21793#issuecomment-667096367 + // btw. SingleOrDefaultAsync do not work properly with NullableReferenceTypes + // See more in: https://github.com/dotnet/efcore/issues/21793#issuecomment-667096367 var product = await products .SingleOrDefaultAsync(p => p.Id == query.ProductId, ct); diff --git a/Sample/Warehouse/Warehouse/Products/RegisteringProduct/RegisterProduct.cs b/Sample/Warehouse/Warehouse/Products/RegisteringProduct/RegisterProduct.cs index 507abff21..5f4adb0e3 100644 --- a/Sample/Warehouse/Warehouse/Products/RegisteringProduct/RegisterProduct.cs +++ b/Sample/Warehouse/Warehouse/Products/RegisteringProduct/RegisterProduct.cs @@ -1,12 +1,14 @@ -using System; +using System; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Warehouse.Core.Commands; +using Warehouse.Core.Primitives; using Warehouse.Products.Primitives; namespace Warehouse.Products.RegisteringProduct { - internal class HandleRegisterProduct : ICommandHandler + internal class HandleRegisterProduct: ICommandHandler { private readonly Func addProduct; private readonly Func> productWithSKUExists; @@ -20,47 +22,51 @@ Func> productWithSKUExists this.productWithSKUExists = productWithSKUExists; } - public async ValueTask Handle(RegisterProduct command, CancellationToken ct) + public async ValueTask Handle(RegisterProduct command, CancellationToken ct) { + var productId = Guid.NewGuid(); + var (skuValue, name, description) = command; + + var sku = SKU.Create(skuValue); + var product = new Product( - command.ProductId, - command.SKU, - command.Name, - command.Description + productId, + sku, + name, + description ); - if (await productWithSKUExists(command.SKU, ct)) + if (await productWithSKUExists(sku, ct)) throw new InvalidOperationException( - $"Product with SKU `{command.SKU} already exists."); + $"Product with SKU `{command.Sku} already exists."); await addProduct(product, ct); + + return CommandResult.Of(productId); } } public record RegisterProduct { - public Guid ProductId { get;} - - public SKU SKU { get; } + public string Sku { get; } public string Name { get; } public string? Description { get; } - private RegisterProduct(Guid productId, SKU sku, string name, string? description) + [JsonConstructor] + public RegisterProduct(string? sku, string? name, string? description) { - ProductId = productId; - SKU = sku; - Name = name; + Sku = sku.AssertNotEmpty(); + Name = name.AssertNotEmpty(); Description = description; } - public static RegisterProduct Create(Guid? id, string? sku, string? name, string? description) + public void Deconstruct(out string sku, out string name, out string? description) { - if (!id.HasValue) throw new ArgumentNullException(nameof(id)); - if (name == null) throw new ArgumentNullException(nameof(name)); - - return new RegisterProduct(id.Value, SKU.Create(sku), name, description); + sku = Sku; + name = Name; + description = Description; } } }