diff --git a/README.md b/README.md index 1937b79..2ff75dc 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ var serviceProvider = await services.BuildServiceProviderWithStartablesAsync(); ## Attribute based registration You can also register services using attributes. +There are generic attributes available allowing you to specify the ServiceType, if your service implements multiple interfaces. + ### RegisterAsSingleton attribute ```csharp [RegisterAsSingleton] @@ -150,6 +152,12 @@ public class Service : IService { } ``` +```csharp +[RegisterAsSingleton] +public class Service : IService1, IService2 +{ +} +``` ### RegisterAsScoped attribute ```csharp @@ -158,6 +166,12 @@ public class Service : IService { } ``` +```csharp +[RegisterAsScoped] +public class Service : IService1, IService2 +{ +} +``` ### RegisterAsTransient attribute ```csharp @@ -166,6 +180,12 @@ public class Service : IService { } ``` +```csharp +[RegisterAsTransient] +public class Service : IService1, IService2 +{ +} +``` ### DuplicateRegistrationStrategy - Try - Adds the new registration, if the service hasn't already been registered @@ -201,4 +221,5 @@ public class Service : IService - FMEDI0003 - Non empty constructor found on Module - FMEDI0004 - Non empty constructor found on Startable - FMEDI0005 - Non async method found on Startable -- FMEDI0006 - Non void method found on Module \ No newline at end of file +- FMEDI0006 - Non void method found on Module +- FMEDI0007 - Register ServiceType not implemented by class \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Generator.Sample/Registration.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Generator.Sample/Registration.cs index 3e5ac8f..40b15a7 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Generator.Sample/Registration.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Generator.Sample/Registration.cs @@ -12,6 +12,10 @@ public interface IService3 { } +public interface IService4 +{ +} + [RegisterAsTransient(InterfaceRegistrationStrategy = InterfaceRegistrationStrategy.Self)] public class TransientService_Self : IService1 { @@ -72,7 +76,7 @@ public class SingletonService_Add : IService1 { } -[RegisterAsScoped(ServiceType = typeof(IService2), DuplicateRegistrationStrategy = DuplicateRegistrationStrategy.Add)] +[RegisterAsScoped(DuplicateRegistrationStrategy = DuplicateRegistrationStrategy.Add)] public class ServiceMultipleInterfaces : IService1, IService2 { } @@ -81,12 +85,12 @@ public interface IService_OpenGeneric { } -[RegisterAsSingleton(ImplementationType = typeof(TransientServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))] +[RegisterAsTransient(ImplementationType = typeof(TransientServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))] public class TransientServiceOpenGeneric : IService_OpenGeneric { } -[RegisterAsSingleton(ImplementationType = typeof(ScopedServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))] +[RegisterAsScoped(ImplementationType = typeof(ScopedServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))] public class ScopedServiceOpenGeneric : IService_OpenGeneric { } diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedGenericService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedGenericService.cs new file mode 100644 index 0000000..de08c67 --- /dev/null +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedGenericService.cs @@ -0,0 +1,6 @@ +namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; + +[RegisterAsScoped] +public class AutomaticScopedGenericService : IService1, IService2 +{ +} \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedService.cs index d9ccdb6..45fa30f 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedService.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticScopedService.cs @@ -1,6 +1,6 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; [RegisterAsScoped] -public class AutomaticScopedService : IService +public class AutomaticScopedService : IService1 { } \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonGenericService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonGenericService.cs new file mode 100644 index 0000000..614826f --- /dev/null +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonGenericService.cs @@ -0,0 +1,6 @@ +namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; + +[RegisterAsSingleton] +public class AutomaticSingletonGenericService : IService1, IService2 +{ +} \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonService.cs index 18b73ca..2bd4c26 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonService.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticSingletonService.cs @@ -1,6 +1,6 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; [RegisterAsSingleton] -public class AutomaticSingletonService : IService +public class AutomaticSingletonService : IService1 { } \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientGenericService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientGenericService.cs new file mode 100644 index 0000000..140860b --- /dev/null +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientGenericService.cs @@ -0,0 +1,6 @@ +namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; + +[RegisterAsTransient] +public class AutomaticTransientGenericService : IService1, IService2 +{ +} \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientService.cs index a0c3aa9..2e6bfcb 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientService.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/AutomaticTransientService.cs @@ -1,6 +1,6 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; [RegisterAsTransient] -public class AutomaticTransientService : IService +public class AutomaticTransientService : IService1 { } \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService1.cs similarity index 72% rename from sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService.cs rename to sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService1.cs index a87af1a..682e648 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService1.cs @@ -1,5 +1,5 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; -public interface IService +public interface IService1 { } \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService2.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService2.cs new file mode 100644 index 0000000..85207e1 --- /dev/null +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/IService2.cs @@ -0,0 +1,5 @@ +namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; + +public interface IService2 +{ +} \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/ManualService.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/ManualService.cs index 1919178..a8ec263 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/ManualService.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/ManualService.cs @@ -1,5 +1,5 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample; -public class ManualService : IService +public class ManualService : IService1 { } \ No newline at end of file diff --git a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/Program.cs b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/Program.cs index d0a4b5b..5a35f27 100644 --- a/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/Program.cs +++ b/sample/Futurum.Microsoft.Extensions.DependencyInjection.Sample/Program.cs @@ -30,7 +30,7 @@ serviceCollection.AddStartable(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); }); var host = builder.Build(); diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/DiagnosticDescriptors.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/DiagnosticDescriptors.cs index de372f3..77e18e0 100644 --- a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/DiagnosticDescriptors.cs +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/DiagnosticDescriptors.cs @@ -54,4 +54,12 @@ public static class DiagnosticDescriptors "Futurum.Microsoft.Extensions.DependencyInjection.Generator", DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor RegistrationServiceTypeNotImplementedByClass = new( + "FMEDI0007", + "Register ServiceType not implemented by class", + $"Class '{{0}}' does not implement ServiceType '{{1}}'.{Environment.NewLine} Class must implement the ServiceType.", + "Futurum.Microsoft.Extensions.DependencyInjection.Generator", + DiagnosticSeverity.Error, + true); } \ No newline at end of file diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/Diagnostics.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/Diagnostics.cs index 5efaa0e..7abd2a3 100644 --- a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/Diagnostics.cs +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/Diagnostics.cs @@ -4,6 +4,73 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Generator; public static class Diagnostics { + public static class Registration + { + public static IEnumerable HasAttribute(INamedTypeSymbol classSymbol) + { + return classSymbol.GetAttributes().Where(IsRegistrationAttribute); + + static bool IsRegistrationAttribute(AttributeData attribute) + { + var attributeClass = attribute.AttributeClass; + + if (attributeClass == null) + return false; + + return attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .StartsWith("global::Futurum.Microsoft.Extensions.DependencyInjection.RegisterAsTransientAttribute") || + attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .StartsWith("global::Futurum.Microsoft.Extensions.DependencyInjection.RegisterAsScopedAttribute") || + attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .StartsWith("global::Futurum.Microsoft.Extensions.DependencyInjection.RegisterAsSingletonAttribute"); + } + } + + public static class ServiceTypeNotImplementedByClass + { + public static IEnumerable Check(INamedTypeSymbol classSymbol, AttributeData attributeData) + { + var serviceType = GetServiceTypeFromAttribute(attributeData); + + if (string.IsNullOrEmpty(serviceType)) + yield break; + + var classImplementsServiceType = classSymbol.AllInterfaces.Any(x => x.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == serviceType); + + if (!classImplementsServiceType) + yield return Diagnostic.Create(DiagnosticDescriptors.RegistrationServiceTypeNotImplementedByClass, + attributeData.ApplicationSyntaxReference?.GetSyntax().GetLocation(), + classSymbol.Name, + serviceType); + } + + private static string? GetServiceTypeFromAttribute(AttributeData attribute) + { + var attributeClass = attribute.AttributeClass; + + string? serviceType = null; + + if (attributeClass?.IsGenericType == true && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length) + { + for (var index = 0; index < attributeClass.TypeParameters.Length; index++) + { + var typeParameter = attributeClass.TypeParameters[index]; + var typeArgument = attributeClass.TypeArguments[index]; + + switch (typeParameter.Name) + { + case "TService": + serviceType = typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + break; + } + } + } + + return serviceType; + } + } + } + public static class Module { public static bool HasAttribute(IMethodSymbol methodSymbol) diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationServiceTypeNotImplementedByClassAnalyzer.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationServiceTypeNotImplementedByClassAnalyzer.cs new file mode 100644 index 0000000..d0adf03 --- /dev/null +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationServiceTypeNotImplementedByClassAnalyzer.cs @@ -0,0 +1,41 @@ +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Futurum.Microsoft.Extensions.DependencyInjection.Generator; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class RegistrationServiceTypeNotImplementedByClassAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } + = ImmutableArray.Create(DiagnosticDescriptors.RegistrationServiceTypeNotImplementedByClass); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSymbolAction(Execute, SymbolKind.NamedType); + } + + private static void Execute(SymbolAnalysisContext context) + { + if (context.Symbol is not INamedTypeSymbol classSymbol) + return; + + var attributes = Diagnostics.Registration.HasAttribute(classSymbol); + if (!attributes.Any()) + return; + + foreach (var attribute in attributes) + { + var diagnostics = Diagnostics.Registration.ServiceTypeNotImplementedByClass.Check(classSymbol, attribute); + + foreach (var diagnostic in diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + } + } +} \ No newline at end of file diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationSourceGenerator.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationSourceGenerator.cs index 00de4db..9fdc4cf 100644 --- a/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationSourceGenerator.cs +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection.Generator/RegistrationSourceGenerator.cs @@ -73,7 +73,7 @@ public static void ExecuteGeneration(SourceProductionContext sourceContext, Immu var (serviceTypes, implementationType, duplicateRegistrationStrategy, interfaceRegistrationStrategy) = GetValuesFromAttribute(attribute); - if (IsInterfaceRegistrationStrategyReallySelfWithInterfaces(interfaceRegistrationStrategy, implementationType, serviceTypes)) + if (IsInterfaceRegistrationStrategySelfWithInterfaces(interfaceRegistrationStrategy, implementationType, serviceTypes)) { interfaceRegistrationStrategy = InterfaceRegistrationStrategy.SelfWithInterfaces; } @@ -107,10 +107,19 @@ private static (HashSet serviceTypes, string? implementationType, DuplicateRegistrationStrategy? duplicateRegistrationStrategy, InterfaceRegistrationStrategy? interfaceRegistrationStrategy) GetValuesFromAttribute(AttributeData attribute) { + var attributeClass = attribute.AttributeClass; + var serviceTypes = new HashSet(); string? implementationType = null; DuplicateRegistrationStrategy? duplicateRegistrationStrategy = null; InterfaceRegistrationStrategy? interfaceRegistrationStrategy = null; + + var genericServiceTypes = GetValuesFromGenericTypes(attribute); + + foreach (var genericServiceType in genericServiceTypes) + { + serviceTypes.Add(genericServiceType); + } foreach (var parameter in attribute.NamedArguments) { @@ -120,26 +129,52 @@ private static if (string.IsNullOrEmpty(name) || value == null) continue; - switch (name) + if (attributeClass?.IsGenericType == false && name == "ServiceType") + { + serviceTypes.Add(value.ToString()); + } + else if (attributeClass?.IsGenericType == false && name == "ImplementationType") + { + implementationType = value.ToString(); + } + else if (name == "Duplicate") { - case "ServiceType": - serviceTypes.Add(value.ToString()); - break; - case "ImplementationType": - implementationType = value.ToString(); - break; - case "Duplicate": - duplicateRegistrationStrategy = ParseEnum(value); - break; - case "Registration": - interfaceRegistrationStrategy = ParseEnum(value); - break; + duplicateRegistrationStrategy = ParseEnum(value); + } + else if (name == "Registration") + { + interfaceRegistrationStrategy = ParseEnum(value); } } return (serviceTypes, implementationType, duplicateRegistrationStrategy, interfaceRegistrationStrategy); } + private static IEnumerable GetValuesFromGenericTypes(AttributeData attribute) + { + var attributeClass = attribute.AttributeClass; + + var serviceTypes = new HashSet(); + + if (attributeClass?.IsGenericType == true && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length) + { + for (var index = 0; index < attributeClass.TypeParameters.Length; index++) + { + var typeParameter = attributeClass.TypeParameters[index]; + var typeArgument = attributeClass.TypeArguments[index]; + + switch (typeParameter.Name) + { + case "TService": + serviceTypes.Add(typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + break; + } + } + } + + return serviceTypes; + } + private static TEnum? ParseEnum(object value) where TEnum : struct, Enum => value switch @@ -149,7 +184,7 @@ private static _ => null }; - private static bool IsInterfaceRegistrationStrategyReallySelfWithInterfaces(InterfaceRegistrationStrategy? interfaceRegistrationStrategy, string? implementationType, IEnumerable serviceTypes) => + private static bool IsInterfaceRegistrationStrategySelfWithInterfaces(InterfaceRegistrationStrategy? interfaceRegistrationStrategy, string? implementationType, IEnumerable serviceTypes) => interfaceRegistrationStrategy == null && implementationType == null && !serviceTypes.Any(); private static bool IsImplementationTypeMissing(string? implementationType) => @@ -182,7 +217,7 @@ private static bool IsRegisterAsAttribute(AttributeData attribute, out Registrat } registrationLifetime = RegistrationLifetime.Transient; - + return false; } diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsScopedAttribute.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsScopedAttribute.cs index 06bc4d0..e8e1306 100644 --- a/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsScopedAttribute.cs +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsScopedAttribute.cs @@ -6,4 +6,12 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class RegisterAsScopedAttribute : RegisterAsAttribute { +} + +/// +/// Register as a scoped in dependency injection +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class RegisterAsScopedAttribute : RegisterAsAttribute +{ } \ No newline at end of file diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsSingletonAttribute.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsSingletonAttribute.cs index 1e4257e..cbf9699 100644 --- a/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsSingletonAttribute.cs +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsSingletonAttribute.cs @@ -6,4 +6,12 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class RegisterAsSingletonAttribute : RegisterAsAttribute { +} + +/// +/// Register as a singleton in dependency injection +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class RegisterAsSingletonAttribute : RegisterAsAttribute +{ } \ No newline at end of file diff --git a/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsTransientAttribute.cs b/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsTransientAttribute.cs index e9d1dec..307e720 100644 --- a/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsTransientAttribute.cs +++ b/src/Futurum.Microsoft.Extensions.DependencyInjection/RegisterAsTransientAttribute.cs @@ -6,4 +6,12 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class RegisterAsTransientAttribute : RegisterAsAttribute { +} + +/// +/// Register as a transient in dependency injection +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class RegisterAsTransientAttribute : RegisterAsAttribute +{ } \ No newline at end of file