diff --git a/src/Repl.Defaults/IReplApp.cs b/src/Repl.Defaults/IReplApp.cs
index a7fc753..264d9c3 100644
--- a/src/Repl.Defaults/IReplApp.cs
+++ b/src/Repl.Defaults/IReplApp.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Repl;
///
@@ -19,7 +21,7 @@ public interface IReplApp : ICoreReplApp
///
/// Module type.
/// The same app contract for fluent chaining.
- IReplApp MapModule()
+ IReplApp MapModule<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TModule>()
where TModule : class, IReplModule;
///
diff --git a/src/Repl.Defaults/ReplApp.cs b/src/Repl.Defaults/ReplApp.cs
index cc5fa86..9d644f1 100644
--- a/src/Repl.Defaults/ReplApp.cs
+++ b/src/Repl.Defaults/ReplApp.cs
@@ -1,9 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq.Expressions;
+using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Repl.Documentation;
-using System.Linq.Expressions;
-using System.Reflection;
namespace Repl;
@@ -132,7 +133,7 @@ public IContextBuilder Context(string segment, Action configure, Deleg
///
/// Maps a module resolved through runtime DI activation.
///
- public ReplApp MapModule()
+ public ReplApp MapModule<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TModule>()
where TModule : class, IReplModule
{
var module = ResolveModuleFromServices();
@@ -435,6 +436,8 @@ IReplApp IReplApp.MapModule(IReplModule module, Func
MapModule(module, isPresent);
+ [UnconditionalSuppressMessage("Trimming", "IL2091", Justification = "Annotation flows from IReplApp.MapModule().")]
+ [UnconditionalSuppressMessage("Trimming", "IL2095", Justification = "Annotation flows from IReplApp.MapModule().")]
IReplApp IReplApp.MapModule() => MapModule();
IReplApp IReplApp.WithBanner(Delegate bannerProvider) => WithBanner(bannerProvider);
@@ -460,15 +463,16 @@ IReplMap IReplMap.MapModule(IReplModule module, Func
_sharedProvider ??= _services.BuildServiceProvider();
- private TModule ResolveModuleFromServices()
+ private TModule ResolveModuleFromServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TModule>()
where TModule : class, IReplModule
{
// Resolve from the shared provider — its lifetime spans the entire app,
// so disposable dependencies captured by the module stay alive.
+ // Uses GetServiceOrCreateInstance so callers don't need to register the
+ // module explicitly — constructor dependencies are resolved from DI.
var provider = EnsureSharedProvider();
- return provider.GetService()
- ?? throw new InvalidOperationException(
- $"Unable to resolve module '{typeof(TModule).FullName}'. Register it in services or call MapModule(IReplModule).");
+ return Microsoft.Extensions.DependencyInjection.ActivatorUtilities
+ .GetServiceOrCreateInstance(provider);
}
private static Func AdaptModulePresencePredicate(Delegate isPresent)
@@ -704,7 +708,7 @@ public IContextBuilder Context(string segment, Action configure, Deleg
validation);
}
- public IReplApp MapModule()
+ public IReplApp MapModule<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TModule>()
where TModule : class, IReplModule
{
return MapModule(root.ResolveModuleFromServices());
diff --git a/src/Repl.IntegrationTests/Given_ModuleComposition.cs b/src/Repl.IntegrationTests/Given_ModuleComposition.cs
index e71e236..1dac356 100644
--- a/src/Repl.IntegrationTests/Given_ModuleComposition.cs
+++ b/src/Repl.IntegrationTests/Given_ModuleComposition.cs
@@ -191,6 +191,24 @@ public void When_ScopedModuleDependsOnDisposableService_Then_ServiceSurvives()
output.Text.Should().Contain("1");
}
+ [TestMethod]
+ [Description("MapModule() auto-creates the module when it is not registered in DI, using ActivatorUtilities to inject constructor dependencies.")]
+ public void When_ModuleNotRegisteredInDI_Then_ActivatorUtilitiesCreatesItWithInjectedDependencies()
+ {
+ var sut = ReplApp.Create(services =>
+ {
+ // Register the dependency but NOT the module itself.
+ services.AddSingleton();
+ });
+ sut.MapModule();
+
+ var output = ConsoleCaptureHelper.Capture(
+ () => sut.Run(["counter", "increment", "--no-logo"]));
+
+ output.ExitCode.Should().Be(0);
+ output.Text.Should().Contain("1");
+ }
+
private sealed class ScopedOpsModule : IReplModule
{
public void Map(IReplMap map)