Skip to content
Merged
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
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;

namespace Eventuous.AspNetCore.Web;
Expand All @@ -14,11 +15,16 @@ public CommandServiceRouteBuilder(IEndpointRouteBuilder builder)
/// the <seealso cref="HttpCommandAttribute"/> if you need a custom route.
/// </summary>
/// <param name="enrichCommand">A function to populate command props from HttpContext</param>
/// <param name="configure">Additional route configuration</param>
/// <typeparam name="TCommand">Command class</typeparam>
/// <returns></returns>
public CommandServiceRouteBuilder<T> MapCommand<TCommand>(EnrichCommandFromHttpContext<TCommand>? enrichCommand = null)
public CommandServiceRouteBuilder<T> MapCommand<TCommand>(
EnrichCommandFromHttpContext<TCommand>? enrichCommand = null,
Action<RouteHandlerBuilder>? configure = null
)
where TCommand : class {
_builder.MapCommand<TCommand, T>(enrichCommand);
var builder = _builder.MapCommand<TCommand, T>(enrichCommand);
configure?.Invoke(builder);
return this;
}

Expand All @@ -27,13 +33,16 @@ public CommandServiceRouteBuilder<T> MapCommand<TCommand>(EnrichCommandFromHttpC
/// </summary>
/// <param name="route">HTTP route for the command</param>
/// <param name="enrichCommand">A function to populate command props from HttpContext</param>
/// <param name="configure">Additional route configuration</param>
/// <typeparam name="TCommand">Command type</typeparam>
/// <returns></returns>
public CommandServiceRouteBuilder<T> MapCommand<TCommand>(
string route,
EnrichCommandFromHttpContext<TCommand>? enrichCommand = null
EnrichCommandFromHttpContext<TCommand>? enrichCommand = null,
Action<RouteHandlerBuilder>? configure = null
) where TCommand : class {
_builder.MapCommand<TCommand, T>(route, enrichCommand);
var builder = _builder.MapCommand<TCommand, T>(route, enrichCommand);
configure?.Invoke(builder);
return this;
}

Expand All @@ -43,22 +52,38 @@ public CommandServiceRouteBuilder<T> MapCommand<TCommand>(
/// </summary>
/// <param name="route"></param>
/// <param name="enrichCommand"></param>
/// <param name="configure">Additional route configuration</param>
/// <typeparam name="TContract"></typeparam>
/// <typeparam name="TCommand"></typeparam>
/// <returns></returns>
public CommandServiceRouteBuilder<T> MapCommand<TContract, TCommand>(
string route,
ConvertAndEnrichCommand<TContract, TCommand> enrichCommand
ConvertAndEnrichCommand<TContract, TCommand> enrichCommand,
Action<RouteHandlerBuilder>? configure = null
) where TCommand : class where TContract : class {
_builder.MapCommand<TContract, TCommand, T>(route, Ensure.NotNull(enrichCommand));
var builder = _builder.MapCommand<TContract, TCommand, T>(route, Ensure.NotNull(enrichCommand));
configure?.Invoke(builder);
return this;
}

public CommandServiceRouteBuilder<T> MapCommand<TContract, TCommand>(ConvertAndEnrichCommand<TContract, TCommand> enrichCommand)
/// <summary>
/// Maps the given command type to an HTTP endpoint using the route from the <see cref="HttpCommandAttribute"/> attribute.
/// Allows conversion between HTTP contract and command type.
/// </summary>
/// <param name="enrichCommand"></param>
/// <param name="configure">Additional route configuration</param>
/// <typeparam name="TContract"></typeparam>
/// <typeparam name="TCommand"></typeparam>
/// <returns></returns>
public CommandServiceRouteBuilder<T> MapCommand<TContract, TCommand>(
ConvertAndEnrichCommand<TContract, TCommand> enrichCommand,
Action<RouteHandlerBuilder>? configure = null
)
where TCommand : class where TContract : class {
var attr = typeof(TContract).GetAttribute<HttpCommandAttribute>();
AttributeCheck.EnsureCorrectAggregate<TContract, T>(attr);
_builder.MapCommand<TContract, TCommand, T>(attr?.Route, Ensure.NotNull(enrichCommand));
var builder = _builder.MapCommand<TContract, TCommand, T>(attr?.Route, Ensure.NotNull(enrichCommand));
configure?.Invoke(builder);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,20 @@ namespace Eventuous.AspNetCore.Web;
[PublicAPI]
[AttributeUsage(AttributeTargets.Class)]
public class HttpCommandAttribute : Attribute {
/// <summary>
/// HTTP POST route for the command
/// </summary>
public string? Route { get; set; }

/// <summary>
/// Aggregate type for the command, will be used to resolve the command service
/// </summary>
public Type? AggregateType { get; set; }

/// <summary>
/// Authorization policy name
/// </summary>
public string? PolicyName { get; set; }
}

public class HttpCommandAttribute<T> : HttpCommandAttribute where T : Aggregate {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Reflection;
using Eventuous.AspNetCore.Web;
using Eventuous.AspNetCore.Web.Diagnostics;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

Expand All @@ -12,7 +14,8 @@ namespace Microsoft.AspNetCore.Routing;

public static partial class RouteBuilderExtensions {
/// <summary>
/// Allows to add an HTTP endpoint for controller-less apps
/// Map command to HTTP POST endpoint.
/// The HTTP command type should be annotated with <seealso cref="HttpCommandAttribute"/> attribute.
/// </summary>
/// <param name="builder">Endpoint route builder instance</param>
/// <param name="enrichCommand">A function to populate command props from HttpContext</param>
Expand All @@ -24,33 +27,35 @@ public static RouteHandlerBuilder MapCommand<TCommand, TAggregate>(
this IEndpointRouteBuilder builder,
EnrichCommandFromHttpContext<TCommand>? enrichCommand = null
) where TAggregate : Aggregate where TCommand : class {
var attr = typeof(TCommand).GetAttribute<HttpCommandAttribute>();
return builder.MapCommand<TCommand, TAggregate>(attr?.Route, enrichCommand);
var attr = typeof(TCommand).GetAttribute<HttpCommandAttribute>();
return builder.MapCommand<TCommand, TAggregate>(attr?.Route, enrichCommand, attr?.PolicyName);
}

/// <summary>
/// Allows to add an HTTP endpoint for controller-less apps
/// Map command to HTTP POST endpoint.
/// </summary>
/// <param name="builder">Endpoint route builder instance</param>
/// <param name="route">HTTP API route</param>
/// <param name="enrichCommand">A function to populate command props from HttpContext</param>
/// <param name="policyName">Authorization policy</param>
/// <typeparam name="TCommand">Command type</typeparam>
/// <typeparam name="TAggregate">Aggregate type on which the command will operate</typeparam>
/// <returns></returns>
[PublicAPI]
public static RouteHandlerBuilder MapCommand<TCommand, TAggregate>(
this IEndpointRouteBuilder builder,
string? route,
EnrichCommandFromHttpContext<TCommand>? enrichCommand = null
EnrichCommandFromHttpContext<TCommand>? enrichCommand = null,
string? policyName = null
) where TAggregate : Aggregate where TCommand : class {
return Map<TAggregate, TCommand, TCommand>(
builder,
route,
enrichCommand != null
? (command, context) => enrichCommand(command, context)
: (command, _) => command
: (command, _) => command,
policyName
);

}

/// <summary>
Expand Down Expand Up @@ -109,7 +114,7 @@ void MapAssemblyCommands(Assembly assembly) {
);

var genericMethod = method.MakeGenericMethod(typeof(TAggregate), type, type);
genericMethod.Invoke(null, new object?[] { builder, attr.Route, null });
genericMethod.Invoke(null, new object?[] { builder, attr.Route, null, attr.PolicyName });
}
}

Expand All @@ -119,15 +124,16 @@ void MapAssemblyCommands(Assembly assembly) {
static RouteHandlerBuilder Map<TAggregate, TContract, TCommand>(
IEndpointRouteBuilder builder,
string? route,
ConvertAndEnrichCommand<TContract, TCommand>? convert = null
ConvertAndEnrichCommand<TContract, TCommand>? convert = null,
string? policyName = null
) where TAggregate : Aggregate where TCommand : class where TContract : class {
if (convert == null && typeof(TCommand) != typeof(TContract))
throw new InvalidOperationException($"Command type {typeof(TCommand).Name} is not assignable from {typeof(TContract).Name}");

var resolvedRoute = GetRoute<TContract>(route);
ExtensionsEventSource.Log.HttpEndpointRegistered<TContract>(resolvedRoute);

return builder
var routeBuilder = builder
.MapPost(
resolvedRoute,
async Task<IResult>(HttpContext context, ICommandService<TAggregate> service) => {
Expand All @@ -143,11 +149,16 @@ async Task<IResult>(HttpContext context, ICommandService<TAggregate> service) =>
return result.AsResult();
}
)
.Accepts<TCommand>(false, "application/json")
.Accepts<TContract>(false, "application/json")
.Produces<Result>()
.Produces<ErrorResult>(StatusCodes.Status404NotFound)
.Produces<ErrorResult>(StatusCodes.Status409Conflict)
.Produces<ErrorResult>(StatusCodes.Status400BadRequest);

routeBuilder.AddPolicy(policyName);
routeBuilder.AddAuthorization(typeof(TContract));

return routeBuilder;
}

/// <summary>
Expand Down Expand Up @@ -186,15 +197,15 @@ void MapAssemblyCommands(Assembly assembly) {
var parentAttribute = type.DeclaringType?.GetAttribute<AggregateCommandsAttribute>();
if (parentAttribute == null) continue;

LocalMap(parentAttribute.AggregateType, type, attr.Route);
LocalMap(parentAttribute.AggregateType, type, attr.Route, attr.PolicyName);
}
}

void LocalMap(Type aggregateType, Type type, string? route) {
void LocalMap(Type aggregateType, Type type, string? route, string? policyName) {
var appServiceBase = typeof(ICommandService<>);
var appServiceType = appServiceBase.MakeGenericType(aggregateType);

builder
var routeBuilder = builder
.MapPost(
GetRoute(type, route),
async Task<IResult>(HttpContext context) => {
Expand All @@ -215,6 +226,9 @@ async Task<IResult>(HttpContext context) => {
.Produces<ErrorResult>(StatusCodes.Status404NotFound)
.Produces<ErrorResult>(StatusCodes.Status409Conflict)
.Produces<ErrorResult>(StatusCodes.Status400BadRequest);

routeBuilder.AddPolicy(policyName);
routeBuilder.AddAuthorization(type);
}
}

Expand All @@ -229,4 +243,13 @@ string Generate() {
return char.ToLowerInvariant(gen[0]) + gen[1..];
}
}

static void AddAuthorization(this RouteHandlerBuilder builder, Type contractType) {
var authAttr = contractType.GetAttribute<AuthorizeAttribute>();
if (authAttr != null) builder.RequireAuthorization(authAttr);
}

static void AddPolicy(this RouteHandlerBuilder builder, string? policyName) {
if (policyName != null) builder.RequireAuthorization(policyName.Split(','));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,42 @@ namespace Microsoft.AspNetCore.Routing;
public delegate TCommand ConvertAndEnrichCommand<in TContract, out TCommand>(TContract command, HttpContext httpContext);

public static partial class RouteBuilderExtensions {
/// <summary>
/// Map command to HTTP POST endpoint.
/// The HTTP command type should be annotated with <seealso cref="HttpCommandAttribute"/> attribute.
/// </summary>
/// <param name="builder">Endpoint route builder instance</param>
/// <param name="convert">Function to convert HTTP command to domain command</param>
/// <typeparam name="TContract">HTTP command type</typeparam>
/// <typeparam name="TCommand">Domain command type</typeparam>
/// <typeparam name="TAggregate">Aggregate type</typeparam>
/// <returns></returns>
[PublicAPI]
public static RouteHandlerBuilder MapCommand<TContract, TCommand, TAggregate>(
this IEndpointRouteBuilder builder,
ConvertAndEnrichCommand<TContract, TCommand> convert
) where TAggregate : Aggregate where TCommand : class where TContract : class {
var attr = typeof(TCommand).GetAttribute<HttpCommandAttribute>();
return Map<TAggregate, TContract, TCommand>(builder, attr?.Route, convert);
var attr = typeof(TContract).GetAttribute<HttpCommandAttribute>();
return Map<TAggregate, TContract, TCommand>(builder, attr?.Route, convert, attr?.PolicyName);
}

/// <summary>
/// Map command to HTTP POST endpoint
/// </summary>
/// <param name="builder">Endpoint route builder instance</param>
/// <param name="route">API route for the POST endpoint</param>
/// <param name="convert">Function to convert HTTP command to domain command</param>
/// <param name="policyName">Optional authorization policy name</param>
/// <typeparam name="TContract">HTTP command type</typeparam>
/// <typeparam name="TCommand">Domain command type</typeparam>
/// <typeparam name="TAggregate">Aggregate type</typeparam>
/// <returns></returns>
[PublicAPI]
public static RouteHandlerBuilder MapCommand<TContract, TCommand, TAggregate>(
this IEndpointRouteBuilder builder,
string? route,
ConvertAndEnrichCommand<TContract, TCommand> convert
ConvertAndEnrichCommand<TContract, TCommand> convert,
string? policyName = null
) where TAggregate : Aggregate where TCommand : class where TContract : class
=> Map<TAggregate, TContract, TCommand>(builder, route, convert);
=> Map<TAggregate, TContract, TCommand>(builder, route, convert, policyName);
}