From b2775b34cea5cffb73bb818e525fc544e82292cb Mon Sep 17 00:00:00 2001 From: cime Date: Tue, 13 Apr 2021 21:43:59 +0200 Subject: [PATCH 1/2] refactoring: CQRS middleware splitted into two separate Command and Query handling middleware feat: HTTP method support on command and query definitions --- .../Attributes/HttpMethodAttribute.cs | 76 +++++++++++ .../CoreSharp.Common.Abstractions.csproj | 1 + ...dleware.cs => CommandHandlerMiddleware.cs} | 116 ++++------------- CoreSharp.Cqrs.AspNetCore/CommandInfo.cs | 19 ++- .../Extensions/IOwinContextExtensions.cs | 4 +- .../Options/AbstractCqrsOptions.cs | 25 ++-- .../Options/ICqrsOptions.cs | 10 +- .../Options/SimpleInjectorCqrsOptions.cs | 4 +- CoreSharp.Cqrs.AspNetCore/Package.cs | 3 +- .../QueryHandlerMiddleware.cs | 122 ++++++++++++++++++ CoreSharp.Cqrs.AspNetCore/QueryInfo.cs | 16 ++- CoreSharp.Cqrs.AspNetCore/README.md | 52 ++++++++ 12 files changed, 324 insertions(+), 124 deletions(-) create mode 100644 CoreSharp.Common.Abstractions/Attributes/HttpMethodAttribute.cs rename CoreSharp.Cqrs.AspNetCore/{CqrsMiddleware.cs => CommandHandlerMiddleware.cs} (52%) create mode 100644 CoreSharp.Cqrs.AspNetCore/QueryHandlerMiddleware.cs create mode 100644 CoreSharp.Cqrs.AspNetCore/README.md diff --git a/CoreSharp.Common.Abstractions/Attributes/HttpMethodAttribute.cs b/CoreSharp.Common.Abstractions/Attributes/HttpMethodAttribute.cs new file mode 100644 index 0000000..8432855 --- /dev/null +++ b/CoreSharp.Common.Abstractions/Attributes/HttpMethodAttribute.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; + +namespace CoreSharp.Common.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class HttpMethodAttribute : Attribute + { + public IEnumerable HttpMethods { get; } + + public HttpMethodAttribute(IEnumerable httpMethods) + { + HttpMethods = httpMethods; + } + } + + public class HttpGetAttribute : HttpMethodAttribute + { + private static readonly IEnumerable SupportedMethods = new [] { "GET" }; + + public HttpGetAttribute() + : base(SupportedMethods) + { + } + } + + public class HttpPostAttribute : HttpMethodAttribute + { + private static readonly IEnumerable SupportedMethods = new [] { "POST" }; + + public HttpPostAttribute() + : base(SupportedMethods) + { + } + } + + public class HttpPutAttribute : HttpMethodAttribute + { + private static readonly IEnumerable SupportedMethods = new [] { "PUT" }; + + public HttpPutAttribute() + : base(SupportedMethods) + { + } + } + + public class HttpDeleteAttribute : HttpMethodAttribute + { + private static readonly IEnumerable SupportedMethods = new [] { "DELETE" }; + + public HttpDeleteAttribute() + : base(SupportedMethods) + { + } + } + + public class HttpPatchAttribute : HttpMethodAttribute + { + private static readonly IEnumerable SupportedMethods = new [] { "PATCH" }; + + public HttpPatchAttribute() + : base(SupportedMethods) + { + } + } + + public class HttpHeadAttribute : HttpMethodAttribute + { + private static readonly IEnumerable SupportedMethods = new [] { "HEAD" }; + + public HttpHeadAttribute() + : base(SupportedMethods) + { + } + } +} diff --git a/CoreSharp.Common.Abstractions/CoreSharp.Common.Abstractions.csproj b/CoreSharp.Common.Abstractions/CoreSharp.Common.Abstractions.csproj index 6a499c1..8cfd98a 100644 --- a/CoreSharp.Common.Abstractions/CoreSharp.Common.Abstractions.csproj +++ b/CoreSharp.Common.Abstractions/CoreSharp.Common.Abstractions.csproj @@ -3,6 +3,7 @@ netstandard2.0 8 + CoreSharp.Common diff --git a/CoreSharp.Cqrs.AspNetCore/CqrsMiddleware.cs b/CoreSharp.Cqrs.AspNetCore/CommandHandlerMiddleware.cs similarity index 52% rename from CoreSharp.Cqrs.AspNetCore/CqrsMiddleware.cs rename to CoreSharp.Cqrs.AspNetCore/CommandHandlerMiddleware.cs index 17fc0b4..c3b5de2 100644 --- a/CoreSharp.Cqrs.AspNetCore/CqrsMiddleware.cs +++ b/CoreSharp.Cqrs.AspNetCore/CommandHandlerMiddleware.cs @@ -11,53 +11,38 @@ using CoreSharp.Cqrs.AspNetCore.Options; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using NHibernate; namespace CoreSharp.Cqrs.AspNetCore { // ReSharper disable once ClassNeverInstantiated.Global - public class CqrsMiddleware : IMiddleware + public class CommandHandlerMiddleware : IMiddleware { - public static readonly string ContextKey = "CQRS"; - private readonly CqrsFormatterRegistry _registry; private readonly ICqrsOptions _options; private readonly Lazy> _commandTypes; - private readonly Lazy> _queryTypes; private readonly ConcurrentDictionary _deserializeMethods = new ConcurrentDictionary(); - private static readonly MethodInfo CreateDeserializeLambdaMethodInfo = typeof(CqrsMiddleware).GetMethod(nameof(CreateDeserializeLambda), BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo CreateDeserializeLambdaMethodInfo = typeof(CommandHandlerMiddleware).GetMethod(nameof(CreateDeserializeLambda), BindingFlags.NonPublic | BindingFlags.Static); - public CqrsMiddleware(CqrsFormatterRegistry registry, ICqrsOptions options) + public CommandHandlerMiddleware(CqrsFormatterRegistry registry, ICqrsOptions options) { _registry = registry; _options = options; - _commandTypes = new Lazy>(() => options.GetCommandTypes().ToDictionary( - keySelector: options.GetCommandKey, - elementSelector: type => type, - comparer: StringComparer.OrdinalIgnoreCase)); - - _queryTypes = new Lazy>(() => options.GetQueryTypes().ToDictionary( - keySelector: options.GetQueryKey, - elementSelector: info => info, - comparer: StringComparer.OrdinalIgnoreCase)); + _commandTypes = new Lazy>(() => + options.GetCommandTypes() + .SelectMany(x => x.HttpMethods, (ci, method) => new { CommandInfo = ci, HttpMethod = method}) + .ToDictionary( + keySelector: (x) => $"{x.HttpMethod} {options.GetCommandPath(x.CommandInfo)}", + elementSelector: x => x.CommandInfo, + comparer: StringComparer.OrdinalIgnoreCase)); } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - if (context.Request.Method == HttpMethod.Post.Method && context.Request.Path.Value.StartsWith(_options.CommandsPath, StringComparison.OrdinalIgnoreCase)) - { - await HandleCommand(context, _options); - } - else if (context.Request.Path.Value.StartsWith(_options.QueriesPath, StringComparison.OrdinalIgnoreCase)) - { - await HandleQuery(context, _options); - } - else - { - await next(context); - } + await HandleCommand(context, _options); } private static Func> CreateDeserializeLambda() @@ -74,16 +59,17 @@ private static Func> CreateDeserialize private async Task HandleCommand(HttpContext context, ICqrsOptions options) { - var path = options.GetCommandPath(context.Request.Path.Value); + var path = context.Request.Path; + var method = context.Request.Method; - if (!_commandTypes.Value.ContainsKey(path)) + if (!_commandTypes.Value.ContainsKey($"{method} {path}")) { - throw new CommandNotFoundException($"Command '{path}' not found"); + throw new CommandNotFoundException($"Command '{method} {path}' not found"); } dynamic result = null; - var info = _commandTypes.Value[path]; + var info = _commandTypes.Value[$"{method} {path}"]; var exposeAttribute = info.CommandType.GetCustomAttribute(); var formatter = _registry.GetFormatter(exposeAttribute.Formatter); @@ -93,10 +79,10 @@ private async Task HandleCommand(HttpContext context, ICqrsOptions options) return mi.Invoke(null, null); }); - dynamic command = await deserializeMethod(formatter, context.Request); + var command = await deserializeMethod(formatter, context.Request); dynamic handler = options.GetInstance(info.CommandHandlerType); - context.Items[ContextKey] = new CqrsContext(context.Request.Path.Value, path, CqrsType.Command, info.CommandHandlerType); + context.Items[IOwinContextExtensions.ContextKey] = new CqrsContext(context.Request.Path.Value, path, CqrsType.Command, info.CommandHandlerType); if (info.IsGeneric) { @@ -143,62 +129,6 @@ private async Task HandleCommand(HttpContext context, ICqrsOptions options) } } - private async Task HandleQuery(HttpContext context, ICqrsOptions options) - { - var path = options.GetQueryPath(context.Request.Path.Value); - - if (!_queryTypes.Value.ContainsKey(path)) - { - throw new QueryNotFoundException($"Query '{path}' not found"); - } - - var info = _queryTypes.Value[path]; - var exposeAttribute = info.QueryType.GetCustomAttribute(); - var formatter = _registry.GetFormatter(exposeAttribute.Formatter); - - var deserializeMethod = _deserializeMethods.GetOrAdd(info.QueryType, (t) => - { - var mi = CreateDeserializeLambdaMethodInfo.MakeGenericMethod(t); - return mi.Invoke(null, null); - }); - - dynamic query = await deserializeMethod(formatter, context.Request); - - dynamic handler = options.GetInstance(info.QueryHandlerType); - - context.Items[ContextKey] = new CqrsContext(context.Request.Path.Value, path, CqrsType.Command, info.QueryHandlerType); - - dynamic result; - - if (info.IsAsync) - { - result = await handler.HandleAsync(query, context.RequestAborted); - } - else - { - result = handler.Handle(query); - } - - string json = null; - - if (result != null) - { - json = result is string ? result : await formatter.SerializeAsync(result, context.Request); - } - - context.Response.ContentType = formatter.ContentType; - - if (json != null) - { - context.Response.StatusCode = (int)HttpStatusCode.OK; - await HttpResponseWritingExtensions.WriteAsync(context.Response, json, context.RequestAborted); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.NoContent; - } - } - private void CloseSession() { var session = (global::NHibernate.ISession) _options.GetInstance(typeof(global::NHibernate.ISession)); @@ -210,7 +140,7 @@ private void CloseSession() if (session.GetSessionImplementation().TransactionInProgress) { - var tx = session.Transaction; + var tx = session.GetCurrentTransaction(); try { if (tx.IsActive) tx.Commit(); @@ -229,11 +159,11 @@ private void CloseSession() } } - public static class CqrsMiddlewareExtensions + public static class CommandHandlerMiddlewareExtensions { - public static IApplicationBuilder UseCqrs(this IApplicationBuilder builder) + public static IApplicationBuilder UseCommands(this IApplicationBuilder builder) { - return builder.UseMiddleware(); + return builder.UseMiddleware(); } } } diff --git a/CoreSharp.Cqrs.AspNetCore/CommandInfo.cs b/CoreSharp.Cqrs.AspNetCore/CommandInfo.cs index 32f8831..65e65a9 100644 --- a/CoreSharp.Cqrs.AspNetCore/CommandInfo.cs +++ b/CoreSharp.Cqrs.AspNetCore/CommandInfo.cs @@ -3,6 +3,8 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using CoreSharp.Common.Attributes; +using CoreSharp.Cqrs.AspNetCore.Options; using CoreSharp.Cqrs.Command; namespace CoreSharp.Cqrs.AspNetCore @@ -15,17 +17,28 @@ public sealed class CommandInfo public readonly Type ResultType; public readonly bool IsAsync; public readonly bool IsGeneric; - public Type[] GenericTypes; + public readonly Type[] GenericTypes; + public readonly string[] HttpMethods; - public CommandInfo(Type commandType) + public CommandInfo(Type commandType, ICqrsOptions options) { CommandType = commandType; IsAsync = typeof(IAsyncCommand).IsAssignableFrom(commandType) || commandType.GetTypeInfo().IsAssignableToGenericType(typeof(IAsyncCommand<>)); IsGeneric = commandType.GetTypeInfo().IsAssignableToGenericType(typeof(ICommand<>)) || commandType.GetTypeInfo().IsAssignableToGenericType(typeof(IAsyncCommand<>)); - GenericTypes = new[] { CommandType }; + HttpMethods = commandType + .GetCustomAttributes(true) + .OfType() + .SelectMany(x => x.HttpMethods) + .Distinct() + .ToArray(); + + if (!HttpMethods.Any()) + { + HttpMethods = options.DefaultCommandHttpMethods; + } if (IsGeneric) { diff --git a/CoreSharp.Cqrs.AspNetCore/Extensions/IOwinContextExtensions.cs b/CoreSharp.Cqrs.AspNetCore/Extensions/IOwinContextExtensions.cs index f200cba..4d5b963 100644 --- a/CoreSharp.Cqrs.AspNetCore/Extensions/IOwinContextExtensions.cs +++ b/CoreSharp.Cqrs.AspNetCore/Extensions/IOwinContextExtensions.cs @@ -6,9 +6,11 @@ namespace Microsoft.AspNetCore.Http // ReSharper disable once InconsistentNaming public static class IOwinContextExtensions { + public static readonly string ContextKey = "CQRS"; + public static CqrsContext GetCqrsContext(this HttpContext context) { - return context.Items[CqrsMiddleware.ContextKey] as CqrsContext; + return context.Items[ContextKey] as CqrsContext; } } } diff --git a/CoreSharp.Cqrs.AspNetCore/Options/AbstractCqrsOptions.cs b/CoreSharp.Cqrs.AspNetCore/Options/AbstractCqrsOptions.cs index f914ac7..9c0cafd 100644 --- a/CoreSharp.Cqrs.AspNetCore/Options/AbstractCqrsOptions.cs +++ b/CoreSharp.Cqrs.AspNetCore/Options/AbstractCqrsOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text.RegularExpressions; using CoreSharp.Common.Attributes; @@ -14,10 +15,10 @@ public abstract class AbstractCqrsOptions : ICqrsOptions protected readonly Regex QueryNameSuffixRegex = new Regex("(?:AsyncQuery|QueryAsync|Query)$", RegexOptions.Compiled); protected readonly Regex QueryNamePrefixRegex = new Regex("^Get", RegexOptions.Compiled); - public string CommandsPath { get; set; } = "/api/command/"; - public string QueriesPath { get; set; } = "/api/query/"; + public virtual string[] DefaultCommandHttpMethods => new[] { HttpMethod.Post.Method }; + public virtual string[] DefaultQueryHttpMethods => new[] { HttpMethod.Get.Method }; - public string GetCommandKey(CommandInfo info) + public virtual string GetCommandPath(CommandInfo info) { var exposeAttribute = info.CommandType.GetCustomAttribute(); var key = exposeAttribute.IsUriSet ? exposeAttribute.Uri.Replace("//", "/").TrimEnd('/') : GetCommandNameFromType(info.CommandType); @@ -27,10 +28,10 @@ public string GetCommandKey(CommandInfo info) throw new FormatException($"Invalid path '{key}' for command '{info.CommandType.Namespace}.{info.CommandType.Name}'"); } - return key; + return $"/{key}"; } - public string GetQueryKey(QueryInfo info) + public virtual string GetQueryPath(QueryInfo info) { var exposeAttribute = info.QueryType.GetCustomAttribute(); @@ -41,10 +42,10 @@ public string GetQueryKey(QueryInfo info) throw new FormatException($"Invalid path '{key}' for query '{info.QueryType.Namespace}.{info.QueryType.Name}'"); } - return key; + return $"/{key}"; } - private string GetCommandNameFromType(Type type) + private string GetCommandNameFromType(Type type) { return CommandNameSuffixRegex.Replace(type.Name, string.Empty); } @@ -56,16 +57,6 @@ private string GetQueryNameFromType(Type type) return QueryNamePrefixRegex.Replace(queryName, String.Empty); } - public string GetQueryPath(string path) - { - return path.Substring(QueriesPath.Length, path.Length - QueriesPath.Length); - } - - public string GetCommandPath(string path) - { - return path.Substring(CommandsPath.Length, path.Length - CommandsPath.Length); - } - public abstract IEnumerable GetCommandTypes(); public abstract IEnumerable GetQueryTypes(); public abstract object GetInstance(Type type); diff --git a/CoreSharp.Cqrs.AspNetCore/Options/ICqrsOptions.cs b/CoreSharp.Cqrs.AspNetCore/Options/ICqrsOptions.cs index 9a4d550..5a5aaa8 100644 --- a/CoreSharp.Cqrs.AspNetCore/Options/ICqrsOptions.cs +++ b/CoreSharp.Cqrs.AspNetCore/Options/ICqrsOptions.cs @@ -5,14 +5,12 @@ namespace CoreSharp.Cqrs.AspNetCore.Options { public interface ICqrsOptions { - string CommandsPath { get; set; } - string QueriesPath { get; set; } + string[] DefaultCommandHttpMethods { get; } + string[] DefaultQueryHttpMethods { get; } - string GetCommandKey(CommandInfo info); - string GetQueryKey(QueryInfo info); + string GetCommandPath(CommandInfo info); + string GetQueryPath(QueryInfo info); - string GetCommandPath(string path); - string GetQueryPath(string path); IEnumerable GetCommandTypes(); IEnumerable GetQueryTypes(); diff --git a/CoreSharp.Cqrs.AspNetCore/Options/SimpleInjectorCqrsOptions.cs b/CoreSharp.Cqrs.AspNetCore/Options/SimpleInjectorCqrsOptions.cs index e960197..9780ec2 100644 --- a/CoreSharp.Cqrs.AspNetCore/Options/SimpleInjectorCqrsOptions.cs +++ b/CoreSharp.Cqrs.AspNetCore/Options/SimpleInjectorCqrsOptions.cs @@ -44,7 +44,7 @@ public override IEnumerable GetQueryTypes() .Select(i => i.GetGenericArguments().First()); }) .Where(x => x.GetCustomAttribute() != null) - .Select(t => new QueryInfo(t)); + .Select(t => new QueryInfo(t, this)); } public override IEnumerable GetCommandTypes() @@ -77,7 +77,7 @@ public override IEnumerable GetCommandTypes() .Select(i => i.GetGenericArguments().First()); }) .Where(x => x.GetCustomAttribute() != null) - .Select(t => new CommandInfo(t)); + .Select(t => new CommandInfo(t, this)); } public override object GetInstance(Type type) diff --git a/CoreSharp.Cqrs.AspNetCore/Package.cs b/CoreSharp.Cqrs.AspNetCore/Package.cs index 3a915a9..ce8b249 100644 --- a/CoreSharp.Cqrs.AspNetCore/Package.cs +++ b/CoreSharp.Cqrs.AspNetCore/Package.cs @@ -9,7 +9,8 @@ public void Register(Container container) { container.RegisterSingleton(() => new SimpleInjectorCqrsOptions(container)); container.RegisterSingleton(); - container.RegisterSingleton(); + container.RegisterSingleton(); + container.RegisterSingleton(); } } } diff --git a/CoreSharp.Cqrs.AspNetCore/QueryHandlerMiddleware.cs b/CoreSharp.Cqrs.AspNetCore/QueryHandlerMiddleware.cs new file mode 100644 index 0000000..c210363 --- /dev/null +++ b/CoreSharp.Cqrs.AspNetCore/QueryHandlerMiddleware.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using CoreSharp.Common.Attributes; +using CoreSharp.Cqrs.AspNetCore.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace CoreSharp.Cqrs.AspNetCore +{ + // ReSharper disable once ClassNeverInstantiated.Global + public class QueryHandlerMiddleware : IMiddleware + { + private readonly CqrsFormatterRegistry _registry; + private readonly ICqrsOptions _options; + + private readonly Lazy> _queryTypes; + + private readonly ConcurrentDictionary _deserializeMethods = new ConcurrentDictionary(); + private static readonly MethodInfo CreateDeserializeLambdaMethodInfo = typeof(QueryHandlerMiddleware).GetMethod(nameof(CreateDeserializeLambda), BindingFlags.NonPublic | BindingFlags.Static); + + public QueryHandlerMiddleware(CqrsFormatterRegistry registry, ICqrsOptions options) + { + _registry = registry; + _options = options; + + _queryTypes = new Lazy>(() => options.GetQueryTypes() + .SelectMany(x => x.HttpMethods, (ci, method) => new {QueryInfo = ci, HttpMethod = method}) + .ToDictionary( + keySelector: (x) => $"{x.HttpMethod} {options.GetQueryPath(x.QueryInfo)}", + elementSelector: x => x.QueryInfo, + comparer: StringComparer.OrdinalIgnoreCase)); + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + await HandleQuery(context, _options); + } + + private static Func> CreateDeserializeLambda() + { + var methodInfo = typeof(ICqrsFormatter) + .GetMethod(nameof(ICqrsFormatter.DeserializeAsync), BindingFlags.Public | BindingFlags.Instance) + .MakeGenericMethod(typeof(T)); + var formatterParameter = Expression.Parameter(typeof(ICqrsFormatter), "formatter"); + var requestParameter = Expression.Parameter(typeof(HttpRequest), "request"); + var call = Expression.Call(formatterParameter, methodInfo, requestParameter); + + return Expression.Lambda>>(call, formatterParameter, requestParameter).Compile(); + } + + private async Task HandleQuery(HttpContext context, ICqrsOptions options) + { + var path = context.Request.Path.Value; + var method = context.Request.Method; + + if (!_queryTypes.Value.ContainsKey($"{method} {path}")) + { + throw new QueryNotFoundException($"Query '{method} {path}' not found"); + } + + var info = _queryTypes.Value[$"{method} {path}"]; + var exposeAttribute = info.QueryType.GetCustomAttribute(); + var formatter = _registry.GetFormatter(exposeAttribute.Formatter); + + var deserializeMethod = _deserializeMethods.GetOrAdd(info.QueryType, (t) => + { + var mi = CreateDeserializeLambdaMethodInfo.MakeGenericMethod(t); + return mi.Invoke(null, null); + }); + + var query = await deserializeMethod(formatter, context.Request); + + dynamic handler = options.GetInstance(info.QueryHandlerType); + + context.Items[IOwinContextExtensions.ContextKey] = new CqrsContext(context.Request.Path.Value, path, CqrsType.Command, info.QueryHandlerType); + + dynamic result; + + if (info.IsAsync) + { + result = await handler.HandleAsync(query, context.RequestAborted); + } + else + { + result = handler.Handle(query); + } + + string json = null; + + if (result != null) + { + json = result is string ? result : await formatter.SerializeAsync(result, context.Request); + } + + context.Response.ContentType = formatter.ContentType; + + if (json != null) + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(json, context.RequestAborted); + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NoContent; + } + } + } + + public static class QueryHandlerMiddlewareExtensions + { + public static IApplicationBuilder UseQueries(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/CoreSharp.Cqrs.AspNetCore/QueryInfo.cs b/CoreSharp.Cqrs.AspNetCore/QueryInfo.cs index 8fe759d..1667315 100644 --- a/CoreSharp.Cqrs.AspNetCore/QueryInfo.cs +++ b/CoreSharp.Cqrs.AspNetCore/QueryInfo.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using CoreSharp.Common.Attributes; +using CoreSharp.Cqrs.AspNetCore.Options; using CoreSharp.Cqrs.Query; namespace CoreSharp.Cqrs.AspNetCore @@ -13,12 +15,24 @@ public sealed class QueryInfo public readonly Type QueryHandlerType; public readonly Type ResultType; public readonly bool IsAsync; + public readonly string[] HttpMethods; - public QueryInfo(Type queryType) + public QueryInfo(Type queryType, ICqrsOptions options) { QueryType = queryType; IsAsync = queryType.IsAssignableToGenericType(typeof(IAsyncQuery<>)); ResultType = DetermineResultTypes(queryType, IsAsync).Single(); + HttpMethods = queryType + .GetCustomAttributes(true) + .OfType() + .SelectMany(x => x.HttpMethods) + .Distinct() + .ToArray(); + + if (!HttpMethods.Any()) + { + HttpMethods = options.DefaultQueryHttpMethods; + } if (IsAsync) { diff --git a/CoreSharp.Cqrs.AspNetCore/README.md b/CoreSharp.Cqrs.AspNetCore/README.md new file mode 100644 index 0000000..22d88a8 --- /dev/null +++ b/CoreSharp.Cqrs.AspNetCore/README.md @@ -0,0 +1,52 @@ +CQRS ASP.NET Core Middleware +============================ + +Commands Middleware +------------------- + +Register Command Handler Middleware + +```c# +app.Map("/api/command", x => +{ + // x.UsePerRequestTransaction(); + x.UseCommands(); +}); +``` + +Queries Middleware +------------------ + +Register Query Handler Middleware + +```c# +app.Map("/api/query", x => +{ + // x.UsePerRequestTransaction(); + x.UseQueries(); +}); +``` + +CQRS options +------------ + +```c# +public class MyCqrsOptions : SimpleInjectorCqrsOptions +{ + // default HTTP methods allowed for commands when not set by HttpMethodAttribute + public override string[] DefaultCommandHttpMethods => new[] { HttpMethod.Post.Method }; + + // default HTTP methods allowed for queries when not set by HttpMethodAttribute + public override string[] DefaultQueryHttpMethods => new[] { HttpMethod.Get.Method, HttpMethod.Post.Method }; + + public MyCqrsOptions(Container container) : base(container) + { + } +} + +... + +container.Options.AllowOverridingRegistrations = true; +container.RegisterSingleton(); +container.Options.AllowOverridingRegistrations = false; +``` From 96cc40b958f003ec74cfa604f5eba1842e623e7e Mon Sep 17 00:00:00 2001 From: cime Date: Tue, 13 Apr 2021 22:56:42 +0200 Subject: [PATCH 2/2] docs: cqrs --- CoreSharp.Cqrs.AspNetCore/README.md | 14 ++++++ CoreSharp.Cqrs/README.md | 66 +++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 CoreSharp.Cqrs/README.md diff --git a/CoreSharp.Cqrs.AspNetCore/README.md b/CoreSharp.Cqrs.AspNetCore/README.md index 22d88a8..5c38b91 100644 --- a/CoreSharp.Cqrs.AspNetCore/README.md +++ b/CoreSharp.Cqrs.AspNetCore/README.md @@ -50,3 +50,17 @@ container.Options.AllowOverridingRegistrations = true; container.RegisterSingleton(); container.Options.AllowOverridingRegistrations = false; ``` + +Command and Query registration +------------------------------ + +```c# +// Register command & query handlers from Assembly of MyCommandOrQueryHandler +container.RegisterCqrsFromAssemblyOf(); + +// Register command handlers from Assembly of MyCommandHandler +container.RegisterCommandHandlersFromAssemblyOf(); + +// Register query handlers from Assembly of MyQueryHandler +container.RegisterQueryHandlersFromAssemblyOf(); +``` diff --git a/CoreSharp.Cqrs/README.md b/CoreSharp.Cqrs/README.md new file mode 100644 index 0000000..cce1e12 --- /dev/null +++ b/CoreSharp.Cqrs/README.md @@ -0,0 +1,66 @@ +CQRS +==== + +Example of a Command and CommandHandler +--------------------------------------- + +```c# +using CoreSharp.Cqrs.Command; + +public class TestCommand : ICommand +{ + public string Input { get; } + + public TestCommand(string input) + { + Input = input ?? throw new ArgumentNullException(nameof(input)); + } +} + +public class TestCommandHandler : ICommandHandler +{ + public void Handle(TestCommand command) + { + Console.WriteLine(command.In()); + } +} +``` + +Example of a Query and QueryHandler +--------------------------------------- + +```c# +using CoreSharp.Cqrs.Query; + +public class TestQuery : IQuery +{ + public string Name { get; } + + public TestQuery(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } +} + +public class TestQueryHandler : IQueryHandler +{ + public string Handle(TestQuery query) + { + return $"Hello {query.Name}"; + } +} +``` + +Command and Query registration +------------------------------ + +```c# +// Register command & query handlers from Assembly of MyCommandOrQueryHandler +container.RegisterCqrsFromAssemblyOf(); + +// Register command handlers from Assembly of MyCommandHandler +container.RegisterCommandHandlersFromAssemblyOf(); + +// Register query handlers from Assembly of MyQueryHandler +container.RegisterQueryHandlersFromAssemblyOf(); +```