Skip to content

Optimize SimpleInjector processor dispatch - #829

Merged
AndreaCuneo merged 3 commits into
masterfrom
copilot/task-001-optimize-performance
Aug 3, 2026
Merged

Optimize SimpleInjector processor dispatch#829
AndreaCuneo merged 3 commits into
masterfrom
copilot/task-001-optimize-performance

Conversation

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Eliminates per-dispatch MakeGenericType and dynamic binder work in Solid SimpleInjector processors while preserving handler resolution and decorators. Task 001 is documented with benchmark results and moved to completed.

  • Dispatch cache
    • Cache immutable compiled invokers by runtime message type.
    • Resolve handlers from SimpleInjector for every call; scoped/transient lifetimes and decorator chains remain intact.
return await QueryHandlerInvokerCache<TResult>
    .ExecuteAsync(_container, query, ctk)
    .ConfigureAwait(false);
  • Coverage and measurement

    • Add decorated handler, cancellation, and exception behavior tests.
    • Add BenchmarkDotNet request/query/command comparisons.
    • Record 77.8–82.8% lower mean dispatch time and reduced allocations.
  • Generator decision

    • Document why source generator/interceptor dispatch was not added for existing interface-based processor calls.

Copilot AI and others added 3 commits August 3, 2026 09:21
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>
@AndreaCuneo
AndreaCuneo marked this pull request as ready for review August 3, 2026 10:15
@AndreaCuneo
AndreaCuneo requested a review from a team as a code owner August 3, 2026 10:15
Copilot AI review requested due to automatic review settings August 3, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes Ark.Tools.Solid.SimpleInjector processor dispatch by caching compiled handler invokers per runtime message type, avoiding per-call MakeGenericType and dynamic binder overhead while preserving per-dispatch handler resolution (scopes/transients/decorators).

Changes:

  • Replace dynamic/per-call reflection dispatch with compiled invoker caches for request/query/command processors.
  • Add MSTest coverage for decorator execution order plus cancellation/exception propagation.
  • Add a BenchmarkDotNet benchmark project and move the performance task doc from pending to completed (with recorded results).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Ark.Tools.Solid.SimpleInjector.Tests/SimpleInjectorProcessorTests.cs New behavioral tests for decorated handlers + cancellation/exception behavior.
tests/Ark.Tools.Solid.SimpleInjector.Tests/Ark.Tools.Solid.SimpleInjector.Tests.csproj New test project wiring for Solid.SimpleInjector.
tests/Ark.Tools.Solid.SimpleInjector.Tests/packages.lock.json Lockfile for the new test project.
src/common/Ark.Tools.Solid.SimpleInjector/SimpleInjectorRequestProcessor.cs Switch request dispatch to cached compiled invoker.
src/common/Ark.Tools.Solid.SimpleInjector/SimpleInjectorQueryProcessor.cs Switch query dispatch to cached compiled invoker.
src/common/Ark.Tools.Solid.SimpleInjector/SimpleInjectorCommandProcessor.cs Switch command dispatch to cached compiled invoker.
src/common/Ark.Tools.Solid.SimpleInjector/HandlerInvokerCaches.cs New compiled-invoker cache implementation.
benchmarks/Ark.Tools.Benchmarks/Ark.Tools.Benchmarks.csproj New BenchmarkDotNet benchmark project.
benchmarks/Ark.Tools.Benchmarks/Program.cs BenchmarkDotNet entrypoint.
benchmarks/Ark.Tools.Benchmarks/ProcessorDispatchBenchmarks.cs Benchmarks comparing reflection/dynamic vs cached invoker dispatch.
benchmarks/Ark.Tools.Benchmarks/packages.lock.json Lockfile for the new benchmark project.
docs/performance/pending/001-solid-simpleinjector-dispatch.md Remove pending task doc (moved to completed).
docs/performance/completed/001-solid-simpleinjector-dispatch.md Completed task doc with benchmark summary + decision record.
Directory.Packages.props Add BenchmarkDotNet version pin.
Ark.Tools.slnx Include new benchmark + new test project in the solution.
Suppressed comments (4)

src/common/Ark.Tools.Solid.SimpleInjector/HandlerInvokerCaches.cs:59

  • This ExecuteAsync method returns the Task directly. Repo guidance requires async methods to use async/await (even for a single await) for stacktrace clarity in production debugging.
    public static Task<TResponse> ExecuteAsync(Container container, IRequest<TResponse> request, CancellationToken cancellationToken)
    {
        var invoker = _invokers.GetOrAdd(request.GetType(), static requestType => CreateInvoker(requestType));
        return invoker(container, request, cancellationToken);
    }

src/common/Ark.Tools.Solid.SimpleInjector/HandlerInvokerCaches.cs:96

  • This ExecuteAsync method returns the Task directly. Repo guidance requires async methods to use async/await (even for a single await) for stacktrace clarity in production debugging.
    public static Task ExecuteAsync(Container container, ICommand command, CancellationToken cancellationToken)
    {
        var invoker = _invokers.GetOrAdd(command.GetType(), static commandType => CreateInvoker(commandType));
        return invoker(container, command, cancellationToken);
    }

benchmarks/Ark.Tools.Benchmarks/ProcessorDispatchBenchmarks.cs:78

  • The reflection/dynamic benchmark path awaits without ConfigureAwait(false), while the cached path uses it. Keeping both consistent avoids context-capture noise and matches the repo’s async style.
        var handlerType = typeof(IRequestHandler<,>).MakeGenericType(_request.GetType(), typeof(int));
        dynamic handler = _container.GetInstance(handlerType);
        return await handler.ExecuteAsync((dynamic)_request);
    }

benchmarks/Ark.Tools.Benchmarks/ProcessorDispatchBenchmarks.cs:92

  • The reflection/dynamic benchmark path awaits without ConfigureAwait(false), while the cached path uses it. Keeping both consistent avoids context-capture noise and matches the repo’s async style.
        var handlerType = typeof(ICommandHandler<>).MakeGenericType(_command.GetType());
        dynamic handler = _container.GetInstance(handlerType);
        await handler.ExecuteAsync((dynamic)_command);
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/common/Ark.Tools.Solid.SimpleInjector/HandlerInvokerCaches.cs
Comment thread benchmarks/Ark.Tools.Benchmarks/ProcessorDispatchBenchmarks.cs
@AndreaCuneo
AndreaCuneo merged commit 1ceaa1f into master Aug 3, 2026
6 of 7 checks passed
@AndreaCuneo
AndreaCuneo deleted the copilot/task-001-optimize-performance branch August 3, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants