Add opt-in interface contracts for module configuration - #42
Conversation
Introduce statically typed, trimming/AOT-friendly alternatives to the convention (reflection) module methods: IServiceConfiguringModule (ConfigureServices), IApplicationConfiguringModule (Configure), and IContainerConfiguringModule / IUseSimpleInjectorModule / IAddSimpleInjectorModule for the SimpleInjector hooks. Each bootstrap call site now dispatches to the interface when the module implements it and falls back to the existing reflection convention otherwise. Purely additive: existing convention-based modules are unaffected; interface modules are compile-time checked and avoid reflection.
There was a problem hiding this comment.
Pull request overview
Adds opt-in, statically typed module configuration interfaces (as alternatives to reflection-based “convention methods”) and updates the bootstrap call sites to dispatch to these interfaces when implemented, falling back to ModuleMethodInvoker otherwise—aiming to improve trimming/AOT friendliness without breaking existing modules.
Changes:
- Introduces new opt-in interfaces for service, app pipeline, and SimpleInjector module/container configuration.
- Updates hosting/bootstrap handlers to prefer direct interface dispatch and preserve the existing reflection convention as fallback.
- Adds an end-to-end test covering interface-only modules (services + container + ASP.NET Core
Configure).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Hosuto.SimpleInjector.Tests/Modules/Hosting/ModuleInterfaceContractsTests.cs | New tests validating interface-based module configuration end-to-end. |
| src/Hosuto/Modules/IServiceConfiguringModule.cs | Adds opt-in ConfigureServices interface contract. |
| src/Hosuto.SimpleInjector/Modules/ModuleContainerContracts.cs | Adds opt-in SimpleInjector-related interface contracts (ConfigureContainer, UseSimpleInjector, AddSimpleInjector). |
| src/Hosuto.SimpleInjector/Modules/Hosting/BootstrapHostFilter.cs | Dispatches UseSimpleInjector / ConfigureContainer via interfaces when implemented. |
| src/Hosuto.SimpleInjector/Modules/Hosting/AddSimpleInjectorModuleServicesFilter.cs | Dispatches AddSimpleInjector via interface when implemented. |
| src/Hosuto.Hosting/Modules/Hosting/DefaultBootstrapHostHandler.cs | Dispatches ConfigureServices via IServiceConfiguringModule when implemented. |
| src/Hosuto.Hosting.AspNetCore/Modules/IApplicationConfiguringModule.cs | Adds opt-in Configure (ASP.NET Core pipeline) interface contract. |
| src/Hosuto.Hosting.AspNetCore/Modules/Hosting/WebModuleBootstrapHostHandler.cs | Dispatches web Configure via IApplicationConfiguringModule when implemented. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// <param name="serviceProvider">The modules host service provider (same instance the | ||
| /// conventional method receives as its first <see cref="IServiceProvider"/> parameter).</param> |
| /// <param name="serviceProvider">The modules host service provider (same instance the | ||
| /// conventional method receives as its first <see cref="IServiceProvider"/> parameter).</param> |
| /// <param name="serviceProvider">The module's service provider - in the SimpleInjector | ||
| /// hosting model this is the module <see cref="Container"/> itself, matching the first | ||
| /// <see cref="IServiceProvider"/> parameter of the conventional method.</param> |
Tests: the interface modules now implement the methods explicitly, so they are invisible to the reflection convention (Type.GetMethod returns null). This proves the interface-dispatch path runs rather than the reflection fallback - verified by forcing the fallback, which makes the test fail. Adds coverage for IUseSimpleInjectorModule and IAddSimpleInjectorModule. Docs: drop the imprecise "this is the module Container itself" claim on ConfigureContainer's serviceProvider (distinct from the container parameter); document that the interface takes precedence over a same-named convention method, and that the injected provider matches the convention only when IServiceProvider was the convention method's first parameter.
|
Addressed the review feedback (Copilot + an independent Sonnet review) in 6257d70:
Consciously left (documented as known, not regressions): the |
* Add opt-in minimal-API (WebApplication) inner host for web modules New UseAspNetCoreMinimal() hosts a web module on a minimal-API WebApplication instead of the classic HostBuilder + ConfigureWebHostDefaults host (net6+, additive - the existing path stays the default). The module authoring model is unchanged: ConfigureServices runs against builder.Services pre-build, and Configure is deferred to pipeline build (start) via an IStartupFilter so it runs after the module container (SimpleInjector/Autofac) has been configured. The SimpleInjector bootstrap filter composes unchanged because WebApplication is an IHost. Builds on the module interfaces from #42, with reflection convention as fallback. Adds a framework test (module services + container + endpoint with a container-resolved dependency, over the real bootstrap path) and a net10 Razor sample under samples/dotnet/minimal. Known limitation (documented, follow-up): module static web assets are not yet mapped on the minimal host - the classic file-provider/XML-manifest loader does not fit .NET 9+ endpoint-based static web assets. * Add IEndpointConfiguringModule; address review feedback on minimal host Endpoint contract (from review discussion): IEndpointConfiguringModule lets a module map endpoints the minimal-API way (endpoints.MapGet/MapRazorPages/...), wired into both the minimal-API and classic web hosts so the same module works on either backend. The Startup-era Configure(IApplicationBuilder) stays for middleware. Review fixes for the minimal host (Copilot + Sonnet): - Honor IModuleHostingOptions.ValidateServiceProvider on the minimal path (was dropped, reintroducing the #40 ValidateOnBuild failure in Development on .NET 9+). Applied via builder.Host.UseDefaultServiceProvider - verified that builder.WebHost's factory is ignored by WebApplicationBuilder while builder.Host is honored. - Pass a HostBuilderContext reflecting the inner module host (its environment + configuration) to the module services/configuration filters, instead of the outer host context. - Add HostDefaults.ApplicationKey to the module configuration for parity with the classic host. - Doc: note ConfigureBuilderAction runs against the restricted WebApplicationBuilder.Host. Tests: idiomatic IEndpointConfiguringModule mapping over the real minimal path, plus ValidateServiceProvider on/off (net9+). Sample updated to author the module with all three contracts (services/middleware/endpoints). Green on net6/8/9. * Fix CI build; support conventional MapEndpoints CI: add the net10 samples/dotnet/minimal projects to Hosuto.sln. The pipeline restores the solution and then builds **/*.csproj --no-restore; projects missing from the solution were never restored and failed with NETSDK1004. MapEndpoints: dispatch also to a conventional MapEndpoints(IEndpointRouteBuilder) method when the module does not implement IEndpointConfiguringModule, matching the interface-or-convention model of the other module methods (both web hosts). Adds a convention-based endpoint test.
Part B of the reflection cleanup. Purely additive, no behavior change for existing modules.
What
Statically typed, trimming/AOT-friendly alternatives to the convention (reflection) module methods:
IServiceConfiguringModuleConfigureServicesIApplicationConfiguringModuleConfigureIContainerConfiguringModuleConfigureContainerIUseSimpleInjectorModuleUseSimpleInjectorIAddSimpleInjectorModuleAddSimpleInjectorHow
Each bootstrap call site now dispatches to the interface when the module implements it, and falls back to the existing
ModuleMethodInvokerreflection convention otherwise. The interfaces receive the same values the convention injects (the firstIServiceProvider= the modules host services, which in the SimpleInjector model is the module container).Why
Tests
New
ModuleInterfaceContractsTests: an interface-only module is configured end-to-end (ConfigureServices + ConfigureContainer run, hosted handler resolves the container-registered service), andIApplicationConfiguringModule.Configureis invoked for a web module. All suites pass on net6.0/net8.0/net9.0; the convention fallback remains covered by existing tests.