Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added example of MapCommand for extreme endpoints handling #43

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.