Dispatch mediator endpoints through Solid processors - #870
Conversation
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Routes Mediator Framework generated endpoints (Minimal API, gRPC, Azure Functions, Rebus) through Ark.Tools.Solid processors so processor-level cross-cutting behavior applies consistently to generated requests/queries/commands.
Changes:
- Update endpoint generators to resolve
IRequestProcessor/IQueryProcessor/ICommandProcessorand dispatch viaExecuteAsync<...>()TSelf overloads. - Update runtime helpers (multipart + Azure Functions invocation) to use processors and TSelf constraints.
- Update tests/contracts and sample/hosts to use TSelf interfaces and register SimpleInjector processor implementations.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Ark.Tools.MediatorFramework.Tests/GeneratorSnapshotTests.cs | Updates snapshot assertions/contracts to expect processor-based dispatch and TSelf interfaces. |
| tests/Ark.Tools.MediatorFramework.Hosting.Tests/HostingTestFixture.cs | Registers Solid processors in the hosting test container. |
| tests/Ark.Tools.MediatorFramework.Hosting.Contracts/HostingContracts.cs | Converts hosting test contracts to TSelf IRequest<TSelf,TResponse> / IQuery<TSelf,TResult> / ICommand<TSelf>. |
| tests/Ark.Tools.MediatorFramework.AzureFunctions.Boundary.TestHost/Program.cs | Registers Solid processors in the Azure Functions boundary test host container. |
| tests/Ark.Tools.MediatorFramework.AzureFunctions.Boundary.TestHost/Ark.Tools.MediatorFramework.AzureFunctions.Boundary.TestHost.csproj | Adds Solid.SimpleInjector project reference needed for processor implementations. |
| src/mediator-framework/Ark.Tools.MediatorFramework.Rebus.Generators/RebusEndpointGenerator.cs | Generates Rebus handlers that dispatch through ICommandProcessor / IRequestProcessor. |
| src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi/ArkMultipartEx.cs | Switches multipart upload endpoint helper to use IRequestProcessor + TSelf request constraint. |
| src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs | Generates Minimal API endpoints that dispatch through query/request/command processors. |
| src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs | Generates gRPC implementations that dispatch through processors instead of handlers. |
| src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions/ArkAzureFunctionsInvocation.cs | Routes Azure Functions invocation helpers through processors + TSelf constraints. |
| src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions.Generators/AzureFunctionsEndpointGenerator.cs | Generates Azure Functions endpoints that dispatch through processors. |
| samples/Ark.MediatorFramework.Sample/src/Ark.MediatorFramework.Sample.Application/Host/ApplicationComposition.cs | Registers processors in the sample application container composition. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
Suppressed comments (10)
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:974
- Same issue as above: explicit ExecuteAsync<TRequest,TResponse> in generated code prevents compilation for non-TSelf contracts. Emit ExecuteAsync(request, cancellationToken) so the best overload is selected automatically.
sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);");
sb.AppendLine(" var processor = container.GetInstance<" + processorService + ">();");
sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, cancellationToken).ConfigureAwait(false);");
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:1222
- Multipart endpoint emission hard-codes ExecuteAsync<TRequest,TResponse>, which will not compile for attachment request types that implement only IRequest. Prefer emitting ExecuteAsync(request, cancellationToken) to support both interface variants while still favoring the TSelf overload when available.
sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);");
sb.AppendLine(" var processor = container.GetInstance<" + processorService + ">();");
sb.AppendLine(" var result = await processor.ExecuteAsync<" + endpoint.TypeFullName + ", " + endpoint.Response + ">(request, cancellationToken).ConfigureAwait(false);");
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:1276
- Download endpoint emission hard-codes ExecuteAsync<TRequest,TResponse>, which breaks compilation for non-TSelf query/request contracts. Emit ExecuteAsync(request, cancellationToken) and rely on overload resolution.
sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);");
sb.AppendLine(" var processor = container.GetInstance<" + processorService + ">();");
sb.AppendLine(" var result = await processor.ExecuteAsync<" + endpoint.TypeFullName + ", " + endpoint.Response + ">(request, cancellationToken).ConfigureAwait(false);");
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:1340
- Command endpoint emission hard-codes ExecuteAsync, which will not compile for contracts that only implement ICommand (non-TSelf). Emit ExecuteAsync(request, cancellationToken) so ICommand is used when available and ICommand fallback otherwise.
sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);");
sb.AppendLine(" var processor = container.GetInstance<global::Ark.Tools.Solid.ICommandProcessor>();");
sb.AppendLine(" await processor.ExecuteAsync<" + endpoint.TypeFullName + ">(request, cancellationToken).ConfigureAwait(false);");
sb.AppendLine(" return global::Microsoft.AspNetCore.Http.TypedResults.NoContent();");
src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs:519
- Same issue: explicit ExecuteAsync<TRequest,TResponse> breaks non-TSelf contracts. Emit ExecuteAsync(request, context.CancellationToken) and rely on overload resolution.
else if (e.AttachmentResponse)
{
sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, context.CancellationToken).ConfigureAwait(false);");
sb.AppendLine(" if (result is null)");
src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs:531
- Same issue for streaming endpoints: avoid explicit generic args so non-TSelf contracts still compile.
else if (e.IsStreaming)
{
sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, context.CancellationToken).ConfigureAwait(false);");
sb.AppendLine(" await foreach (var item in result.WithCancellation(context.CancellationToken).ConfigureAwait(false))");
src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs:537
- Command dispatch is emitted as ExecuteAsync, which doesn’t compile for contracts that only implement ICommand. Emit ExecuteAsync(request, context.CancellationToken) so ICommand is used when available and ICommand fallback otherwise.
else if (e.Kind == HandlerKind.Command)
{
sb.AppendLine(" await processor.ExecuteAsync<" + e.TypeFullName + ">(request, context.CancellationToken).ConfigureAwait(false);");
sb.AppendLine(" return new global::Google.Protobuf.WellKnownTypes.Empty();");
src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs:542
- Same issue for non-streaming request/query: explicit ExecuteAsync<TRequest,TResponse> breaks non-TSelf contracts. Emit ExecuteAsync(request, context.CancellationToken).
else
{
sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, context.CancellationToken).ConfigureAwait(false);");
AppendNotFoundGuard(sb);
src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions.Generators/AzureFunctionsEndpointGenerator.cs:354
- Azure Functions generator hard-codes ExecuteAsync<TQuery,TResponse>, which won’t compile for query contracts that only implement IQuery. Emit ExecuteAsync(body, cancellationToken) so the TSelf overload is used when available and the non-TSelf overload otherwise.
else if (endpoint.Kind == HandlerKind.Query)
{
source.AppendLine(" var _processor = _container.GetInstance<global::Ark.Tools.Solid.IQueryProcessor>();");
source.Append(" var _result = await _processor.ExecuteAsync<").Append(endpoint.FullyQualifiedType).Append(", ").Append(endpoint.ResponseType).AppendLine(">(body, cancellationToken).ConfigureAwait(false);");
source.Append(" if (_result is null) return global::Microsoft.AspNetCore.Http.Results.StatusCode(")
src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions.Generators/AzureFunctionsEndpointGenerator.cs:364
- Azure Functions generator hard-codes ExecuteAsync<TRequest,TResponse>, which breaks compilation for request contracts that only implement IRequest. Emit ExecuteAsync(body, cancellationToken) to support both interface variants.
else
{
source.AppendLine(" var _processor = _container.GetInstance<global::Ark.Tools.Solid.IRequestProcessor>();");
source.Append(" var _result = await _processor.ExecuteAsync<").Append(endpoint.FullyQualifiedType).Append(", ").Append(endpoint.ResponseType).AppendLine(">(body, cancellationToken).ConfigureAwait(false);");
source.Append(" if (_result is null) return global::Microsoft.AspNetCore.Http.Results.StatusCode(")
| sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);"); | ||
| sb.AppendLine(" var handler = container.GetInstance<" + handlerService + ">();"); | ||
| sb.AppendLine(" var result = await handler.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);"); | ||
| sb.AppendLine(" var processor = container.GetInstance<" + processorService + ">();"); | ||
| sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, cancellationToken).ConfigureAwait(false);"); |
| : "new global::Ark.MediatorFramework.StreamingArkAttachment(chunks)"; | ||
| sb.AppendLine(" var request = new " + e.TypeFullName + " { " + e.AttachmentPropertyName + " = " + attachmentValue + " };"); | ||
| sb.AppendLine(" var result = await handler.ExecuteAsync(request, context.CancellationToken).ConfigureAwait(false);"); | ||
| sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, context.CancellationToken).ConfigureAwait(false);"); | ||
| AppendNotFoundGuard(sb); |
| var dispatch = e.IsCommand | ||
| ? "_processor.ExecuteAsync<" + e.TypeFullName + ">(message, " | ||
| : "_processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(message, "; | ||
| sb.AppendLine(" => await " + dispatch + "global::Rebus.Extensions.MessageContextExtensions.GetCancellationToken(global::Rebus.Pipeline.MessageContext.Current)).ConfigureAwait(false);"); |
| if (endpoint.Kind == HandlerKind.Command) | ||
| { | ||
| source.Append(" var _handler = _container.GetInstance<global::Ark.Tools.Solid.ICommandHandler<").Append(endpoint.FullyQualifiedType).AppendLine(">>();"); | ||
| source.AppendLine(" await _handler.ExecuteAsync(body, cancellationToken).ConfigureAwait(false);"); | ||
| source.AppendLine(" var _processor = _container.GetInstance<global::Ark.Tools.Solid.ICommandProcessor>();"); | ||
| source.Append(" await _processor.ExecuteAsync<").Append(endpoint.FullyQualifiedType).AppendLine(">(body, cancellationToken).ConfigureAwait(false);"); | ||
| source.Append(" return global::Microsoft.AspNetCore.Http.Results.StatusCode(") |
| """); | ||
|
|
||
| generated.Should().Contain("ICommandHandler<global::DeleteCommand>"); | ||
| generated.Should().Contain("ICommandProcessor"); | ||
| generated.Should().Contain("TypedResults.NoContent()"); |
| container.RegisterSingleton<IRequestProcessor, SimpleInjectorRequestProcessor>(); | ||
| container.RegisterSingleton<IQueryProcessor, SimpleInjectorQueryProcessor>(); | ||
| container.RegisterSingleton<ICommandProcessor, SimpleInjectorCommandProcessor>(); |
| { | ||
| ArgumentNullException.ThrowIfNull(container); | ||
| container.RegisterSingleton<IRequestProcessor, SimpleInjectorRequestProcessor>(); | ||
| container.RegisterSingleton<IQueryProcessor, SimpleInjectorQueryProcessor>(); | ||
| container.RegisterSingleton<ICommandProcessor, SimpleInjectorCommandProcessor>(); |
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (10)
src/mediator-framework/Ark.Tools.MediatorFramework.Rebus.Generators/RebusEndpointGenerator.cs:457
- The generated Rebus handler XML summary still says it dispatches to the "pure handler", but the wrapper now depends on an IProcessor. Update the summary so generated docs match behavior.
sb.AppendLine(" /// <summary>Generated Rebus wrapper dispatching to the pure handler for <c>" + e.TypeName + "</c>.</summary>");
src/mediator-framework/Ark.Tools.MediatorFramework.Rebus.Generators/RebusEndpointGenerator.cs:469
- The generator hard-codes generic ExecuteAsync<...> calls, which will fail to compile for legacy contracts implementing ICommand / IRequest (not the TSelf interfaces). Emitting ExecuteAsync(message, ctk) lets overload resolution pick the typed overload when available and fall back to the untyped one otherwise.
var dispatch = e.IsCommand
? "_processor.ExecuteAsync<" + e.TypeFullName + ">(message, "
: "_processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(message, ";
sb.AppendLine(" => await " + dispatch + "global::Rebus.Extensions.MessageContextExtensions.GetCancellationToken(global::Rebus.Pipeline.MessageContext.Current)).ConfigureAwait(false);");
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:1222
- Multipart endpoint generation also hard-codes ExecuteAsync<EndpointType, Response>(...). For upload/download endpoints this will fail to compile for legacy IRequest/IQuery contracts. Prefer ExecuteAsync(request, ctk) and let overload resolution pick the best method.
sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);");
sb.AppendLine(" var processor = container.GetInstance<" + processorService + ">();");
sb.AppendLine(" var result = await processor.ExecuteAsync<" + endpoint.TypeFullName + ", " + endpoint.Response + ">(request, cancellationToken).ConfigureAwait(false);");
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:913
- Generated Minimal API endpoints currently call processor.ExecuteAsync<TRequest,TResponse>(...). This breaks compilation for endpoints whose contracts only implement IRequest/IQuery (legacy shape). Emit ExecuteAsync(request, ctk) so the compiler selects the typed overload when the contract implements the TSelf interface, otherwise the legacy overload.
sb.AppendLine(" var processor = container.GetInstance<" + processorService + ">();");
sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, cancellationToken).ConfigureAwait(false);");
if (e.IsStreaming)
src/mediator-framework/Ark.Tools.MediatorFramework.MinimalApi.Generators/MinimalApiEndpointGenerator.cs:1340
- Command endpoint generation hard-codes ExecuteAsync(...). This won’t compile for legacy ICommand contracts. Emitting ExecuteAsync(request, ctk) allows ICommand to use the typed overload while ICommand uses the untyped one.
sb.AppendLine(" var container = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::SimpleInjector.Container>(httpContext.RequestServices);");
sb.AppendLine(" var processor = container.GetInstance<global::Ark.Tools.Solid.ICommandProcessor>();");
sb.AppendLine(" await processor.ExecuteAsync<" + endpoint.TypeFullName + ">(request, cancellationToken).ConfigureAwait(false);");
sb.AppendLine(" return global::Microsoft.AspNetCore.Http.TypedResults.NoContent();");
src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions.Generators/AzureFunctionsEndpointGenerator.cs:346
- Azure Functions generator currently emits ExecuteAsync<TContract,...>(body, ...). This will not compile for endpoints whose contracts only implement the legacy IRequest/IQuery/ICommand interfaces. Emit ExecuteAsync(body, ctk) so typed contracts use the TSelf overload and legacy contracts still compile via the fallback overload.
if (endpoint.Kind == HandlerKind.Command)
{
source.AppendLine(" var _processor = _container.GetInstance<global::Ark.Tools.Solid.ICommandProcessor>();");
source.Append(" await _processor.ExecuteAsync<").Append(endpoint.FullyQualifiedType).AppendLine(">(body, cancellationToken).ConfigureAwait(false);");
source.Append(" return global::Microsoft.AspNetCore.Http.Results.StatusCode(")
src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions.Generators/AzureFunctionsEndpointGenerator.cs:356
- Azure Functions generator: query dispatch uses the typed ExecuteAsync<TQuery,TResult> overload unconditionally. That will fail for legacy IQuery contracts. Emit ExecuteAsync(body, ctk) to let overload resolution choose the typed path when available.
else if (endpoint.Kind == HandlerKind.Query)
{
source.AppendLine(" var _processor = _container.GetInstance<global::Ark.Tools.Solid.IQueryProcessor>();");
source.Append(" var _result = await _processor.ExecuteAsync<").Append(endpoint.FullyQualifiedType).Append(", ").Append(endpoint.ResponseType).AppendLine(">(body, cancellationToken).ConfigureAwait(false);");
source.Append(" if (_result is null) return global::Microsoft.AspNetCore.Http.Results.StatusCode(")
.Append(endpoint.NullResultStatusCode == 0 ? "404" : endpoint.NullResultStatusCode.ToString(CultureInfo.InvariantCulture))
.AppendLine(");");
src/mediator-framework/Ark.Tools.MediatorFramework.AzureFunctions.Generators/AzureFunctionsEndpointGenerator.cs:367
- Azure Functions generator: request dispatch uses the typed ExecuteAsync<TRequest,TResponse> overload unconditionally, which breaks legacy IRequest contracts. Emit ExecuteAsync(body, ctk) so both typed and legacy contracts compile.
else
{
source.AppendLine(" var _processor = _container.GetInstance<global::Ark.Tools.Solid.IRequestProcessor>();");
source.Append(" var _result = await _processor.ExecuteAsync<").Append(endpoint.FullyQualifiedType).Append(", ").Append(endpoint.ResponseType).AppendLine(">(body, cancellationToken).ConfigureAwait(false);");
source.Append(" if (_result is null) return global::Microsoft.AspNetCore.Http.Results.StatusCode(")
.Append(endpoint.NullResultStatusCode == 0 ? "204" : endpoint.NullResultStatusCode.ToString(CultureInfo.InvariantCulture))
.AppendLine(");");
_emitResponseETag(source, endpoint, "_result");
src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs:537
- gRPC generator: command dispatch hard-codes ExecuteAsync(...). This will not compile for legacy ICommand contracts. Emit ExecuteAsync(request, ctk) to support both ICommand and ICommand.
else if (e.Kind == HandlerKind.Command)
{
sb.AppendLine(" await processor.ExecuteAsync<" + e.TypeFullName + ">(request, context.CancellationToken).ConfigureAwait(false);");
sb.AppendLine(" return new global::Google.Protobuf.WellKnownTypes.Empty();");
src/mediator-framework/Ark.Tools.MediatorFramework.Grpc.Generators/GrpcEndpointGenerator.cs:513
- gRPC generator emits processor.ExecuteAsync<TContract,TResponse>(...) which requires contracts to implement IQuery<TSelf,TResult>/IRequest<TSelf,TResponse>. Because the endpoint extraction still accepts legacy IQuery/IRequest, the generated code can become uncompilable. Emit ExecuteAsync(request, ctk) so overload resolution picks the typed overload when available and falls back to the legacy overload otherwise (apply similarly to the other ExecuteAsync<...> emissions in this method).
sb.AppendLine(" var request = new " + e.TypeFullName + " { " + e.AttachmentPropertyName + " = " + attachmentValue + " };");
sb.AppendLine(" var result = await processor.ExecuteAsync<" + e.TypeFullName + ", " + e.Response + ">(request, context.CancellationToken).ConfigureAwait(false);");
AppendNotFoundGuard(sb);
…rators-and-tests Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/mediator-framework/Ark.Tools.MediatorFramework.Rebus.Generators/RebusEndpointGenerator.cs:457
- The generated XML summary still says this Rebus wrapper dispatches to the "pure handler", but the generated code now resolves an ICommandProcessor/IRequestProcessor and executes through the processor pipeline. Update the summary to avoid misleading generated API docs.
sb.AppendLine(" /// <summary>Generated Rebus wrapper dispatching to the pure handler for <c>" + e.TypeName + "</c>.</summary>");
Mediator Framework generators bypassed Ark.Tools.Solid processors and invoked handlers directly, preventing processor-level behavior from applying to generated Requests, Queries, and Commands.
Generated code now follows this pattern: