Skip to content

docs: research AoT-ready DI container replacement for SimpleInjector#741

Draft
AndreaCuneo with Copilot wants to merge 4 commits into
masterfrom
copilot/research-replacement-for-simpleinjector
Draft

docs: research AoT-ready DI container replacement for SimpleInjector#741
AndreaCuneo with Copilot wants to merge 4 commits into
masterfrom
copilot/research-replacement-for-simpleinjector

Conversation

Copilot AI commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

SimpleInjector is not NativeAOT/trimming ready and blocks AoT adoption across Ark.Tools. This PR contains research only — no library code changed — into source-generation-based replacements, per the problem statement.

Report — docs/aot-di-container-research.md

  • SimpleInjector upstream status: maintainer explicitly refuses AoT support (#1013, chore(deps): update dependency microsoft.identity.web to 3.3.0 #264, #914); trimming request #972 parked in the perpetual v6.0 milestone since 2023; no AoT-capable forks exist. Failure mode is trimmed reflection metadata, not expression compilation.
  • Feature catalog (F1–F12): everything src/ and samples/ rely on — open-generic decorators, RegisterConditional fallback, runtime mediator dispatch, GetTypesToRegister scanning, Verify() diagnostics, async scopes, Func<T> factories, ASP.NET Core cross-wiring.
  • Alternatives evaluated: Pure.DI, MEDI+Injectio, StrongInject (feature-perfect but abandonware), Jab (disqualified: no open generics/decorators), forking SimpleInjector (rejected: the engine is reflection+Expression throughout — a fork is effectively a rewrite).
  • Proposed approach (explicitly pending clarification): decouple Ark.Tools.Solid from the container now; choose Pure.DI vs MEDI+Ark-owned-generator based on answers to §8.
  • Resumable progress log so research can be continued.

Feasibility PoCs — evaluations/ (isolated from slnx, CI, repo MSBuild)

Two assert-based console projects replicating the Ark.Tools.Solid pipeline (open-generic decorator chain, validator fallback, mediator dispatch by runtime Type, scopes), both passing on CoreCLR and on published NativeAOT binaries:

  • PureDi.Evaluation — open-generic decorators via marker types work, but the TQuery : IQuery<TResult> constraint requires a custom marker:
    [GenericTypeArgument]
    internal interface TTQuery : IQuery<TT> { }
    // .Bind<IQueryHandler<TTQuery, TT>>().To<AuditDecorator<TTQuery, TT>>()
  • MediInjectio.Evaluation — Injectio's open-generic decoration crashes on NativeAOT out-of-the-box (runtime-closed decorator ctors trimmed). Proven fix an Ark generator would emit: [DynamicDependency(PublicConstructors, ...)] roots per closed pair, plus explicit instantiation for value-type generic args.
  • Cross-cutting finding: the dynamic dispatch in SimpleInjector*Processor is AoT-hostile regardless of container; both PoCs replace it with an AoT-safe default-interface-method bridge (IQueryHandlerBase<TResult>).

Clarifications requested (report §8) — blocking final recommendation

  1. AoT ambition: full NativeAOT hosts vs trimmed deployment vs opt-in only?
  2. Is compile-time graph verification (Pure.DI) a hard requirement, or is runtime ValidateOnBuild acceptable?
  3. May application composition roots change API, or is a near-source-compatible facade required?
  4. Is adopting/forking unmaintained StrongInject acceptable? (assumed no)
  5. Is a multi-version dual-container transition acceptable?
  6. Confirm handler types may be required to be statically visible at compile time (no dynamic assembly loading).
  7. Are the variance helpers in Ark.Tools.SimpleInjector/Ex.cs used downstream, or droppable?

Copilot AI and others added 2 commits July 2, 2026 05:36
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>
@AndreaCuneo

Copy link
Copy Markdown
Contributor

@copilot here the clarifications

  1. ambition is trimmed deployment with very low startup times. aot is just a way to achieve it
  2. runtime at startup validation is sufficient but compile time verification is a much interesting nice to have addition
  3. unclear, explain
  4. no
  5. yes
  6. yes. source generator can be used for scanning helpers at compile time. there is no need for runtime plugin-load
  7. they are used but as long there is a replacement (e.g. via source generator) that's ok. the main used features are auto Func factory registration (can be dropped for explicit registration) and Fallback (this cannot be dropped).

Continue the work based on above

…add ASP.NET Core cross-wiring PoC

Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>

Copilot AI commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot here the clarifications

  1. ambition is trimmed deployment with very low startup times. aot is just a way to achieve it...

Clarifications recorded in §8 of the report and the recommendation finalized in §7 (commit 4d2ae67 — hash on branch): Pure.DI primary, MEDI-native fallback. Both PoCs re-verified under the actual target — PublishTrimmed=true, TrimMode=full self-contained: Pure.DI trims warning-free (0.09 s startup); MEDI+Injectio survives only via our generated DynamicDependency roots and stays IL2026-flagged (0.20 s).

Q3 explained (§8.3): today's composition roots are imperative runtime calls (container.RegisterDecorator(typeof(IQueryHandler<,>), ...)). Source-generated containers need registrations to be statically analyzable, so that exact API cannot survive. The question is how much application ApiHost code may change:

  • (a) apps rewrite their registration section into a new declarative surface (Pure.DI DI.Setup(...) partial class and/or attributes) — mechanical, ~50–150 lines per app;
  • (b) Ark ships a near-source-compatible facade that looks like container.Register* but is a compile-time DSL — near-zero app diff, but Ark owns a mini-language/analyzer forever.

§7 assumes (a); please confirm or require (b).

Also completed the pending ASP.NET Core cross-wiring PoC (evaluations/PureDiWeb.Evaluation): controller activation, health checks and minimal APIs all resolve from the Pure.DI composition via Pure.DI.MS. Hard container-independent finding: MVC cannot run trimmed at all (AddControllers is RequiresUnreferencedCode; MapControllers throws under TrimMode=full) — the trimmed web surface must be minimal-API based, so a minimal-API ArkStartup flavour is now on the plan (§6.7, §7).

Copilot AI requested a review from AndreaCuneo July 2, 2026 06:56
public static bool LoggerWasInjected;

// ILogger<T> is a FRAMEWORK service: Pure.DI.MS resolves it from IServiceProvider (two-way cross-wiring)
public PingHandler(ILogger<PingHandler> logger) { LoggerWasInjected = logger is not null; }

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
Ran = true;
@AndreaCuneo

Copy link
Copy Markdown
Contributor

@copilot regarding Q3 is important that OpenGeneric Decorator support is maintained. Is not acceptable that each Handler needs to be manually registered (use source generators) and that all matching are decorated.

The goal is that during feature development, new Handlers are created and those must be guaranteed to be Decorated with all registered decorators.

Add to Evaluation an example of Open Generic Decoration.

…generator PoC

Co-authored-by: AndreaCuneo <5227688+AndreaCuneo@users.noreply.github.com>

Copilot AI commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot regarding Q3 is important that OpenGeneric Decorator support is maintained. Is not acceptable that each Handler needs to be manually registered (use source generators) and that all matching a...

Added evaluations/HandlerGen.Generator + HandlerGen.Evaluation (commit c9edb4d — hash on branch): an Ark-owned ~150-LOC Roslyn incremental generator that discovers every IQueryHandler<,> implementation at compile time and emits registrations already wrapped in all [HandlerDecorator]-marked open-generic decorators, plus closed NullValidator<Q> fallbacks and a query→service dispatch map. No per-handler registration exists anywhere — the PoC's NewFeatureHandler (implement interface, touch nothing else) is asserted registered and fully decorated; the guarantee is structural (generator closes every decorator over every discovered handler) and assertable via a generated KnownHandlers manifest.

Verified: CoreCLR, TrimMode=full and NativeAOT all pass warning-free (the generated dispatch map also removed the last IL3050 the earlier PoCs carried); AoT binary 2.3 MB, full self-check ~4 ms.

Important finding recorded in §6.8: Roslyn generators cannot see each other's output, so this scanner cannot feed Pure.DI's DI.Setup DSL. The handler pipeline must be owned by the Ark generator on either path (emitting IServiceCollection registrations directly, as PoC'd); Pure.DI's role narrows to the rest of the graph. §7 and §8.4 updated accordingly.

Comment on lines +105 to +109
foreach (var q in handlers.Select(h => h.Query).Distinct(SymbolEqualityComparer.Default).Cast<ITypeSymbol>())
{
if (covered.Contains(q)) continue;
sb.AppendLine($" global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient<{validatorIface.Construct((INamedTypeSymbol)q).ToDisplayString(_fqn)}, {nullValidator.Construct((INamedTypeSymbol)q).ToDisplayString(_fqn)}>(services);");
}
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.

2 participants