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 17, 2021
1 parent b9341c4 commit fcb1f4c
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
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.GettingProducts;
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
Expand Up @@ -2,6 +2,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static T AssertNotNull<T>(this T? value, string? paramName = null)
}

public static string AssertNotEmpty(this string? value, string? paramName = null)
=> string.IsNullOrWhiteSpace(value) ? value! : throw new ArgumentOutOfRangeException(paramName);
=> !string.IsNullOrWhiteSpace(value) ? value! : throw new ArgumentOutOfRangeException(paramName);

public static T AssertNotEmpty<T>(this T value, string? paramName = null)
where T : struct
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,47 +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) 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;
}
}
}
36 changes: 0 additions & 36 deletions Sample/Warehouse/Warehouse/Products/RegisteringProduct/Route.cs

This file was deleted.

0 comments on commit fcb1f4c

Please sign in to comment.