From d29d363592f9d68efb2cae44215eec8903f9a7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Cimper=C5=A1ek?= Date: Tue, 13 Apr 2021 09:43:59 +0200 Subject: [PATCH 1/3] 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 28a8cd3980810253f2d3e4301d670584a68534e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Cimper=C5=A1ek?= Date: Tue, 13 Apr 2021 10:56:42 +0200 Subject: [PATCH 2/3] 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(); +``` From 9fddeaa224e45127f7735249a494fb2621c5446b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Cimper=C5=A1ek?= Date: Tue, 20 Apr 2021 15:47:51 +0200 Subject: [PATCH 3/3] feat: CoreSharp.NHibernate source generator delete: controllers generator fix: sln --- .github/workflows/dotnetcore.yml | 17 +- CoreSharp.Breeze.Tests/BaseDatabaseTest.cs | 8 +- CoreSharp.Breeze.Tests/BreezeDatabaseTest.cs | 2 +- .../CoreSharp.Breeze.Tests.csproj | 4 +- CoreSharp.Common.Tests/BaseTest.cs | 6 +- .../CoreSharp.Common.Tests.csproj | 3 +- .../CoreSharp.Cqrs.AspNetCore.csproj | 2 +- .../CoreSharp.Cqrs.Tests.csproj | 2 +- ...oreSharp.NHibernate.SQLServer.Tests.csproj | 2 +- .../AnalyzersConfig.cs | 41 +++ ...oreSharp.NHibernate.SourceGenerator.csproj | 43 +++ .../NHibernateGenerator.cs | 245 ++++++++++++++++++ .../PropertyOrderAnalyzer.cs | 11 + .../README.md | 42 +++ .../SyntaxNodeExtensions.cs | 59 +++++ .../SyntaxReceiver.cs | 27 ++ .../ValidTypes.cs | 8 + .../VirtualModifierAnalyzer.cs | 11 + .../CoreSharp.Validation.Tests.csproj | 2 +- CoreSharp.sln | 8 +- README.md | 7 +- 21 files changed, 532 insertions(+), 18 deletions(-) create mode 100644 CoreSharp.NHibernate.SourceGenerator/AnalyzersConfig.cs create mode 100644 CoreSharp.NHibernate.SourceGenerator/CoreSharp.NHibernate.SourceGenerator.csproj create mode 100644 CoreSharp.NHibernate.SourceGenerator/NHibernateGenerator.cs create mode 100644 CoreSharp.NHibernate.SourceGenerator/PropertyOrderAnalyzer.cs create mode 100644 CoreSharp.NHibernate.SourceGenerator/README.md create mode 100644 CoreSharp.NHibernate.SourceGenerator/SyntaxNodeExtensions.cs create mode 100644 CoreSharp.NHibernate.SourceGenerator/SyntaxReceiver.cs create mode 100644 CoreSharp.NHibernate.SourceGenerator/ValidTypes.cs create mode 100644 CoreSharp.NHibernate.SourceGenerator/VirtualModifierAnalyzer.cs diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 616a132..4da1426 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -7,12 +7,27 @@ jobs: runs-on: ubuntu-latest + services: + postgres: + image: postgres:12 + env: + POSTGRES_USER: coresharp + POSTGRES_PASSWORD: coresharp + POSTGRES_DB: coresharp + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v1 - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.100 + dotnet-version: '5.0.x' - name: Test with dotnet run: dotnet test - name: Build with dotnet diff --git a/CoreSharp.Breeze.Tests/BaseDatabaseTest.cs b/CoreSharp.Breeze.Tests/BaseDatabaseTest.cs index 09eb8b0..35eecc9 100644 --- a/CoreSharp.Breeze.Tests/BaseDatabaseTest.cs +++ b/CoreSharp.Breeze.Tests/BaseDatabaseTest.cs @@ -61,10 +61,12 @@ protected virtual Configuration CreateNHibernateConfiguration(Container containe var persistenceModel = GetPersistenceModel(container, config); return Fluently.Configure(config) .Database( - MsSqlConfiguration.MsSql2012.ConnectionString(o => o + PostgreSQLConfiguration.PostgreSQL82.ConnectionString(o => o .Database("coresharp") - .Server("(local)") - .TrustedConnection()) + .Host("localhost") + .Username("coresharp") + .Password("coresharp") + .Port(5432)) ) //.AppendEventListeners(container) .SetDefaultProperties() diff --git a/CoreSharp.Breeze.Tests/BreezeDatabaseTest.cs b/CoreSharp.Breeze.Tests/BreezeDatabaseTest.cs index 96975f2..d3f8864 100644 --- a/CoreSharp.Breeze.Tests/BreezeDatabaseTest.cs +++ b/CoreSharp.Breeze.Tests/BreezeDatabaseTest.cs @@ -97,7 +97,7 @@ protected virtual void FillDatabase(ISession session) { CompositeOrder = compositeOrder, Product = products[(i + j) % 10], - Price = i * j, + Price = (int)(i * j), Quantity = i + j }); } diff --git a/CoreSharp.Breeze.Tests/CoreSharp.Breeze.Tests.csproj b/CoreSharp.Breeze.Tests/CoreSharp.Breeze.Tests.csproj index e565529..dcda5b1 100644 --- a/CoreSharp.Breeze.Tests/CoreSharp.Breeze.Tests.csproj +++ b/CoreSharp.Breeze.Tests/CoreSharp.Breeze.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 latest false @@ -20,6 +20,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/CoreSharp.Common.Tests/BaseTest.cs b/CoreSharp.Common.Tests/BaseTest.cs index 5ccf48a..b109935 100644 --- a/CoreSharp.Common.Tests/BaseTest.cs +++ b/CoreSharp.Common.Tests/BaseTest.cs @@ -1,4 +1,5 @@ -using SimpleInjector; +using CoreSharp.Cqrs.Events; +using SimpleInjector; using SimpleInjector.Lifestyles; using Xunit; @@ -30,7 +31,7 @@ protected virtual void ConfigureContainer(Container container) protected virtual void SetUp() { - + } protected virtual void Cleanup() @@ -39,6 +40,7 @@ protected virtual void Cleanup() private void Configure(Container container) { + var ep = new EventAggregator(container); // TODO: remove container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle(); ConfigureContainer(container); container.RegisterPackages(); diff --git a/CoreSharp.Common.Tests/CoreSharp.Common.Tests.csproj b/CoreSharp.Common.Tests/CoreSharp.Common.Tests.csproj index b389156..aa22e91 100644 --- a/CoreSharp.Common.Tests/CoreSharp.Common.Tests.csproj +++ b/CoreSharp.Common.Tests/CoreSharp.Common.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false @@ -17,6 +17,7 @@ + diff --git a/CoreSharp.Cqrs.AspNetCore/CoreSharp.Cqrs.AspNetCore.csproj b/CoreSharp.Cqrs.AspNetCore/CoreSharp.Cqrs.AspNetCore.csproj index a8f285b..6e8f54a 100644 --- a/CoreSharp.Cqrs.AspNetCore/CoreSharp.Cqrs.AspNetCore.csproj +++ b/CoreSharp.Cqrs.AspNetCore/CoreSharp.Cqrs.AspNetCore.csproj @@ -1,7 +1,7 @@  - net5.0;netcoreapp3.1;netcoreapp3.0;netstandard2.0 + net5.0;netcoreapp3.1;netstandard2.0 CoreSharp.Cqrs.AspNetCore cime diff --git a/CoreSharp.Cqrs.Tests/CoreSharp.Cqrs.Tests.csproj b/CoreSharp.Cqrs.Tests/CoreSharp.Cqrs.Tests.csproj index 9bfdd24..a05730a 100644 --- a/CoreSharp.Cqrs.Tests/CoreSharp.Cqrs.Tests.csproj +++ b/CoreSharp.Cqrs.Tests/CoreSharp.Cqrs.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false diff --git a/CoreSharp.NHibernate.SQLServer.Tests/CoreSharp.NHibernate.SQLServer.Tests.csproj b/CoreSharp.NHibernate.SQLServer.Tests/CoreSharp.NHibernate.SQLServer.Tests.csproj index 9d63676..dcc9de2 100644 --- a/CoreSharp.NHibernate.SQLServer.Tests/CoreSharp.NHibernate.SQLServer.Tests.csproj +++ b/CoreSharp.NHibernate.SQLServer.Tests/CoreSharp.NHibernate.SQLServer.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 false diff --git a/CoreSharp.NHibernate.SourceGenerator/AnalyzersConfig.cs b/CoreSharp.NHibernate.SourceGenerator/AnalyzersConfig.cs new file mode 100644 index 0000000..024df60 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/AnalyzersConfig.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Serialization; + +namespace CoreSharp.NHibernate.SourceGenerator +{ + [XmlRoot("Analyzers")] + public class AnalyzersConfig + { + private static AnalyzersConfig _instance; + + public VirtualModifierAnalyzer VirtualModifierAnalyzer { get; set; } = new VirtualModifierAnalyzer(); + public PropertyOrderAnalyzer PropertyOrderAnalyzer { get; set; } = new PropertyOrderAnalyzer(); + + [XmlArray("ValidTypes")] + [XmlArrayItem("ValidType")] + public ValidTypes ValidTypes { get; set; } = new ValidTypes() { "CoreSharp.DataAccess.IEntity", "CoreSharp.DataAccess.ICodeList" }; + + public List VirtualModifierAnalyzerValidTypes => new List().Concat(ValidTypes ?? new List()).Concat(VirtualModifierAnalyzer?.ValidTypes ?? new List()).Distinct().ToList(); + public List PropertyOrderAnalyzerValidTypes => new List().Concat(ValidTypes ?? new List()).Concat(PropertyOrderAnalyzer?.ValidTypes ?? new List()).Distinct().ToList(); + + [DebuggerStepThrough] + public static AnalyzersConfig Deserialize(string content) + { + try + { + var xs = new XmlSerializer(typeof(AnalyzersConfig)); + var sr = new StringReader(content); + + return (AnalyzersConfig)xs.Deserialize(sr); + } + catch (Exception) + { + return _instance ?? (_instance = new AnalyzersConfig()); + } + } + } +} diff --git a/CoreSharp.NHibernate.SourceGenerator/CoreSharp.NHibernate.SourceGenerator.csproj b/CoreSharp.NHibernate.SourceGenerator/CoreSharp.NHibernate.SourceGenerator.csproj new file mode 100644 index 0000000..3b0743f --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/CoreSharp.NHibernate.SourceGenerator.csproj @@ -0,0 +1,43 @@ + + + + netstandard2.0 + 9 + + CoreSharp.NHibernate.SourceGenerator + + cime + .NET standard NHibernate extensions, convetions + false + Copyright 2021 (c) cime. All rights reserved. + Core# nhibernate dataaccess db + https://github.com/cime/CoreSharp + https://github.com/cime/CoreSharp/blob/master/LICENSE + https://github.com/cime/CoreSharp + enable + 0.1.1 + + + + true + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/CoreSharp.NHibernate.SourceGenerator/NHibernateGenerator.cs b/CoreSharp.NHibernate.SourceGenerator/NHibernateGenerator.cs new file mode 100644 index 0000000..d282d32 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/NHibernateGenerator.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Humanizer; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace CoreSharp.NHibernate.SourceGenerator +{ + [Generator] + public class NHibernateGenerator : ISourceGenerator + { + protected static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); + protected IList ValidTypes = new List() { "CoreSharp.DataAccess.IEntity", "CoreSharp.DataAccess.ICodeList" }; + + public void Initialize(GeneratorInitializationContext context) + { +#if DEBUG + if (!Debugger.IsAttached) + { + // Debugger.Launch(); + } +#endif + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + try + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + { + return; + } + + var classSymbols = GetClassSymbols(context.Compilation, receiver); + foreach (var classSymbol in classSymbols) + { + var properties = classSymbol.GetMembers().OfType() + .Where(x => !x.ExplicitInterfaceImplementations.Any() && !x.Name.Contains(".")); + var methods = classSymbol.GetMembers().OfType(); + var modified = false; + + foreach (var propertySymbol in properties) + { + var propertyInterfaces = propertySymbol.Type.AllInterfaces; + var namedTypeSymbol = propertySymbol.Type as INamedTypeSymbol; + + if (propertySymbol.SetMethod == null) + { + continue; + } + + if (!(namedTypeSymbol?.IsGenericType == true && + (propertySymbol.Type.Name == "IEnumerable" + || propertySymbol.Type.Name == "ISet" + || propertySymbol.Type.Name == "IList" + || propertySymbol.Type.Name == "HashSet" + || propertySymbol.Type.Name == "List" + || propertyInterfaces.Any(x => + x.Name == "ISet" || x.Name == "IList" || x.Name == "IEnumerable")) + && propertySymbol.Type.Kind != SymbolKind.ArrayType)) + { + continue; + } + + var propertyType = (INamedTypeSymbol)propertySymbol.Type; + + var parentAttribute = propertySymbol.GetAttributes().SingleOrDefault(x => x.AttributeClass.Name == "ParentAttribute"); + var parentPropertyName = parentAttribute?.ConstructorArguments.Single().Value.ToString() ?? classSymbol.Name; + var childType = propertyType.TypeArguments.Single(); + var childTypeName = childType.Name; + var propertyName = propertySymbol.Name; + var propertyNameSingular = propertyName.Singularize(); + var methodNames = new[] { $"Add{propertyNameSingular}", $"Remove{propertyNameSingular}", $"Clear{propertyName}" }; + var notFound = methodNames.Where(x => !methods.Any(m => m.Name == x && + (m.Name == $"Clear{propertyName}" || (m.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, childType))))).ToList(); + + if (!notFound.Any()) + { + continue; + } + + modified = true; + var addMethod = GenerateAddMethod(notFound, propertyNameSingular, classSymbol, childTypeName, propertySymbol, parentPropertyName); + var removeMethod = GenerateRemoveMethod(notFound, propertyNameSingular, classSymbol, childTypeName, propertySymbol, parentPropertyName); + var clearMethod = GenerateClearMethod(notFound, propertyName, propertySymbol, classSymbol, propertyNameSingular); + + var source = $@"//---------------------- +// +// Generated by NHibernateGenerator +// +//---------------------- +using System; +using System.Linq; +using CoreSharp.NHibernate; +using {childType.ContainingNamespace}; + +namespace {classSymbol.ContainingNamespace} +{{ +public static partial class {classSymbol.Name}Extensions +{{ + {addMethod} + {removeMethod} + {clearMethod} +}} +}} +"; + var desiredFileName = $"{classSymbol.Name}.{propertySymbol.Name}.Methods.cs"; + var sourceText = SourceText.From(source, Encoding.UTF8); // If no encoding specified then SourceText is not debugable + + context.AddSource(desiredFileName, sourceText); + } + + if (modified) + { + var syntax = classSymbol.DeclaringSyntaxReferences + .First().SyntaxTree.GetRoot().FindNode(classSymbol.Locations.First().SourceSpan) as ClassDeclarationSyntax; + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } + + private static string GenerateClearMethod(List notFound, string propertyName, IPropertySymbol propertySymbol, INamedTypeSymbol classSymbol, string propertyNameSingular) + { + return notFound.Contains($"Clear{propertyName}") ? @$" + public static void Clear{propertySymbol.Name}(this {classSymbol.Name} entity) + {{ + foreach (var item in entity.{propertySymbol.Name}.ToList()) + {{ + entity.Remove{propertyNameSingular}(item); + }} + }} +" : ""; + } + + private static string GenerateRemoveMethod(List notFound, string propertyNameSingular, INamedTypeSymbol classSymbol, string childTypeName, IPropertySymbol propertySymbol, string parentPropertyName) + { + return notFound.Contains($"Remove{propertyNameSingular}") ? @$" + public static void Remove{propertyNameSingular}(this {classSymbol.Name} entity, {childTypeName} item) + {{ + entity.RemoveOneToMany(o => o.{propertySymbol.Name}, item, o => o.{parentPropertyName}); + }} +" : ""; + } + + private static string GenerateAddMethod(List notFound, string propertyNameSingular, INamedTypeSymbol classSymbol, + string childTypeName, IPropertySymbol propertySymbol, string parentPropertyName) + { + var addMethod = notFound.Contains($"Add{propertyNameSingular}") + ? @$" + public static void Add{propertyNameSingular}(this {classSymbol.Name} entity, {childTypeName} item) + {{ + entity.AddOneToMany(o => o.{propertySymbol.Name}, item, o => o.{parentPropertyName}, o => o.Remove{propertyNameSingular}); + }} +" + : ""; + return addMethod; + } + + private static INamedTypeSymbol GetClassSymbol(Compilation compilation, ClassDeclarationSyntax clazz) + { + var model = compilation.GetSemanticModel(clazz.SyntaxTree); + var classSymbol = model.GetDeclaredSymbol(clazz)!; + return classSymbol; + } + + private IList GetClassSymbols(Compilation compilation, SyntaxReceiver receiver) + { + // var classDeclarationSyntaxes = compilation.SyntaxTrees.SelectMany(x => x.GetRoot().DescendantNodes().OfType()).ToList(); + var classSymbols = new List(); + foreach (var clazz in receiver.CandidateClasses) + { + var classSymbol = GetClassSymbol(compilation, clazz); + if (IsValidType(classSymbol) && !classSymbols.Contains(classSymbol)) + { + classSymbols.Add(classSymbol); + } + } + + return classSymbols; + } + + private bool IsValidType(INamedTypeSymbol cls) + { + var interfaces = cls.AllInterfaces.Select(x => x.ToDisplayString(SymbolDisplayFormat)); + + if (interfaces.Any(x => ValidTypes.Contains(x))) + { + return true; + } + + return false; + } + + protected bool IsValidType(SyntaxNodeAnalysisContext nodeContext) + { + var classNode = nodeContext.Node.Parent as ClassDeclarationSyntax; + + if (classNode == null) + { + return false; + } + + var symbol = ModelExtensions.GetDeclaredSymbol(nodeContext.SemanticModel, classNode) as INamedTypeSymbol; + var interfaces = symbol?.AllInterfaces.Select(x => x.ToDisplayString(SymbolDisplayFormat)); + + if (interfaces?.Any(x => ValidTypes.Contains(x)) == true) + { + return true; + } + + return false; + } + + protected bool IsValidType(ClassDeclarationSyntax classSyntax, SemanticModel semanticModel) + { + var classNode = classSyntax; + + if (classNode == null) + { + return false; + } + + var symbol = ModelExtensions.GetDeclaredSymbol(semanticModel, classNode) as INamedTypeSymbol; + var interfaces = symbol?.AllInterfaces.Select(x => x.ToDisplayString(SymbolDisplayFormat)); + + if (interfaces?.Any(x => ValidTypes.Contains(x)) == true) + { + return true; + } + + return false; + } + } +} diff --git a/CoreSharp.NHibernate.SourceGenerator/PropertyOrderAnalyzer.cs b/CoreSharp.NHibernate.SourceGenerator/PropertyOrderAnalyzer.cs new file mode 100644 index 0000000..ace622d --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/PropertyOrderAnalyzer.cs @@ -0,0 +1,11 @@ +using System.Xml.Serialization; + +namespace CoreSharp.NHibernate.SourceGenerator +{ + public class PropertyOrderAnalyzer + { + [XmlArray("ValidTypes")] + [XmlArrayItem("ValidType")] + public ValidTypes ValidTypes { get; set; } + } +} diff --git a/CoreSharp.NHibernate.SourceGenerator/README.md b/CoreSharp.NHibernate.SourceGenerator/README.md new file mode 100644 index 0000000..7a63368 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/README.md @@ -0,0 +1,42 @@ +CoreSharp NHibernate Source Generator +===================================== + +Extension methods generator for CoreSharp NHibernate entities.\ +This package generates Add/Remove/Clear methods for each collection property. + +Requirements +------------ + +.NET 5 + +Installation +------------ + +Add `CoreSharp.NHibernate.SourceGenerator` reference to csproj: + +```json + + ... + + ... + +``` + +Add `analyzers.config` to root of project: + +```xml + + + + CoreSharp.DataAccess.IEntity + CoreSharp.DataAccess.ICodeList + + + + + + + + + +``` diff --git a/CoreSharp.NHibernate.SourceGenerator/SyntaxNodeExtensions.cs b/CoreSharp.NHibernate.SourceGenerator/SyntaxNodeExtensions.cs new file mode 100644 index 0000000..172bb91 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/SyntaxNodeExtensions.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CoreSharp.NHibernate.SourceGenerator +{ + public static class SyntaxNodeExtensions + { + public static bool HasKeyword(this SyntaxNode node, SyntaxKind kind) + { + return node.DescendantTokens().SingleOrDefault(x => x.Kind() == kind) != default(SyntaxToken); + } + + public static bool IsReadonly(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.ReadOnlyKeyword); + } + + public static bool IsAbstract(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.AbstractKeyword); + } + + public static bool IsVirtual(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.VirtualKeyword) || HasKeyword(node, SyntaxKind.OverrideKeyword); + } + + public static bool IsPublic(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.PublicKeyword); + } + + public static bool IsPrivate(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.PrivateKeyword); + } + + public static bool IsStatic(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.StaticKeyword); + } + + public static bool IsPartial(this SyntaxNode node) + { + return HasKeyword(node, SyntaxKind.PartialKeyword); + } + + public static SyntaxToken GetTokenWithKeyword(this SyntaxNode node, SyntaxKind kind) + { + return node.DescendantTokens().FirstOrDefault(x => x.Kind() == kind); + } + + public static string GetIdentifierValue(this SyntaxNode node) + { + return node.ChildTokens().FirstOrDefault(x => x.Kind() == SyntaxKind.IdentifierToken).ValueText; + } + } +} diff --git a/CoreSharp.NHibernate.SourceGenerator/SyntaxReceiver.cs b/CoreSharp.NHibernate.SourceGenerator/SyntaxReceiver.cs new file mode 100644 index 0000000..9213878 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/SyntaxReceiver.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +/// +/// Created on demand before each generation pass +/// +namespace CoreSharp.NHibernate.SourceGenerator +{ + internal class SyntaxReceiver : ISyntaxReceiver + { + public IList CandidateClasses { get; } = new List(); + + /// + /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // any field with at least one attribute is a candidate for property generation + if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax) // && + // classDeclarationSyntax.AttributeLists.Count > 0) + { + CandidateClasses.Add(classDeclarationSyntax); + } + } + } +} diff --git a/CoreSharp.NHibernate.SourceGenerator/ValidTypes.cs b/CoreSharp.NHibernate.SourceGenerator/ValidTypes.cs new file mode 100644 index 0000000..2fdb7d1 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/ValidTypes.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace CoreSharp.NHibernate.SourceGenerator +{ + public class ValidTypes : List + { + } +} diff --git a/CoreSharp.NHibernate.SourceGenerator/VirtualModifierAnalyzer.cs b/CoreSharp.NHibernate.SourceGenerator/VirtualModifierAnalyzer.cs new file mode 100644 index 0000000..777bc04 --- /dev/null +++ b/CoreSharp.NHibernate.SourceGenerator/VirtualModifierAnalyzer.cs @@ -0,0 +1,11 @@ +using System.Xml.Serialization; + +namespace CoreSharp.NHibernate.SourceGenerator +{ + public class VirtualModifierAnalyzer + { + [XmlArray("ValidTypes")] + [XmlArrayItem("ValidType")] + public ValidTypes ValidTypes { get; set; } + } +} diff --git a/CoreSharp.Validation.Tests/CoreSharp.Validation.Tests.csproj b/CoreSharp.Validation.Tests/CoreSharp.Validation.Tests.csproj index 79762db..8fd20d8 100644 --- a/CoreSharp.Validation.Tests/CoreSharp.Validation.Tests.csproj +++ b/CoreSharp.Validation.Tests/CoreSharp.Validation.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false 9.0 diff --git a/CoreSharp.sln b/CoreSharp.sln index 559c040..2c35958 100644 --- a/CoreSharp.sln +++ b/CoreSharp.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29509.3 @@ -95,6 +95,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.Mvc.Formatters", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{FBF7815A-2F16-43C1-8D21-EF4E6616446A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.NHibernate.SourceGenerator", "CoreSharp.NHibernate.SourceGenerator\CoreSharp.NHibernate.SourceGenerator.csproj", "{19881C1A-D1CA-40BD-8579-7543C4315902}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,10 @@ Global {A6D76085-63EF-4B2C-A3D6-B096392E98CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6D76085-63EF-4B2C-A3D6-B096392E98CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6D76085-63EF-4B2C-A3D6-B096392E98CC}.Release|Any CPU.Build.0 = Release|Any CPU + {19881C1A-D1CA-40BD-8579-7543C4315902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19881C1A-D1CA-40BD-8579-7543C4315902}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19881C1A-D1CA-40BD-8579-7543C4315902}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19881C1A-D1CA-40BD-8579-7543C4315902}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 5da8d03..52126eb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ -# CoreSharp # - -## CoreSharp.Breeze +![example workflow](https://github.com/cime/coresharp/actions/workflows/dotnetcore.yml/badge.svg) +[![CodeFactor](https://www.codefactor.io/repository/github/cime/coresharp/badge)](https://www.codefactor.io/repository/github/cime/coresharp) -CoreSharp.Breeze is a customized .NET Standard 2.0 implementation of Breeze server side. +# CoreSharp # ### Installation