Skip to content

Commit 008724c

Browse files
committed
Added example of MapCommand for extreme endpoints handling
1 parent 9c348dc commit 008724c

File tree

10 files changed

+93
-62
lines changed

10 files changed

+93
-62
lines changed

Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
using Core.Testing;
55
using FluentAssertions;
66
using Microsoft.AspNetCore.Hosting;
7+
using Warehouse.Api.Tests.Products.RegisteringProduct;
78
using Warehouse.Products.GettingProductDetails;
8-
using Warehouse.Products.RegisteringProduct;
99
using Xunit;
1010

1111
namespace Warehouse.Api.Tests.Products.GettingProductDetails

Sample/Warehouse/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
using Core.Testing;
77
using FluentAssertions;
88
using Microsoft.AspNetCore.Hosting;
9+
using Warehouse.Api.Tests.Products.RegisteringProduct;
910
using Warehouse.Products.GettingProducts;
10-
using Warehouse.Products.RegisteringProduct;
1111
using Xunit;
1212

1313
namespace Warehouse.Api.Tests.Products.GettingProducts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Warehouse.Api.Tests.Products.RegisteringProduct
2+
{
3+
public record RegisterProductRequest(
4+
string? SKU,
5+
string? Name,
6+
string? Description
7+
);
8+
}

Sample/Warehouse/Warehouse/Core/Commands/ICommandHandler.cs

+14-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@ namespace Warehouse.Core.Commands
88
{
99
public interface ICommandHandler<in T>
1010
{
11-
ValueTask Handle(T command, CancellationToken token);
11+
ValueTask<CommandResult> Handle(T command, CancellationToken token);
12+
}
13+
14+
public record CommandResult
15+
{
16+
public object? Result { get; }
17+
18+
private CommandResult(object? result = null)
19+
=> Result = result;
20+
21+
public static CommandResult None => new();
22+
23+
public static CommandResult Of(object result) => new(result);
1224
}
1325

1426
public static class CommandHandlerConfiguration
@@ -37,7 +49,7 @@ public static ICommandHandler<T> GetCommandHandler<T>(this HttpContext context)
3749
=> context.RequestServices.GetRequiredService<ICommandHandler<T>>();
3850

3951

40-
public static ValueTask SendCommand<T>(this HttpContext context, T command)
52+
public static ValueTask<CommandResult> SendCommand<T>(this HttpContext context, T command)
4153
=> context.GetCommandHandler<T>()
4254
.Handle(command, context.RequestAborted);
4355
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Net;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Routing;
5+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
6+
using Warehouse.Core.Commands;
7+
8+
namespace Warehouse.Core.Extensions
9+
{
10+
internal static class EndpointsExtensions
11+
{
12+
internal static IEndpointRouteBuilder MapCommand<TRequest>(
13+
this IEndpointRouteBuilder endpoints,
14+
HttpMethod httpMethod,
15+
string url,
16+
HttpStatusCode statusCode = HttpStatusCode.OK)
17+
{
18+
endpoints.MapMethods(url, new []{httpMethod.ToString()} , async context =>
19+
{
20+
var command = await context.FromBody<TRequest>();
21+
22+
var commandResult = await context.SendCommand(command);
23+
24+
if (commandResult == CommandResult.None)
25+
{
26+
context.Response.StatusCode = (int)statusCode;
27+
return;
28+
}
29+
30+
await context.ReturnJSON(commandResult.Result, statusCode);
31+
});
32+
33+
return endpoints;
34+
}
35+
}
36+
}

Sample/Warehouse/Warehouse/Core/Extensions/HttpExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.ComponentModel;
33
using System.Net;
4+
using System.Text.Json;
45
using System.Threading.Tasks;
56
using Microsoft.AspNetCore.Http;
67
using Microsoft.AspNetCore.Http.Headers;

Sample/Warehouse/Warehouse/Products/Configuration.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
using System.Collections.Generic;
2+
using System.Net;
23
using Microsoft.AspNetCore.Routing;
4+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
35
using Microsoft.EntityFrameworkCore;
46
using Microsoft.Extensions.DependencyInjection;
57
using Warehouse.Core.Commands;
68
using Warehouse.Core.Entities;
9+
using Warehouse.Core.Extensions;
710
using Warehouse.Core.Queries;
811
using Warehouse.Products.GettingProductDetails;
912
using Warehouse.Products.GettingProducts;
@@ -35,7 +38,7 @@ public static IServiceCollection AddProductServices(this IServiceCollection serv
3538

3639
public static IEndpointRouteBuilder UseProductsEndpoints(this IEndpointRouteBuilder endpoints) =>
3740
endpoints
38-
.UseRegisterProductEndpoint()
41+
.MapCommand<RegisterProduct>(HttpMethod.Post, "/api/products", HttpStatusCode.Created)
3942
.UseGetProductsEndpoint()
4043
.UseGetProductDetailsEndpoint();
4144

Sample/Warehouse/Warehouse/Products/GettingProductDetails/GetProductDetails.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public HandleGetProductDetails(IQueryable<Product> products)
1919

2020
public async ValueTask<ProductDetails?> Handle(GetProductDetails query, CancellationToken ct)
2121
{
22-
// await is needed because of https://github.com/dotnet/efcore/issues/21793#issuecomment-667096367
22+
// btw. SingleOrDefaultAsync do not work properly with NullableReferenceTypes
23+
// See more in: https://github.com/dotnet/efcore/issues/21793#issuecomment-667096367
2324
var product = await products
2425
.SingleOrDefaultAsync(p => p.Id == query.ProductId, ct);
2526

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
using System;
2+
using System.Text.Json.Serialization;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using Warehouse.Core.Commands;
6+
using Warehouse.Core.Primitives;
57
using Warehouse.Products.Primitives;
68

79
namespace Warehouse.Products.RegisteringProduct
810
{
9-
internal class HandleRegisterProduct : ICommandHandler<RegisterProduct>
11+
internal class HandleRegisterProduct: ICommandHandler<RegisterProduct>
1012
{
1113
private readonly Func<Product, CancellationToken, ValueTask> addProduct;
1214
private readonly Func<SKU, CancellationToken, ValueTask<bool>> productWithSKUExists;
@@ -20,47 +22,51 @@ Func<SKU, CancellationToken, ValueTask<bool>> productWithSKUExists
2022
this.productWithSKUExists = productWithSKUExists;
2123
}
2224

23-
public async ValueTask Handle(RegisterProduct command, CancellationToken ct)
25+
public async ValueTask<CommandResult> Handle(RegisterProduct command, CancellationToken ct)
2426
{
27+
var productId = Guid.NewGuid();
28+
var (skuValue, name, description) = command;
29+
30+
var sku = SKU.Create(skuValue);
31+
2532
var product = new Product(
26-
command.ProductId,
27-
command.SKU,
28-
command.Name,
29-
command.Description
33+
productId,
34+
sku,
35+
name,
36+
description
3037
);
3138

32-
if (await productWithSKUExists(command.SKU, ct))
39+
if (await productWithSKUExists(sku, ct))
3340
throw new InvalidOperationException(
34-
$"Product with SKU `{command.SKU} already exists.");
41+
$"Product with SKU `{command.Sku} already exists.");
3542

3643
await addProduct(product, ct);
44+
45+
return CommandResult.Of(productId);
3746
}
3847
}
3948

4049
public record RegisterProduct
4150
{
42-
public Guid ProductId { get;}
43-
44-
public SKU SKU { get; }
51+
public string Sku { get; }
4552

4653
public string Name { get; }
4754

4855
public string? Description { get; }
4956

50-
private RegisterProduct(Guid productId, SKU sku, string name, string? description)
57+
[JsonConstructor]
58+
public RegisterProduct(string? sku, string? name, string? description)
5159
{
52-
ProductId = productId;
53-
SKU = sku;
54-
Name = name;
60+
Sku = sku.AssertNotEmpty();
61+
Name = name.AssertNotEmpty();
5562
Description = description;
5663
}
5764

58-
public static RegisterProduct Create(Guid? id, string? sku, string? name, string? description)
65+
public void Deconstruct(out string sku, out string name, out string? description)
5966
{
60-
if (!id.HasValue) throw new ArgumentNullException(nameof(id));
61-
if (name == null) throw new ArgumentNullException(nameof(name));
62-
63-
return new RegisterProduct(id.Value, SKU.Create(sku), name, description);
67+
sku = Sku;
68+
name = Name;
69+
description = Description;
6470
}
6571
}
6672
}

Sample/Warehouse/Warehouse/Products/RegisteringProduct/Route.cs

-36
This file was deleted.

0 commit comments

Comments
 (0)