From 9c26319ac18a0154a111a4b12025fee849afa00d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:21:22 +0000 Subject: [PATCH 1/4] Initial plan From 7bff22d6d9e982e5cff4fbcdf9679d845246a5c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:28:10 +0000 Subject: [PATCH 2/4] Support external ScanForTypes handler methods Agent-Logs-Url: https://github.com/Dreamescaper/ServiceScan.SourceGenerator/sessions/0756f7e6-b7ae-4089-a255-d98eb893f68a Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- .../CustomHandlerTests.cs | 97 ++++++++++++++++++- .../TestServices.cs | 8 +- ...ependencyInjectionGenerator.FilterTypes.cs | 4 +- ...jectionGenerator.FindServicesToRegister.cs | 18 +++- ...encyInjectionGenerator.ParseMethodModel.cs | 19 +++- .../DependencyInjectionGenerator.cs | 3 +- .../GenerateAttributeInfo.cs | 2 +- .../Model/AttributeModel.cs | 41 +++++++- .../Model/ServiceRegistrationModel.cs | 2 +- 9 files changed, 181 insertions(+), 13 deletions(-) diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index fbf3545..5a434fd 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1554,6 +1554,101 @@ public static partial class ServicesExtensions await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); } + [Test] + public async Task ScanForTypesAttribute_ReturnsCollection_WithExternalHandler() + { + var source = """ + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(External.ExternalHandlers.GetServiceName))] + public static partial string[] GetServiceNames(); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService1 : IService { } + public class MyService2 : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = """ + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + public static partial string[] GetServiceNames() + { + return [ + global::External.ExternalHandlers.GetServiceName(), + global::External.ExternalHandlers.GetServiceName() + ]; + } + } + """; + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + + [Test] + public async Task ScanForTypesAttribute_WithExternalHandlerAndMatchedGenericArguments() + { + var source = """ + using Microsoft.Extensions.DependencyInjection; + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(ICommandHandler<>), Handler = nameof(External.ExternalHandlers.Register))] + public static partial IServiceCollection RegisterHandlers(this IServiceCollection services); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface ICommandHandler { } + public class MyCommand { } + public class MyCommandHandler : ICommandHandler { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = """ + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection RegisterHandlers(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + global::External.ExternalHandlers.Register(services); + return services; + } + } + """; + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + [Test] public async Task ScanForTypesAttribute_ReturnsTypeArray_MultipleAttributes() { @@ -1849,4 +1944,4 @@ public class MyService : IService { } await Assert.That(DiagnosticDescriptors.CantUseBothHandlerAndHandlerTemplate).IsEqualTo(results.Diagnostics.Single().Descriptor); } -} \ No newline at end of file +} diff --git a/ServiceScan.SourceGenerator.Tests/TestServices.cs b/ServiceScan.SourceGenerator.Tests/TestServices.cs index 4987319..16d3eb7 100644 --- a/ServiceScan.SourceGenerator.Tests/TestServices.cs +++ b/ServiceScan.SourceGenerator.Tests/TestServices.cs @@ -3,6 +3,12 @@ public interface IExternalService; public class ExternalService1 : IExternalService { } public class ExternalService2 : IExternalService { } +public static class ExternalHandlers +{ + public static string GetServiceName() => typeof(T).Name; + + public static void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { } +} // Shouldn't be added as type is not accessible from other assembly -internal class InternalExternalService2 : IExternalService { } \ No newline at end of file +internal class InternalExternalService2 : IExternalService { } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index 678df2c..6cb9b8a 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; +using ServiceScan.SourceGenerator.Extensions; using ServiceScan.SourceGenerator.Model; namespace ServiceScan.SourceGenerator; @@ -50,7 +51,8 @@ public partial class DependencyInjectionGenerator } var customHandlerMethod = attribute.CustomHandler != null && attribute.CustomHandlerType == CustomHandlerType.Method - ? containingType.GetMembers().OfType().FirstOrDefault(m => m.Name == attribute.CustomHandler) + ? containingType.GetMethod(attribute.CustomHandler, semanticModel, position) + ?? GetExternalCustomHandlerMethod(attribute, compilation, semanticModel, position) : null; foreach (var type in assemblies.SelectMany(GetTypesFromAssembly)) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs index c866dee..3ce6e5a 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs @@ -131,7 +131,7 @@ private static void AddCollectionItems( .Concat(matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))); if (attribute.CustomHandlerType == CustomHandlerType.Method) - collectionItems.Add($"{attribute.CustomHandler}<{typeArguments}>({arguments})"); + collectionItems.Add(FormatCustomHandlerInvocation(attribute.CustomHandlerDeclaringTypeName, attribute.CustomHandler, typeArguments, arguments)); else collectionItems.Add($"{implementationTypeName}.{attribute.CustomHandler}({arguments})"); } @@ -139,7 +139,7 @@ private static void AddCollectionItems( else { if (attribute.CustomHandlerType == CustomHandlerType.Method) - collectionItems.Add($"{attribute.CustomHandler}<{implementationTypeName}>({arguments})"); + collectionItems.Add(FormatCustomHandlerInvocation(attribute.CustomHandlerDeclaringTypeName, attribute.CustomHandler, implementationTypeName, arguments)); else collectionItems.Add($"{implementationTypeName}.{attribute.CustomHandler}({arguments})"); } @@ -167,7 +167,9 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F customHandlers.Add(new CustomHandlerModel( attribute.CustomHandlerType.Value, attribute.CustomHandler, - implementationTypeName, + attribute.CustomHandlerType == CustomHandlerType.Method + ? attribute.CustomHandlerDeclaringTypeName + : implementationTypeName, typeArguments)); } } @@ -176,7 +178,9 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F customHandlers.Add(new CustomHandlerModel( attribute.CustomHandlerType.Value, attribute.CustomHandler, - implementationTypeName, + attribute.CustomHandlerType == CustomHandlerType.Method + ? attribute.CustomHandlerDeclaringTypeName + : implementationTypeName, [implementationTypeName])); } } @@ -203,6 +207,12 @@ private static string ExpandTemplate(string template, string typeName) return TypePlaceholderRegex.Replace(template, typeName); } + private static string FormatCustomHandlerInvocation(string? typeName, string handlerName, string typeArguments, string arguments) + { + var target = typeName is null ? "" : $"{typeName}."; + return $"{target}{handlerName}<{typeArguments}>({arguments})"; + } + private static IEnumerable GetSuitableInterfaces(ITypeSymbol type) { return type.AllInterfaces.Where(x => !ExcludedInterfaces.Contains(x.ToDisplayString())); diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index 6d13549..01e0e9b 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -58,7 +58,8 @@ public partial class DependencyInjectionGenerator if (attribute.CustomHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position) + ?? GetExternalCustomHandlerMethod(attribute, context.SemanticModel.Compilation, context.SemanticModel, position); if (customHandlerMethod != null) { @@ -141,7 +142,8 @@ public partial class DependencyInjectionGenerator } else if (attribute.CustomHandler != null) { - var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position) + ?? GetExternalCustomHandlerMethod(attribute, context.SemanticModel.Compilation, context.SemanticModel, position); if (customHandlerMethod != null) { @@ -185,4 +187,17 @@ public partial class DependencyInjectionGenerator var model = MethodModel.Create(method, context.TargetNode); return new MethodWithAttributesModel(model, [.. attributeData]); } + + private static IMethodSymbol? GetExternalCustomHandlerMethod( + AttributeModel attribute, + Compilation compilation, + SemanticModel semanticModel, + int position) + { + var handlerType = attribute.CustomHandlerDeclaringTypeMetadataName is null + ? null + : compilation.GetTypeByMetadataName(attribute.CustomHandlerDeclaringTypeMetadataName); + + return handlerType?.GetMethod(attribute.CustomHandler!, semanticModel, position, isStatic: true); + } } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs index f5be124..729e6d9 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs @@ -166,7 +166,8 @@ private static string GenerateCustomHandlingSource(MethodModel method, Equatable { var genericArguments = string.Join(", ", h.TypeArguments); var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); - return $" {h.HandlerMethodName}<{genericArguments}>({arguments});"; + var target = h.TypeName is null ? "" : $"{h.TypeName}."; + return $" {target}{h.HandlerMethodName}<{genericArguments}>({arguments});"; } else { diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs index da25145..07765b9 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs @@ -124,6 +124,7 @@ internal class ScanForTypesAttribute : Attribute /// Sets this property to invoke a custom method for each type found. /// This property should point to one of the following: /// - Name of a generic method in the current type. + /// - nameof(OtherType.Method) for a static generic method on another type. /// - Static method name in found types. /// This property is incompatible with . /// @@ -198,4 +199,3 @@ internal class ScanForTypesAttribute : Attribute } """; } - diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index 6d51878..a72106e 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -1,5 +1,6 @@ using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using ServiceScan.SourceGenerator.Extensions; namespace ServiceScan.SourceGenerator.Model; @@ -25,6 +26,8 @@ record AttributeModel( string? CustomHandler, CustomHandlerType? CustomHandlerType, int CustomHandlerMethodTypeParametersCount, + string? CustomHandlerDeclaringTypeMetadataName, + string? CustomHandlerDeclaringTypeName, bool AsImplementedInterfaces, bool AsSelf, Location Location, @@ -71,10 +74,20 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho CustomHandlerType? customHandlerType = null; var customHandlerGenericParameters = 0; + string? customHandlerDeclaringTypeMetadataName = null; + string? customHandlerDeclaringTypeName = null; if (customHandler != null) { + var explicitHandlerType = GetExplicitHandlerDeclaringType(attribute, semanticModel, "Handler", "CustomHandler"); var customHandlerMethod = method.ContainingType.GetMethod(customHandler, semanticModel, position); + if (customHandlerMethod == null && explicitHandlerType != null) + { + customHandlerMethod = explicitHandlerType.GetMethod(customHandler, semanticModel, position, isStatic: true); + customHandlerDeclaringTypeMetadataName = explicitHandlerType.ToFullMetadataName(); + customHandlerDeclaringTypeName = explicitHandlerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + customHandlerType = customHandlerMethod != null ? Model.CustomHandlerType.Method : Model.CustomHandlerType.TypeMethod; customHandlerGenericParameters = customHandlerMethod?.TypeParameters.Length ?? 0; } @@ -136,10 +149,36 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho customHandler, customHandlerType, customHandlerGenericParameters, + customHandlerDeclaringTypeMetadataName, + customHandlerDeclaringTypeName, asImplementedInterfaces, asSelf, location, hasError, handlerTemplate); } -} \ No newline at end of file + + private static INamedTypeSymbol? GetExplicitHandlerDeclaringType(AttributeData attribute, SemanticModel semanticModel, params string[] argumentNames) + { + if (attribute.ApplicationSyntaxReference?.GetSyntax() is not AttributeSyntax attributeSyntax) + return null; + + var handlerArgument = attributeSyntax.ArgumentList?.Arguments + .FirstOrDefault(a => a.NameEquals?.Name.Identifier.ValueText is { } name && argumentNames.Contains(name)); + + if (handlerArgument?.Expression is not InvocationExpressionSyntax + { + Expression: IdentifierNameSyntax { Identifier.ValueText: "nameof" }, + ArgumentList.Arguments: [{ Expression: MemberAccessExpressionSyntax memberAccessExpression }] + }) + { + return null; + } + + var symbol = semanticModel.GetSymbolInfo(memberAccessExpression.Expression).Symbol; + if (symbol is IAliasSymbol aliasSymbol) + symbol = aliasSymbol.Target; + + return symbol as INamedTypeSymbol; + } +} diff --git a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs index 973a34a..c52e0a1 100644 --- a/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs +++ b/ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs @@ -12,5 +12,5 @@ record ServiceRegistrationModel( record CustomHandlerModel( CustomHandlerType CustomHandlerType, string HandlerMethodName, - string TypeName, + string? TypeName, EquatableArray TypeArguments); From 04360308f7836429d828890c282b78a9901a7c17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:30:48 +0000 Subject: [PATCH 3/4] Address validation feedback Agent-Logs-Url: https://github.com/Dreamescaper/ServiceScan.SourceGenerator/sessions/0756f7e6-b7ae-4089-a255-d98eb893f68a Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- ...DependencyInjectionGenerator.FindServicesToRegister.cs | 8 ++++++-- .../DependencyInjectionGenerator.ParseMethodModel.cs | 4 ++++ ServiceScan.SourceGenerator/Model/AttributeModel.cs | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs index 3ce6e5a..b14d437 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs @@ -207,10 +207,14 @@ private static string ExpandTemplate(string template, string typeName) return TypePlaceholderRegex.Replace(template, typeName); } - private static string FormatCustomHandlerInvocation(string? typeName, string handlerName, string typeArguments, string arguments) + /// + /// Formats a custom handler invocation, prefixing the method with a type name when the handler is declared on + /// another type. + /// + private static string FormatCustomHandlerInvocation(string? typeName, string methodName, string typeArguments, string arguments) { var target = typeName is null ? "" : $"{typeName}."; - return $"{target}{handlerName}<{typeArguments}>({arguments})"; + return $"{target}{methodName}<{typeArguments}>({arguments})"; } private static IEnumerable GetSuitableInterfaces(ITypeSymbol type) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index 01e0e9b..bfde66a 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -188,6 +188,10 @@ public partial class DependencyInjectionGenerator return new MethodWithAttributesModel(model, [.. attributeData]); } + /// + /// Resolves a static custom handler method declared on another type using the metadata name captured from + /// nameof(Type.Method). + /// private static IMethodSymbol? GetExternalCustomHandlerMethod( AttributeModel attribute, Compilation compilation, diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index a72106e..daf599f 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -158,6 +158,13 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho handlerTemplate); } + /// + /// Extracts the declaring type from a nameof(Type.Method) attribute argument. + /// + /// The attribute containing the handler argument. + /// The semantic model used to resolve the type symbol. + /// The supported attribute argument names to inspect. + /// The declaring type when the handler is specified as nameof(Type.Method); otherwise, . private static INamedTypeSymbol? GetExplicitHandlerDeclaringType(AttributeData attribute, SemanticModel semanticModel, params string[] argumentNames) { if (attribute.ApplicationSyntaxReference?.GetSyntax() is not AttributeSyntax attributeSyntax) From 36898c8b3a35dc93aba7855da489120bd5f8c6b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:32:50 +0000 Subject: [PATCH 4/4] Add alias coverage for external handlers Agent-Logs-Url: https://github.com/Dreamescaper/ServiceScan.SourceGenerator/sessions/0756f7e6-b7ae-4089-a255-d98eb893f68a Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- .../CustomHandlerTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index 5a434fd..5b8cade 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1602,6 +1602,53 @@ public static partial string[] GetServiceNames() await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); } + [Test] + public async Task ScanForTypesAttribute_ReturnsCollection_WithAliasedExternalHandler() + { + var source = """ + using ServiceScan.SourceGenerator; + using Handlers = External.ExternalHandlers; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(Handlers.GetServiceName))] + public static partial string[] GetServiceNames(); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService1 : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var expected = """ + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + public static partial string[] GetServiceNames() + { + return [ + global::External.ExternalHandlers.GetServiceName() + ]; + } + } + """; + await Assert.That(results.GeneratedTrees[2].ToString()).IsEqualTo(expected); + } + [Test] public async Task ScanForTypesAttribute_WithExternalHandlerAndMatchedGenericArguments() {