Skip to content

MapModule<T>(): resolve unregistered modules via ActivatorUtilities#18

Merged
carldebilly merged 2 commits intomainfrom
dev/cdb/module-with-services
Apr 4, 2026
Merged

MapModule<T>(): resolve unregistered modules via ActivatorUtilities#18
carldebilly merged 2 commits intomainfrom
dev/cdb/module-with-services

Conversation

@carldebilly
Copy link
Copy Markdown
Member

@carldebilly carldebilly commented Apr 4, 2026

Summary

  • ResolveModuleFromServices<T>() used GetService<T>(), which only resolved types explicitly registered in the DI container. Callers of MapModule<T>() had to pre-register their module or get an InvalidOperationException.
  • Switch to ActivatorUtilities.GetServiceOrCreateInstance<T>() so modules with a parameterless constructor — or constructor dependencies already in the container — resolve without explicit registration.
  • This makes MapModule<T>() a true first-class composition path alongside MapModule(new T()), enabling constructor injection in modules.

Motivation

Without this fix, the only way to use MapModule<T>() was to manually register the module type in the service collection. This defeated the purpose of the generic overload — callers ended up using MapModule(new MyModule()) instead, which bypasses DI entirely and forces [FromServices] on every handler parameter.

With this fix, a module like:

class SessionModule(SessionManager manager, ViewerRegistry viewers) : IReplModule { ... }

resolves naturally via app.MapModule<SessionModule>() as long as SessionManager and ViewerRegistry are registered — no module registration needed.

Changes

File What
IReplApp.cs Add [DynamicallyAccessedMembers] on MapModule<T>() generic parameter
ReplApp.cs ResolveModuleFromServices<T>()ActivatorUtilities.GetServiceOrCreateInstance<T>() with trim annotations

Test plan

  • Existing Given_ModuleComposition tests pass (7/7) — validates both registered and unregistered module resolution
  • Full integration test suite passes (382/382)

Open with Devin

… parameterless modules

ResolveModuleFromServices<T>() previously used GetService<T>(), which only
resolved types explicitly registered in the container. Callers of
MapModule<T>() had to pre-register their module or get an
InvalidOperationException.

Switch to ActivatorUtilities.GetServiceOrCreateInstance so that modules with
a parameterless constructor (or constructor-injected dependencies already in
the container) resolve without explicit registration — making MapModule<T>()
a true first-class composition path alongside MapModule(new T()).

Add [DynamicallyAccessedMembers] annotations for trim compatibility.
devin-ai-integration[bot]

This comment was marked as resolved.

…ule path

- Comment now says "constructor dependencies are resolved from DI" instead
  of "a parameterless constructor is enough" — ActivatorUtilities supports
  full constructor injection, not just parameterless types.
- Add test that calls MapModule<T>() without registering the module in DI,
  verifying the CreateInstance fallback path with injected dependencies.
@carldebilly carldebilly merged commit d801295 into main Apr 4, 2026
12 checks passed
@carldebilly carldebilly deleted the dev/cdb/module-with-services branch April 4, 2026 20:38
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.

1 participant