Skip to content

Commit 9fadc54

Browse files
authored
Allow CustomHandler to invoke static methods from matched types (#41)
1 parent 832a415 commit 9fadc54

11 files changed

+198
-72
lines changed

README.md

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,8 @@ public class HelloWorldEndpoint : IEndpoint
9393

9494
public static partial class ServiceCollectionExtensions
9595
{
96-
[GenerateServiceRegistrations(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(MapEndpoint))]
96+
[GenerateServiceRegistrations(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(IEndpoint.MapEndpoint))]
9797
public static partial IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints);
98-
99-
private static void MapEndpoint<T>(IEndpointRouteBuilder endpoints) where T : IEndpoint
100-
{
101-
T.MapEndpoint(endpoints);
102-
}
10398
}
10499
```
105100

@@ -157,16 +152,16 @@ public static partial class ModelBuilderExtensions
157152
`GenerateServiceRegistrations` attribute has the following properties:
158153
| Property | Description |
159154
| --- | --- |
160-
| **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. |
161-
| **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.* |
162-
| **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. |
163-
| **Lifetime** | Set the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. |
164-
| **AsImplementedInterfaces** | If true, the registered types will be registered as implemented interfaces instead of their actual type. |
165-
| **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. |
166-
| **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. |
167-
| **AttributeFilter** | Filter types by the specified attribute type present. |
168-
| **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. |
169-
| **ExcludeByAttribute** | Exclude matching types by the specified attribute type present. |
170-
| **ExcludeAssignableTo** | Set the type that the registered types must not be assignable to. |
171-
| **KeySelector** | Set this property to add types as keyed services. This property should point to one of the following: <br>- 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`. <br>- Const field or static property in the implementation type. |
172-
| **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. |
155+
| **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. |
156+
| **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.* |
157+
| **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. |
158+
| **ExcludeAssignableTo** | Sets the type that the registered types must *not* be assignable to. |
159+
| **Lifetime** | Sets the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. |
160+
| **AsImplementedInterfaces** | If set to true, types will be registered as their implemented interfaces instead of their actual type. |
161+
| **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. |
162+
| **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. |
163+
| **AttributeFilter** | Filters types by the specified attribute type being present. |
164+
| **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. |
165+
| **ExcludeByAttribute** | Excludes matching types by the specified attribute type being present. |
166+
| **KeySelector** | Sets this property to add types as keyed services. This property should point to one of the following: <br>- 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`. <br>- A constant field or static property in the implementation type. |
167+
| **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: <br>- Name of a generic method in the current type. <br>- Static method name in found types. <br>This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, and `KeySelector` properties. |

ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,113 @@ public partial void ProcessServices()
445445
Assert.Equal(expected, results.GeneratedTrees[1].ToString());
446446
}
447447

448+
[Fact]
449+
public void UseStaticMethodFromMatchedClassAsCustomHandler_WithoutParameters()
450+
{
451+
var source = $$"""
452+
using ServiceScan.SourceGenerator;
453+
454+
namespace GeneratorTests;
455+
456+
public partial class ServicesExtensions
457+
{
458+
[GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = "Handler"))]
459+
public partial void ProcessServices();
460+
}
461+
""";
462+
463+
var services =
464+
"""
465+
namespace GeneratorTests;
466+
467+
public interface IService { }
468+
469+
public class MyService1 : IService
470+
{
471+
public static void Handler() { }
472+
}
473+
474+
public class MyService2 : IService
475+
{
476+
public static void Handler() { }
477+
}
478+
""";
479+
480+
var compilation = CreateCompilation(source, services);
481+
482+
var results = CSharpGeneratorDriver
483+
.Create(_generator)
484+
.RunGenerators(compilation)
485+
.GetRunResult();
486+
487+
var expected = $$"""
488+
namespace GeneratorTests;
489+
490+
public partial class ServicesExtensions
491+
{
492+
public partial void ProcessServices()
493+
{
494+
global::GeneratorTests.MyService1.Handler();
495+
global::GeneratorTests.MyService2.Handler();
496+
}
497+
}
498+
""";
499+
Assert.Equal(expected, results.GeneratedTrees[1].ToString());
500+
}
501+
502+
[Fact]
503+
public void UseStaticMethodFromMatchedStaticClassAsCustomHandler_WithParameters()
504+
{
505+
var source = $$"""
506+
using ServiceScan.SourceGenerator;
507+
using Microsoft.Extensions.DependencyInjection;
508+
509+
namespace GeneratorTests;
510+
511+
public partial class ServicesExtensions
512+
{
513+
[GenerateServiceRegistrations(TypeNameFilter = "*StaticService", CustomHandler = "Handler"))]
514+
public partial void ProcessServices(IServiceCollection services);
515+
}
516+
""";
517+
518+
var services =
519+
"""
520+
namespace GeneratorTests;
521+
522+
public static class FirstStaticService
523+
{
524+
public static void Handler(IServiceCollection services) { }
525+
}
526+
527+
public static class SecondStaticService
528+
{
529+
public static void Handler(IServiceCollection services) { }
530+
}
531+
""";
532+
533+
var compilation = CreateCompilation(source, services);
534+
535+
var results = CSharpGeneratorDriver
536+
.Create(_generator)
537+
.RunGenerators(compilation)
538+
.GetRunResult();
539+
540+
var expected = $$"""
541+
namespace GeneratorTests;
542+
543+
public partial class ServicesExtensions
544+
{
545+
public partial void ProcessServices( global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)
546+
{
547+
global::GeneratorTests.FirstStaticService.Handler(services);
548+
global::GeneratorTests.SecondStaticService.Handler(services);
549+
}
550+
}
551+
""";
552+
Assert.Equal(expected, results.GeneratedTrees[1].ToString());
553+
}
554+
448555
private static Compilation CreateCompilation(params string[] source)
449556
{
450557
var path = Path.GetDirectoryName(typeof(object).Assembly.Location)!;

ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ public partial class DependencyInjectionGenerator
5151

5252
foreach (var type in assemblies.SelectMany(GetTypesFromAssembly))
5353
{
54-
if (type.IsAbstract || type.IsStatic || !type.CanBeReferencedByName || type.TypeKind != TypeKind.Class)
54+
if (type.IsAbstract || !type.CanBeReferencedByName || type.TypeKind != TypeKind.Class)
55+
continue;
56+
57+
// Static types are allowed for custom handlers (with type method)
58+
if (type.IsStatic && attribute.CustomHandlerType != CustomHandlerType.TypeMethod)
5559
continue;
5660

5761
if (attributeFilterType != null)

ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,34 @@ private static DiagnosticModel<MethodImplementationModel> FindServicesToRegister
3737

3838
if (attribute.CustomHandler != null)
3939
{
40+
var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
41+
4042
// If CustomHandler method has multiple type parameters, which are resolvable from the first one - we try to provide them.
4143
// e.g. ApplyConfiguration<T, TEntity>(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration<TEntity>
42-
if (attribute.CustomHandlerTypeParametersCount > 1 && matchedTypes != null)
44+
if (attribute.CustomHandlerMethodTypeParametersCount > 1 && matchedTypes != null)
4345
{
4446
foreach (var matchedType in matchedTypes)
4547
{
4648
EquatableArray<string> typeArguments =
4749
[
48-
implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
50+
implementationTypeName,
4951
.. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
5052
];
5153

52-
customHandlers.Add(new CustomHandlerModel(attribute.CustomHandler, typeArguments));
54+
customHandlers.Add(new CustomHandlerModel(
55+
attribute.CustomHandlerType.Value,
56+
attribute.CustomHandler,
57+
implementationTypeName,
58+
typeArguments));
5359
}
5460
}
5561
else
5662
{
57-
customHandlers.Add(new CustomHandlerModel(attribute.CustomHandler, [implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)]));
63+
customHandlers.Add(new CustomHandlerModel(
64+
attribute.CustomHandlerType.Value,
65+
attribute.CustomHandler,
66+
implementationTypeName,
67+
[implementationTypeName]));
5868
}
5969
}
6070
else

ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,27 @@ public partial class DependencyInjectionGenerator
5656
var customHandlerMethod = method.ContainingType.GetMembers().OfType<IMethodSymbol>()
5757
.FirstOrDefault(m => m.Name == attribute.CustomHandler);
5858

59-
if (customHandlerMethod is null)
60-
return Diagnostic.Create(CustomHandlerMethodNotFound, attribute.Location);
61-
62-
if (!customHandlerMethod.IsGenericMethod)
63-
return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location);
64-
65-
var typesMatch = Enumerable.SequenceEqual(
66-
method.Parameters.Select(p => p.Type),
67-
customHandlerMethod.Parameters.Select(p => p.Type),
68-
SymbolEqualityComparer.Default);
69-
70-
if (!typesMatch)
71-
return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location);
72-
73-
// If CustomHandler has more than 1 type parameters, we try to resolve them from
74-
// matched assignableTo type arguments.
75-
// e.g. ApplyConfiguration<T, TEntity>(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration<TEntity>
76-
if (customHandlerMethod.TypeParameters.Length > 1
77-
&& customHandlerMethod.TypeParameters.Length != attribute.AssignableToTypeParametersCount + 1)
59+
if (customHandlerMethod != null)
7860
{
79-
return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location);
61+
if (!customHandlerMethod.IsGenericMethod)
62+
return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location);
63+
64+
var typesMatch = Enumerable.SequenceEqual(
65+
method.Parameters.Select(p => p.Type),
66+
customHandlerMethod.Parameters.Select(p => p.Type),
67+
SymbolEqualityComparer.Default);
68+
69+
if (!typesMatch)
70+
return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location);
71+
72+
// If CustomHandler has more than 1 type parameters, we try to resolve them from
73+
// matched assignableTo type arguments.
74+
// e.g. ApplyConfiguration<T, TEntity>(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration<TEntity>
75+
if (customHandlerMethod.TypeParameters.Length > 1
76+
&& customHandlerMethod.TypeParameters.Length != attribute.AssignableToTypeParametersCount + 1)
77+
{
78+
return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location);
79+
}
8080
}
8181
}
8282

ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,17 @@ private static string GenerateCustomHandlingSource(MethodModel method, Equatable
109109
{
110110
var invocations = string.Join("\n", customHandlers.Select(h =>
111111
{
112-
var genericArguments = string.Join(", ", h.TypeArguments);
113-
var arguments = string.Join(", ", method.Parameters.Select(p => p.Name));
114-
return $" {h.HandlerMethodName}<{genericArguments}>({arguments});";
112+
if (h.CustomHandlerType == CustomHandlerType.Method)
113+
{
114+
var genericArguments = string.Join(", ", h.TypeArguments);
115+
var arguments = string.Join(", ", method.Parameters.Select(p => p.Name));
116+
return $" {h.HandlerMethodName}<{genericArguments}>({arguments});";
117+
}
118+
else
119+
{
120+
var arguments = string.Join(", ", method.Parameters.Select(p => p.Name));
121+
return $" {h.TypeName}.{h.HandlerMethodName}({arguments});";
122+
}
115123
}));
116124

117125
var namespaceDeclaration = method.Namespace is null ? "" : $"namespace {method.Namespace};";

ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,6 @@ public static class DiagnosticDescriptors
6060
DiagnosticSeverity.Error,
6161
true);
6262

63-
public static readonly DiagnosticDescriptor CustomHandlerMethodNotFound = new("DI0012",
64-
"Provided CustomHandler method is not found",
65-
"CustomHandler parameter should point to a method in the class",
66-
"Usage",
67-
DiagnosticSeverity.Error,
68-
true);
69-
7063
public static readonly DiagnosticDescriptor CustomHandlerMethodHasIncorrectSignature = new("DI0011",
7164
"Provided CustomHandler method has incorrect signature",
7265
"CustomHandler method must be generic, and must have the same parameters as the method with the attribute",

0 commit comments

Comments
 (0)