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 May 24, 2021
1 parent 1aeddc6 commit 12b547f
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 64 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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.AspNetCore.Http.Headers;
Expand Down
5 changes: 4 additions & 1 deletion Sample/Warehouse/Warehouse/Products/Configuration.cs
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 @@ Func<SKU, CancellationToken, ValueTask<bool>> productWithSKUExists
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 12b547f

Please sign in to comment.