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 @@
+