Skip to content

Commit f198d26

Browse files
committed
Feat: Add open Keyed Attributes
Introduced `InjectableScopedAttribute` and `InjectableTransientAttribute` for scoped and transient dependency injection. Updated the service registration generator to handle these attributes and added support for optional keys in `InjectableServiceAttribute`. Refactored keyed service logic to simplify lifetime handling. Updated TUnit package version across multiple projects.
1 parent 6551417 commit f198d26

File tree

16 files changed

+104
-57
lines changed

16 files changed

+104
-57
lines changed

src/CodeOfChaos.Extensions.DependencyInjection.Generators/Registrations/InjectableServiceRegistration.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using CodeOfChaos.GeneratorTools;
66
using Microsoft.CodeAnalysis;
77
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using System.Collections.Immutable;
9+
using System.Linq;
810

911
namespace CodeOfChaos.Extensions.DependencyInjection.Generators.Registrations;
1012
// ---------------------------------------------------------------------------------------------------------------------
@@ -14,15 +16,18 @@ namespace CodeOfChaos.Extensions.DependencyInjection.Generators.Registrations;
1416
public record struct InjectableServiceRegistration(
1517
INamedTypeSymbol ServiceTypeName,
1618
INamedTypeSymbol ImplementationTypeName,
17-
string LifeTime
19+
string LifeTime,
20+
string? Key = null
1821
) : IServiceRegistration {
1922

2023
// -----------------------------------------------------------------------------------------------------------------
2124
// Methods
2225
// -----------------------------------------------------------------------------------------------------------------
23-
public void FormatText(GeneratorStringBuilder builder, string _) => builder
24-
.AppendLine($"services.Add{LifeTime}<{ServiceTypeName.ToDisplayString()}, {ImplementationTypeName.ToDisplayString()}>();");
25-
26+
public void FormatText(GeneratorStringBuilder builder, string _) {
27+
if (Key is not null) builder.AppendLine($"services.AddKeyed{LifeTime}<{ServiceTypeName.ToDisplayString()}, {ImplementationTypeName.ToDisplayString()}>({Key.ToQuotedString()});");
28+
builder.AppendLine($"services.Add{LifeTime}<{ServiceTypeName.ToDisplayString()}, {ImplementationTypeName.ToDisplayString()}>();");
29+
}
30+
2631
// -----------------------------------------------------------------------------------------------------------------
2732
// Constructors
2833
// -----------------------------------------------------------------------------------------------------------------
@@ -39,16 +44,26 @@ out InjectableServiceRegistration registration
3944
{ Name: GenericNameSyntax genericNameSyntaxByItself } => genericNameSyntaxByItself,
4045
_ => null
4146
};
42-
47+
48+
ImmutableArray<AttributeData> attributes = implementationTypeSymbol.GetAttributes();
49+
4350
if (genericNameSyntax?.TypeArgumentList.Arguments.FirstOrDefault() is not {} serviceTypeSyntax) return false;
4451
if (resolver.ResolveSymbol(serviceTypeSyntax) is not INamedTypeSymbol serviceNamedTypeSymbol) return false;
45-
if (attribute.ArgumentList?.Arguments.FirstOrDefault()?.Expression is not MemberAccessExpressionSyntax memberAccess) return false;
46-
if (!memberAccess.TryGetAsServiceLifetimeString(out string? lifeTime)) return false;
52+
53+
AttributeData? keyedServiceAttribute = attributes.FirstOrDefault(attr => attr.AttributeClass?.ToDisplayString().Contains("KeyedInjectableServiceAttribute") ?? false);
54+
string? key = (string?)keyedServiceAttribute?.ConstructorArguments.ElementAtOrDefault(1).Value;
55+
int lifeTime = (int)(keyedServiceAttribute?.ConstructorArguments.ElementAtOrDefault(0).Value ?? -1);
4756

4857
registration = new InjectableServiceRegistration(
4958
serviceNamedTypeSymbol,
5059
implementationTypeSymbol,
51-
lifeTime
60+
lifeTime switch {
61+
0 => "Singleton",
62+
1 => "Scoped",
63+
2 => "Transient",
64+
_ => "Transient"
65+
},
66+
key
5267
);
5368

5469
return true;
Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,29 @@ namespace CodeOfChaos.Extensions.DependencyInjection.Generators.Registrations;
1313
// Code
1414
// ---------------------------------------------------------------------------------------------------------------------
1515
// ReSharper disable once StructCanBeMadeReadOnly
16-
public record struct KeyedInjectableServiceRegistration(
16+
public record struct SpecifcInjectableServiceRegistration(
1717
INamedTypeSymbol ServiceTypeName,
1818
INamedTypeSymbol ImplementationTypeName,
1919
string LifeTime,
20-
string Key
20+
string? Key = null
2121
) : IServiceRegistration {
2222

2323
// -----------------------------------------------------------------------------------------------------------------
2424
// Methods
2525
// -----------------------------------------------------------------------------------------------------------------
26-
public void FormatText(GeneratorStringBuilder builder, string _) => builder
27-
.AppendLine($"services.AddKeyed{LifeTime}<{ServiceTypeName.ToDisplayString()}, {ImplementationTypeName.ToDisplayString()}>({Key.ToQuotedString()});");
28-
26+
public void FormatText(GeneratorStringBuilder builder, string _) {
27+
if (!string.IsNullOrWhiteSpace(Key)) builder.AppendLine($"services.AddKeyed{LifeTime}<{ServiceTypeName.ToDisplayString()}, {ImplementationTypeName.ToDisplayString()}>({Key!.ToQuotedString()});");
28+
builder.AppendLine($"services.Add{LifeTime}<{ServiceTypeName.ToDisplayString()}, {ImplementationTypeName.ToDisplayString()}>();");
29+
}
30+
2931
// -----------------------------------------------------------------------------------------------------------------
3032
// Constructors
3133
// -----------------------------------------------------------------------------------------------------------------
3234
public static bool TryCreateFromModel(
3335
INamedTypeSymbol implementationTypeSymbol,
3436
AttributeSyntax attribute,
3537
ISymbolResolver resolver,
36-
out KeyedInjectableServiceRegistration registration
38+
out SpecifcInjectableServiceRegistration registration
3739
) {
3840
registration = default;
3941

@@ -44,24 +46,23 @@ out KeyedInjectableServiceRegistration registration
4446
};
4547

4648
ImmutableArray<AttributeData> attributes = implementationTypeSymbol.GetAttributes();
47-
4849

4950
if (genericNameSyntax?.TypeArgumentList.Arguments.FirstOrDefault() is not {} serviceTypeSyntax) return false;
5051
if (resolver.ResolveSymbol(serviceTypeSyntax) is not INamedTypeSymbol serviceNamedTypeSymbol) return false;
5152

52-
AttributeData? keyedServiceAttribute = attributes.FirstOrDefault(attr => attr.AttributeClass?.ToDisplayString().Contains("KeyedInjectableServiceAttribute") ?? false);
53-
string key = (string)(keyedServiceAttribute?.ConstructorArguments.ElementAtOrDefault(0).Value ?? string.Empty);
54-
int lifeTime = (int)(keyedServiceAttribute?.ConstructorArguments.ElementAtOrDefault(1).Value ?? -1);
53+
AttributeData? keyedServiceAttribute = attributes.FirstOrDefault(attr => attr.AttributeClass?.ToDisplayString().Contains("KeyedInjectableServiceAttribute") ?? false);
54+
string? key = (string?)keyedServiceAttribute?.ConstructorArguments.ElementAtOrDefault(0).Value;
55+
string lifeTimeName = attribute.Name.ToFullString();
56+
57+
string lifeTime = "Transient";
58+
if (lifeTimeName.Contains("Singleton")) lifeTime = "Singleton";
59+
if (lifeTimeName.Contains("Scoped")) lifeTime = "Scoped";
60+
if (lifeTimeName.Contains("Transient")) lifeTime = "Transient";
5561

56-
registration = new KeyedInjectableServiceRegistration(
62+
registration = new SpecifcInjectableServiceRegistration(
5763
serviceNamedTypeSymbol,
5864
implementationTypeSymbol,
59-
lifeTime switch {
60-
0 => "Singleton",
61-
1 => "Scoped",
62-
2 => "Transient",
63-
_ => "Transient"
64-
},
65+
lifeTime,
6566
key
6667
);
6768

src/CodeOfChaos.Extensions.DependencyInjection.Generators/ServiceRegistrationGenerator.cs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,20 @@ public class ServiceRegistrationGenerator : IIncrementalGenerator {
2424
private const string PooledServicesFileName = "AutoPooledServices.g.cs";
2525

2626
private const string InjectableServiceAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.InjectableServiceAttribute`1";
27+
private const string InjectableSingletonAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.InjectableSingletonAttribute`1";
28+
private const string InjectableScopedAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.InjectableScopedAttribute`1";
29+
private const string InjectableTransientAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.InjectableTransientAttribute`1";
2730
private const string FactoryCreatedServiceAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.FactoryCreatedServiceAttribute`2";
2831
private const string PooledInjectableServiceAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.PooledInjectableServiceAttribute`2";
29-
private const string KeyedInjectableServiceAttributeMetadataName = "CodeOfChaos.Extensions.DependencyInjection.KeyedInjectableServiceAttribute`1";
3032

3133
private static readonly string[] MetaDataNames = [
3234
InjectableServiceAttributeMetadataName,
35+
InjectableSingletonAttributeMetadataName,
36+
InjectableScopedAttributeMetadataName,
37+
InjectableTransientAttributeMetadataName,
38+
3339
FactoryCreatedServiceAttributeMetadataName,
34-
PooledInjectableServiceAttributeMetadataName,
35-
KeyedInjectableServiceAttributeMetadataName
40+
PooledInjectableServiceAttributeMetadataName
3641
];
3742

3843
private static Regex RegexSanitizeAssemblyName { get; } = new(@"((?im)[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?)|(\.dll)", RegexOptions.Compiled);
@@ -99,9 +104,12 @@ private static List<IServiceRegistration> GetRegistrations(SourceProductionConte
99104

100105
// See above if check to know why we ! for nullablity
101106
INamedTypeSymbol injectableServiceAttributeType = types[InjectableServiceAttributeMetadataName]!;
107+
INamedTypeSymbol injectableSingletonAttributeType = types[InjectableSingletonAttributeMetadataName]!;
108+
INamedTypeSymbol injectableScopedAttributeType = types[InjectableScopedAttributeMetadataName]!;
109+
INamedTypeSymbol injectableTransientAttributeType = types[InjectableTransientAttributeMetadataName]!;
110+
102111
INamedTypeSymbol factoryCreateServiceAttributeType = types[FactoryCreatedServiceAttributeMetadataName]!;
103112
INamedTypeSymbol injectablePooledServiceAttributeType = types[PooledInjectableServiceAttributeMetadataName]!;
104-
INamedTypeSymbol keyedInjectableServiceAttributeType = types[KeyedInjectableServiceAttributeMetadataName]!;
105113

106114
List<IServiceRegistration> registrations = [];
107115
foreach (ClassDeclarationSyntax candidate in classDeclarations) {
@@ -110,28 +118,30 @@ private static List<IServiceRegistration> GetRegistrations(SourceProductionConte
110118

111119
foreach (AttributeSyntax attribute in candidate.AttributeLists.SelectMany(attrList => attrList.Attributes)) {
112120
if (model.GetTypeInfo(attribute).Type is not INamedTypeSymbol attributeTypeInfo) continue;
121+
INamedTypeSymbol constructedFrom = attributeTypeInfo.ConstructedFrom;
113122

114-
if (SymbolEqualityComparer.Default.Equals(attributeTypeInfo.ConstructedFrom, factoryCreateServiceAttributeType)
123+
if (SymbolEqualityComparer.Default.Equals(constructedFrom, factoryCreateServiceAttributeType)
115124
&& FactoryCreatedServiceRegistration.TryCreateFromModel(implementationTypeSymbol, attribute, new SymbolResolver(model), out FactoryCreatedServiceRegistration factoryCreated)) {
116125
registrations.Add(factoryCreated);
117126
continue;
118127
}
119-
120-
if (SymbolEqualityComparer.Default.Equals(attributeTypeInfo.ConstructedFrom, injectableServiceAttributeType)
121-
&& InjectableServiceRegistration.TryCreateFromModel(implementationTypeSymbol, attribute, new SymbolResolver(model), out InjectableServiceRegistration injectable)) {
122-
registrations.Add(injectable);
128+
if ((SymbolEqualityComparer.Default.Equals(constructedFrom, injectableSingletonAttributeType)
129+
|| SymbolEqualityComparer.Default.Equals(constructedFrom, injectableScopedAttributeType)
130+
|| SymbolEqualityComparer.Default.Equals(constructedFrom, injectableTransientAttributeType))
131+
&& SpecifcInjectableServiceRegistration.TryCreateFromModel(implementationTypeSymbol, attribute, new SymbolResolver(model), out SpecifcInjectableServiceRegistration specificInjectable)) {
132+
registrations.Add(specificInjectable);
123133
continue;
124134
}
125135

126-
if (SymbolEqualityComparer.Default.Equals(attributeTypeInfo.ConstructedFrom, keyedInjectableServiceAttributeType)
127-
&& KeyedInjectableServiceRegistration.TryCreateFromModel(implementationTypeSymbol, attribute, new SymbolResolver(model), out KeyedInjectableServiceRegistration keyedInjectable)) {
128-
registrations.Add(keyedInjectable);
136+
if (SymbolEqualityComparer.Default.Equals(constructedFrom, injectableServiceAttributeType)
137+
&& InjectableServiceRegistration.TryCreateFromModel(implementationTypeSymbol, attribute, new SymbolResolver(model), out InjectableServiceRegistration injectable)) {
138+
registrations.Add(injectable);
129139
continue;
130140
}
131141

132142
// ReSharper disable once InvertIf
133143
// ReSharper disable once RedundantJumpStatement
134-
if (SymbolEqualityComparer.Default.Equals(attributeTypeInfo.ConstructedFrom, injectablePooledServiceAttributeType)
144+
if (SymbolEqualityComparer.Default.Equals(constructedFrom, injectablePooledServiceAttributeType)
135145
&& InjectablePoolableServiceRegistration.TryCreateFromModel(attribute, new SymbolResolver(model), out InjectablePoolableServiceRegistration pooledInjectable)) {
136146
registrations.Add(pooledInjectable);
137147
continue;
@@ -157,7 +167,7 @@ private static string GenerateServiceRegistrationFile(SourceProductionContext _,
157167

158168
b.Indent(b2 => b2.ForEach(
159169
registrations,
160-
static (builder, registration, assemblyName) => registration.FormatText(builder, assemblyName),
170+
itemFormatter: static (builder, registration, assemblyName) => registration.FormatText(builder, assemblyName),
161171
assemblyName
162172
));
163173
b.AppendLineIndented("return services;");
@@ -179,7 +189,7 @@ private static string GeneratePooledServicesFile(SourceProductionContext _, stri
179189
.AppendLine("private static readonly DefaultObjectPoolProvider _objectPoolProvider = new();")
180190
.Indent(b2 => b2.ForEach(
181191
registrations.OfType<InjectablePoolableServiceRegistration>(),
182-
static (builder, registration) => registration.FormatPoolText(builder)
192+
itemFormatter: static (builder, registration) => registration.FormatPoolText(builder)
183193
))
184194
.AppendLine()
185195
)
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
// ---------------------------------------------------------------------------------------------------------------------
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
4-
5-
using Microsoft.Extensions.DependencyInjection;
6-
74
namespace CodeOfChaos.Extensions.DependencyInjection;
85
// ---------------------------------------------------------------------------------------------------------------------
96
// Code
107
// ---------------------------------------------------------------------------------------------------------------------
118
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
12-
public class KeyedInjectableServiceAttribute<TService>(string key, ServiceLifetime lifetime) : Attribute {
13-
public string Key { get; } = key;
14-
public ServiceLifetime Lifetime { get; } = lifetime;
9+
public class InjectableScopedAttribute<TService>(string? key = null) : Attribute {
1510
public Type ServiceType { get; } = typeof(TService);
11+
public string? Key { get; } = key;
1612
}

src/CodeOfChaos.Extensions.DependencyInjection/InjectableService/InjectableServiceAttribute.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ namespace CodeOfChaos.Extensions.DependencyInjection;
99
// Code
1010
// ---------------------------------------------------------------------------------------------------------------------
1111
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
12-
public class InjectableServiceAttribute<TService>(ServiceLifetime lifetime) : Attribute {
12+
public class InjectableServiceAttribute<TService>(ServiceLifetime lifetime, string? key = null) : Attribute {
1313
public ServiceLifetime Lifetime { get; } = lifetime;
1414
public Type ServiceType { get; } = typeof(TService);
15+
public string? Key { get; } = key;
1516
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
namespace CodeOfChaos.Extensions.DependencyInjection;
5+
// ---------------------------------------------------------------------------------------------------------------------
6+
// Code
7+
// ---------------------------------------------------------------------------------------------------------------------
8+
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
9+
public class InjectableSingletonAttribute<TService>(string? key = null) : Attribute {
10+
public Type ServiceType { get; } = typeof(TService);
11+
public string? Key { get; } = key;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
namespace CodeOfChaos.Extensions.DependencyInjection;
5+
// ---------------------------------------------------------------------------------------------------------------------
6+
// Code
7+
// ---------------------------------------------------------------------------------------------------------------------
8+
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
9+
public class InjectableTransientAttribute<TService>(string? key = null) : Attribute {
10+
public Type ServiceType { get; } = typeof(TService);
11+
public string? Key { get; } = key;
12+
}

tests/Tests.CodeOfChaos.Extensions.Analyzers/Tests.CodeOfChaos.Extensions.Analyzers.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="CodeOfChaos.Testing.TUnit" Version="0.8.1"/>
14-
<PackageReference Include="TUnit" Version="0.19.24" />
14+
<PackageReference Include="TUnit" Version="0.19.32" />
1515
</ItemGroup>
1616

1717
<ItemGroup>

tests/Tests.CodeOfChaos.Extensions.AspNetCore/Tests.CodeOfChaos.Extensions.AspNetCore.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<ItemGroup>
1414
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
1515
<PackageReference Include="Moq" Version="4.20.72"/>
16-
<PackageReference Include="TUnit" Version="0.19.24" />
16+
<PackageReference Include="TUnit" Version="0.19.32" />
1717
<PackageReference Include="Bogus" Version="35.6.2"/>
1818
</ItemGroup>
1919

tests/Tests.CodeOfChaos.Extensions.DependencyInjection.Generators/Tests.CodeOfChaos.Extensions.DependencyInjection.Generators.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0"/>
2121
<PackageReference Include="Moq" Version="4.20.72"/>
2222
<PackageReference Include="System.Formats.Asn1" Version="9.0.3"/>
23-
<PackageReference Include="TUnit" Version="0.19.24" />
23+
<PackageReference Include="TUnit" Version="0.19.32" />
2424
</ItemGroup>
2525

2626
<ItemGroup>

0 commit comments

Comments
 (0)