Skip to content

Commit e1adab8

Browse files
committed
Add support for multiple keyed service registrations
Fixes #108
1 parent 8ac296b commit e1adab8

File tree

5 files changed

+94
-48
lines changed

5 files changed

+94
-48
lines changed

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public class SmsNotificationService : INotificationService
8181
}
8282

8383
[Service<string>("email")]
84+
[Service<string>("default")]
8485
public class EmailNotificationService : INotificationService
8586
{
8687
public string Notify(string message) => $"[Email] {message}";
@@ -101,6 +102,8 @@ public class SmsService([FromKeyedServices("sms")] INotificationService sms)
101102
In this case, when resolving the `SmsService` from the service provider, the
102103
right `INotificationService` will be injected, based on the key provided.
103104

105+
Note you can also register the same service using multiple keys, as shown in the
106+
`EmailNotificationService` above.
104107

105108
## How It Works
106109

src/DependencyInjection.Attributed.Tests/GenerationTests.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,23 @@ public void ResolvesKeyedFromContracts()
200200
Assert.NotNull(singleton.Dependency);
201201
}
202202

203+
[Fact]
204+
public void ResolveMultipleKeys()
205+
{
206+
var collection = new ServiceCollection();
207+
collection.AddServices();
208+
var services = collection.BuildServiceProvider();
209+
210+
var sms = services.GetRequiredKeyedService<INotificationService>("sms");
211+
var email = services.GetRequiredKeyedService<INotificationService>("email");
212+
var def = services.GetRequiredKeyedService<INotificationService>("default");
213+
214+
// Each gets its own instance, since we can't tell apart. Lifetimes can also be disparate.
215+
Assert.NotSame(sms, email);
216+
Assert.NotSame(sms, def);
217+
Assert.NotSame(email, def);
218+
}
219+
203220
[Fact]
204221
public void ResolvesDependency()
205222
{
@@ -349,4 +366,22 @@ public class DependencyFromKeyedContract([Import("contract")] KeyedByContractNam
349366

350367
public interface IService { }
351368
[Service]
352-
class InternalService : IService { }
369+
class InternalService : IService { }
370+
371+
public interface INotificationService
372+
{
373+
string Notify(string message);
374+
}
375+
376+
[Service<string>("sms")]
377+
public class SmsNotificationService : INotificationService
378+
{
379+
public string Notify(string message) => $"[SMS] {message}";
380+
}
381+
382+
[Service<string>("email")]
383+
[Service<string>("default")]
384+
public class EmailNotificationService : INotificationService
385+
{
386+
public string Notify(string message) => $"[Email] {message}";
387+
}

src/DependencyInjection.Attributed/Attributed.csproj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@
1616
<ItemGroup>
1717
<None Update="Devlooped.Extensions.DependencyInjection.Attributed.props" CopyToOutputDirectory="PreserveNewest" PackFolder="buildTransitive" />
1818
<None Update="Devlooped.Extensions.DependencyInjection.Attributed.targets" CopyToOutputDirectory="PreserveNewest" PackFolder="buildTransitive" />
19-
<EmbeddedResource Include="ServiceAttribute*.cs;AddServicesExtension.cs" />
2019
</ItemGroup>
2120

2221
<ItemGroup>
2322
<PackageReference Include="NuGetizer" Version="1.2.1" />
2423
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" Pack="false" />
2524
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
25+
<PackageReference Include="PolySharp" Version="1.14.1" PrivateAssets="all" />
2626
<PackageReference Include="ThisAssembly.Resources" Version="2.0.8" PrivateAssets="all" />
2727
</ItemGroup>
2828

29+
<Target Name="AddEmbeddedResources" BeforeTargets="SplitResourcesByCulture" Condition="'$(DesignTimeBuild)' != 'true'">
30+
<ItemGroup>
31+
<EmbeddedResource Include="ServiceAttribute*.cs;AddServicesExtension.cs" Type="Non-Resx" />
32+
</ItemGroup>
33+
</Target>
34+
2935
<Target Name="PokePackageVersion" BeforeTargets="GetPackageContents" DependsOnTargets="CopyFilesToOutputDirectory" Condition="'$(dotnet-nugetize)' == '' and Exists('$(OutputPath)\Devlooped.Extensions.DependencyInjection.Attributed.props')">
3036
<XmlPoke XmlInputPath="$(OutputPath)\Devlooped.Extensions.DependencyInjection.Attributed.props" Query="/Project/PropertyGroup/DevloopedExtensionsDependencyInjectionVersion" Value="$(PackageVersion)" />
3137
</Target>

src/DependencyInjection.Attributed/IncrementalGenerator.cs

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace Devlooped.Extensions.DependencyInjection.Attributed;
1818
[Generator(LanguageNames.CSharp)]
1919
public class IncrementalGenerator : IIncrementalGenerator
2020
{
21+
record ServiceSymbol(INamedTypeSymbol Type, TypedConstant? Key, int Lifetime);
22+
2123
public void Initialize(IncrementalGeneratorInitializationContext context)
2224
{
2325
var types = context.CompilationProvider.SelectMany((x, c) =>
@@ -58,65 +60,65 @@ bool IsExport(AttributeData attr)
5860
// NOTE: we recognize the attribute by name, not precise type. This makes the generator
5961
// more flexible and avoids requiring any sort of run-time dependency.
6062
var services = types
61-
.Select((x, _) =>
63+
.SelectMany((x, _) =>
6264
{
6365
var name = x.Name;
6466
var attrs = x.GetAttributes();
65-
var serviceAttr = attrs.FirstOrDefault(IsService) ?? attrs.FirstOrDefault(IsKeyedService);
66-
var service = serviceAttr != null || attrs.Any(IsExport);
67+
var services = new List<ServiceSymbol>();
6768

68-
if (!service)
69-
return null;
69+
foreach (var attr in attrs)
70+
{
71+
var serviceAttr = IsService(attr) || IsKeyedService(attr) ? attr : null;
72+
if (serviceAttr == null && !IsExport(attr))
73+
continue;
7074

71-
TypedConstant? key = default;
75+
TypedConstant? key = default;
7276

73-
// Default lifetime is singleton for [Service], Transient for MEF
74-
var lifetime = serviceAttr != null ? 0 : 2;
75-
if (serviceAttr != null)
76-
{
77-
if (IsKeyedService(serviceAttr))
77+
// Default lifetime is singleton for [Service], Transient for MEF
78+
var lifetime = serviceAttr != null ? 0 : 2;
79+
if (serviceAttr != null)
7880
{
79-
key = serviceAttr.ConstructorArguments[0];
80-
lifetime = (int)serviceAttr.ConstructorArguments[1].Value!;
81+
if (IsKeyedService(serviceAttr))
82+
{
83+
key = serviceAttr.ConstructorArguments[0];
84+
lifetime = (int)serviceAttr.ConstructorArguments[1].Value!;
85+
}
86+
else
87+
{
88+
lifetime = (int)serviceAttr.ConstructorArguments[0].Value!;
89+
}
8190
}
8291
else
8392
{
84-
lifetime = (int)serviceAttr.ConstructorArguments[0].Value!;
85-
}
86-
}
87-
else
88-
{
89-
// In NuGet MEF, [Shared] makes exports singleton
90-
if (attrs.Any(a => a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.Composition.SharedAttribute"))
91-
{
92-
lifetime = 0;
93-
}
94-
// In .NET MEF, [PartCreationPolicy(CreationPolicy.Shared)] does it.
95-
else if (attrs.Any(a =>
96-
a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.ComponentModel.Composition.PartCreationPolicyAttribute" &&
97-
a.ConstructorArguments.Length == 1 &&
98-
a.ConstructorArguments[0].Kind == TypedConstantKind.Enum &&
99-
a.ConstructorArguments[0].Type?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.ComponentModel.Composition.CreationPolicy" &&
100-
(int)a.ConstructorArguments[0].Value! == 1))
101-
{
102-
lifetime = 0;
103-
}
93+
// In NuGet MEF, [Shared] makes exports singleton
94+
if (attrs.Any(a => a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.Composition.SharedAttribute"))
95+
{
96+
lifetime = 0;
97+
}
98+
// In .NET MEF, [PartCreationPolicy(CreationPolicy.Shared)] does it.
99+
else if (attrs.Any(a =>
100+
a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.ComponentModel.Composition.PartCreationPolicyAttribute" &&
101+
a.ConstructorArguments.Length == 1 &&
102+
a.ConstructorArguments[0].Kind == TypedConstantKind.Enum &&
103+
a.ConstructorArguments[0].Type?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::System.ComponentModel.Composition.CreationPolicy" &&
104+
(int)a.ConstructorArguments[0].Value! == 1))
105+
{
106+
lifetime = 0;
107+
}
104108

105-
// Consider the [Export(contractName)] as a keyed service with the contract name as the key.
106-
if (attrs.FirstOrDefault(IsExport) is { } export &&
107-
export.ConstructorArguments.Length > 0 &&
108-
export.ConstructorArguments[0].Kind == TypedConstantKind.Primitive)
109-
{
110-
key = export.ConstructorArguments[0];
109+
// Consider the [Export(contractName)] as a keyed service with the contract name as the key.
110+
if (attrs.FirstOrDefault(IsExport) is { } export &&
111+
export.ConstructorArguments.Length > 0 &&
112+
export.ConstructorArguments[0].Kind == TypedConstantKind.Primitive)
113+
{
114+
key = export.ConstructorArguments[0];
115+
}
111116
}
117+
118+
services.Add(new(x, key, lifetime));
112119
}
113120

114-
return new
115-
{
116-
Type = x,
117-
Key = key,
118-
Lifetime = lifetime
119-
};
121+
return services.ToImmutableArray();
120122
})
121123
.Where(x => x != null);
122124

src/DependencyInjection.Attributed/ServiceAttribute`1.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.DependencyInjection
99
/// Requires v8 or later of Microsoft.Extensions.DependencyInjection package.
1010
/// </summary>
1111
/// <typeparam name="TKey">Type of service key.</typeparam>
12-
[AttributeUsage(AttributeTargets.Class)]
12+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
1313
partial class ServiceAttribute<TKey> : Attribute
1414
{
1515
/// <summary>

0 commit comments

Comments
 (0)