diff --git a/README.md b/README.md index 695584d..3a0c62f 100644 --- a/README.md +++ b/README.md @@ -93,13 +93,8 @@ public class HelloWorldEndpoint : IEndpoint public static partial class ServiceCollectionExtensions { - [GenerateServiceRegistrations(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(MapEndpoint))] + [GenerateServiceRegistrations(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(IEndpoint.MapEndpoint))] public static partial IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints); - - private static void MapEndpoint(IEndpointRouteBuilder endpoints) where T : IEndpoint - { - T.MapEndpoint(endpoints); - } } ``` @@ -157,16 +152,16 @@ public static partial class ModelBuilderExtensions `GenerateServiceRegistrations` attribute has the following properties: | 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. | -| **AssemblyNameFilter** | Set this value to filter scanned assemblies by assembly name. It allows to apply an attribute to multiple assemblies. For example, this allows to scan all assemblies from your solution. You can use '\*' wildcards. You can also use ',' to separate multiple filters. *Be careful to include limited amount of assemblies, as it can affect build and editor performance.* | -| **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. | -| **AsSelf** | If true, types will be registered with their actual type. It can be combined with `AsImplementedInterfaces`. In that case, implemented 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. | -| **AttributeFilter** | Filter types by the specified attribute type present. | -| **ExcludeByTypeName** | Set this value to exclude types from being registered by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | -| **ExcludeByAttribute** | Exclude matching types by the specified attribute type present. | -| **ExcludeAssignableTo** | Set the type that the registered types must not be assignable to. | -| **KeySelector** | Set this property to add types as keyed services. This property should point to one of the following:
- Name of the static method in the current type with a string return type. The method should be either generic or have a single parameter of type `Type`.
- Const field or static property in the implementation type. | -| **CustomHandler** | Set this property to a static generic method name in the current class. This method will be invoked for each type found by the filter instead of the regular registration logic. This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, and `KeySelector` properties. | \ No newline at end of file +| **FromAssemblyOf** | Sets 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. | +| **AssemblyNameFilter** | Sets this value to filter scanned assemblies by assembly name. It allows applying an attribute to multiple assemblies. For example, this allows scanning all assemblies from your solution. This option is incompatible with `FromAssemblyOf`. You can use '*' wildcards. You can also use ',' to separate multiple filters. *Be careful to include a limited number of assemblies, as it can affect build and editor performance.* | +| **AssignableTo** | Sets 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. | +| **ExcludeAssignableTo** | Sets the type that the registered types must *not* be assignable to. | +| **Lifetime** | Sets the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. | +| **AsImplementedInterfaces** | If set to true, types will be registered as their implemented interfaces instead of their actual type. | +| **AsSelf** | If set to true, types will be registered with their actual type. It can be combined with `AsImplementedInterfaces`. In this case, implemented interfaces will be "forwarded" to the "self" implementation. | +| **TypeNameFilter** | Sets this value to filter the types to register by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | +| **AttributeFilter** | Filters types by the specified attribute type being present. | +| **ExcludeByTypeName** | Sets this value to exclude types from being registered by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | +| **ExcludeByAttribute** | Excludes matching types by the specified attribute type being present. | +| **KeySelector** | Sets this property to add types as keyed services. This property should point to one of the following:
- The name of a static method in the current type with a string return type. The method should be either generic or have a single parameter of type `Type`.
- A constant field or static property in the implementation type. | +| **CustomHandler** | Sets this property to invoke a custom method for each type found instead of regular registration logic. This property should point to one of the following:
- Name of a generic method in the current type.
- Static method name in found types.
This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, and `KeySelector` properties. | \ No newline at end of file diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index 17a94db..81f70aa 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -445,6 +445,113 @@ public partial void ProcessServices() Assert.Equal(expected, results.GeneratedTrees[1].ToString()); } + [Fact] + public void UseStaticMethodFromMatchedClassAsCustomHandler_WithoutParameters() + { + var source = $$""" + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public partial class ServicesExtensions + { + [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = "Handler"))] + public partial void ProcessServices(); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + + public class MyService1 : IService + { + public static void Handler() { } + } + + public class MyService2 : IService + { + public static void Handler() { } + } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = $$""" + namespace GeneratorTests; + + public partial class ServicesExtensions + { + public partial void ProcessServices() + { + global::GeneratorTests.MyService1.Handler(); + global::GeneratorTests.MyService2.Handler(); + } + } + """; + Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void UseStaticMethodFromMatchedStaticClassAsCustomHandler_WithParameters() + { + var source = $$""" + using ServiceScan.SourceGenerator; + using Microsoft.Extensions.DependencyInjection; + + namespace GeneratorTests; + + public partial class ServicesExtensions + { + [GenerateServiceRegistrations(TypeNameFilter = "*StaticService", CustomHandler = "Handler"))] + public partial void ProcessServices(IServiceCollection services); + } + """; + + var services = + """ + namespace GeneratorTests; + + public static class FirstStaticService + { + public static void Handler(IServiceCollection services) { } + } + + public static class SecondStaticService + { + public static void Handler(IServiceCollection services) { } + } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = $$""" + namespace GeneratorTests; + + public partial class ServicesExtensions + { + public partial void ProcessServices( global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + global::GeneratorTests.FirstStaticService.Handler(services); + global::GeneratorTests.SecondStaticService.Handler(services); + } + } + """; + Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + } + private static Compilation CreateCompilation(params string[] source) { var path = Path.GetDirectoryName(typeof(object).Assembly.Location)!; diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index 2543d77..33f8ac3 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -51,7 +51,11 @@ public partial class DependencyInjectionGenerator foreach (var type in assemblies.SelectMany(GetTypesFromAssembly)) { - if (type.IsAbstract || type.IsStatic || !type.CanBeReferencedByName || type.TypeKind != TypeKind.Class) + if (type.IsAbstract || !type.CanBeReferencedByName || type.TypeKind != TypeKind.Class) + continue; + + // Static types are allowed for custom handlers (with type method) + if (type.IsStatic && attribute.CustomHandlerType != CustomHandlerType.TypeMethod) continue; if (attributeFilterType != null) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs index 8f37d5a..3adfe1f 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs @@ -37,24 +37,34 @@ private static DiagnosticModel FindServicesToRegister if (attribute.CustomHandler != null) { + var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + // If CustomHandler method has multiple type parameters, which are resolvable from the first one - we try to provide them. // e.g. ApplyConfiguration(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration - if (attribute.CustomHandlerTypeParametersCount > 1 && matchedTypes != null) + if (attribute.CustomHandlerMethodTypeParametersCount > 1 && matchedTypes != null) { foreach (var matchedType in matchedTypes) { EquatableArray typeArguments = [ - implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + implementationTypeName, .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) ]; - customHandlers.Add(new CustomHandlerModel(attribute.CustomHandler, typeArguments)); + customHandlers.Add(new CustomHandlerModel( + attribute.CustomHandlerType.Value, + attribute.CustomHandler, + implementationTypeName, + typeArguments)); } } else { - customHandlers.Add(new CustomHandlerModel(attribute.CustomHandler, [implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)])); + customHandlers.Add(new CustomHandlerModel( + attribute.CustomHandlerType.Value, + attribute.CustomHandler, + implementationTypeName, + [implementationTypeName])); } } else diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index 3ef68de..18b2628 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -56,27 +56,27 @@ public partial class DependencyInjectionGenerator var customHandlerMethod = method.ContainingType.GetMembers().OfType() .FirstOrDefault(m => m.Name == attribute.CustomHandler); - if (customHandlerMethod is null) - return Diagnostic.Create(CustomHandlerMethodNotFound, attribute.Location); - - if (!customHandlerMethod.IsGenericMethod) - return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); - - var typesMatch = Enumerable.SequenceEqual( - method.Parameters.Select(p => p.Type), - customHandlerMethod.Parameters.Select(p => p.Type), - SymbolEqualityComparer.Default); - - if (!typesMatch) - return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); - - // If CustomHandler has more than 1 type parameters, we try to resolve them from - // matched assignableTo type arguments. - // e.g. ApplyConfiguration(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration - if (customHandlerMethod.TypeParameters.Length > 1 - && customHandlerMethod.TypeParameters.Length != attribute.AssignableToTypeParametersCount + 1) + if (customHandlerMethod != null) { - return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); + if (!customHandlerMethod.IsGenericMethod) + return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); + + var typesMatch = Enumerable.SequenceEqual( + method.Parameters.Select(p => p.Type), + customHandlerMethod.Parameters.Select(p => p.Type), + SymbolEqualityComparer.Default); + + if (!typesMatch) + return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); + + // If CustomHandler has more than 1 type parameters, we try to resolve them from + // matched assignableTo type arguments. + // e.g. ApplyConfiguration(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration + if (customHandlerMethod.TypeParameters.Length > 1 + && customHandlerMethod.TypeParameters.Length != attribute.AssignableToTypeParametersCount + 1) + { + return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); + } } } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs index f966606..7c0ea16 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs @@ -109,9 +109,17 @@ private static string GenerateCustomHandlingSource(MethodModel method, Equatable { var invocations = string.Join("\n", customHandlers.Select(h => { - var genericArguments = string.Join(", ", h.TypeArguments); - var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); - return $" {h.HandlerMethodName}<{genericArguments}>({arguments});"; + if (h.CustomHandlerType == CustomHandlerType.Method) + { + var genericArguments = string.Join(", ", h.TypeArguments); + var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); + return $" {h.HandlerMethodName}<{genericArguments}>({arguments});"; + } + else + { + var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); + return $" {h.TypeName}.{h.HandlerMethodName}({arguments});"; + } })); var namespaceDeclaration = method.Namespace is null ? "" : $"namespace {method.Namespace};"; diff --git a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs index fa41055..36bdcef 100644 --- a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs +++ b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs @@ -60,13 +60,6 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Error, true); - public static readonly DiagnosticDescriptor CustomHandlerMethodNotFound = new("DI0012", - "Provided CustomHandler method is not found", - "CustomHandler parameter should point to a method in the class", - "Usage", - DiagnosticSeverity.Error, - true); - public static readonly DiagnosticDescriptor CustomHandlerMethodHasIncorrectSignature = new("DI0011", "Provided CustomHandler method has incorrect signature", "CustomHandler method must be generic, and must have the same parameters as the method with the attribute", diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs index 0d7a719..9fef6b0 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs @@ -18,36 +18,36 @@ namespace ServiceScan.SourceGenerator; internal class GenerateServiceRegistrationsAttribute : Attribute { /// - /// Set the assembly containing the given type as the source of types to register. + /// Sets 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. /// public Type? FromAssemblyOf { get; set; } /// - /// Set this value to filter scanned assemblies by assembly name. - /// It allows to apply an attribute to multiple assemblies. - /// For example, this allows to scan all assemblies from your solution. + /// Sets this value to filter scanned assemblies by assembly name. + /// It allows applying an attribute to multiple assemblies. + /// For example, this allows scanning all assemblies from your solution. /// This option is incompatible with . /// You can use '*' wildcards. You can also use ',' to separate multiple filters. /// - /// Be careful to include limited amount of assemblies, as it can affect build and editor performance. + /// Be careful to include a limited number of assemblies, as it can affect build and editor performance. /// My.Product.* public string? AssemblyNameFilter { get; set; } /// - /// Set the type that the registered types must be assignable to. + /// Sets the type that the registered types must be assignable to. /// Types will be registered with this type as the service type, /// unless or is set. /// public Type? AssignableTo { get; set; } /// - /// Set the type that the registered types must *not* be assignable to. + /// Sets the type that the registered types must *not* be assignable to. /// public Type? ExcludeAssignableTo { get; set; } /// - /// Set the lifetime of the registered services. + /// Sets the lifetime of the registered services. /// is used if not specified. /// public ServiceLifetime Lifetime { get; set; } @@ -65,7 +65,7 @@ internal class GenerateServiceRegistrationsAttribute : Attribute public bool AsSelf { get; set; } /// - /// Set this value to filter the types to register by their full name. + /// Sets this value to filter the types to register by their full name. /// You can use '*' wildcards. /// You can also use ',' to separate multiple filters. /// @@ -74,12 +74,12 @@ internal class GenerateServiceRegistrationsAttribute : Attribute public string? TypeNameFilter { get; set; } /// - /// Filter types by the specified attribute type present. + /// Filters types by the specified attribute type being present. /// public Type? AttributeFilter { get; set; } /// - /// Set this value to exclude types from being registered by their full name. + /// Sets this value to exclude types from being registered by their full name. /// You can use '*' wildcards. /// You can also use ',' to separate multiple filters. /// @@ -88,12 +88,12 @@ internal class GenerateServiceRegistrationsAttribute : Attribute public string? ExcludeByTypeName { get; set; } /// - /// Exclude matching types by the specified attribute type present. + /// Excludes matching types by the specified attribute type being present. /// public Type? ExcludeByAttribute { get; set; } /// - /// Set this property to add types as keyed services. + /// Sets this property to add types as keyed services. /// This property should point to one of the following: /// - The name of a static method in the current type with a string return type. /// The method should be either generic or have a single parameter of type . @@ -103,8 +103,10 @@ internal class GenerateServiceRegistrationsAttribute : Attribute public string? KeySelector { get; set; } /// - /// Set this property to a static generic method name in the current class. - /// This method will be invoked for each type found by the filter instead of the regular registration logic. + /// Sets this property to invoke a custom method for each type found instead of regular registration logic. + /// This property should point to one of the following: + /// - Name of a generic method in the current type. + /// - Static method name in found types. /// This property is incompatible with , , , /// and properties. /// diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index 1188374..5816ed8 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -4,6 +4,7 @@ namespace ServiceScan.SourceGenerator.Model; enum KeySelectorType { Method, GenericMethod, TypeMember }; +enum CustomHandlerType { Method, TypeMethod }; record AttributeModel( string? AssignableToTypeName, @@ -21,7 +22,8 @@ record AttributeModel( string? KeySelector, KeySelectorType? KeySelectorType, string? CustomHandler, - int CustomHandlerTypeParametersCount, + CustomHandlerType? CustomHandlerType, + int CustomHandlerMethodTypeParametersCount, bool AsImplementedInterfaces, bool AsSelf, Location Location, @@ -63,13 +65,15 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho } } + CustomHandlerType? customHandlerType = null; var customHandlerGenericParameters = 0; if (customHandler != null) { var customHandlerMethod = method.ContainingType.GetMembers() .OfType() - .FirstOrDefault(m => m.IsStatic && m.Name == customHandler); + .FirstOrDefault(m => m.Name == customHandler); + customHandlerType = customHandlerMethod != null ? Model.CustomHandlerType.Method : Model.CustomHandlerType.TypeMethod; customHandlerGenericParameters = customHandlerMethod?.TypeParameters.Length ?? 0; } @@ -125,6 +129,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho keySelector, keySelectorType, customHandler, + customHandlerType, customHandlerGenericParameters, asImplementedInterfaces, asSelf, diff --git a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs index 09aa36e..973a34a 100644 --- a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs +++ b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs @@ -10,5 +10,7 @@ record ServiceRegistrationModel( KeySelectorType? KeySelectorType); record CustomHandlerModel( + CustomHandlerType CustomHandlerType, string HandlerMethodName, + string TypeName, EquatableArray TypeArguments); diff --git a/version.json b/version.json index 2edee38..6ed9d64 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "2.2", + "version": "2.3", "publicReleaseRefSpec": [ "^refs/heads/main", "^refs/heads/v\\d+(?:\\.\\d+)?$"