Skip to content

Commit 3404f3a

Browse files
committed
Add host-aware module registration support
1 parent fffaf74 commit 3404f3a

51 files changed

Lines changed: 1054 additions & 59 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.verified.txt text eol=lf working-tree-encoding=UTF-8

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
</PropertyGroup>
66
<ItemGroup>
77
<PackageVersion Include="AssemblyMetadata.Generators" Version="2.2.0" />
8-
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
8+
<PackageVersion Include="AwesomeAssertions" Version="9.6.0" />
99
<PackageVersion Include="coverlet.MTP" Version="10.0.1" />
1010
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
1111
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.14.0]" />
1212
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
13+
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.11" />
1314
<PackageVersion Include="MinVer" Version="7.0.0" />
1415
<PackageVersion Include="Verify.XunitV3" Version="31.28.0" />
1516
<PackageVersion Include="xunit.v3.mtp-v2" Version="4.0.0" />

README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Source generator that helps register attribute marked services in the dependency
1515
- Transient, Singleton, Scoped service registration
1616
- Factory registration
1717
- Module method registration
18+
- Host-aware module registration with `IHostApplicationBuilder`
1819
- Duplicate Strategy - Skip,Replace,Append
1920
- Registration Strategy - Self, Implemented Interfaces, Self With Interfaces
2021
- Decorator registration (`RegisterDecorator`) — no runtime dependencies
@@ -332,7 +333,7 @@ public class FrontEndLoggingDecorator : IService
332333

333334
#### Register Method
334335

335-
When the service registration is complex, use the `RegisterServices` attribute on a method that has a parameter of `IServiceCollection` or `ServiceCollection`
336+
When the service registration is complex, use the `RegisterServices` attribute on a method whose first parameter is `IServiceCollection`, `ServiceCollection`, or `IHostApplicationBuilder`. The method may optionally receive a supported string tag collection as its second parameter.
336337

337338
```c#
338339
public class RegistrationModule
@@ -351,15 +352,58 @@ public class RegistrationModule
351352
}
352353
```
353354

355+
Register methods can inspect the tags passed to the generated extension method and conditionally add services. Supported tag parameters include `IEnumerable<string>`, `IReadOnlySet<string>`, `IReadOnlyCollection<string>`, `ICollection<string>`, `ISet<string>`, and `HashSet<string>`.
356+
357+
```c#
358+
public static class TaggedRegistrationModule
359+
{
360+
[RegisterServices]
361+
public static void Register(IServiceCollection services, IReadOnlySet<string> tags)
362+
{
363+
if (tags.Contains("Client"))
364+
{
365+
services.AddSingleton<ClientService>();
366+
}
367+
}
368+
}
369+
370+
var services = new ServiceCollection();
371+
services.AddInjectioTestsConsole("Client");
372+
```
373+
374+
Host-aware modules can configure the full application builder:
375+
376+
```c#
377+
public static class HostRegistrationModule
378+
{
379+
[RegisterServices]
380+
public static void Register(IHostApplicationBuilder builder)
381+
{
382+
builder.Configuration.AddJsonFile("feature.json", optional: true);
383+
}
384+
}
385+
```
386+
354387
#### Add to container
355388

356-
The source generator creates an extension method with all the discovered services registered. Call the generated extension method to add the services to the container. The extension method will be called `Add[AssemblyName]`. The assembly name will have the dots removed.
389+
The source generator creates an `IServiceCollection` extension method named `Add[AssemblyName]`, with dots removed from the assembly name. This extension registers discovered services and decorators, then invokes registration methods whose first parameter is `IServiceCollection` or `ServiceCollection`.
357390

358391
```c#
359392
var services = new ServiceCollection();
360393
services.AddInjectioTestsConsole();
361394
```
362395

396+
When the consuming project references `Microsoft.Extensions.Hosting.Abstractions`, the generator also creates an overload for `IHostApplicationBuilder`. Injectio does not add this dependency to DI-only projects.
397+
398+
The host extension first calls the service collection extension through `builder.Services`, then invokes registration methods whose first parameter is `IHostApplicationBuilder`. Calling the host extension is therefore sufficient to run both service and host registrations.
399+
400+
```c#
401+
var builder = Host.CreateApplicationBuilder(args);
402+
builder.AddInjectioTestsConsole();
403+
```
404+
405+
Generated registration methods are idempotent. Service collection registrations and host registrations are tracked independently, so calling the service extension before the host extension still runs each portion once. For each portion, the tags supplied to its first invocation determine the registrations; later invocations are ignored.
406+
363407
Override the extension method name by using the `InjectioName` MSBuild property.
364408

365409
```xml

src/Injectio.Generators/KnownTypes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public static class KnownTypes
2929
public const string ModuleAttributeTypeName = $"{ModuleAttributeShortName}Attribute";
3030
public const string ModuleAttributeFullName = $"{AbstractionNamespace}.{ModuleAttributeTypeName}";
3131

32+
public const string HostApplicationBuilderFullName = "Microsoft.Extensions.Hosting.IHostApplicationBuilder";
33+
3234
public const string DecoratorAttributeShortName = "RegisterDecorator";
3335
public const string DecoratorAttributeTypeName = $"{DecoratorAttributeShortName}Attribute";
3436
public const string DecoratorAttributeFullName = $"{AbstractionNamespace}.{DecoratorAttributeTypeName}";
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
namespace Injectio.Generators.Models;
22

3+
public enum ModuleParameterType
4+
{
5+
ServiceCollection,
6+
HostApplicationBuilder
7+
}
8+
39
public record ModuleRegistration(
410
string ClassName,
511
string MethodName,
612
bool IsStatic,
7-
bool HasTagCollection
13+
bool HasTagCollection,
14+
ModuleParameterType ParameterType = ModuleParameterType.ServiceCollection
815
);

src/Injectio.Generators/Models/RegistrationContext.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ public record RegistrationContext(
77
EquatableArray<ModuleRegistration> ModuleRegistrations,
88
EquatableArray<DecoratorRegistration> DecoratorRegistrations,
99
string? AssemblyName,
10-
MethodOptions? MethodOptions
10+
MethodOptions? MethodOptions,
11+
bool HasHostApplicationBuilder
1112
);

src/Injectio.Generators/ServiceRegistrationAnalyzer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,10 @@ private static void ValidateMethod(
107107
return;
108108
}
109109

110-
var hasServiceCollection = SymbolHelpers.IsServiceCollection(methodSymbol.Parameters[0]);
110+
var hasSupportedParameter = SymbolHelpers.IsServiceCollection(methodSymbol.Parameters[0])
111+
|| SymbolHelpers.IsHostApplicationBuilder(methodSymbol.Parameters[0]);
111112

112-
if (!hasServiceCollection)
113+
if (!hasSupportedParameter)
113114
{
114115
Diagnostic diagnostic = Diagnostic.Create(
115116
DiagnosticDescriptors.InvalidMethodSignature,

src/Injectio.Generators/ServiceRegistrationGenerator.cs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
132132
.Select(static (c, _) => c.AssemblyName)
133133
.WithTrackingName("AssemblyName");
134134

135+
var hasHostApplicationBuilder = context.CompilationProvider
136+
.Select(static (c, _) => c.GetTypeByMetadataName(KnownTypes.HostApplicationBuilderFullName) is not null)
137+
.WithTrackingName("HasHostApplicationBuilder");
138+
135139
// include config options
136140
var methodOptions = context.AnalyzerConfigOptionsProvider
137141
.Select(static (c, _) =>
@@ -177,16 +181,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
177181
.Combine(allDecoratorRegistrations)
178182
.Combine(assemblyName)
179183
.Combine(methodOptions)
184+
.Combine(hasHostApplicationBuilder)
180185
.Select(static (combined, _) =>
181186
{
182-
var ((((services, modules), decorators), assemblyName), options) = combined;
187+
var (((((services, modules), decorators), assemblyName), options), hasHostApplicationBuilder) = combined;
183188

184189
return new RegistrationContext(
185190
ServiceRegistrations: services,
186191
ModuleRegistrations: CreateModuleRegistrations(modules),
187192
DecoratorRegistrations: decorators,
188193
AssemblyName: assemblyName,
189-
MethodOptions: options
194+
MethodOptions: options,
195+
HasHostApplicationBuilder: hasHostApplicationBuilder
190196
);
191197
})
192198
.WithTrackingName("Generation");
@@ -223,7 +229,8 @@ private void ExecuteGeneration(SourceProductionContext sourceContext, Registrati
223229
decoratorRegistrations,
224230
source.AssemblyName,
225231
methodName,
226-
methodInternal);
232+
methodInternal,
233+
source.HasHostApplicationBuilder);
227234

228235
// add source file
229236
sourceContext.AddSource("Injectio.g.cs", SourceText.From(result, Encoding.UTF8));
@@ -287,15 +294,16 @@ private static EquatableArray<DecoratorRegistration> TransformDecoratorRegistrat
287294
if (context.TargetSymbol is not IMethodSymbol methodSymbol)
288295
return null;
289296

290-
var (isValid, hasTagCollection) = ValidateMethod(methodSymbol);
297+
var (isValid, hasTagCollection, parameterType) = ValidateMethod(methodSymbol);
291298
if (!isValid)
292299
return null;
293300

294301
return new ModuleRegistration(
295302
ClassName: methodSymbol.ContainingType.ToDisplayString(SymbolHelpers.FullyQualifiedNullableFormat),
296303
MethodName: methodSymbol.Name,
297304
IsStatic: methodSymbol.IsStatic,
298-
HasTagCollection: hasTagCollection
305+
HasTagCollection: hasTagCollection,
306+
ParameterType: parameterType
299307
);
300308
}
301309

@@ -408,32 +416,41 @@ private static EquatableArray<DecoratorRegistration> TransformDecoratorRegistrat
408416
IsOpenGeneric: isOpenGeneric);
409417
}
410418

411-
private static (bool isValid, bool hasTagCollection) ValidateMethod(IMethodSymbol methodSymbol)
419+
private static (bool isValid, bool hasTagCollection, ModuleParameterType parameterType) ValidateMethod(IMethodSymbol methodSymbol)
412420
{
413-
var hasServiceCollection = false;
421+
var parameterType = ModuleParameterType.ServiceCollection;
422+
var hasSupportedParameter = false;
414423

415-
// validate first parameter should be service collection
424+
// validate first parameter should be service collection or host application builder
416425
if (methodSymbol.Parameters.Length is 1 or 2)
417426
{
418427
var parameterSymbol = methodSymbol.Parameters[0];
419-
hasServiceCollection = SymbolHelpers.IsServiceCollection(parameterSymbol);
428+
if (SymbolHelpers.IsServiceCollection(parameterSymbol))
429+
{
430+
hasSupportedParameter = true;
431+
}
432+
else if (SymbolHelpers.IsHostApplicationBuilder(parameterSymbol))
433+
{
434+
hasSupportedParameter = true;
435+
parameterType = ModuleParameterType.HostApplicationBuilder;
436+
}
420437
}
421438

422439
if (methodSymbol.Parameters.Length is 1)
423-
return (hasServiceCollection, false);
440+
return (hasSupportedParameter, false, parameterType);
424441

425442
// validate second parameter should be string collection
426443
if (methodSymbol.Parameters.Length is 2)
427444
{
428445
var parameterSymbol = methodSymbol.Parameters[1];
429446
bool hasTagCollection = SymbolHelpers.IsStringCollection(parameterSymbol);
430447

431-
// to be valid, parameter 0 must be service collection and parameter 1 must be string collection,
432-
return (hasServiceCollection && hasTagCollection, hasTagCollection);
448+
// to be valid, parameter 0 must be supported and parameter 1 must be string collection
449+
return (hasSupportedParameter && hasTagCollection, hasTagCollection, parameterType);
433450
}
434451

435452
// invalid method
436-
return (false, false);
453+
return (false, false, parameterType);
437454
}
438455

439456
private static ServiceRegistration? CreateServiceRegistration(INamedTypeSymbol classSymbol, AttributeData attribute, string serviceLifetime)

0 commit comments

Comments
 (0)