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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.Extensions.Primitives;
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.