diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cd78fde..83c58ea 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -30,7 +30,8 @@ "Bash(python3:*)", "Bash(grpcurl:*)", "Bash(lsof:*)", - "Bash(xargs kill -9)" + "Bash(xargs kill -9)", + "Bash(dotnet run:*)" ], "deny": [], "ask": [] diff --git a/Svrnty.CQRS.Abstractions/Attributes/IgnoreCommandAttribute.cs b/Svrnty.CQRS.Abstractions/Attributes/IgnoreCommandAttribute.cs new file mode 100644 index 0000000..bac0005 --- /dev/null +++ b/Svrnty.CQRS.Abstractions/Attributes/IgnoreCommandAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Svrnty.CQRS.Abstractions.Attributes; + +/// +/// Marks a command to be ignored by all endpoint generators (AspNetCore MVC, MinimalApi, gRPC) +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class IgnoreCommandAttribute : Attribute +{ +} diff --git a/Svrnty.CQRS.Abstractions/Attributes/IgnoreQueryAttribute.cs b/Svrnty.CQRS.Abstractions/Attributes/IgnoreQueryAttribute.cs new file mode 100644 index 0000000..b8fd182 --- /dev/null +++ b/Svrnty.CQRS.Abstractions/Attributes/IgnoreQueryAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Svrnty.CQRS.Abstractions.Attributes; + +/// +/// Marks a query to be ignored by all endpoint generators (AspNetCore MVC, MinimalApi, gRPC) +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class IgnoreQueryAttribute : Attribute +{ +} diff --git a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs deleted file mode 100644 index cff96f5..0000000 --- a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/CommandControllerIgnoreAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes; - -[AttributeUsage(AttributeTargets.Class, Inherited = false)] -public class CommandControllerIgnoreAttribute : Attribute -{ -} \ No newline at end of file diff --git a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerAuthorizationAttribute.cs b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerAuthorizationAttribute.cs deleted file mode 100644 index 73861dd..0000000 --- a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerAuthorizationAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes; - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class QueryControllerAuthorizationAttribute : Attribute -{ -} diff --git a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs b/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs deleted file mode 100644 index 21340d4..0000000 --- a/Svrnty.CQRS.AspNetCore.Abstractions/Attributes/QueryControllerIgnoreAttribute.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Svrnty.CQRS.AspNetCore.Abstractions.Attributes; - -[AttributeUsage(AttributeTargets.Class, Inherited = false)] -public class QueryControllerIgnoreAttribute : Attribute -{ -} diff --git a/Svrnty.CQRS.AspNetCore.Abstractions/Svrnty.CQRS.AspNetCore.Abstractions.csproj b/Svrnty.CQRS.AspNetCore.Abstractions/Svrnty.CQRS.AspNetCore.Abstractions.csproj deleted file mode 100644 index 6b63613..0000000 --- a/Svrnty.CQRS.AspNetCore.Abstractions/Svrnty.CQRS.AspNetCore.Abstractions.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - net10.0 - false - 14 - Svrnty - icon.png - README.md - https://github.com/svrnty/dotnet-cqrs - git - true - MIT - - portable - true - true - true - snupkg - - - - - - - diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs deleted file mode 100644 index f6cdb76..0000000 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryController.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Svrnty.CQRS.Abstractions; -using Svrnty.CQRS.AspNetCore.Abstractions.Attributes; -using Svrnty.CQRS.DynamicQuery.Abstractions; -using PoweredSoft.DynamicQuery.Core; - -namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc; - -[ApiController, Route("api/query/[controller]")] -public class DynamicQueryController : Controller - where TSource : class - where TDestination : class -{ - [HttpPost, QueryControllerAuthorization] - public async Task> HandleAsync( - [FromBody] DynamicQuery query, - [FromServices]IQueryHandler, IQueryExecutionResult> queryHandler - ) - { - var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted); - return result; - } - - [HttpGet, QueryControllerAuthorization] - public async Task> HandleGetAsync( - [FromQuery] DynamicQuery query, - [FromServices] IQueryHandler, IQueryExecutionResult> queryHandler - ) - { - var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted); - return result; - } -} - -[ApiController, Route("api/query/[controller]")] -public class DynamicQueryController : Controller - where TSource : class - where TDestination : class - where TParams : class -{ - [HttpPost, QueryControllerAuthorization] - public async Task> HandleAsync( - [FromBody] DynamicQuery query, - [FromServices] IQueryHandler, IQueryExecutionResult> queryHandler - ) - { - var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted); - return result; - } - - [HttpGet, QueryControllerAuthorization] - public async Task> HandleGetAsync( - [FromQuery] DynamicQuery query, - [FromServices] IQueryHandler, IQueryExecutionResult> queryHandler - ) - { - var result = await queryHandler.HandleAsync(query, HttpContext.RequestAborted); - return result; - } -} diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs deleted file mode 100644 index bd6ac7d..0000000 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerConvention.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.Extensions.DependencyInjection; -using Svrnty.CQRS.Abstractions.Discovery; - -namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc; - -public class DynamicQueryControllerConvention : IControllerModelConvention -{ - private readonly IServiceProvider _serviceProvider; - - public DynamicQueryControllerConvention(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public void Apply(ControllerModel controller) - { - if (controller.ControllerType.IsGenericType && controller.ControllerType.Name.Contains("DynamicQueryController") && controller.ControllerType.Assembly == typeof(DynamicQueryControllerConvention).Assembly) - { - var genericType = controller.ControllerType.GenericTypeArguments[0]; - var queryDiscovery = _serviceProvider.GetRequiredService(); - var query = queryDiscovery.FindQuery(genericType); - controller.ControllerName = query.LowerCamelCaseName; - } - } -} \ No newline at end of file diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs deleted file mode 100644 index 2694a08..0000000 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerFeatureProvider.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.Extensions.DependencyInjection; -using Svrnty.CQRS.Abstractions.Discovery; -using Svrnty.CQRS.AspNetCore.Abstractions.Attributes; -using Svrnty.CQRS.DynamicQuery.Discover; - -namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc; - -public class DynamicQueryControllerFeatureProvider(ServiceProvider serviceProvider) - : IApplicationFeatureProvider -{ - - /** - * public void PopulateFeature(IEnumerable parts, ControllerFeature feature) - { - var queryDiscovery = this.serviceProvider.GetRequiredService(); - foreach (var f in queryDiscovery.GetQueries()) - { - var ignoreAttribute = f.QueryType.GetCustomAttribute(); - if (ignoreAttribute != null) - continue; - - if (f.Category != "DynamicQuery") - continue; - - if (f is DynamicQueryMeta dynamicQueryMeta) - { - if (dynamicQueryMeta.ParamsType == null) - { - var controllerType = typeof(DynamicQueryController<,,>).MakeGenericType(f.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType); - var controllerTypeInfo = controllerType.GetTypeInfo(); - feature.Controllers.Add(controllerTypeInfo); - } - else - { - var controllerType = typeof(DynamicQueryController<,,,>).MakeGenericType(f.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType, dynamicQueryMeta.ParamsType); - var controllerTypeInfo = controllerType.GetTypeInfo(); - feature.Controllers.Add(controllerTypeInfo); - } - } - } - */ - public void PopulateFeature(IEnumerable parts, ControllerFeature feature) - { - var queryDiscovery = serviceProvider.GetRequiredService(); - foreach (var queryMeta in queryDiscovery.GetQueries()) - { - var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); - if (ignoreAttribute != null) - continue; - - if (queryMeta.Category != "DynamicQuery") - continue; - - if (queryMeta is DynamicQueryMeta dynamicQueryMeta) - { - // todo: add better error output for the user - - if (dynamicQueryMeta.ParamsType == null) - { - // todo: not aot friendly - var controllerType = typeof(DynamicQueryController<,,>).MakeGenericType(queryMeta.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType); - var controllerTypeInfo = controllerType.GetTypeInfo(); - feature.Controllers.Add(controllerTypeInfo); - } - else - { - // todo: not aot friendly - var controllerType = typeof(DynamicQueryController<,,,>).MakeGenericType(queryMeta.QueryType, dynamicQueryMeta.SourceType, dynamicQueryMeta.DestinationType, dynamicQueryMeta.ParamsType); - var controllerTypeInfo = controllerType.GetTypeInfo(); - feature.Controllers.Add(controllerTypeInfo); - } - } - } - } -} diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs deleted file mode 100644 index 5bf9bde..0000000 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/Mvc/DynamicQueryControllerOptions.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc; - -public class DynamicQueryControllerOptions -{ -} \ No newline at end of file diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs b/Svrnty.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs deleted file mode 100644 index 5d7bf38..0000000 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/MvcBuilderExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Svrnty.CQRS.DynamicQuery.AspNetCore.Mvc; - -namespace Svrnty.CQRS.DynamicQuery.AspNetCore; - -public static class MvcBuilderExtensions -{ - public static IMvcBuilder AddOpenHarborDynamicQueries(this IMvcBuilder builder, Action configuration = null) - { - var options = new DynamicQueryControllerOptions(); - configuration?.Invoke(options); - var services = builder.Services; - var serviceProvider = services.BuildServiceProvider(); - builder.AddMvcOptions(o => o.Conventions.Add(new DynamicQueryControllerConvention(serviceProvider))); - builder.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(new DynamicQueryControllerFeatureProvider(serviceProvider))); - return builder; - } -} diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/Svrnty.CQRS.DynamicQuery.AspNetCore.csproj b/Svrnty.CQRS.DynamicQuery.AspNetCore/Svrnty.CQRS.DynamicQuery.AspNetCore.csproj deleted file mode 100644 index db2c20f..0000000 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/Svrnty.CQRS.DynamicQuery.AspNetCore.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - net10.0 - false - 14 - Svrnty - icon.png - README.md - https://github.com/svrnty/dotnet-cqrs - git - true - MIT - - portable - true - true - true - snupkg - - - - - - - - - - - - - - - - - - diff --git a/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs b/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs index 085d6bb..a7eb10f 100644 --- a/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs +++ b/Svrnty.CQRS.DynamicQuery.MinimalApi/EndpointRouteBuilderExtensions.cs @@ -9,11 +9,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Security; -using Svrnty.CQRS.AspNetCore.Abstractions.Attributes; +using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.DynamicQuery.Abstractions; -using Svrnty.CQRS.DynamicQuery.AspNetCore; using Svrnty.CQRS.DynamicQuery.Discover; using PoweredSoft.DynamicQuery.Core; @@ -32,7 +32,7 @@ public static class EndpointRouteBuilderExtensions if (queryMeta.Category != "DynamicQuery") continue; - var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); + var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); if (ignoreAttribute != null) continue; diff --git a/Svrnty.CQRS.DynamicQuery.MinimalApi/Svrnty.CQRS.DynamicQuery.MinimalApi.csproj b/Svrnty.CQRS.DynamicQuery.MinimalApi/Svrnty.CQRS.DynamicQuery.MinimalApi.csproj index c169ad9..f3a7c2d 100644 --- a/Svrnty.CQRS.DynamicQuery.MinimalApi/Svrnty.CQRS.DynamicQuery.MinimalApi.csproj +++ b/Svrnty.CQRS.DynamicQuery.MinimalApi/Svrnty.CQRS.DynamicQuery.MinimalApi.csproj @@ -32,6 +32,5 @@ - diff --git a/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs b/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs index d715f54..54fc225 100644 --- a/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs +++ b/Svrnty.CQRS.DynamicQuery/Discover/DynamicQueryMeta.cs @@ -4,14 +4,10 @@ using Svrnty.CQRS.Abstractions.Discovery; namespace Svrnty.CQRS.DynamicQuery.Discover; -public class DynamicQueryMeta : QueryMeta +public class DynamicQueryMeta(Type queryType, Type serviceType, Type queryResultType) + : QueryMeta(queryType, serviceType, queryResultType) { - public DynamicQueryMeta(Type queryType, Type serviceType, Type queryResultType) : base(queryType, serviceType, queryResultType) - { - - } - - public Type SourceType => QueryType.GetGenericArguments()[0]; + public Type SourceType => QueryType.GetGenericArguments()[0]; public Type DestinationType => QueryType.GetGenericArguments()[1]; public override string Category => "DynamicQuery"; public override string Name diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs b/Svrnty.CQRS.DynamicQuery/DynamicQuery.cs similarity index 94% rename from Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs rename to Svrnty.CQRS.DynamicQuery/DynamicQuery.cs index 3dc9595..fd322eb 100644 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQuery.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQuery.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Svrnty.CQRS.DynamicQuery.Abstractions; using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; -namespace Svrnty.CQRS.DynamicQuery.AspNetCore; +namespace Svrnty.CQRS.DynamicQuery; public class DynamicQuery : DynamicQuery, IDynamicQuery where TSource : class diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs similarity index 80% rename from Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs rename to Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs index 01418b5..f04f8e0 100644 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryAggregate.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryAggregate.cs @@ -1,8 +1,8 @@ -using PoweredSoft.DynamicQuery; +using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; using System; -namespace Svrnty.CQRS.DynamicQuery.AspNetCore; +namespace Svrnty.CQRS.DynamicQuery; public class DynamicQueryAggregate { diff --git a/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryFilter.cs similarity index 94% rename from Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs rename to Svrnty.CQRS.DynamicQuery/DynamicQueryFilter.cs index ba5f0a8..826b965 100644 --- a/Svrnty.CQRS.DynamicQuery.AspNetCore/DynamicQueryFilter.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryFilter.cs @@ -1,12 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; -using Microsoft.AspNetCore.Mvc; using PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; -namespace Svrnty.CQRS.DynamicQuery.AspNetCore; +namespace Svrnty.CQRS.DynamicQuery; public class DynamicQueryFilter { @@ -17,7 +16,6 @@ public class DynamicQueryFilter public string Path { get; set; } public object Value { get; set; } - [FromQuery(Name ="value")] public string QueryValue { get @@ -71,7 +69,7 @@ public class DynamicQueryFilter value = null; break; } - + } var simpleFilter = new SimpleFilter diff --git a/Svrnty.CQRS.FluentValidation/CqrsBuilderExtensions.cs b/Svrnty.CQRS.FluentValidation/CqrsBuilderExtensions.cs new file mode 100644 index 0000000..72ef62d --- /dev/null +++ b/Svrnty.CQRS.FluentValidation/CqrsBuilderExtensions.cs @@ -0,0 +1,69 @@ +#nullable enable + +using System.Diagnostics.CodeAnalysis; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.Configuration; + +namespace Svrnty.CQRS.FluentValidation; + +/// +/// Extension methods for CqrsBuilder to support FluentValidation +/// +public static class CqrsBuilderExtensions +{ + /// + /// Adds a command handler with FluentValidation validator to the CQRS pipeline + /// + public static CqrsBuilder AddCommand( + this CqrsBuilder builder) + where TCommand : class + where TCommandHandler : class, ICommandHandler + where TValidator : class, IValidator + { + // Add the command handler + builder.AddCommand(); + + // Add the validator + builder.Services.AddTransient, TValidator>(); + + return builder; + } + + /// + /// Adds a command handler with result and FluentValidation validator to the CQRS pipeline + /// + public static CqrsBuilder AddCommand( + this CqrsBuilder builder) + where TCommand : class + where TCommandHandler : class, ICommandHandler + where TValidator : class, IValidator + { + // Add the command handler + builder.AddCommand(); + + // Add the validator + builder.Services.AddTransient, TValidator>(); + + return builder; + } + + /// + /// Adds a query handler with FluentValidation validator to the CQRS pipeline + /// + public static CqrsBuilder AddQuery( + this CqrsBuilder builder) + where TQuery : class + where TQueryHandler : class, IQueryHandler + where TValidator : class, IValidator + { + // Add the query handler + builder.AddQuery(); + + // Add the validator + builder.Services.AddTransient, TValidator>(); + + return builder; + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs index 839a6ef..f01cabb 100644 --- a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -785,6 +785,72 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); + sb.AppendLine(); + + // Add configuration-based methods + sb.AppendLine(" /// "); + sb.AppendLine(" /// Registers gRPC services based on configuration"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)"); + sb.AppendLine(" {"); + sb.AppendLine(" var config = services.BuildServiceProvider().GetService();"); + sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); + sb.AppendLine(" if (grpcOptions != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" services.AddGrpc();"); + sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); + sb.AppendLine(" services.AddGrpcReflection();"); + sb.AppendLine(); + if (hasCommands) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); + sb.AppendLine(" services.AddSingleton();"); + } + if (hasQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" services.AddSingleton();"); + } + if (hasDynamicQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" services.AddSingleton();"); + } + sb.AppendLine(" }"); + sb.AppendLine(" return services;"); + sb.AppendLine(" }"); + sb.AppendLine(); + + sb.AppendLine(" /// "); + sb.AppendLine(" /// Maps gRPC service endpoints based on configuration"); + sb.AppendLine(" /// "); + sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)"); + sb.AppendLine(" {"); + sb.AppendLine(" var config = endpoints.ServiceProvider.GetService();"); + sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); + sb.AppendLine(" if (grpcOptions != null)"); + sb.AppendLine(" {"); + if (hasCommands) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + if (hasQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + if (hasDynamicQueries) + { + sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); + sb.AppendLine(" endpoints.MapGrpcService();"); + } + sb.AppendLine(); + sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); + sb.AppendLine(" endpoints.MapGrpcReflectionService();"); + sb.AppendLine(" }"); + sb.AppendLine(" return endpoints;"); + sb.AppendLine(" }"); } sb.AppendLine(" }"); @@ -1318,11 +1384,11 @@ namespace Svrnty.CQRS.Grpc.Generators // Build the dynamic query object if (dynamicQuery.HasParams) { - sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); + sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); } else { - sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>"); + sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>"); } sb.AppendLine(" {"); sb.AppendLine(" Page = request.Page > 0 ? request.Page : null,"); @@ -1362,15 +1428,15 @@ namespace Svrnty.CQRS.Grpc.Generators } // Add helper methods for converting proto messages to AspNetCore types - sb.AppendLine(" private static List? ConvertFilters(Google.Protobuf.Collections.RepeatedField protoFilters)"); + sb.AppendLine(" private static List? ConvertFilters(Google.Protobuf.Collections.RepeatedField protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); - sb.AppendLine(" var filters = new List();"); + sb.AppendLine(" var filters = new List();"); sb.AppendLine(" foreach (var protoFilter in protoFilters)"); sb.AppendLine(" {"); - sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter"); + sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" Path = protoFilter.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)protoFilter.Type).ToString(),"); @@ -1395,12 +1461,12 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" return filters;"); sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" private static List ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField protoFilters)"); + sb.AppendLine(" private static List ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField protoFilters)"); sb.AppendLine(" {"); - sb.AppendLine(" var result = new List();"); + sb.AppendLine(" var result = new List();"); sb.AppendLine(" foreach (var pf in protoFilters)"); sb.AppendLine(" {"); - sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter"); + sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" Path = pf.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)pf.Type).ToString(),"); @@ -1447,12 +1513,12 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine(" private static List? ConvertAggregates(Google.Protobuf.Collections.RepeatedField protoAggregates)"); + sb.AppendLine(" private static List? ConvertAggregates(Google.Protobuf.Collections.RepeatedField protoAggregates)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoAggregates == null || protoAggregates.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); - sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryAggregate"); + sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate"); sb.AppendLine(" {"); sb.AppendLine(" Path = a.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()"); diff --git a/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs b/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs new file mode 100644 index 0000000..d5927d6 --- /dev/null +++ b/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs @@ -0,0 +1,72 @@ +#nullable enable +using System; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Svrnty.CQRS.Configuration; + +namespace Svrnty.CQRS.Grpc; + +/// +/// Extension methods for CqrsBuilder to add gRPC support +/// +public static class CqrsBuilderExtensions +{ + /// + /// Adds gRPC support to the CQRS pipeline + /// + /// The CQRS builder + /// Optional configuration for gRPC endpoints + /// The CQRS builder for method chaining + public static CqrsBuilder AddGrpc(this CqrsBuilder builder, Action? configure = null) + { + var options = new GrpcCqrsOptions(); + configure?.Invoke(options); + builder.Configuration.SetConfiguration(options); + + // Try to find and call the generated AddGrpcFromConfiguration method + var addGrpcMethod = FindExtensionMethod("AddGrpcFromConfiguration"); + if (addGrpcMethod != null) + { + addGrpcMethod.Invoke(null, new object[] { builder.Services }); + } + else + { + Console.WriteLine("Warning: AddGrpcFromConfiguration not found. gRPC services were not registered."); + Console.WriteLine("Make sure your project has source generators enabled and references Svrnty.CQRS.Grpc.Generators."); + } + + return builder; + } + + private static MethodInfo? FindExtensionMethod(string methodName) + { + // Search through all loaded assemblies for the extension method + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + var types = assembly.GetTypes() + .Where(t => t.IsClass && t.IsSealed && !t.IsGenericType && t.IsPublic); + + foreach (var type in types) + { + var method = type.GetMethod(methodName, + BindingFlags.Static | BindingFlags.Public, + null, + new[] { typeof(IServiceCollection) }, + null); + + if (method != null) + return method; + } + } + catch + { + // Skip assemblies that can't be inspected + } + } + + return null; + } +} diff --git a/Svrnty.CQRS.Grpc/GrpcCqrsOptions.cs b/Svrnty.CQRS.Grpc/GrpcCqrsOptions.cs new file mode 100644 index 0000000..2e93f18 --- /dev/null +++ b/Svrnty.CQRS.Grpc/GrpcCqrsOptions.cs @@ -0,0 +1,68 @@ +#nullable enable + +namespace Svrnty.CQRS.Grpc; + +/// +/// Configuration options for gRPC CQRS endpoints +/// +public class GrpcCqrsOptions +{ + /// + /// Gets whether reflection should be enabled + /// + public bool ShouldEnableReflection { get; private set; } + + private bool ShouldMapCommands { get; set; } + private bool ShouldMapQueries { get; set; } + private bool WasMappingMethodCalled { get; set; } + + /// + /// Enables gRPC reflection for the service + /// + public GrpcCqrsOptions EnableReflection() + { + ShouldEnableReflection = true; + return this; + } + + /// + /// Maps command endpoints + /// + public GrpcCqrsOptions MapCommands() + { + WasMappingMethodCalled = true; + ShouldMapCommands = true; + return this; + } + + /// + /// Maps query endpoints (includes dynamic queries) + /// + public GrpcCqrsOptions MapQueries() + { + WasMappingMethodCalled = true; + ShouldMapQueries = true; + return this; + } + + /// + /// Maps both command and query endpoints + /// + public GrpcCqrsOptions MapCommandsAndQueries() + { + WasMappingMethodCalled = true; + ShouldMapCommands = true; + ShouldMapQueries = true; + return this; + } + + /// + /// Gets whether commands should be mapped (defaults to true if no mapping methods were called) + /// + public bool GetShouldMapCommands() => WasMappingMethodCalled ? ShouldMapCommands : true; + + /// + /// Gets whether queries should be mapped (defaults to true if no mapping methods were called) + /// + public bool GetShouldMapQueries() => WasMappingMethodCalled ? ShouldMapQueries : true; +} diff --git a/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj b/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj index 190c1bb..a78b17d 100644 --- a/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj +++ b/Svrnty.CQRS.Grpc/Svrnty.CQRS.Grpc.csproj @@ -2,10 +2,10 @@ net10.0 false - David Lebee, Mathias Beaulieu-Duncan 14 - Svrnty enable + David Lebee, Mathias Beaulieu-Duncan + Svrnty icon.png README.md https://github.com/svrnty/dotnet-cqrs @@ -23,21 +23,13 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + - - - + diff --git a/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs b/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs new file mode 100644 index 0000000..3c26660 --- /dev/null +++ b/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs @@ -0,0 +1,25 @@ +#nullable enable +using System; +using Svrnty.CQRS.Configuration; + +namespace Svrnty.CQRS.MinimalApi; + +/// +/// Extension methods for CqrsBuilder to add MinimalApi support +/// +public static class CqrsBuilderExtensions +{ + /// + /// Adds MinimalApi support to the CQRS pipeline + /// + /// The CQRS builder + /// Optional configuration for MinimalApi endpoints + /// The CQRS builder for method chaining + public static CqrsBuilder AddMinimalApi(this CqrsBuilder builder, Action? configure = null) + { + var options = new MinimalApiCqrsOptions(); + configure?.Invoke(options); + builder.Configuration.SetConfiguration(options); + return builder; + } +} diff --git a/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs b/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs index bb8f7f9..8aef222 100644 --- a/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs +++ b/Svrnty.CQRS.MinimalApi/EndpointRouteBuilderExtensions.cs @@ -8,9 +8,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.Abstractions.Attributes; using Svrnty.CQRS.Abstractions.Discovery; using Svrnty.CQRS.Abstractions.Security; -using Svrnty.CQRS.AspNetCore.Abstractions.Attributes; namespace Svrnty.CQRS.MinimalApi; @@ -23,7 +23,7 @@ public static class EndpointRouteBuilderExtensions foreach (var queryMeta in queryDiscovery.GetQueries()) { - var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); + var ignoreAttribute = queryMeta.QueryType.GetCustomAttribute(); if (ignoreAttribute != null) continue; @@ -157,7 +157,7 @@ public static class EndpointRouteBuilderExtensions foreach (var commandMeta in commandDiscovery.GetCommands()) { - var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute(); + var ignoreAttribute = commandMeta.CommandType.GetCustomAttribute(); if (ignoreAttribute != null) continue; diff --git a/Svrnty.CQRS.MinimalApi/MinimalApiCqrsOptions.cs b/Svrnty.CQRS.MinimalApi/MinimalApiCqrsOptions.cs new file mode 100644 index 0000000..8ea299e --- /dev/null +++ b/Svrnty.CQRS.MinimalApi/MinimalApiCqrsOptions.cs @@ -0,0 +1,39 @@ +#nullable enable + +namespace Svrnty.CQRS.MinimalApi; + +/// +/// Configuration options for MinimalApi CQRS endpoints +/// +public class MinimalApiCqrsOptions +{ + /// + /// Whether to map command endpoints (default: true) + /// + public bool MapCommands { get; set; } = true; + + /// + /// Whether to map query endpoints (default: true) + /// + public bool MapQueries { get; set; } = true; + + /// + /// Whether to map dynamic query endpoints (default: true) + /// + public bool MapDynamicQueries { get; set; } = true; + + /// + /// Route prefix for command endpoints (default: "api/command") + /// + public string CommandRoutePrefix { get; set; } = "api/command"; + + /// + /// Route prefix for query endpoints (default: "api/query") + /// + public string QueryRoutePrefix { get; set; } = "api/query"; + + /// + /// Route prefix for dynamic query endpoints (default: "api/query") + /// + public string DynamicQueryRoutePrefix { get; set; } = "api/query"; +} diff --git a/Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj b/Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj index c5e3cfe..772da5a 100644 --- a/Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj +++ b/Svrnty.CQRS.MinimalApi/Svrnty.CQRS.MinimalApi.csproj @@ -35,6 +35,6 @@ - + diff --git a/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs b/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs new file mode 100644 index 0000000..6bb7406 --- /dev/null +++ b/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs @@ -0,0 +1,101 @@ +#nullable enable + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Svrnty.CQRS.Configuration; +using System; +using System.Linq; +using System.Reflection; + +namespace Svrnty.CQRS.MinimalApi; + +public static class WebApplicationExtensions +{ + /// + /// Maps Svrnty CQRS endpoints based on configuration (supports both gRPC and MinimalApi) + /// + public static WebApplication UseSvrntyCqrs(this WebApplication app) + { + var config = app.Services.GetService(); + + // Handle gRPC configuration if available + // Note: GrpcCqrsOptions type is from Svrnty.CQRS.Grpc package + var grpcOptionsType = Type.GetType("Svrnty.CQRS.Grpc.GrpcCqrsOptions, Svrnty.CQRS.Grpc"); + if (grpcOptionsType != null && config != null) + { + var getConfigMethod = typeof(CqrsConfiguration).GetMethod("GetConfiguration")!.MakeGenericMethod(grpcOptionsType); + var grpcOptions = getConfigMethod.Invoke(config, null); + + if (grpcOptions != null) + { + // Try to find and call MapGrpcFromConfiguration extension method via reflection + // This is generated by the source generator in consumer projects + var grpcMethod = FindExtensionMethod("MapGrpcFromConfiguration"); + if (grpcMethod != null) + { + grpcMethod.Invoke(null, new object[] { app }); + } + else + { + Console.WriteLine("Warning: MapGrpcFromConfiguration not found. gRPC endpoints were not mapped."); + Console.WriteLine("Make sure your project references Svrnty.CQRS.Grpc and has source generators enabled."); + } + } + } + + // Handle MinimalApi configuration if available + var minimalApiOptions = config?.GetConfiguration(); + if (minimalApiOptions != null) + { + if (minimalApiOptions.MapCommands) + { + app.MapSvrntyCommands(minimalApiOptions.CommandRoutePrefix); + } + + if (minimalApiOptions.MapQueries) + { + app.MapSvrntyQueries(minimalApiOptions.QueryRoutePrefix); + } + + // TODO: Add dynamic query mapping when available + // if (minimalApiOptions.MapDynamicQueries) + // { + // app.MapSvrntyDynamicQueries(minimalApiOptions.DynamicQueryRoutePrefix); + // } + } + + return app; + } + + private static MethodInfo? FindExtensionMethod(string methodName) + { + // Search through all loaded assemblies for the extension method + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + try + { + var types = assembly.GetTypes() + .Where(t => t.IsClass && t.IsSealed && !t.IsGenericType && t.IsPublic); + + foreach (var type in types) + { + var method = type.GetMethod(methodName, + BindingFlags.Static | BindingFlags.Public, + null, + new[] { typeof(IEndpointRouteBuilder) }, + null); + + if (method != null) + return method; + } + } + catch + { + // Skip assemblies that can't be inspected + } + } + + return null; + } +} diff --git a/Svrnty.CQRS.sln b/Svrnty.CQRS.sln index b333cef..fcbe35b 100644 --- a/Svrnty.CQRS.sln +++ b/Svrnty.CQRS.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.Abstractions", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS", "Svrnty.CQRS\Svrnty.CQRS.csproj", "{7069B98F-8736-4114-8AF5-1ACE094E6238}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.AspNetCore.Abstractions", "Svrnty.CQRS.AspNetCore.Abstractions\Svrnty.CQRS.AspNetCore.Abstractions.csproj", "{4C466827-31D3-4081-A751-C2FC7C381D7E}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{617BA357-1A1F-40C5-B19A-A65A960E6142}" ProjectSection(SolutionItems) = preProject LICENSE = LICENSE @@ -17,8 +15,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery", "Svrnty.CQRS.DynamicQuery\Svrnty.CQRS.DynamicQuery.csproj", "{A38CE930-191F-417C-B5BE-8CC62DB47513}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.DynamicQuery.AspNetCore", "Svrnty.CQRS.DynamicQuery.AspNetCore\Svrnty.CQRS.DynamicQuery.AspNetCore.csproj", "{0829B99A-0A20-4CAC-A91E-FB67E18444DE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Svrnty.CQRS.FluentValidation", "Svrnty.CQRS.FluentValidation\Svrnty.CQRS.FluentValidation.csproj", "{70BD37C4-7497-474D-9A40-A701203971D8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Svrnty.CQRS.DynamicQuery.Abstractions", "Svrnty.CQRS.DynamicQuery.Abstractions\Svrnty.CQRS.DynamicQuery.Abstractions.csproj", "{8B9F8ACE-10EA-4215-9776-DE29EC93B020}" @@ -69,18 +65,6 @@ Global {7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x64.Build.0 = Release|Any CPU {7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.ActiveCfg = Release|Any CPU {7069B98F-8736-4114-8AF5-1ACE094E6238}.Release|x86.Build.0 = Release|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.ActiveCfg = Debug|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x64.Build.0 = Debug|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.ActiveCfg = Debug|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Debug|x86.Build.0 = Debug|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|Any CPU.Build.0 = Release|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.ActiveCfg = Release|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x64.Build.0 = Release|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.ActiveCfg = Release|Any CPU - {4C466827-31D3-4081-A751-C2FC7C381D7E}.Release|x86.Build.0 = Release|Any CPU {A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|Any CPU.Build.0 = Debug|Any CPU {A38CE930-191F-417C-B5BE-8CC62DB47513}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -93,18 +77,6 @@ Global {A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x64.Build.0 = Release|Any CPU {A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.ActiveCfg = Release|Any CPU {A38CE930-191F-417C-B5BE-8CC62DB47513}.Release|x86.Build.0 = Release|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.ActiveCfg = Debug|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x64.Build.0 = Debug|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.ActiveCfg = Debug|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Debug|x86.Build.0 = Debug|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|Any CPU.Build.0 = Release|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.ActiveCfg = Release|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x64.Build.0 = Release|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.ActiveCfg = Release|Any CPU - {0829B99A-0A20-4CAC-A91E-FB67E18444DE}.Release|x86.Build.0 = Release|Any CPU {70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70BD37C4-7497-474D-9A40-A701203971D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {70BD37C4-7497-474D-9A40-A701203971D8}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/Svrnty.CQRS/Configuration/CqrsBuilder.cs b/Svrnty.CQRS/Configuration/CqrsBuilder.cs new file mode 100644 index 0000000..2de17d4 --- /dev/null +++ b/Svrnty.CQRS/Configuration/CqrsBuilder.cs @@ -0,0 +1,77 @@ +#nullable enable + +using Microsoft.Extensions.DependencyInjection; +using Svrnty.CQRS.Abstractions; +using Svrnty.CQRS.Discovery; + +namespace Svrnty.CQRS.Configuration; + +/// +/// Builder for configuring CQRS services +/// +public class CqrsBuilder +{ + /// + /// Gets the service collection for manual service registration + /// + public IServiceCollection Services { get; } + + /// + /// Gets the CQRS configuration object (used by extension packages) + /// + public CqrsConfiguration Configuration { get; } + + internal CqrsBuilder(IServiceCollection services) + { + Services = services; + Configuration = new CqrsConfiguration(); + + // Add discovery services by default + services.AddDefaultCommandDiscovery(); + services.AddDefaultQueryDiscovery(); + + // Register configuration as singleton so it can be accessed later + services.AddSingleton(Configuration); + } + + /// + /// Completes the builder configuration + /// + internal void Build() + { + // Configuration is now handled by extension methods in respective packages + } + + /// + /// Adds a command handler to the CQRS pipeline + /// + public CqrsBuilder AddCommand() + where TCommand : class + where TCommandHandler : class, ICommandHandler + { + Services.AddCommand(); + return this; + } + + /// + /// Adds a command handler with result to the CQRS pipeline + /// + public CqrsBuilder AddCommand() + where TCommand : class + where TCommandHandler : class, ICommandHandler + { + Services.AddCommand(); + return this; + } + + /// + /// Adds a query handler to the CQRS pipeline + /// + public CqrsBuilder AddQuery() + where TQuery : class + where TQueryHandler : class, IQueryHandler + { + Services.AddQuery(); + return this; + } +} diff --git a/Svrnty.CQRS/Configuration/CqrsConfiguration.cs b/Svrnty.CQRS/Configuration/CqrsConfiguration.cs new file mode 100644 index 0000000..9441388 --- /dev/null +++ b/Svrnty.CQRS/Configuration/CqrsConfiguration.cs @@ -0,0 +1,38 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace Svrnty.CQRS.Configuration; + +/// +/// Configuration for CQRS services and endpoints. +/// Supports extension by third-party packages through generic configuration storage. +/// +public class CqrsConfiguration +{ + private readonly Dictionary _configurations = new(); + + /// + /// Sets a configuration object for a specific type + /// + public void SetConfiguration(T config) where T : class + { + _configurations[typeof(T)] = config; + } + + /// + /// Gets a configuration object for a specific type + /// + public T? GetConfiguration() where T : class + { + return _configurations.TryGetValue(typeof(T), out var config) ? config as T : null; + } + + /// + /// Checks if a configuration exists for a specific type + /// + public bool HasConfiguration() where T : class + { + return _configurations.ContainsKey(typeof(T)); + } +} diff --git a/Svrnty.CQRS/ServiceCollectionExtensions.cs b/Svrnty.CQRS/ServiceCollectionExtensions.cs index b806aa4..4ad06cf 100644 --- a/Svrnty.CQRS/ServiceCollectionExtensions.cs +++ b/Svrnty.CQRS/ServiceCollectionExtensions.cs @@ -1,12 +1,27 @@ -using Microsoft.Extensions.DependencyInjection; +#nullable enable + +using System; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Svrnty.CQRS.Abstractions.Discovery; +using Svrnty.CQRS.Configuration; using Svrnty.CQRS.Discovery; namespace Svrnty.CQRS; public static class ServiceCollectionExtensions { + /// + /// Adds Svrnty CQRS services with fluent configuration + /// + public static IServiceCollection AddSvrntyCqrs(this IServiceCollection services, Action? configure = null) + { + var builder = new CqrsBuilder(services); + configure?.Invoke(builder); + builder.Build(); // Execute deferred registrations + return services; + } + public static IServiceCollection AddDefaultQueryDiscovery(this IServiceCollection services) { services.TryAddTransient(); diff --git a/Svrnty.Sample/Program.cs b/Svrnty.Sample/Program.cs index 14cdef6..dac7534 100644 --- a/Svrnty.Sample/Program.cs +++ b/Svrnty.Sample/Program.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Svrnty.CQRS; -using Svrnty.CQRS.Abstractions; using Svrnty.CQRS.FluentValidation; +using Svrnty.CQRS.Grpc; using Svrnty.Sample; -using Svrnty.Sample.Grpc.Extensions; using Svrnty.CQRS.MinimalApi; using Svrnty.CQRS.DynamicQuery; using Svrnty.CQRS.DynamicQuery.MinimalApi; @@ -19,37 +18,44 @@ builder.WebHost.ConfigureKestrel(options => options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1); }); -builder.Services.AddCommand(); -builder.Services.AddCommand(); - -builder.Services.AddQuery(); - +// IMPORTANT: Register dynamic query dependencies FIRST +// (before AddSvrntyCqrs, so gRPC services can find the handlers) builder.Services.AddTransient(); builder.Services.AddTransient(); - builder.Services.AddDynamicQueryWithProvider(); -builder.Services.AddDefaultCommandDiscovery(); -builder.Services.AddDefaultQueryDiscovery(); +// Configure CQRS with fluent API +builder.Services.AddSvrntyCqrs(cqrs => +{ + // Register commands and queries with validators + cqrs.AddCommand(); + cqrs.AddCommand(); + cqrs.AddQuery(); + // Enable gRPC endpoints with reflection + cqrs.AddGrpc(grpc => + { + grpc.EnableReflection(); + }); + // Enable MinimalApi endpoints + cqrs.AddMinimalApi(); +}); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); -builder.Services.AddGrpcCommandsAndQueries(); -app.MapGrpcCommandsAndQueries(); -app.MapGrpcReflectionService(); +// Map all configured CQRS endpoints (gRPC and MinimalApi) +app.UseSvrntyCqrs(); + +// Map dynamic queries manually for now +app.MapSvrntyDynamicQueries(); app.UseSwagger(); app.UseSwaggerUI(); -app.MapSvrntyCommands(); -app.MapSvrntyQueries(); -app.MapSvrntyDynamicQueries(); - Console.WriteLine("Auto-Generated gRPC Server with Reflection, Validation, MinimalApi and Swagger"); Console.WriteLine("gRPC (HTTP/2): http://localhost:6000"); diff --git a/Svrnty.Sample/Svrnty.Sample.csproj b/Svrnty.Sample/Svrnty.Sample.csproj index 8aabf02..2410a08 100644 --- a/Svrnty.Sample/Svrnty.Sample.csproj +++ b/Svrnty.Sample/Svrnty.Sample.csproj @@ -32,6 +32,7 @@ +