From 6bf6acd10819158f63631e12d55de942b2047e04 Mon Sep 17 00:00:00 2001 From: Ian Johnson Date: Mon, 10 Aug 2026 07:06:52 -0400 Subject: [PATCH] Rewrite the README as a landing page and prepare 1.0.0-rc9220 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README had grown to 414 lines and duplicated most of a nineteen-page documentation site, ending in about ninety lines of dumped generated code. It now leads with the pitch, shows what the library looks like in use, and then walks through what you can actually do with it โ€” conventions, decorators and interception, environment conditions, testing with mocks, trimming and AOT, build-time diagnostics โ€” each with a short example and a link to the page that goes deep. Everything the site covers better is a link rather than a section. Every link is absolute. The README is packed into all nine packages and is the listing page on nuget.org, where relative links do not resolve; the changelog link, the licence link and the licence badge were all broken there. Sidebar section "Registering in bulk" becomes "Bulk Registration". Rolls the changelog's Unreleased section into 1.0.0-rc9220, matching how rc9210 was cut. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013me9Eodb4PjJgR4ZNKrC9B --- CHANGELOG.md | 3 +- README.md | 463 ++++++++++------------------------- website/.vitepress/config.ts | 2 +- 3 files changed, 134 insertions(+), 334 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 555d5fa..8a75024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.0-rc9220] - 2026-08-10 ### Changed @@ -352,4 +352,5 @@ The entries below were written for a 1.0.0 that was not cut. They describe the s Enable it with ``. - A tag-driven release workflow publishing to nuget.org and GitHub Packages. +[1.0.0-rc9220]: https://github.com/ipjohnson/DependencyModules/releases/tag/v1.0.0-rc9220 [1.0.0-rc9210]: https://github.com/ipjohnson/DependencyModules/releases/tag/v1.0.0-rc9210 diff --git a/README.md b/README.md index 94a2e4c..58b6ae4 100644 --- a/README.md +++ b/README.md @@ -3,412 +3,211 @@ [![NuGet](https://img.shields.io/nuget/v/DependencyModules.Runtime.svg)](https://www.nuget.org/packages/DependencyModules.Runtime/) [![build](https://github.com/ipjohnson/DependencyModules/actions/workflows/build-package.yaml/badge.svg)](https://github.com/ipjohnson/DependencyModules/actions/workflows/build-package.yaml) [![coverage](https://raw.githubusercontent.com/ipjohnson/DependencyModules/badges/coverage.svg)](https://github.com/ipjohnson/DependencyModules/actions/workflows/build-package.yaml) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/ipjohnson/DependencyModules/blob/main/LICENSE.txt) -DependencyModules is a C# source generator package that uses attributes to create -dependency injection registration modules. These modules can then be used to populate -an IServiceCollection instance. +**Dependency injection, decided at compile time.** -Registration code is generated at compile time, so there is no reflection or assembly -scanning at run time. +Declare registration next to the class it belongs to. A source generator writes the +`IServiceCollection` calls during the build โ€” so nothing reflects, nothing scans at startup, and the +trimmer can follow every registration you declared. -## Installation +๐Ÿ“– **[Full documentation](https://ipjohnson.github.io/DependencyModules/)** -```shell -dotnet add package DependencyModules.Runtime -dotnet add package DependencyModules.SourceGenerator -``` +## The problem -Requires .NET 8.0 or later. The packages ship both `net8.0` and `net10.0` assemblies, so a project on -either LTS release gets one built against its own framework. See [CHANGELOG.md](CHANGELOG.md) for -release notes. - -## Service Attributes - -* `[DependencyModule]` - used to attribute class that will become dependency module (must be partial) -* `[SingletonService]` - registers service as `AddSingleton` -* `[ScopedService]` - registers service as `AddScoped` -* `[TransientService]` - registers service as `AddTransient` -* `[CrossWireService]` - registers implementation and interfaces with the same lifetime +Every .NET application keeps a list like this, and nothing checks that it is complete: ```csharp -// Registration example -[DependencyModule] -public partial class ApplicationModule; - -// registers SomeClass implementation for ISomeService -[SingletonService] -public class SomeClass : ISomeService -{ - public string SomeProp => "SomeString"; -} - -// registers OtherService implementation -[TransientService] -public class OtherService -{ - public OtherService(ISomeService service) - { - SomeProp = service.SomeProp; - } - public string SomeProp { get; } -} +services.AddScoped(); +services.AddSingleton(); +// โ€ฆ another two hundred lines ``` -Note: `[DependencyModule]` is not required for [Top-level](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) statement applications. +Forget a line and you find out at run time, in the environment you deployed to. Reach for a runtime +scanner instead and you trade that for three new problems: you can no longer read what was +registered, the scan runs on every start, and the trimmer cannot see through reflection โ€” so a +published, trimmed build registers nothing at all. -Note: a `[DependencyModule]` class must be declared directly in a namespace, not nested inside -another type. A nested module generates a separate, detached class rather than completing the -partial declaration, so its registrations never run. Services registered with -`[SingletonService]` and friends may be nested freely. -## Container Instantiation +## What it looks like instead -* `AddModule` - method adds root module to service collection -* `AddModules` - add a list of modules to the service collection +Mark the class, and the registration is written for you during the build. ```csharp -// AddModule and AddModules are extension methods in the DependencyModules.Runtime namespace -using DependencyModules.Runtime; - -var serviceCollection = new ServiceCollection(); - -serviceCollection.AddModule(); -// or -serviceCollection.AddModules(new ApplicationModule(), ...); - -var provider = serviceCollection.BuildServiceProvider(); +[SingletonService] +public class SmtpEmailSender : IEmailSender { } -var service = provider.GetService(); +[DependencyModule] +public partial class ApplicationModule; ``` -Note: to avoid duplicate modules it's recommended to only call AddModule(s) once in an application and never inside a Module. -## Factories - -Sometimes it's not possible to construct all types through normal registration. -Factories can be registered with a module using the registration attributes. - ```csharp -public class SomeClass : ISomeInterface { - public SomeClass(IDep one, IDepTwo two, DateTime dateTime) { ... } - - [SingletonService] - public static ISomeInterface Factory(IDep one, IDepTwo two) { - return new SomeClass(one, two, DateTime.Now()); - } -} -``` -## Module Re-use - -DependencyModules creates an `Attribute` class that can be used to apply sub dependencies. +var services = new ServiceCollection(); -```csharp -// Modules can be re-used with the generated attributes -[DependencyModule] -[ApplicationModule] -public partial class AnotherModule; +services.AddModule(); ``` -## Parameters +That is the whole idea. Everything below builds on it. -Sometimes you want to provide extra registration for your module. -This can be achieved by adding a constructor to your module or optional properties. -Note these parameters and properties will be correspondingly implemented in the module attribute. +## Install -```csharp -[DependencyModule] -public partial class SomeModule(bool someFlag) : IServiceCollectionConfiguration -{ - public string OptionalString { get; set; } = ""; - - public void ConfigureServices(IServiceCollection services) - { - if (someFlag) - { - // custom registration - } - } -} - -[DependencyModule] -[SomeModule(true, OptionalString = "otherString")] -public partial class SomeOtherModule; +```shell +dotnet add package DependencyModules.Runtime +dotnet add package DependencyModules.SourceGenerator ``` -## Module Features -Because module configuration happens before the dependency injection container is instantiated it's impossible to use the container for configuration. -To support configuration discovery before registration, the feature interface can be -implemented in modules and be passed to a handler at registration time. Features are applied before services and decorators. - -```csharp -// feature interface -public interface IFeature { } +Requires .NET 8.0 or later. The packages ship both `net8.0` and `net10.0` assemblies, so a project on +either LTS release gets one built against its own framework. -[DependencyModule] -public partial class ModuleImplementation : ISomeFeature -{ -} +โ†’ [Getting started](https://ipjohnson.github.io/DependencyModules/guide/getting-started.html) -[DependencyModule] -[ModuleImplementation] -public partial class FeatureHandlerModule : IDependencyModuleFeature -{ - public void HandleFeature(IServiceCollection collection, IEnumerable features) - { - // invoked with service collection and one instance of the ModuleImplementation class - } -} -``` +## What you can do with it -## Managing duplicate registration +### Register by rule, resolved during the build -By default a module will only be loaded once, assuming attributes are used or the modules are specified in the same `AddModules` call. Separate calls to `AddModule` will result in modules being loaded multiple times. If a module uses parameters it can be useful to load a module more than once. That can be accomplished by overriding the `Equals` and `GetHashcode` methods to allow for multiple loads. +Declare a rule once and let it cover everything that fits โ€” including the handler somebody adds next +year. The body never runs; it is read at compile time and turned into ordinary registration calls. ```csharp -// CustomModule will be loaded as long as someString is unique. -// Duplicate modules with the same someString value will be ignored [DependencyModule] -public partial class CustomModule(string someString) : IServiceCollectionConfiguration -{ - public void ConfigureServices(IServiceCollection services) - { - // custom logic - } - - public override bool Equals(object obj) - { - if (obj is CustomModule module) - { - return someString.Equals(module.someString); +public partial class HandlerModule : IConventionModule { + void IConventionModule.Conventions(IConventionDefinitions conventions) { + conventions.RegisterAll(typeof(IRequestHandler<,>)).AsScoped(); + conventions.RegisterAll().InNamespaceOf().AsScoped(); } - - return false; - } - - public override int GetHashCode() - { - return someString.GetHashCode(); - } } ``` -Services will be registered using an `Add` method by default. This can be overridden with the `Using` property on individual service or at the `DependencyModule` level. Note: the following are valid registration types Add, Try, TryEnumerable, Replace. +Assignability, namespaces, attributes and name globs all match โ€” including types in a referenced +package. -```csharp -[SingletonService(Using = RegistrationType.Try)] -public class SomeService; - -[DependencyModule(Using = RegistrationType.Try)] -public partial class SomeModule; -``` +โ†’ [Conventions](https://ipjohnson.github.io/DependencyModules/guide/conventions.html) ยท +[Scanning a package](https://ipjohnson.github.io/DependencyModules/guide/scanning.html) -## Realm +### Decorate and intercept -By default, all dependencies are registered in all modules within the same assembly. -The realm allows the developer to scope down the registration within a given module. +Wrap a service with a decorator you write, or with a generated wrapper that routes every member +through an interceptor. Both compose with conventions, and both are ordered globally. ```csharp -// register only dependencies specifically marked for this realm -[DependencyModule(OnlyRealm = true)] -public partial class AnotherModule; +[Decorator(Order = 10)] public class Retrying(IRepository inner) : IRepository { } +[Decorator(Order = 20)] public class Logging(IRepository inner) : IRepository { } -[SingletonService(Realm = typeof(AnotherModule))] -public class SomeDep : ISomeInterface { } +// resolves as Logging(Retrying(SqlRepository)) ``` -## Keyed Registration +โ†’ [Decorators](https://ipjohnson.github.io/DependencyModules/guide/decorators.html) ยท +[Interception](https://ipjohnson.github.io/DependencyModules/guide/interception.html) -Registration attributes have a `Key` property that allows for specifying the key at registration time. +### Gate registrations on the environment + +A service, decorator or whole convention can exist only where it is wanted. Where the condition does +not hold the registration is never made โ€” so the service resolves undecorated rather than being +wrapped by something that re-checks the environment on every call. ```csharp -[SingletonService(Key = "SomeKey")] -public class KeyService : IKeyService { } +[SingletonService] +[IfEnvironment("Development")] +public class ConsoleEmailSender : IEmailSender { } -// yields this registration line -services.AddKeyedSingleton(typeof(IKeyService), "SomeKey", typeof(KeyService)); +[Decorator] +[IfEnvironment("Production")] +public class CircuitBreaker(IPaymentGateway inner) : IPaymentGateway { } ``` -## As Registration - -Sometimes it's useful to register a type with a specific type vs. letting auto-registration pick a type. -The `As` property allows you to control the service type for the registration. - ```csharp -[SingletonService(As = typeof(ISomeOtherInterface))] -public class KeyService : IKeyService, ISomeOtherInterface { } - -// yields this registration line -services.AddSingleton(typeof(KeyService)); +conventions.RegisterAll().IfEnvironmentValue("AUDIT", "on").AsSingleton(); ``` -## Try, Replace, TryEnumerable +โ†’ [Environments](https://ipjohnson.github.io/DependencyModules/guide/environments.html) + +### Test against real modules, with mocks where you want them -By default registrations are done using a standard `Add___` method. -It can be useful to change the registration to `Try`, `Replace`, and `TryEnumerable` with the `Using` property. +The xUnit package builds a provider from the modules a test names and injects the services the test +asks for. Mocking comes from whichever library you already use โ€” NSubstitute, Moq or FakeItEasy. ```csharp -[SingletonService(Using = RegistrationType.Try)] -public class KeyService : IKeyService { } +[assembly: ApplicationModule] +[assembly: MoqSupport] -// yields this registration line -services.TryAddSingleton(typeof(IKeyService), typeof(KeyService)); +public class OrderServiceTests { + [ModuleTest] + public void SendsTheReceipt(OrderService orders, Mock email) { + orders.Place(new Order()); + + email.Verify(x => x.Send(It.IsAny())); + } +} ``` -## Autogenerated Modules +The service under test is built against the same mock the test configures โ€” no wiring in between. -To simplify registration for [Top-Level](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) statement applications, an `ApplicationModule` will be autogenerated for file named Program.cs. +โ†’ [Testing modules](https://ipjohnson.github.io/DependencyModules/guide/testing.html) ยท +[Mocks and values](https://ipjohnson.github.io/DependencyModules/guide/testing-mocks.html) -```csharp -[assembly: SomeOtherModule] +### Survive trimming and Native AOT -var serviceCollection = new ServiceCollection(); +Each match is emitted as a literal `typeof()`, which the trimmer roots and which carries the +constructor along with it. The capability that breaks reflection-based scanners is the one that +works here. -// load SomeOtherModule as well as all registrations in the current project -serviceCollection.AddModule(); -``` +โ†’ [Trimming and AOT](https://ipjohnson.github.io/DependencyModules/guide/aot.html) -## Unit testing & Mocking +### Find out at build time, not at startup -DependencyModules provides an xUnit extension to make testing much easier. -It handles the population and construction of a service provider using specified modules. +A convention that matches nothing, a service that cannot be constructed, two conventions claiming one +service type โ€” each is a `DM####` diagnostic in the IDE rather than an exception in production. -```shell -dotnet add package DependencyModules.xUnit -dotnet add package DependencyModules.NSubstitute -``` +โ†’ [Diagnostics reference](https://ipjohnson.github.io/DependencyModules/reference/diagnostics.html) -Mocking is supplied by a separate package, so use whichever library you already have โ€” -`DependencyModules.NSubstitute`, `DependencyModules.Moq` or `DependencyModules.FakeItEasy` โ€” and -apply its `[NSubstituteSupport]`, `[MoqSupport]` or `[FakeItEasySupport]` attribute. +## Registrations you can read + +There is no container graph to reason about. Set `EmitCompilerGeneratedFiles` and the file under +`obj/` is the ground truth: ```csharp -// applies module & nsubstitute support to all tests. -// test attributes can be applied at the assembly, class, and test method level -[assembly: MyModule] -[assembly: NSubstituteSupport] - -public class OtherServiceTests -{ - [ModuleTest] - public void SomeTest(OtherService test, [Mock]ISomeService service) - { - service.SomeProp.Returns("some mock value"); - Assert.Equals("some mock value", test.SomeProp); - } -} +services.AddScoped(typeof(IRequestHandler), typeof(CreateOrderHandler)); +services.AddSingleton(typeof(IEmailSender), typeof(SmtpEmailSender)); ``` -## Reporting a problem +Any `IServiceCollection`-compatible container works, because that is all the generator produces. -If services are not being registered as you expect, these three steps produce almost everything -needed to diagnose it: +## Documentation -1. **Look at the generated code.** Set `true` - and read the files under `obj/`. The registrations the generator produced are the ground truth. -2. **Turn on the generator log**, which records the configuration in effect, every module and - service discovered, and anything skipped along with the reason: - ```xml - - $(MSBuildProjectDirectory)/dmlogs - - ``` -3. **Check for `DM####` warnings** in the build output. The generator reports these for mistakes it - can detect, such as a service type that cannot be constructed or a module missing `partial`. +| | | +|---|---| +| [Getting started](https://ipjohnson.github.io/DependencyModules/guide/getting-started.html) | Install and register your first service | +| [Modules](https://ipjohnson.github.io/DependencyModules/guide/modules.html) | Composition, parameters, features, realms | +| [Registering services](https://ipjohnson.github.io/DependencyModules/guide/services.html) | Lifetimes, keys, factories, `As`, `Try`/`Replace` | +| [Conventions](https://ipjohnson.github.io/DependencyModules/guide/conventions.html) | Bulk registration by rule | +| [Decorators](https://ipjohnson.github.io/DependencyModules/guide/decorators.html) ยท [Interception](https://ipjohnson.github.io/DependencyModules/guide/interception.html) | Wrapping services | +| [Environments](https://ipjohnson.github.io/DependencyModules/guide/environments.html) | Conditional registration | +| [Testing](https://ipjohnson.github.io/DependencyModules/guide/testing.html) | Module tests, mocks, asserting registrations | +| [Trimming and AOT](https://ipjohnson.github.io/DependencyModules/guide/aot.html) | Publishing trimmed and Native AOT | +| [Extending](https://ipjohnson.github.io/DependencyModules/guide/extending.html) | Building your own generator on top | +| [Reference](https://ipjohnson.github.io/DependencyModules/reference/attributes.html) | Attributes, diagnostics, MSBuild properties | -Please include the log and the generated file in any [issue](https://github.com/ipjohnson/DependencyModules/issues). +## Packages -## Implementation +| Package | Purpose | +|---|---| +| `DependencyModules.Runtime` | Attributes, module interfaces, `AddModule` | +| `DependencyModules.SourceGenerator` | Generates the registration code | +| `DependencyModules.Conventions` | Convention-based registration | +| `DependencyModules.xUnit` | `[ModuleTest]` for xUnit v3 | +| `DependencyModules.NSubstitute` ยท `.Moq` ยท `.FakeItEasy` | Mocking support, pick one | +| `DependencyModules.Testing` | Shared test seam, referenced for you | +| `DependencyModules.SourceGenerator.Impl` | Source-only, for building your own generator | -Behind the scenes the library generates registration code that can be used with any `IServiceCollection` compatible DI container. +## Something not registering? -Example generated code for [SutModule.cs](integ-tests/SutProject/SutModule.cs) -```csharp - // SutModule.Dependencies.g.cs - public partial class SutModule - { - [DynamicDependency(nameof(ModuleDependencies))] - private static int moduleField = global::DependencyModules.Runtime.Helpers.DependencyRegistry.Add(ModuleDependencies); - - private static void ModuleDependencies(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient( - typeof(global::SutProject.IDependencyOne), - typeof(global::SutProject.DependencyOne) - ); - services.AddSingleton( - typeof(global::SutProject.IGenericInterface<>), - typeof(global::SutProject.GenericClass<>) - ); - services.AddKeyedTransient( - typeof(global::SutProject.KeyedService), - Constants.StringValue, - typeof(global::SutProject.KeyedService) - ); - services.AddScoped( - typeof(global::SutProject.IScopedService), - typeof(global::SutProject.ScopedService) - ); - services.AddSingleton( - typeof(global::SutProject.ISingletonService), - typeof(global::SutProject.SingletonService) - ); - services.AddSingleton( - typeof(global::SutProject.IGenericInterface), - typeof(global::SutProject.StringGeneric) - ); - } - } +The [troubleshooting guide](https://ipjohnson.github.io/DependencyModules/guide/troubleshooting.html) +covers reading the generated output and turning on the generator log, which together explain almost +every surprise. Please include both in any +[issue](https://github.com/ipjohnson/DependencyModules/issues) you open. - // SutModule.Modules.g.cs -namespace SutProject -{ - #nullable enable - public partial class SutModule : global::DependencyModules.Runtime.Interfaces.IDependencyModule - { - - static SutModule() - { - } - - public void PopulateServiceCollection(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - global::DependencyModules.Runtime.Helpers.DependencyRegistry.LoadModules(services, this); - } - - [Browsable(false)] - void global::DependencyModules.Runtime.Interfaces.IDependencyModule.InternalApplyServices(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - global::DependencyModules.Runtime.Helpers.DependencyRegistry.ApplyServices(services); - } - - [Browsable(false)] - global::System.Collections.Generic.IEnumerable global::DependencyModules.Runtime.Interfaces.IDependencyModule.InternalGetModules() - { - return global::DependencyModules.Runtime.Helpers.DependencyRegistry.GetModules(); - } - - public override bool Equals(object? obj) - { - return obj is SutModule; - } - - public override int GetHashCode() - { - return HashCode.Combine(base.GetHashCode()); - } - } - #nullable disable - - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Assembly | global::System.AttributeTargets.Method | global::System.AttributeTargets.Parameter, AllowMultiple = true)] - #nullable enable - public partial class SutModuleAttribute : global::System.Attribute, global::DependencyModules.Runtime.Interfaces.IDependencyModuleProvider - { - - public global::DependencyModules.Runtime.Interfaces.IDependencyModule GetModule() - { - var newModule = new global::SutProject.SutModule(); - return newModule; - } - } - #nullable disable -} -``` +## Contributing + +Issues and pull requests are welcome. See the +[changelog](https://github.com/ipjohnson/DependencyModules/blob/main/CHANGELOG.md) for release notes. + +Licensed under the [MIT License](https://github.com/ipjohnson/DependencyModules/blob/main/LICENSE.txt). diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index be37d5e..965aea4 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -83,7 +83,7 @@ export default defineConfig({ ], }, { - text: 'Registering in bulk', + text: 'Bulk Registration', items: [ { text: 'Conventions', link: '/guide/conventions' }, { text: 'Scanning a package', link: '/guide/scanning' },