Skip to content

Commit

Permalink
Merge branch 'cqrs_middleware'
Browse files Browse the repository at this point in the history
  • Loading branch information
cime committed Apr 14, 2021
2 parents 8e7ed47 + 96cc40b commit 1bc9a94
Show file tree
Hide file tree
Showing 13 changed files with 404 additions and 124 deletions.
76 changes: 76 additions & 0 deletions CoreSharp.Common.Abstractions/Attributes/HttpMethodAttribute.cs
Original file line number Diff line number Diff line change
@@ -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<string> HttpMethods { get; }

public HttpMethodAttribute(IEnumerable<string> httpMethods)
{
HttpMethods = httpMethods;
}
}

public class HttpGetAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new [] { "GET" };

public HttpGetAttribute()
: base(SupportedMethods)
{
}
}

public class HttpPostAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new [] { "POST" };

public HttpPostAttribute()
: base(SupportedMethods)
{
}
}

public class HttpPutAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new [] { "PUT" };

public HttpPutAttribute()
: base(SupportedMethods)
{
}
}

public class HttpDeleteAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new [] { "DELETE" };

public HttpDeleteAttribute()
: base(SupportedMethods)
{
}
}

public class HttpPatchAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new [] { "PATCH" };

public HttpPatchAttribute()
: base(SupportedMethods)
{
}
}

public class HttpHeadAttribute : HttpMethodAttribute
{
private static readonly IEnumerable<string> SupportedMethods = new [] { "HEAD" };

public HttpHeadAttribute()
: base(SupportedMethods)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8</LangVersion>
<RootNamespace>CoreSharp.Common</RootNamespace>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dictionary<string, CommandInfo>> _commandTypes;
private readonly Lazy<Dictionary<string, QueryInfo>> _queryTypes;

private readonly ConcurrentDictionary<Type, dynamic> _deserializeMethods = new ConcurrentDictionary<Type, dynamic>();
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<Dictionary<string, CommandInfo>>(() => options.GetCommandTypes().ToDictionary(
keySelector: options.GetCommandKey,
elementSelector: type => type,
comparer: StringComparer.OrdinalIgnoreCase));

_queryTypes = new Lazy<Dictionary<string, QueryInfo>>(() => options.GetQueryTypes().ToDictionary(
keySelector: options.GetQueryKey,
elementSelector: info => info,
comparer: StringComparer.OrdinalIgnoreCase));
_commandTypes = new Lazy<Dictionary<string, CommandInfo>>(() =>
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<ICqrsFormatter, HttpRequest, ValueTask<T>> CreateDeserializeLambda<T>()
Expand All @@ -74,16 +59,17 @@ private static Func<ICqrsFormatter, HttpRequest, ValueTask<T>> 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<ExposeAttribute>();
var formatter = _registry.GetFormatter(exposeAttribute.Formatter);

Expand All @@ -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)
{
Expand Down Expand Up @@ -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<ExposeAttribute>();
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));
Expand All @@ -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();
Expand All @@ -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<CqrsMiddleware>();
return builder.UseMiddleware<CommandHandlerMiddleware>();
}
}
}
19 changes: 16 additions & 3 deletions CoreSharp.Cqrs.AspNetCore/CommandInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<HttpMethodAttribute>()
.SelectMany(x => x.HttpMethods)
.Distinct()
.ToArray();

if (!HttpMethods.Any())
{
HttpMethods = options.DefaultCommandHttpMethods;
}

if (IsGeneric)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
25 changes: 8 additions & 17 deletions CoreSharp.Cqrs.AspNetCore/Options/AbstractCqrsOptions.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<ExposeAttribute>();
var key = exposeAttribute.IsUriSet ? exposeAttribute.Uri.Replace("//", "/").TrimEnd('/') : GetCommandNameFromType(info.CommandType);
Expand All @@ -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<ExposeAttribute>();

Expand All @@ -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);
}
Expand All @@ -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<CommandInfo> GetCommandTypes();
public abstract IEnumerable<QueryInfo> GetQueryTypes();
public abstract object GetInstance(Type type);
Expand Down
Loading

0 comments on commit 1bc9a94

Please sign in to comment.