DependencyModules.NSubstitute
1.0.0
dotnet add package DependencyModules.NSubstitute --version 1.0.0
NuGet\Install-Package DependencyModules.NSubstitute -Version 1.0.0
<PackageReference Include="DependencyModules.NSubstitute" Version="1.0.0" />
<PackageVersion Include="DependencyModules.NSubstitute" Version="1.0.0" />
<PackageReference Include="DependencyModules.NSubstitute" />
paket add DependencyModules.NSubstitute --version 1.0.0
#r "nuget: DependencyModules.NSubstitute, 1.0.0"
#:package DependencyModules.NSubstitute@1.0.0
#addin nuget:?package=DependencyModules.NSubstitute&version=1.0.0
#tool nuget:?package=DependencyModules.NSubstitute&version=1.0.0
DependencyModules
Your DI registrations, written as attributes and compiled into your assembly. No reflection, no assembly scanning, no startup cost โ and Native AOT works, because there is nothing left to trim away.
๐ Documentation ยท Getting started ยท Conventions ยท Decorators ยท Testing ยท AOT
The whole trick
You mark a class:
[SingletonService]
public class SmtpEmailSender : IEmailSender;
At build time the generator writes the registration you would have written yourself:
// ApplicationModule.Dependencies.g.cs
services.AddSingleton(
typeof(global::MyApp.IEmailSender),
typeof(global::MyApp.SmtpEmailSender)
);
That is the entire mechanism. The output is ordinary C# that you can read, grep, set a breakpoint in, and check into a review. Nothing inspects your assembly at run time, so there is no startup scan to pay for and nothing for the trimmer to guess about.
Why not a runtime scanner?
If you have used Scrutor, Autofac modules, or hand-written AddScoped lists, this is what
changes:
| Runtime scanning | DependencyModules | |
|---|---|---|
| When registration is decided | First request to the container | dotnet build |
| A convention that matches nothing | Silent | DM0005 at build |
| A service that cannot be constructed | InvalidOperationException, eventually |
DM0002 at build |
| Trimming / Native AOT | Types disappear; scanner finds nothing | Literal typeof(), so the trimmer keeps them |
| Startup cost | Proportional to assembly size | None |
| What actually got registered | Debugger, at run time | A file you can open |
The interesting half is the third row. A trimmer removes CreateOrderHandler because
nothing statically references it โ a scanner that would have found it by reflection does
not count. Emitting typeof(CreateOrderHandler) into your assembly is a static reference,
which is why this approach and Native AOT get along.
Install
dotnet add package DependencyModules.Runtime
dotnet add package DependencyModules.SourceGenerator
Requires .NET 8.0 or later. The packages ship net8.0 and net10.0 assemblies, so a
project on either LTS gets one built against its own framework. Console applications also
want Microsoft.Extensions.DependencyInjection.
Quick start
Mark the services, declare a module, load it once:
// Services.cs
using DependencyModules.Runtime.Attributes;
namespace MyApp;
[SingletonService]
public class SmtpEmailSender : IEmailSender;
[ScopedService]
public class OrderRepository : IOrderRepository;
// Program.cs
using MyApp; // the generated module lives in your root namespace
using DependencyModules.Runtime;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddModule<ApplicationModule>();
var provider = services.BuildServiceProvider();
ApplicationModule is generated for you in a project whose entry point is a top-level
Program.cs. Anywhere else โ a class library, or a project that wants more than one module โ
declare your own:
[DependencyModule]
public partial class ApplicationModule;
A module must be partial, and must be declared directly in a namespace rather than nested
inside another type. Services marked with [SingletonService] and friends may be nested freely.
Coming from top-level statements? The generated module takes your project's
RootNamespace, and top-level statements sit in the global namespace โ soProgram.csneedsusing YourRootNamespace;before it can nameApplicationModule.
Registering forty things without writing forty attributes
Declare the rule once. It is matched by the compiler, against the types that exist at build time:
[DependencyModule]
public partial class HandlerModule : IConventionModule {
void IConventionModule.Conventions(IConventionDefinitions conventions) {
conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped();
conventions.RegisterAll(typeof(IValidator<>))
.IncludeBaseClasses()
.AlsoAsSelf()
.AsScoped();
}
}
Every handler in the project is registered against the closed interface it implements. Add a handler tomorrow and it joins; delete one and the registration goes with it. A convention that stops matching anything is a build warning rather than a runtime surprise.
The body of Conventions is never executed โ it is read from source at compile time, which is
why only the documented calls can appear in it. See the
conventions guide.
Composing modules
A module generates an attribute of the same name, so modules compose by attribute:
[DependencyModule]
[DomainModule]
[InfrastructureModule(useInMemory: true, ConnectionName = "primary")]
public partial class ApiModule;
Constructor parameters and settable properties on a module are mirrored onto its generated
attribute, so a module can be configured by whoever composes it. For anything the attributes
cannot express, implement IServiceCollectionConfiguration and write the registrations by hand.
Decorators and interception
Wrap a service without touching it or its callers. The first constructor parameter is the wrapped instance; the rest resolve normally:
[Decorator(Order = 2000)]
public class CachingRepository(IRepository inner, IMemoryCache cache) : IRepository;
[Decorator(Order = 1000)]
public class TracingRepository(IRepository inner, ILogger<TracingRepository> log) : IRepository;
// resolves as CachingRepository(TracingRepository(SqlRepository))
Lower orders sit closer to the implementation. Ordering is global across every module in an
AddModule(s) call, so an application's decorators can wrap those a library contributed โ
by convention framework code uses 0โ999 and application code starts at 1000.
For cross-cutting behaviour across every member of a service, [Intercept] generates a typed
wrapper rather than a dynamic proxy. See
decorators and interception.
Testing
Tests receive their dependencies as method parameters, against the real registration graph:
[assembly: ApplicationModule]
[assembly: NSubstituteSupport]
public class OrderTests {
[ModuleTest]
public async Task PlaceOrder_PricesThroughTheChannel(
IRequestHandler<PlaceOrder, Order> handler,
[Mock] IBookRepository books) {
books.Find("isbn-1", Arg.Any<CancellationToken>())
.Returns(new Book("isbn-1", 20m));
var order = await handler.Handle(new PlaceOrder("isbn-1", 10), default);
Assert.Equal(140m, order.Total);
}
}
dotnet add package DependencyModules.xUnit # or DependencyModules.NUnit
dotnet add package DependencyModules.NSubstitute # or .Moq, or .FakeItEasy
Each test gets its own provider, so singletons cannot leak between them. See the testing guide.
Native AOT
Verified end to end: a console application using conventions, keyed registrations, decorators, a static factory and an intercepted open generic publishes to a 2.2 MB self-contained binary with zero IL trim or AOT warnings, behaving identically to the JIT build.
The one limitation is not this library's to fix: the container cannot close an open generic
over a value type without dynamic code, so IRepository<Order> resolves and IRepository<int>
throws. Setting PublishAot makes that fail in an ordinary dotnet run rather than only after
publishing. See the AOT guide.
Feature reference
[SingletonService] [ScopedService] [TransientService] |
Register with the matching lifetime |
[CrossWireService] |
One instance shared across the implementation and its interfaces |
As = typeof(IFoo) |
Choose the service type explicitly |
Key = "primary" |
Keyed registration |
Using = RegistrationType.Try |
Add, Try, TryEnumerable or Replace |
Realm = typeof(SomeModule) |
Restrict a registration to one module |
[IfEnvironment("Development")] |
Register only in named environments |
[Decorator] [Decorate] [Intercept] |
Wrap a service, or one you do not own |
A static method carrying a service attribute |
Factory, for types the container cannot build |
Full details for each, with the rules and the edge cases, are in the documentation.
Samples
The integ-tests/
directory is a working sample gallery, built and tested on every commit:
| Sample | Shows |
|---|---|
SutProject |
Every registration shape, in one project |
SutProject.Tests |
Conventions, realms, keyed services, cross-wiring, factories, features, and all three mocking libraries |
ConsoleTestProject |
Top-level statements and the generated ApplicationModule |
web/WebApiApp |
An ASP.NET Core host, with its own test project |
Reporting a problem
If services are not registered as you expect, these three steps produce almost everything needed to diagnose it:
- Read the generated code. Set
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>and look underobj/. The registrations the generator produced are the ground truth. (PointCompilerGeneratedFilesOutputPathinsideobj/โ a folder in the project directory gets compiled as ordinary source on the next build.) - Turn on the generator log, which records the configuration in effect, every module and
service discovered, and anything skipped along with the reason:
<PropertyGroup> <DependencyModules_LogOutputDirectory>$(MSBuildProjectDirectory)/dmlogs</DependencyModules_LogOutputDirectory> </PropertyGroup> - Check for
DM####warnings in the build output. The generator reports these for mistakes it can detect โ see the diagnostics reference.
Please include the log and the generated file in any issue.
License
MIT. See LICENSE.txt and CHANGELOG.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DependencyModules.Testing (>= 1.0.0)
- NSubstitute (>= 5.3.0)
-
net8.0
- DependencyModules.Testing (>= 1.0.0)
- NSubstitute (>= 5.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 0 | 8/16/2026 |
| 1.0.0-rc9340 | 0 | 8/16/2026 |
| 1.0.0-rc9230 | 52 | 8/12/2026 |
| 1.0.0-rc9220 | 40 | 8/11/2026 |
| 1.0.0-rc9210 | 46 | 8/9/2026 |