checkpoint

This commit is contained in:
Mathias Beaulieu-Duncan 2025-11-04 15:05:07 -05:00
parent facc8d7851
commit e19fad2baa
5 changed files with 115 additions and 94 deletions

View File

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

View File

@ -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<MinimalApiCqrsOptions>();
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;
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public static WebApplication UseSvrntyCqrs(this WebApplication app)
{
var config = app.Services.GetService<CqrsConfiguration>();
// 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<MinimalApiCqrsOptions>();
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<CqrsConfiguration>();
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;
}
}

View File

@ -11,6 +11,7 @@ namespace Svrnty.CQRS.Configuration;
public class CqrsConfiguration
{
private readonly Dictionary<Type, object> _configurations = new();
private readonly List<Action<object>> _mappingCallbacks = new();
/// <summary>
/// Sets a configuration object for a specific type
@ -35,4 +36,27 @@ public class CqrsConfiguration
{
return _configurations.ContainsKey(typeof(T));
}
/// <summary>
/// Registers a callback to be executed during endpoint mapping.
/// Used by extension packages to register their endpoint mapping logic.
/// </summary>
/// <param name="callback">Callback that accepts an application builder (typically WebApplication)</param>
public void AddMappingCallback(Action<object> callback)
{
_mappingCallbacks.Add(callback);
}
/// <summary>
/// Executes all registered mapping callbacks.
/// Called by UseSvrntyCqrs() to map endpoints from all configured packages.
/// </summary>
/// <param name="app">The application builder (typically WebApplication)</param>
public void ExecuteMappingCallbacks(object app)
{
foreach (var callback in _mappingCallbacks)
{
callback(app);
}
}
}

View File

@ -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();