From bab2b3b3c62cb808dc48c447dab9315b2a362e80 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Fri, 14 Jun 2024 00:27:18 +0300 Subject: [PATCH] Support AsSelf registration (#6) --- README.md | 5 +- .../AddServicesTests.cs | 88 +++++++++++++++++++ .../DependencyInjectionGenerator.cs | 28 +++--- .../GenerateAttributeSource.cs | 15 +++- .../Model/AttributeModel.cs | 4 +- .../Model/ServiceRegistrationModel.cs | 1 + 6 files changed, 119 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5bdf511..3e6a3f8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ It adds MediatR handlers, which would work for simple cases, although you might | Property | Description | | --- | --- | | **FromAssemblyOf** |Set the assembly containing the given type as the source of types to register. If not specified, the assembly containing the method with this attribute will be used. | -| **AssignableTo** | Set the type that the registered types must be assignable to. Types will be registered with this type as the service type. | +| **AssignableTo** | Set the type that the registered types must be assignable to. Types will be registered with this type as the service type, unless `AsImplementedInterfaces` or `AsSelf` is set. | | **Lifetime** | Set the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. | -| **AsImplementedInterfaces** | If true, the registered types will be registered as implemented interfaces instead of their actual type. This option is ignored if `AssignableTo` is set. | +| **AsImplementedInterfaces** | If true, the registered types will be registered as implemented interfaces instead of their actual type. | +| **AsSelf** | If true, types will be registered with their actual type. It can be combined with `AsImplementedInterfaces`. In that case implemeted interfaces will be "forwarded" to an actual implementation type | | **TypeNameFilter** | Set this value to filter the types to register by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | diff --git a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs index c61dfd5..0109f2f 100644 --- a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs +++ b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs @@ -200,6 +200,59 @@ public class MyService2 : AbstractService { } Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); } + [Fact] + public void AddServicesAssignableToAbstractClassAsSelf() + { + var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(AbstractService), AsSelf = true)]"; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public abstract class AbstractService { } + public class MyService1 : AbstractService { } + public class MyService2 : AbstractService { } + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void AddServiceAssignableToSelf() + { + var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(MyService))]"; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public class MyService { } + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + [Fact] public void AddServicesAssignableToOpenGenericAbstractClass() { @@ -374,6 +427,41 @@ public class InterfacelessService {} Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); } + [Fact] + public void AddServicesBothAsSelfAndAsImplementedInterfaces() + { + var attribute = """ + [GenerateServiceRegistrations( + TypeNameFilter = "*Service", + AsImplementedInterfaces = true, + AsSelf = true, + Lifetime = ServiceLifetime.Singleton))] + """; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public interface IServiceA {} + public interface IServiceB {} + public class MyService: IServiceA, IServiceB {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => s.GetRequiredService()); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + [Fact] public void DontGenerateAnythingIfTypeIsInvalid() { diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs index 1d6ef17..bf67db5 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs @@ -50,7 +50,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } else { - sb.AppendLine($" .Add{registration.Lifetime}<{registration.ServiceTypeName}, {registration.ImplementationTypeName}>()"); + if (registration.ResolveImplementation) + sb.AppendLine($" .Add{registration.Lifetime}<{registration.ServiceTypeName}>(s => s.GetRequiredService<{registration.ImplementationTypeName}>())"); + else + sb.AppendLine($" .Add{registration.Lifetime}<{registration.ServiceTypeName}, {registration.ImplementationTypeName}>()"); } } @@ -122,18 +125,13 @@ private static DiagnosticModel FindServicesToRegister if (assignableToType != null && !IsAssignableTo(implementationType, assignableToType, out matchedType)) continue; - IEnumerable serviceTypes = null; - - if (matchedType != null) - { - serviceTypes = [matchedType]; - } - else + IEnumerable serviceTypes = (attribute.AsSelf, attribute.AsImplementedInterfaces) switch { - serviceTypes = attribute.AsImplementedInterfaces - ? implementationType.AllInterfaces - : [implementationType]; - } + (true, true) => new[] { implementationType }.Concat(implementationType.AllInterfaces), + (false, true) => implementationType.AllInterfaces, + (true, false) => [implementationType], + _ => [matchedType ?? implementationType] + }; foreach (var serviceType in serviceTypes) { @@ -144,13 +142,13 @@ private static DiagnosticModel FindServicesToRegister ? serviceType.ConstructUnboundGenericType().ToDisplayString() : serviceType.ToDisplayString(); - var registration = new ServiceRegistrationModel(attribute.Lifetime, serviceTypeName, implementationTypeName, true); + var registration = new ServiceRegistrationModel(attribute.Lifetime, serviceTypeName, implementationTypeName, false, true); registrations.Add(registration); } else { - - var registration = new ServiceRegistrationModel(attribute.Lifetime, serviceType.ToDisplayString(), implementationType.ToDisplayString(), false); + var shouldResolve = attribute.AsSelf && attribute.AsImplementedInterfaces && !SymbolEqualityComparer.Default.Equals(implementationType, serviceType); + var registration = new ServiceRegistrationModel(attribute.Lifetime, serviceType.ToDisplayString(), implementationType.ToDisplayString(), shouldResolve, false); registrations.Add(registration); } diff --git a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs index a5fa3d0..9783064 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs @@ -23,7 +23,8 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// /// Set the type that the registered types must be assignable to. - /// Types will be registered with this type as the service type. + /// Types will be registered with this type as the service type, + /// unless or is set. /// public Type? AssignableTo { get; set; } @@ -32,12 +33,18 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// is used if not specified. /// public ServiceLifetime Lifetime { get; set; } - + /// - /// If set to true, the registered types will be registered as implemented interfaces instead of their actual type. - /// This option is ignored if is set. + /// If set to true, types will be registered as implemented interfaces instead of their actual type. /// public bool AsImplementedInterfaces { get; set; } + + /// + /// If set to true, types will be registered with their actual type. + /// It can be combined with , in that case implemeted interfaces will be + /// "forwarded" to "self" implementation. + /// + public bool AsSelf { get; set; } /// /// Set this value to filter the types to register by their full name. diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index 45e60a5..64f0ae4 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -10,6 +10,7 @@ record AttributeModel( string Lifetime, string? TypeNameFilter, bool AsImplementedInterfaces, + bool AsSelf, Location Location, bool HasErrors) { @@ -20,6 +21,7 @@ public static AttributeModel Create(AttributeData attribute) var assemblyType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "FromAssemblyOf").Value.Value as INamedTypeSymbol; var assignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssignableTo").Value.Value as INamedTypeSymbol; var asImplementedInterfaces = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsImplementedInterfaces").Value.Value is true; + var asSelf = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsSelf").Value.Value is true; var typeNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "TypeNameFilter").Value.Value as string; if (string.IsNullOrWhiteSpace(typeNameFilter)) @@ -44,6 +46,6 @@ public static AttributeModel Create(AttributeData attribute) var hasError = assemblyType is { TypeKind: TypeKind.Error } || assignableTo is { TypeKind: TypeKind.Error }; - return new(assignableToTypeName, assignableToGenericArguments, assemblyOfTypeName, lifetime, typeNameFilter, asImplementedInterfaces, location, hasError); + return new(assignableToTypeName, assignableToGenericArguments, assemblyOfTypeName, lifetime, typeNameFilter, asImplementedInterfaces, asSelf, location, hasError); } } \ No newline at end of file diff --git a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs index 10fcb27..e0f8b63 100644 --- a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs +++ b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs @@ -4,4 +4,5 @@ record ServiceRegistrationModel( string Lifetime, string ServiceTypeName, string ImplementationTypeName, + bool ResolveImplementation, bool IsOpenGeneric);