Skip to content

Commit

Permalink
Added example of MapCommand for extreme endpoints handling
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Sep 8, 2021
1 parent b480d9d commit 0af3b20
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 64 deletions.
Expand Up @@ -4,8 +4,8 @@
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
Expand Down
Expand Up @@ -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
Expand Down
@@ -0,0 +1,8 @@
namespace Warehouse.Api.Tests.Products.RegisteringProduct
{
public record RegisterProductRequest(
string? SKU,
string? Name,
string? Description
);
}
16 changes: 14 additions & 2 deletions Sample/Warehouse/Warehouse/Core/Commands/ICommandHandler.cs
Expand Up @@ -8,7 +8,19 @@ namespace Warehouse.Core.Commands
{
public interface ICommandHandler<in T>
{
ValueTask Handle(T command, CancellationToken token);
ValueTask<CommandResult> 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
Expand Down Expand Up @@ -37,7 +49,7 @@ public static ICommandHandler<T> GetCommandHandler<T>(this HttpContext context)
=> context.RequestServices.GetRequiredService<ICommandHandler<T>>();


public static ValueTask SendCommand<T>(this HttpContext context, T command)
public static ValueTask<CommandResult> SendCommand<T>(this HttpContext context, T command)
=> context.GetCommandHandler<T>()
.Handle(command, context.RequestAborted);
}
Expand Down
36 changes: 36 additions & 0 deletions 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<TRequest>(
this IEndpointRouteBuilder endpoints,
HttpMethod httpMethod,
string url,
HttpStatusCode statusCode = HttpStatusCode.OK)
{
endpoints.MapMethods(url, new []{httpMethod.ToString()} , async context =>
{
var command = await context.FromBody<TRequest>();
var commandResult = await context.SendCommand(command);
if (commandResult == CommandResult.None)
{
context.Response.StatusCode = (int)statusCode;
return;
}
await context.ReturnJSON(commandResult.Result, statusCode);
});

return endpoints;
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
Expand Down
5 changes: 4 additions & 1 deletion Sample/Warehouse/Warehouse/Products/Configuration.cs
@@ -1,9 +1,12 @@
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;
Expand Down Expand Up @@ -35,7 +38,7 @@ public static IServiceCollection AddProductServices(this IServiceCollection serv

public static IEndpointRouteBuilder UseProductsEndpoints(this IEndpointRouteBuilder endpoints) =>
endpoints
.UseRegisterProductEndpoint()
.MapCommand<RegisterProduct>(HttpMethod.Post, "/api/products", HttpStatusCode.Created)
.UseGetProductsEndpoint()
.UseGetProductDetailsEndpoint();

Expand Down
Expand Up @@ -19,7 +19,8 @@ public HandleGetProductDetails(IQueryable<Product> products)

public async ValueTask<ProductDetails?> 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);

Expand Down
@@ -1,12 +1,14 @@
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<RegisterProduct>
internal class HandleRegisterProduct: ICommandHandler<RegisterProduct>
{
private readonly Func<Product, CancellationToken, ValueTask> addProduct;
private readonly Func<SKU, CancellationToken, ValueTask<bool>> productWithSKUExists;
Expand All @@ -20,49 +22,51 @@ internal class HandleRegisterProduct : ICommandHandler<RegisterProduct>
this.productWithSKUExists = productWithSKUExists;
}

public async ValueTask Handle(RegisterProduct command, CancellationToken ct)
public async ValueTask<CommandResult> 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 || id == Guid.Empty) throw new ArgumentOutOfRangeException(nameof(id));
if (string.IsNullOrEmpty(sku)) throw new ArgumentOutOfRangeException(nameof(sku));
if (string.IsNullOrEmpty(name)) throw new ArgumentOutOfRangeException(nameof(name));
if (description is "") throw new ArgumentOutOfRangeException(nameof(name));

return new RegisterProduct(id.Value, SKU.Create(sku), name, description);
sku = Sku;
name = Name;
description = Description;
}
}
}
36 changes: 0 additions & 36 deletions Sample/Warehouse/Warehouse/Products/RegisteringProduct/Route.cs

This file was deleted.

0 comments on commit 0af3b20

Please sign in to comment.