From e19fad2baa7512f0f6b84074afaeac301eab7ae6 Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Tue, 4 Nov 2025 15:05:07 -0500 Subject: [PATCH] checkpoint --- Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs | 19 +++- .../CqrsBuilderExtensions.cs | 68 ++++++++++++++ .../WebApplicationExtensions.cs | 88 +------------------ .../Configuration/CqrsConfiguration.cs | 24 +++++ Svrnty.Sample/Program.cs | 10 +-- 5 files changed, 115 insertions(+), 94 deletions(-) diff --git a/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs b/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs index d5927d6..bfc3a0e 100644 --- a/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs +++ b/Svrnty.CQRS.Grpc/CqrsBuilderExtensions.cs @@ -24,8 +24,8 @@ public static class CqrsBuilderExtensions configure?.Invoke(options); builder.Configuration.SetConfiguration(options); - // Try to find and call the generated AddGrpcFromConfiguration method - var addGrpcMethod = FindExtensionMethod("AddGrpcFromConfiguration"); + // Try to find and call the generated AddGrpcFromConfiguration method for service registration + var addGrpcMethod = FindExtensionMethod("AddGrpcFromConfiguration", typeof(IServiceCollection)); if (addGrpcMethod != null) { addGrpcMethod.Invoke(null, new object[] { builder.Services }); @@ -36,10 +36,21 @@ public static class CqrsBuilderExtensions Console.WriteLine("Make sure your project has source generators enabled and references Svrnty.CQRS.Grpc.Generators."); } + // Register mapping callback for automatic endpoint mapping + builder.Configuration.AddMappingCallback(app => + { + // Find the generated MapGrpcFromConfiguration method + var mapGrpcMethod = FindExtensionMethod("MapGrpcFromConfiguration", typeof(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)); + if (mapGrpcMethod != null) + { + mapGrpcMethod.Invoke(null, new object[] { app }); + } + }); + return builder; } - private static MethodInfo? FindExtensionMethod(string methodName) + private static MethodInfo? FindExtensionMethod(string methodName, Type parameterType) { // Search through all loaded assemblies for the extension method foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) @@ -54,7 +65,7 @@ public static class CqrsBuilderExtensions var method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public, null, - new[] { typeof(IServiceCollection) }, + new[] { parameterType }, null); if (method != null) diff --git a/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs b/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs index 3c26660..4679091 100644 --- a/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs +++ b/Svrnty.CQRS.MinimalApi/CqrsBuilderExtensions.cs @@ -1,5 +1,8 @@ #nullable enable using System; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; using Svrnty.CQRS.Configuration; namespace Svrnty.CQRS.MinimalApi; @@ -20,6 +23,71 @@ public static class CqrsBuilderExtensions var options = new MinimalApiCqrsOptions(); configure?.Invoke(options); builder.Configuration.SetConfiguration(options); + + // Register mapping callback for automatic endpoint mapping + builder.Configuration.AddMappingCallback(app => + { + if (app is not WebApplication webApp) + return; + + var config = webApp.Services.GetService(typeof(CqrsConfiguration)) as CqrsConfiguration; + var minimalApiOptions = config?.GetConfiguration(); + + if (minimalApiOptions == null) + return; + + // Map commands if enabled + if (minimalApiOptions.MapCommands) + { + webApp.MapSvrntyCommands(minimalApiOptions.CommandRoutePrefix); + } + + // Map queries if enabled + if (minimalApiOptions.MapQueries) + { + webApp.MapSvrntyQueries(minimalApiOptions.QueryRoutePrefix); + } + + // Map dynamic queries if enabled (automatically included) + if (minimalApiOptions.MapDynamicQueries) + { + // Try to find the DynamicQuery.MinimalApi assembly and call MapSvrntyDynamicQueries + try + { + // Force load the assembly by looking for a known type from it + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + var dynamicQueryAssembly = assemblies.FirstOrDefault(a => a.GetName().Name == "Svrnty.CQRS.DynamicQuery.MinimalApi"); + + if (dynamicQueryAssembly == null) + { + // Try to load it by finding the type + var extensionType = Type.GetType("Svrnty.CQRS.DynamicQuery.MinimalApi.EndpointRouteBuilderExtensions, Svrnty.CQRS.DynamicQuery.MinimalApi"); + if (extensionType != null) + { + dynamicQueryAssembly = extensionType.Assembly; + } + } + + if (dynamicQueryAssembly != null) + { + var extensionType = dynamicQueryAssembly.GetType("Svrnty.CQRS.DynamicQuery.MinimalApi.EndpointRouteBuilderExtensions"); + if (extensionType != null) + { + var mapMethod = extensionType.GetMethod("MapSvrntyDynamicQueries", BindingFlags.Public | BindingFlags.Static); + if (mapMethod != null) + { + mapMethod.Invoke(null, new object[] { webApp, minimalApiOptions.DynamicQueryRoutePrefix }); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Could not map dynamic queries: {ex.Message}"); + } + } + }); + return builder; } } diff --git a/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs b/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs index 6bb7406..a60a6c5 100644 --- a/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs +++ b/Svrnty.CQRS.MinimalApi/WebApplicationExtensions.cs @@ -1,101 +1,21 @@ #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) + /// Maps all configured CQRS endpoints (gRPC, MinimalApi, and Dynamic Queries). + /// This method is framework-agnostic and executes mapping callbacks registered by extension packages. /// 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); - // } - } - + var config = app.Services.GetRequiredService(); + config.ExecuteMappingCallbacks(app); 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/Configuration/CqrsConfiguration.cs b/Svrnty.CQRS/Configuration/CqrsConfiguration.cs index 9441388..bd19f34 100644 --- a/Svrnty.CQRS/Configuration/CqrsConfiguration.cs +++ b/Svrnty.CQRS/Configuration/CqrsConfiguration.cs @@ -11,6 +11,7 @@ namespace Svrnty.CQRS.Configuration; public class CqrsConfiguration { private readonly Dictionary _configurations = new(); + private readonly List> _mappingCallbacks = new(); /// /// Sets a configuration object for a specific type @@ -35,4 +36,27 @@ public class CqrsConfiguration { return _configurations.ContainsKey(typeof(T)); } + + /// + /// Registers a callback to be executed during endpoint mapping. + /// Used by extension packages to register their endpoint mapping logic. + /// + /// Callback that accepts an application builder (typically WebApplication) + public void AddMappingCallback(Action callback) + { + _mappingCallbacks.Add(callback); + } + + /// + /// Executes all registered mapping callbacks. + /// Called by UseSvrntyCqrs() to map endpoints from all configured packages. + /// + /// The application builder (typically WebApplication) + public void ExecuteMappingCallbacks(object app) + { + foreach (var callback in _mappingCallbacks) + { + callback(app); + } + } } diff --git a/Svrnty.Sample/Program.cs b/Svrnty.Sample/Program.cs index dac7534..27380cc 100644 --- a/Svrnty.Sample/Program.cs +++ b/Svrnty.Sample/Program.cs @@ -5,7 +5,6 @@ using Svrnty.CQRS.Grpc; using Svrnty.Sample; using Svrnty.CQRS.MinimalApi; using Svrnty.CQRS.DynamicQuery; -using Svrnty.CQRS.DynamicQuery.MinimalApi; var builder = WebApplication.CreateBuilder(args); @@ -39,7 +38,9 @@ builder.Services.AddSvrntyCqrs(cqrs => }); // Enable MinimalApi endpoints - cqrs.AddMinimalApi(); + cqrs.AddMinimalApi(configure => + { + }); }); builder.Services.AddEndpointsApiExplorer(); @@ -47,12 +48,9 @@ builder.Services.AddSwaggerGen(); var app = builder.Build(); -// Map all configured CQRS endpoints (gRPC and MinimalApi) +// Map all configured CQRS endpoints (gRPC, MinimalApi, and Dynamic Queries) app.UseSvrntyCqrs(); -// Map dynamic queries manually for now -app.MapSvrntyDynamicQueries(); - app.UseSwagger(); app.UseSwaggerUI();