Skip to content

Commit 08b5b10

Browse files
authored
Merge pull request #214 from TheConstructor/feature/auto-register-open-generics
Support self-registration of open-generic types
2 parents 4349f87 + 58abbf9 commit 08b5b10

File tree

6 files changed

+168
-2
lines changed

6 files changed

+168
-2
lines changed

src/Injectio.Generators/ServiceRegistrationGenerator.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ private static (EquatableArray<Diagnostic> diagnostics, bool hasServiceCollectio
334334
// no implementation type set, use class attribute is on
335335
if (implementationType.IsNullOrWhiteSpace())
336336
{
337-
implementationType = classSymbol.ToDisplayString(_fullyQualifiedNullableFormat);
337+
implementationType = ToNamedTypeWithoutPlaceholders(classSymbol).ToDisplayString(_fullyQualifiedNullableFormat);
338338
}
339339

340340
// add implemented interfaces
@@ -345,7 +345,7 @@ private static (EquatableArray<Diagnostic> diagnostics, bool hasServiceCollectio
345345
{
346346
// This interface is typically not injected into services and, more specifically, record types auto-implement it.
347347
if(implementedInterface.ConstructedFrom.ToString() == "System.IEquatable<T>") continue;
348-
var interfaceName = implementedInterface.ToDisplayString(_fullyQualifiedNullableFormat);
348+
var interfaceName = ToNamedTypeWithoutPlaceholders(implementedInterface).ToDisplayString(_fullyQualifiedNullableFormat);
349349
serviceTypes.Add(interfaceName);
350350
}
351351
}
@@ -366,6 +366,26 @@ private static (EquatableArray<Diagnostic> diagnostics, bool hasServiceCollectio
366366
tags.ToArray());
367367
}
368368

369+
private static INamedTypeSymbol ToNamedTypeWithoutPlaceholders(INamedTypeSymbol typeSymbol)
370+
{
371+
if (!typeSymbol.IsGenericType
372+
|| typeSymbol.IsUnboundGenericType)
373+
{
374+
return typeSymbol;
375+
}
376+
377+
foreach (var typeArgument in typeSymbol.TypeArguments)
378+
{
379+
// If TypeKind is TypeParameter, it's actually the name of a locally declared type-parameter -> placeholder
380+
if (typeArgument.TypeKind != TypeKind.TypeParameter)
381+
{
382+
return typeSymbol;
383+
}
384+
}
385+
386+
return typeSymbol.ConstructUnboundGenericType();
387+
}
388+
369389
private static bool IsKnownAttribute(AttributeData attribute, out string serviceLifetime)
370390
{
371391
if (IsSingletonAttribute(attribute))

tests/Injectio.Tests.Console/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
var module = provider.GetRequiredService<IModuleService>();
1919

2020
var generic = provider.GetRequiredService<IOpenGeneric<string>>();
21+
var generic2 = provider.GetRequiredService<IOpenGeneric2<string>>();
2122
var tagService = provider.GetService<IServiceTag>();
2223

2324
var alpaService = provider.GetKeyedService<IServiceKeyed>("Alpha");

tests/Injectio.Tests.Library/Service.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ public class OpenGeneric<T> : IOpenGeneric<T>
7979
{
8080
}
8181

82+
public interface IOpenGeneric2<T>
83+
{
84+
}
85+
86+
[RegisterSingleton]
87+
public class OpenGeneric2<T> : IOpenGeneric2<T>
88+
{
89+
}
90+
8291
public interface IServiceTag
8392
{
8493
}

tests/Injectio.Tests/ServiceRegistrationGeneratorTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,58 @@ public class OpenGeneric<T> : IOpenGeneric<T>
396396
.ScrubLinesContaining("GeneratedCodeAttribute");
397397
}
398398

399+
[Fact]
400+
public Task GenerateRegisterSingletonSelfAsOpenGeneric()
401+
{
402+
var source = @"
403+
using Injectio.Attributes;
404+
405+
namespace Injectio.Sample;
406+
407+
public interface IOpenGeneric<T>
408+
{ }
409+
410+
[RegisterSingleton]
411+
public class OpenGeneric<T> : IOpenGeneric<T>
412+
{ }
413+
";
414+
415+
var (diagnostics, output) = GetGeneratedOutput<ServiceRegistrationGenerator>(source);
416+
417+
diagnostics.Should().BeEmpty();
418+
419+
return Verifier
420+
.Verify(output)
421+
.UseDirectory("Snapshots")
422+
.ScrubLinesContaining("GeneratedCodeAttribute");
423+
}
424+
425+
[Fact]
426+
public Task GenerateRegisterSingletonSelfAsClosedGeneric()
427+
{
428+
var source = @"
429+
using Injectio.Attributes;
430+
431+
namespace Injectio.Sample;
432+
433+
public interface IClosedGeneric<T>
434+
{ }
435+
436+
[RegisterSingleton]
437+
public class Service : IClosedGeneric<object>
438+
{ }
439+
";
440+
441+
var (diagnostics, output) = GetGeneratedOutput<ServiceRegistrationGenerator>(source);
442+
443+
diagnostics.Should().BeEmpty();
444+
445+
return Verifier
446+
.Verify(output)
447+
.UseDirectory("Snapshots")
448+
.ScrubLinesContaining("GeneratedCodeAttribute");
449+
}
450+
399451
[Fact]
400452
public Task GenerateRegisterSingletonTags()
401453
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
namespace Microsoft.Extensions.DependencyInjection
5+
{
6+
/// <summary>
7+
/// Extension methods for discovered service registrations
8+
/// </summary>
9+
public static class DiscoveredServicesExtensions
10+
{
11+
/// <summary>
12+
/// Adds discovered services from Test.Generator to the specified service collection
13+
/// </summary>
14+
/// <param name="serviceCollection">The service collection.</param>
15+
/// <param name="tags">The service registration tags to include.</param>
16+
/// <returns>The service collection</returns>
17+
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestGenerator(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection serviceCollection, params string[]? tags)
18+
{
19+
var tagSet = new global::System.Collections.Generic.HashSet<string>(tags ?? global::System.Linq.Enumerable.Empty<string>());
20+
21+
global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(
22+
serviceCollection,
23+
global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Describe(
24+
typeof(global::Injectio.Sample.IClosedGeneric<object>),
25+
typeof(global::Injectio.Sample.Service),
26+
global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton
27+
)
28+
);
29+
30+
global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(
31+
serviceCollection,
32+
global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Describe(
33+
typeof(global::Injectio.Sample.Service),
34+
typeof(global::Injectio.Sample.Service),
35+
global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton
36+
)
37+
);
38+
39+
return serviceCollection;
40+
}
41+
}
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// <auto-generated />
2+
#nullable enable
3+
4+
namespace Microsoft.Extensions.DependencyInjection
5+
{
6+
/// <summary>
7+
/// Extension methods for discovered service registrations
8+
/// </summary>
9+
public static class DiscoveredServicesExtensions
10+
{
11+
/// <summary>
12+
/// Adds discovered services from Test.Generator to the specified service collection
13+
/// </summary>
14+
/// <param name="serviceCollection">The service collection.</param>
15+
/// <param name="tags">The service registration tags to include.</param>
16+
/// <returns>The service collection</returns>
17+
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestGenerator(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection serviceCollection, params string[]? tags)
18+
{
19+
var tagSet = new global::System.Collections.Generic.HashSet<string>(tags ?? global::System.Linq.Enumerable.Empty<string>());
20+
21+
global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(
22+
serviceCollection,
23+
global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Describe(
24+
typeof(global::Injectio.Sample.IOpenGeneric<>),
25+
typeof(global::Injectio.Sample.OpenGeneric<>),
26+
global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton
27+
)
28+
);
29+
30+
global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAdd(
31+
serviceCollection,
32+
global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Describe(
33+
typeof(global::Injectio.Sample.OpenGeneric<>),
34+
typeof(global::Injectio.Sample.OpenGeneric<>),
35+
global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton
36+
)
37+
);
38+
39+
return serviceCollection;
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)