Skip to content

Commit a16baae

Browse files
committed
Add support for convention-based registrations
A new overload of `AddServices` allows passing a type and/or a regex to evaluate against types in the current compilation (at compile-time) and emit registrations for all of them, just as if they had been annotated with a corresponding `[Service]` attribute. Key-based registrations not supported for this mechanism since it's not clear how to express a dynamic key based on either the type or regex that can be evaluated by the source generator. Instead of adding a separte assembly-level attribute, extending the existing API with this pseudo-reflection approach is far more discoverable. Fixes #116
1 parent 4cc4b86 commit a16baae

20 files changed

+686
-118
lines changed

DependencyInjection.Attributed.sln

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attributed", "src\Dependenc
77
EndProject
88
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attributed.Tests", "src\DependencyInjection.Attributed.Tests\Attributed.Tests.csproj", "{F2E67084-FED3-4E17-A012-0E8948FD3E06}"
99
EndProject
10-
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FE116B5E-AE0C-4901-B0FE-BE41EF18EF06}"
11-
ProjectSection(SolutionItems) = preProject
12-
src\Directory.props = src\Directory.props
13-
readme.md = readme.md
14-
EndProjectSection
15-
EndProject
1610
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeAnalysis.Tests", "src\CodeAnalysis.Tests\CodeAnalysis.Tests.csproj", "{E512DEBA-FB35-47FD-AF25-3BAECCF667B1}"
1711
EndProject
1812
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{3C5A7AC8-E8CC-40D6-B472-A693F742152A}"

readme.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ And that's it. The source generator will discover annotated types in the current
6363
project and all its references too. Since the registration code is generated at
6464
compile-time, there is no run-time reflection (or dependencies) whatsoever.
6565

66+
You can also avoid attributes entirely by using a convention-based approach, which
67+
is nevertheless still compile-time checked and source-generated. This allows
68+
registering services for which you don't even have the source code to annotate:
69+
70+
```csharp
71+
var builder = WebApplication.CreateBuilder(args);
72+
73+
builder.Services.AddServices(typeof(IRepository), ServiceLifetime.Scoped);
74+
// ...
75+
```
76+
77+
You can also use a regular expression to match services by name instead:
78+
79+
```csharp
80+
var builder = WebApplication.CreateBuilder(args);
81+
82+
builder.Services.AddServices(".*Service$"); // defaults to ServiceLifetime.Singleton
83+
// ...
84+
```
85+
86+
Or a combination of both, as needed. In all cases, NO run-time reflection is
87+
ever performed, and the compile-time source generator will evaluate the types
88+
that are assignable to the given type or matching full type names and emit
89+
the typed registrations as needed.
90+
6691
### Keyed Services
6792

6893
[Keyed services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0#keyed-services)
@@ -105,6 +130,8 @@ right `INotificationService` will be injected, based on the key provided.
105130
Note you can also register the same service using multiple keys, as shown in the
106131
`EmailNotificationService` above.
107132

133+
> Keyed services are a feature of version 8.0+ of Microsoft.Extensions.DependencyInjection
134+
108135
## How It Works
109136

110137
The generated code that implements the registration looks like the following:

src/CodeAnalysis.Tests/AddServicesAnalyzerTests.cs

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
namespace Tests.CodeAnalysis;
1717

18-
public record AddServicesAnalyzerTests(ITestOutputHelper Output)
18+
public class AddServicesAnalyzerTests(ITestOutputHelper Output)
1919
{
2020
[Fact]
2121
public async Task NoWarningIfAddServicesPresent()
@@ -41,13 +41,19 @@ public static void Main()
4141
""",
4242
TestState =
4343
{
44+
Sources =
45+
{
46+
ThisAssembly.Resources.AttributedServicesExtension.Text,
47+
ThisAssembly.Resources.ServiceAttribute.Text,
48+
ThisAssembly.Resources.ServiceAttribute_1.Text,
49+
},
4450
ReferenceAssemblies = new ReferenceAssemblies(
45-
"net6.0",
51+
"net8.0",
4652
new PackageIdentity(
47-
"Microsoft.NETCore.App.Ref", "6.0.0"),
48-
Path.Combine("ref", "net6.0"))
53+
"Microsoft.NETCore.App.Ref", "8.0.0"),
54+
Path.Combine("ref", "net8.0"))
4955
.AddPackages(ImmutableArray.Create(
50-
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "6.0.0")))
56+
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
5157
},
5258
};
5359

@@ -81,13 +87,19 @@ public static void Main()
8187
""",
8288
TestState =
8389
{
90+
Sources =
91+
{
92+
ThisAssembly.Resources.AttributedServicesExtension.Text,
93+
ThisAssembly.Resources.ServiceAttribute.Text,
94+
ThisAssembly.Resources.ServiceAttribute_1.Text,
95+
},
8496
ReferenceAssemblies = new ReferenceAssemblies(
85-
"net6.0",
97+
"net8.0",
8698
new PackageIdentity(
87-
"Microsoft.NETCore.App.Ref", "6.0.0"),
88-
Path.Combine("ref", "net6.0"))
99+
"Microsoft.NETCore.App.Ref", "8.0.0"),
100+
Path.Combine("ref", "net8.0"))
89101
.AddPackages(ImmutableArray.Create(
90-
new PackageIdentity("Microsoft.Extensions.DependencyInjection.Abstractions", "6.0.0")))
102+
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
91103
},
92104
};
93105

@@ -116,6 +128,12 @@ public static void Main()
116128
""",
117129
TestState =
118130
{
131+
Sources =
132+
{
133+
ThisAssembly.Resources.AttributedServicesExtension.Text,
134+
ThisAssembly.Resources.ServiceAttribute.Text,
135+
ThisAssembly.Resources.ServiceAttribute_1.Text,
136+
},
119137
ReferenceAssemblies = new ReferenceAssemblies(
120138
"net8.0",
121139
new PackageIdentity(
@@ -157,6 +175,12 @@ public static void Main()
157175
""",
158176
TestState =
159177
{
178+
Sources =
179+
{
180+
ThisAssembly.Resources.AttributedServicesExtension.Text,
181+
ThisAssembly.Resources.ServiceAttribute.Text,
182+
ThisAssembly.Resources.ServiceAttribute_1.Text,
183+
},
160184
ReferenceAssemblies = new ReferenceAssemblies(
161185
"net8.0",
162186
new PackageIdentity(
@@ -173,8 +197,8 @@ public static void Main()
173197
await test.RunAsync();
174198
}
175199

176-
class GeneratorsTest : CSharpSourceGeneratorTest<StaticGenerator, DefaultVerifier>
200+
class GeneratorsTest : CSharpSourceGeneratorTest<IncrementalGenerator, DefaultVerifier>
177201
{
178-
protected override IEnumerable<Type> GetSourceGenerators() => base.GetSourceGenerators().Concat([typeof(IncrementalGenerator)]);
202+
//protected override IEnumerable<Type> GetSourceGenerators() => base.GetSourceGenerators().Concat([typeof(IncrementalGenerator)]);
179203
}
180204
}

src/CodeAnalysis.Tests/CodeAnalysis.Tests.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
@@ -13,10 +13,13 @@
1313
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
1414
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
1515
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
16+
<PackageReference Include="ThisAssembly.Resources" Version="2.0.8" PrivateAssets="all" />
1617
</ItemGroup>
1718

1819
<ItemGroup>
1920
<ProjectReference Include="..\DependencyInjection.Attributed\Attributed.csproj" />
2021
</ItemGroup>
2122

23+
<Import Project="..\DependencyInjection.Attributed.Tests\ContentFiles.targets" />
24+
2225
</Project>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
using Devlooped.Extensions.DependencyInjection.Attributed;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Testing;
10+
using Microsoft.CodeAnalysis.Testing;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
using AnalyzerTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest<Devlooped.Extensions.DependencyInjection.Attributed.ConventionsAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
14+
using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<Devlooped.Extensions.DependencyInjection.Attributed.ConventionsAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
15+
16+
namespace Tests.CodeAnalysis;
17+
18+
public class ConventionAnalyzerTests(ITestOutputHelper Output)
19+
{
20+
[Fact]
21+
public async Task ErrorIfNonTypeOf()
22+
{
23+
var test = new AnalyzerTest
24+
{
25+
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
26+
TestCode =
27+
"""
28+
using System;
29+
using Microsoft.Extensions.DependencyInjection;
30+
31+
public static class Program
32+
{
33+
public static void Main()
34+
{
35+
var services = new ServiceCollection();
36+
var type = typeof(IDisposable);
37+
services.AddServices({|#0:type|});
38+
}
39+
}
40+
""",
41+
TestState =
42+
{
43+
Sources =
44+
{
45+
ThisAssembly.Resources.AttributedServicesExtension.Text,
46+
ThisAssembly.Resources.ServiceAttribute.Text,
47+
ThisAssembly.Resources.ServiceAttribute_1.Text,
48+
},
49+
ReferenceAssemblies = new ReferenceAssemblies(
50+
"net8.0",
51+
new PackageIdentity(
52+
"Microsoft.NETCore.App.Ref", "8.0.0"),
53+
Path.Combine("ref", "net8.0"))
54+
.AddPackages(ImmutableArray.Create(
55+
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
56+
},
57+
};
58+
59+
var expected = Verifier.Diagnostic(ConventionsAnalyzer.AssignableTypeOfRequired).WithLocation(0);
60+
test.ExpectedDiagnostics.Add(expected);
61+
62+
await test.RunAsync();
63+
}
64+
65+
[Fact]
66+
public async Task NoErrorOnTypeOfAndLifetime()
67+
{
68+
var test = new AnalyzerTest
69+
{
70+
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
71+
TestCode =
72+
"""
73+
using System;
74+
using Microsoft.Extensions.DependencyInjection;
75+
76+
public static class Program
77+
{
78+
public static void Main()
79+
{
80+
var services = new ServiceCollection();
81+
services.AddServices(typeof(IDisposable), ServiceLifetime.Scoped);
82+
}
83+
}
84+
""",
85+
TestState =
86+
{
87+
Sources =
88+
{
89+
ThisAssembly.Resources.AttributedServicesExtension.Text,
90+
ThisAssembly.Resources.ServiceAttribute.Text,
91+
ThisAssembly.Resources.ServiceAttribute_1.Text,
92+
},
93+
ReferenceAssemblies = new ReferenceAssemblies(
94+
"net8.0",
95+
new PackageIdentity(
96+
"Microsoft.NETCore.App.Ref", "8.0.0"),
97+
Path.Combine("ref", "net8.0"))
98+
.AddPackages(ImmutableArray.Create(
99+
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
100+
},
101+
};
102+
103+
//var expected = Verifier.Diagnostic(ConventionsAnalyzer.AssignableTypeOfRequired).WithLocation(0);
104+
//test.ExpectedDiagnostics.Add(expected);
105+
106+
await test.RunAsync();
107+
}
108+
109+
[Fact]
110+
public async Task WarnIfOpenGeneric()
111+
{
112+
var test = new AnalyzerTest
113+
{
114+
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck,
115+
TestCode =
116+
"""
117+
using System;
118+
using Microsoft.Extensions.DependencyInjection;
119+
120+
public interface IRepository<T> { }
121+
public class Repository<T> : IRepository<T> { }
122+
123+
public static class Program
124+
{
125+
public static void Main()
126+
{
127+
var services = new ServiceCollection();
128+
services.AddServices({|#0:typeof(Repository<>)|}, ServiceLifetime.Scoped);
129+
}
130+
}
131+
""",
132+
TestState =
133+
{
134+
Sources =
135+
{
136+
ThisAssembly.Resources.AttributedServicesExtension.Text,
137+
ThisAssembly.Resources.ServiceAttribute.Text,
138+
ThisAssembly.Resources.ServiceAttribute_1.Text,
139+
},
140+
ReferenceAssemblies = new ReferenceAssemblies(
141+
"net8.0",
142+
new PackageIdentity(
143+
"Microsoft.NETCore.App.Ref", "8.0.0"),
144+
Path.Combine("ref", "net8.0"))
145+
.AddPackages(ImmutableArray.Create(
146+
new PackageIdentity("Microsoft.Extensions.DependencyInjection", "8.0.0")))
147+
},
148+
};
149+
150+
var expected = Verifier.Diagnostic(ConventionsAnalyzer.OpenGenericType).WithLocation(0);
151+
test.ExpectedDiagnostics.Add(expected);
152+
153+
await test.RunAsync();
154+
}
155+
156+
}

src/DependencyInjection.Attributed.Tests/Attributed.Tests.csproj

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<Import Project="..\DependencyInjection.Attributed\Devlooped.Extensions.DependencyInjection.Attributed.props" />
44

@@ -8,6 +8,18 @@
88
<RootNamespace>Tests</RootNamespace>
99
</PropertyGroup>
1010

11+
<ItemGroup>
12+
<Compile Remove="ComponentModelTests.cs" />
13+
<Compile Remove="CompositionTests.cs" />
14+
<Compile Remove="GenerationTests.cs" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<None Include="ComponentModelTests.cs" />
19+
<None Include="CompositionTests.cs" />
20+
<None Include="GenerationTests.cs" />
21+
</ItemGroup>
22+
1123
<ItemGroup>
1224
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
1325
<PackageReference Include="xunit" Version="2.7.0" />
@@ -28,6 +40,7 @@
2840
<Using Include="Xunit.Abstractions" />
2941
</ItemGroup>
3042

43+
<Import Project="ContentFiles.targets" />
3144
<Import Project="..\DependencyInjection.Attributed\Devlooped.Extensions.DependencyInjection.Attributed.targets" />
3245

3346
</Project>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project>
2+
<!-- Simulates including the files under contentFiles/cs/netstandard2.0 in the final package -->
3+
4+
<ItemGroup>
5+
<Compile Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\AttributedServicesExtension.cs" Link="AttributedServicesExtension.cs" Visible="false" />
6+
<Compile Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute.cs" Link="ServiceAttribute.cs" Visible="false" />
7+
<Compile Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute`1.cs" Link="ServiceAttribute`1.cs" Visible="false" />
8+
</ItemGroup>
9+
10+
<ItemGroup>
11+
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\AttributedServicesExtension.cs" Type="Non-Resx" />
12+
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute.cs" Type="Non-Resx" />
13+
<EmbeddedResource Include="$(MSBuildThisFileDirectory)..\DependencyInjection.Attributed\ServiceAttribute`1.cs" Type="Non-Resx" />
14+
</ItemGroup>
15+
16+
</Project>

0 commit comments

Comments
 (0)