Remove runtime generic construction from module hosting path - #41
Conversation
Module hosting reconstructed the closed generic types (IModuleHost<TModule>, ModulesHostService<TModule>, module bootstrap) from a stored Type via MakeGenericType, and the two Generic*Adapter helpers built their per-module filter via MakeGenericType + Activator.CreateInstance. These runtime constructions are the main obstacle to trimming/Native AOT. Introduce a generic ModuleRegistration<TModule> captured at HostModule<TModule>() so all closed generics are statically visible, and construct the three filter adapters that live in generic classes (DefaultBootstrapHostHandler<TModule>, BootstrapHostFilter<TModule>) directly instead of reflectively. Behavior is unchanged. Reflection remains only on two clearly-scoped legacy paths: the non-generic HostModule(Type, ...) overload and the non-generic AddSimpleInjectorModuleServicesFilter adapter.
There was a problem hiding this comment.
Pull request overview
Refactors module hosting to avoid runtime closed-generic construction (reflection-based MakeGenericType/Activator.CreateInstance) on the primary hosting path, improving trimming / Native AOT friendliness while aiming to preserve behavior and public API shape.
Changes:
- Replaces per-module runtime generic reconstruction in
ModulesHostBuilderwithIModuleRegistration/ModuleRegistration<TModule>captured atHostModule<TModule>(). - Updates bootstrap filter pipelines to use directly constructed closed-generic adapters instead of reflective adapter factories.
- Introduces
ModuleRegistration<TModule>to centralize module + host registration and bootstrapping withoutMakeGenericTypeon the generic path.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Hosuto.SimpleInjector/Modules/Hosting/BootstrapHostFilter.cs | Replaces reflective adapter creation with direct closed-generic adapter instances in SimpleInjector bootstrap filters. |
| src/Hosuto.Hosting/Modules/Hosting/ModulesHostBuilder.cs | Switches module tracking to registrations and removes reflection-based host registration on the generic path. |
| src/Hosuto.Hosting/Modules/Hosting/Internal/ModuleRegistration.cs | Adds IModuleRegistration and ModuleRegistration<TModule> to encapsulate module/host registration and bootstrapping. |
| src/Hosuto.Hosting/Modules/Hosting/DefaultBootstrapHostHandler.cs | Replaces reflective adapter creation with direct closed-generic adapter construction for module services filters. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| moduleContext.Advanced.FrameworkServices.GetServices<IConfigureContainerFilter>() | ||
| .Append(GenericModuleContextFilterAdapter<Container>.Create(typeof(IConfigureContainerFilter<>))), | ||
| .Append(configureContainerAdapter), | ||
| (ctx, c) => | ||
| { | ||
| ModuleMethodInvoker.CallOptionalMethod(ctx, "ConfigureContainer", container); |
There was a problem hiding this comment.
Pre-existing behavior, not changed by this PR — the diff here only swaps the adapter construction, not this delegate (same pattern in UseSimpleInjector/ConfigureServices). In practice the container/options filters mutate the passed instance rather than substituting a different one, so c == the captured container. Leaving it out of scope of this no-behavior-change refactor; can be cleaned up separately.
First step toward trimming/Native-AOT friendliness (part A of the reflection cleanup). No behavior change, no public-API break.
What
Module hosting previously reconstructed the closed generic types from a stored
Typeat runtime:ModulesHostBuilderusedtypeof(IModuleHost<>).MakeGenericType(module.Key)(andModulesHostService<>, plus a second site for bootstrap) to register/resolve the per-module host.Generic*Adapterhelpers built their per-module filter viaMakeGenericType+Activator.CreateInstance.These runtime generic constructions are the main obstacle to Native AOT (arbitrary generic instantiations that aren't statically visible).
How
IModuleRegistration/ModuleRegistration<TModule>, captured atHostModule<TModule>(). The module type stays a static generic parameter, soIModuleHost<TModule>,ModulesHostService<TModule>and the bootstrap call are all statically visible — noMakeGenericType.DefaultBootstrapHostHandler<TModule>,BootstrapHostFilter<TModule>) directly as closed generics instead of via the reflectiveCreate(Type)helper.Residual (intentional, documented)
Reflection remains only on two legacy paths, both clearly scoped:
HostModule(Type, ...)overload (module type only known at runtime), andAddSimpleInjectorModuleServicesFilteradapter.Tests
All existing suites pass unchanged on net6.0/net8.0/net9.0 (behavior-preserving refactor).