Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ jobs:
src/DependencyModules.SourceGenerator/DependencyModules.SourceGenerator.csproj \
src/DependencyModules.SourceGenerator.Impl/DependencyModules.SourceGenerator.Impl.csproj \
src/DependencyModules.Conventions/DependencyModules.Conventions.csproj \
src/DependencyModules.Testing/DependencyModules.Testing.csproj \
src/DependencyModules.xUnit/DependencyModules.xUnit.csproj \
src/DependencyModules.xUnit.NSubstitute/DependencyModules.xUnit.NSubstitute.csproj; do
src/DependencyModules.NSubstitute/DependencyModules.NSubstitute.csproj \
src/DependencyModules.Moq/DependencyModules.Moq.csproj \
src/DependencyModules.FakeItEasy/DependencyModules.FakeItEasy.csproj; do
dotnet pack "$project" --configuration Release --output ./artifacts \
"/p:PackageVersion=${{ steps.version.outputs.version }}"
done
Expand Down
129 changes: 125 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,131 @@ 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).

## [1.0.0] - 2026-08-06
## [1.0.0-rc9210] - 2026-08-09

First stable release. The public API is unchanged from the `1.0.0-rc*` line; this release
fixes packaging and generated-code defects and commits to the API surface going forward.
Everything since `1.0.0-rc9200`. Still a release candidate: convention registration is new and
large, and the environment API changed shape late, so the surface is not committed to yet.

### Added

- **Moq and FakeItEasy mocking support**, in new `DependencyModules.Moq` and
`DependencyModules.FakeItEasy` packages. Apply `[MoqSupport]` or `[FakeItEasySupport]` where you
would have applied `[NSubstituteSupport]`; `[Mock]` then works the same way. With NSubstitute and
FakeItEasy the injected instance is also what you configure, while Moq separates the two, so the
container receives `Mock<T>.Object` and the mock is reached with `Mock.Get(instance)`.
- **`DependencyModules.Testing`**, a test-framework-neutral package holding the pieces the mocking
packages need — `IMockSupportAttribute`, `IOrderedAttribute`, `IInjectValueAttribute`,
`InjectValuesAttribute` and `AttributeUtility`. None of them referenced xUnit, but living in
`DependencyModules.xUnit` meant every mocking package had to depend on a test framework it does not
use.
- **Convention registration**, in a new `DependencyModules.Conventions` package. A module
implements `IConventionModule` and declares what to register; the generator resolves the matches
at compile time and emits ordinary registrations. Selection by assignability, namespace,
attribute or name glob; shapes `AsSelf`, `AlsoAsSelf`, `AsSelfWithInterfaces`,
`AsMatchingInterface` and `As<T>`; `Using` and `WithKey` pass through to the registration. The
declaration body is read rather than executed, so anything that cannot be evaluated at build time
is reported as DM0009 instead of ignored. Ships as its own analyzer, so a project that does not
use conventions never loads the class-scanning providers.
- **Scanning a referenced assembly.** `InAssemblyOf<T>()` points a convention at a package instead
of the project being built. Types are read as symbols during the build and emitted as literal
`typeof()`, so this survives trimming where a reflection-based scan does not. One named assembly
at a time; only `public` types are visible.
- **Interception.** `[Intercept]` wraps a service in a generated type that routes every member
through an interceptor. `IInterceptor`, `IAsyncInterceptor` and `IAsyncEnumerableInterceptor` are
chosen per member, and a member no interceptor can serve is forwarded untouched. Properties,
indexers and events are supported; shapes that cannot be wrapped are refused with DM0008.
- **Environment-conditional registration.** `[IfEnvironment]`, `[IfNotEnvironment]`,
`[IfEnvironmentValue]` and `[IfNotEnvironmentValue]` gate a registration on the environment.
Conditions of different kinds combine with and. `ModuleEnvironment.Default` reads
`ASPNETCORE_ENVIRONMENT`, then `DOTNET_ENVIRONMENT`, then `"Production"`. A `ModuleEnvironment` is
a collection of its values, so they can be written inline —
`new ModuleEnvironment("Development") { { "REGION", "eu" } }` — and enumerated back out. A key not
written there falls back to an environment variable of that name; a key written as `null` hides
one. Lead with `false` — `new ModuleEnvironment(false, "Development")` — to read only what is at
the call site, which is what a test asserting registrations wants. Conditional registrations are
emitted after unconditional ones so they can override a default; across modules, module order
decides.
- **A documentation site** at <https://ipjohnson.github.io/DependencyModules/>, covering conventions,
decorators, interception, environments, testing, trimming and AOT, and a DM diagnostics reference —
all of which were previously undocumented.
- DM0004 through DM0012, covering convention ambiguity, a convention matching nothing, an
unconstructable match, an unreadable declaration, provenance for convention registrations, and
environment conditions.

### Changed

- **`DependencyModules.xUnit.NSubstitute` is now `DependencyModules.NSubstitute`.** The mocking
packages do not touch xUnit, and naming one of them after it would have been misleading next to
`DependencyModules.Moq` and `DependencyModules.FakeItEasy`. Update the `PackageReference` and the
`using` — the attribute itself is unchanged. Types that moved to `DependencyModules.Testing`
changed namespace to match, so `DependencyModules.xUnit.Attributes.InjectValuesAttribute` is now
`DependencyModules.Testing.Attributes.InjectValuesAttribute`, and the extension methods on
`MethodInfo`/`ParameterInfo` moved from `DependencyModules.xUnit.Impl` to
`DependencyModules.Testing.Impl`. The xUnit-bound interfaces — `ITestStartupAttribute`,
`ITestParameterValueProvider` and `IServiceProviderBuilderAttribute`, all of which take an
`IXunitTestMethod` — stay where they were.
- **`IEnvironmentServiceCollectionConfiguration.ConfigureServices` takes a non-nullable
`IModuleEnvironment`.** There is now always an environment, so an implementation that branched on
`null` takes the other branch. Existing implementations still compile.
- **`AddModules` registers the environment it used.** Previously nothing was registered when none
was supplied, so `GetRequiredService<IModuleEnvironment>()` threw while conditions had been decided
against the process default. An environment passed to `AddModules` now replaces one already in the
collection rather than joining it.
- **An `IModuleEnvironment` registered by type or factory is refused.** It cannot be constructed
while the collection is still being populated, and was previously ignored in favour of the process
default — so a service gated on `"Development"` quietly took its production branch.
- Generated modules implement both overloads of `InternalApplyServices`. The generator package
declares no dependency on the runtime package, so a new generator paired with an older runtime
would otherwise register nothing at all.
- Attribute providers use `ForAttributeWithMetadataName`, roughly halving generator time on a
2,000-class compilation. This also fixed selection of namespace-qualified attribute usages, which
were silently not matched.

### Fixed

- **A capability interface could win the default service type.** The service type is inferred from
the first interface a class declares, so `class ConnectionPool : IDisposable, IPool` registered as
`IDisposable` and was unresolvable as `IPool` — and a class whose only interface was `IDisposable`
registered as `IDisposable` rather than as itself. Interfaces describing what a class *can do*
rather than what it *is* are now passed over: `IDisposable`, `IAsyncDisposable`, `IEquatable<T>`,
`IComparable`, `ICloneable`, `IConvertible`, `IFormattable`, `IParsable<T>`, `ISerializable`,
`IEnumerable`/`IEnumerable<T>` and the `INotify*` family. Interfaces that are genuine service roles
are untouched, including framework ones such as `IEqualityComparer<T>`, `IJsonTypeInfoResolver` and
`IHttpClientFactory`, as is any type named with `As`. Previously only `INotifyPropertyChanged` was
skipped.
- **A handler implementing several closings of one interface registered only the first**, silently.
The MediatR notification shape — one class handling two events — lost every event but one.
- **A `[Decorator]` was matched by conventions as though it were a service.** A decorator implements
the interface it decorates, so a convention scanning that interface matched the decorator; being
generic and closing nothing it registered as an open generic, and decoration then refused
everything with an error blaming the open generic limitation. One open generic decorator over
convention-registered handlers — the ordinary MediatR shape — could not work.
- **A partial class was two convention candidates**, so a type whose parts each reached the scanned
interface was reported as ambiguous and registered nothing.
- **A nested type's constructor was used as the outer type's.** Constructor discovery walked the
whole subtree rather than the type's own members, so a service containing a nested class with a
parameterised constructor was registered against that constructor. Only visible with
`DependencyModules_GenerateFactories`.
- **A decorator or interceptor file declared a record module a class**, failing the build with
CS0261. Two of the four writers contributing to a module's partial carried the record rewrite and
two did not.
- **`AsSelfWithInterfaces` cross-wired BCL interfaces**, so any type whose base implemented
`IDisposable` became resolvable as `IDisposable`. Interfaces in `System` and below are no longer
expanded into.
- **A metadata scan re-registered a package's own services**, ignoring the service attributes that
exclude a type in the project being built.
- Constructor discovery no longer walks every method body of every candidate, which was the dominant
cost of the generator on ordinary code: 73 ms to 12 ms on a 2,000-class compilation, measured on
the run after an edit.
- The README had a code fence opened at *Unit testing* and closed at *Implementation*, so the whole
*Reporting a problem* section rendered as a C# block on GitHub and on every NuGet package page.

---

## Earlier, in the 1.0.0-rc line

The entries below were written for a 1.0.0 that was not cut. They describe the state reached at
`1.0.0-rc9200` and the decorator work that followed it, and are kept here rather than restated.

### Fixed

Expand Down Expand Up @@ -120,4 +241,4 @@ fixes packaging and generated-code defects and commits to the API surface going
Enable it with `<DependencyModules_LogOutputDirectory>`.
- A tag-driven release workflow publishing to nuget.org and GitHub Packages.

[1.0.0]: https://github.com/ipjohnson/DependencyModules/releases/tag/v1.0.0
[1.0.0-rc9210]: https://github.com/ipjohnson/DependencyModules/releases/tag/v1.0.0-rc9210
35 changes: 28 additions & 7 deletions DependencyModules.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SutProject.Tests", "integ-t
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.xUnit", "src\DependencyModules.xUnit\DependencyModules.xUnit.csproj", "{0960CFB0-87F7-418F-9673-FAE5A6C9BB74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.xUnit.NSubstitute", "src\DependencyModules.xUnit.NSubstitute\DependencyModules.xUnit.NSubstitute.csproj", "{444349D1-F2D1-4E4B-87C2-F3DBCF0D1D19}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleTestProject", "integ-tests\ConsoleTestProject\ConsoleTestProject.csproj", "{9A4D50DE-3995-494F-85CF-68F803051644}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "web", "web", "{D6D764B2-B906-4386-BC37-7EE29D7821DF}"
Expand All @@ -39,6 +37,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.Benchmarks", "benchmarks\DependencyModules.Benchmarks\DependencyModules.Benchmarks.csproj", "{F72AFF5C-C9FB-406E-B65A-192933E697D2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.Testing", "src\DependencyModules.Testing\DependencyModules.Testing.csproj", "{6091F3CE-7C70-464D-AD37-E0F78BC95F2C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.NSubstitute", "src\DependencyModules.NSubstitute\DependencyModules.NSubstitute.csproj", "{5430B97D-5012-4CEB-BBCA-219C0A53D3C1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.Moq", "src\DependencyModules.Moq\DependencyModules.Moq.csproj", "{F453C574-0EC6-44D2-BFBF-F5BB9097EC4D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.FakeItEasy", "src\DependencyModules.FakeItEasy\DependencyModules.FakeItEasy.csproj", "{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -73,10 +79,6 @@ Global
{0960CFB0-87F7-418F-9673-FAE5A6C9BB74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0960CFB0-87F7-418F-9673-FAE5A6C9BB74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0960CFB0-87F7-418F-9673-FAE5A6C9BB74}.Release|Any CPU.Build.0 = Release|Any CPU
{444349D1-F2D1-4E4B-87C2-F3DBCF0D1D19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{444349D1-F2D1-4E4B-87C2-F3DBCF0D1D19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{444349D1-F2D1-4E4B-87C2-F3DBCF0D1D19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{444349D1-F2D1-4E4B-87C2-F3DBCF0D1D19}.Release|Any CPU.Build.0 = Release|Any CPU
{9A4D50DE-3995-494F-85CF-68F803051644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A4D50DE-3995-494F-85CF-68F803051644}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A4D50DE-3995-494F-85CF-68F803051644}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -101,6 +103,22 @@ Global
{F72AFF5C-C9FB-406E-B65A-192933E697D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F72AFF5C-C9FB-406E-B65A-192933E697D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F72AFF5C-C9FB-406E-B65A-192933E697D2}.Release|Any CPU.Build.0 = Release|Any CPU
{6091F3CE-7C70-464D-AD37-E0F78BC95F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6091F3CE-7C70-464D-AD37-E0F78BC95F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6091F3CE-7C70-464D-AD37-E0F78BC95F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6091F3CE-7C70-464D-AD37-E0F78BC95F2C}.Release|Any CPU.Build.0 = Release|Any CPU
{5430B97D-5012-4CEB-BBCA-219C0A53D3C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5430B97D-5012-4CEB-BBCA-219C0A53D3C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5430B97D-5012-4CEB-BBCA-219C0A53D3C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5430B97D-5012-4CEB-BBCA-219C0A53D3C1}.Release|Any CPU.Build.0 = Release|Any CPU
{F453C574-0EC6-44D2-BFBF-F5BB9097EC4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F453C574-0EC6-44D2-BFBF-F5BB9097EC4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F453C574-0EC6-44D2-BFBF-F5BB9097EC4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F453C574-0EC6-44D2-BFBF-F5BB9097EC4D}.Release|Any CPU.Build.0 = Release|Any CPU
{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{1E7E9023-435E-499B-9A6F-6C3E20A9A4F1} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
Expand All @@ -110,13 +128,16 @@ Global
{01878BB5-D308-4EF3-8144-C35C58188E1A} = {DC7C95FE-F58F-4F16-8DFA-87E1B67DCEAE}
{93187389-37C0-4DA0-8D25-E672ED858052} = {DC7C95FE-F58F-4F16-8DFA-87E1B67DCEAE}
{0960CFB0-87F7-418F-9673-FAE5A6C9BB74} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{444349D1-F2D1-4E4B-87C2-F3DBCF0D1D19} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{9A4D50DE-3995-494F-85CF-68F803051644} = {DC7C95FE-F58F-4F16-8DFA-87E1B67DCEAE}
{D6D764B2-B906-4386-BC37-7EE29D7821DF} = {DC7C95FE-F58F-4F16-8DFA-87E1B67DCEAE}
{3946D986-65E0-4103-AFD6-1C1D9549163F} = {D6D764B2-B906-4386-BC37-7EE29D7821DF}
{DA2D8D38-FA99-4483-B375-449E0EF86549} = {D6D764B2-B906-4386-BC37-7EE29D7821DF}
{1AFFBCCF-FD6E-4232-9DC7-40FE2784ECD1} = {F4DC9AA2-7F61-4DEE-A7D1-2BCDBEEFD1C5}
{B39ACDFB-85D2-4764-B06E-74744E683268} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{F72AFF5C-C9FB-406E-B65A-192933E697D2} = {2E4BAA8C-195F-490B-B9F9-57194B08CD12}
{6091F3CE-7C70-464D-AD37-E0F78BC95F2C} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{5430B97D-5012-4CEB-BBCA-219C0A53D3C1} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{F453C574-0EC6-44D2-BFBF-F5BB9097EC4D} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
EndGlobalSection
EndGlobal
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,13 @@ It handles the population and construction of a service provider using specified

```shell
dotnet add package DependencyModules.xUnit
dotnet add package DependencyModules.xUnit.NSubstitute
dotnet add package DependencyModules.NSubstitute
```

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.

```csharp
// applies module & nsubstitute support to all tests.
// test attributes can be applied at the assembly, class, and test method level
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using DependencyModules.FakeItEasy;
using DependencyModules.xUnit.Attributes;
using FakeItEasy;
using Xunit;

namespace SutProject.Tests.FakeItEasy;

/// <summary>
/// The same scenario as the NSubstitute and Moq tests, so the three can be read against each other.
/// </summary>
[FakeItEasySupport]
public class FakeItEasyAttributeTests {

[ModuleTest]
[SutModule]
public void MockTest([Mock] IDependencyOne dependencyOne,
[Mock] IScopedService scopedService, ISingletonService singletonService) {
A.CallTo(() => dependencyOne.SingletonService).Returns(singletonService);
A.CallTo(() => dependencyOne.ScopedService).Returns(scopedService);

Assert.Same(dependencyOne.SingletonService, singletonService);
Assert.Same(dependencyOne.ScopedService, scopedService);
}

/// <summary>
/// The injected fake is the thing you configure, unlike Moq — no unwrapping step.
/// </summary>
[ModuleTest]
[SutModule]
public void TheInjectedInstanceIsTheFake([Mock] IDependencyOne dependencyOne) {
Assert.True(Fake.GetFakeManager(dependencyOne) is not null);
}
}
Loading
Loading