From d8917bfd3200a43385cc8791868313eb634c4407 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:31:24 +0000 Subject: [PATCH 1/5] Initial plan From 1b28b9326073ccdc91e62102e72ee6ac46c59268 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:43:54 +0000 Subject: [PATCH 2/5] Add GenerateServiceHandlerAttribute and mark CustomHandler as Obsolete on GenerateServiceRegistrationsAttribute Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- README.md | 21 +- .../CustomHandlerTests.cs | 244 ++++++++++++++++++ ...encyInjectionGenerator.ParseMethodModel.cs | 66 +++++ .../DependencyInjectionGenerator.cs | 43 ++- .../DiagnosticDescriptors.cs | 14 + .../GenerateAttributeInfo.cs | 71 +++++ 6 files changed, 442 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4a54718..5d5b459 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ public class HelloWorldEndpoint : IEndpoint public static partial class ServiceCollectionExtensions { - [GenerateServiceRegistrations(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(IEndpoint.MapEndpoint))] + [GenerateServiceHandler(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(IEndpoint.MapEndpoint))] public static partial IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints); } ``` @@ -116,7 +116,7 @@ public record SectionOption { } public static partial class ServiceCollectionExtensions { - [GenerateServiceRegistrations(AttributeFilter = typeof(OptionAttribute), CustomHandler = nameof(AddOption))] + [GenerateServiceHandler(AttributeFilter = typeof(OptionAttribute), CustomHandler = nameof(AddOption))] public static partial IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration); private static void AddOption(IServiceCollection services, IConfiguration configuration) where T : class @@ -133,7 +133,7 @@ public static partial class ServiceCollectionExtensions ```csharp public static partial class ModelBuilderExtensions { - [GenerateServiceRegistrations(AssignableTo = typeof(IEntityTypeConfiguration<>), CustomHandler = nameof(ApplyConfiguration))] + [GenerateServiceHandler(AssignableTo = typeof(IEntityTypeConfiguration<>), CustomHandler = nameof(ApplyConfiguration))] public static partial ModelBuilder ApplyEntityConfigurations(this ModelBuilder modelBuilder); private static void ApplyConfiguration(ModelBuilder modelBuilder) @@ -164,4 +164,17 @@ public static partial class ModelBuilderExtensions | **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.
**Note:** When using a generic `CustomHandler` method, types are automatically filtered by the generic constraints defined on the method's type parameters (e.g., `class`, `struct`, `new()`, interface constraints). | +| **CustomHandler** | *(Obsolete — use `GenerateServiceHandler` instead.)* Sets this property to invoke a custom method for each type found instead of regular registration logic. | + +`GenerateServiceHandler` attribute is used to invoke a custom method for each matched type. It has the same filtering properties as `GenerateServiceRegistrations`, but without the registration-specific ones (`Lifetime`, `AsImplementedInterfaces`, `AsSelf`, `KeySelector`): +| Property | Description | +| --- | --- | +| **CustomHandler** | 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.
- Static method name in found types.
**Note:** Types are automatically filtered by the generic constraints defined on the method's type parameters (e.g., `class`, `struct`, `new()`, interface constraints). | +| **FromAssemblyOf** | Sets the assembly containing the given type as the source of types to scan. 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. This option is incompatible with `FromAssemblyOf`. You can use '*' wildcards. You can also use ',' to separate multiple filters. | +| **AssignableTo** | Sets the type that the scanned types must be assignable to. | +| **ExcludeAssignableTo** | Sets the type that the scanned types must *not* be assignable to. | +| **TypeNameFilter** | Sets this value to filter the types 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 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. | diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index 3fb9e01..356ca51 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1099,4 +1099,248 @@ private static Compilation CreateCompilation(params string[] source) ], new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } + + [Fact] + public void GenerateServiceHandlerAttribute_WithNoParameters() + { + var source = $$""" + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [GenerateServiceHandler(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] + public static partial void ProcessServices(); + + private static void HandleType() => System.Console.WriteLine(typeof(T).Name); + } + """; + + 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 void ProcessServices() + { + HandleType(); + HandleType(); + } + } + """; + Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void GenerateServiceHandlerAttribute_WithParameters() + { + var source = $$""" + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [GenerateServiceHandler(TypeNameFilter = "*Service", CustomHandler = nameof(HandleType))] + public static partial void ProcessServices(string value); + + private static void HandleType(string value) => System.Console.WriteLine(value + typeof(T).Name); + } + """; + + var services = + """ + namespace GeneratorTests; + + public class MyFirstService {} + public class MySecondService {} + public class ServiceWithNonMatchingName {} + """; + + 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 void ProcessServices( string value) + { + HandleType(value); + HandleType(value); + } + } + """; + Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void GenerateServiceHandlerAttribute_MultipleAttributes() + { + var source = $$""" + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [GenerateServiceHandler(AssignableTo = typeof(IFirstService), CustomHandler = nameof(HandleFirstType))] + [GenerateServiceHandler(AssignableTo = typeof(ISecondService), CustomHandler = nameof(HandleSecondType))] + public static partial void ProcessServices(); + + private static void HandleFirstType() => System.Console.WriteLine("First:" + typeof(T).Name); + private static void HandleSecondType() => System.Console.WriteLine("Second:" + typeof(T).Name); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IFirstService { } + public interface ISecondService { } + public class MyService1 : IFirstService { } + public class MyService2 : ISecondService { } + """; + + 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 void ProcessServices() + { + HandleFirstType(); + HandleSecondType(); + } + } + """; + Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void GenerateServiceHandlerAttribute_MissingCustomHandler_ReportsDiagnostic() + { + var source = $$""" + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [GenerateServiceHandler(AssignableTo = typeof(IService))] + public static partial void ProcessServices(); + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.MissingCustomHandlerOnGenerateServiceHandler); + } + + [Fact] + public void GenerateServiceHandlerAttribute_MissingSearchCriteria_ReportsDiagnostic() + { + var source = $$""" + using ServiceScan.SourceGenerator; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [GenerateServiceHandler(CustomHandler = nameof(HandleType))] + public static partial void ProcessServices(); + + private static void HandleType() { } + } + """; + + var compilation = CreateCompilation(source); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.MissingSearchCriteria); + } + + [Fact] + public void MixingGenerateServiceRegistrationsAndGenerateServiceHandler_ReportsDiagnostic() + { + var source = $$""" + using ServiceScan.SourceGenerator; + using Microsoft.Extensions.DependencyInjection; + + namespace GeneratorTests; + + public static partial class ServicesExtensions + { + [GenerateServiceRegistrations(AssignableTo = typeof(IService))] + [GenerateServiceHandler(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] + public static partial IServiceCollection ProcessServices(this IServiceCollection services); + + private static void HandleType() { } + } + """; + + var services = + """ + namespace GeneratorTests; + + public interface IService { } + public class MyService : IService { } + """; + + var compilation = CreateCompilation(source, services); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + Assert.Contains(results.Diagnostics, d => d.Descriptor == DiagnosticDescriptors.CantMixServiceRegistrationsAndServiceHandler); + } } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index 27e774a..adcf20c 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -17,6 +17,13 @@ public partial class DependencyInjectionGenerator if (!method.IsPartialDefinition) return Diagnostic.Create(NotPartialDefinition, method.Locations[0]); + // Check if GenerateServiceHandlerAttribute is also on this method - that's not allowed + var hasServiceHandlerAttribute = method.GetAttributes() + .Any(a => a.AttributeClass?.ToDisplayString() == GenerateAttributeInfo.HandlerMetadataName); + + if (hasServiceHandlerAttribute) + return Diagnostic.Create(CantMixServiceRegistrationsAndServiceHandler, method.Locations[0]); + var position = context.TargetNode.SpanStart; var attributeData = context.Attributes.Select(a => AttributeModel.Create(a, method, context.SemanticModel)).ToArray(); var hasCustomHandlers = attributeData.Any(a => a.CustomHandler != null); @@ -94,4 +101,63 @@ public partial class DependencyInjectionGenerator var model = MethodModel.Create(method, context.TargetNode); return new MethodWithAttributesModel(model, [.. attributeData]); } + + private static DiagnosticModel? ParseHandlerMethodModel(GeneratorAttributeSyntaxContext context) + { + if (context.TargetSymbol is not IMethodSymbol method) + return null; + + if (!method.IsPartialDefinition) + return Diagnostic.Create(NotPartialDefinition, method.Locations[0]); + + // Skip if this method also has GenerateServiceRegistrationsAttribute - that provider reports the mixing error + var hasServiceRegistrationsAttribute = method.GetAttributes() + .Any(a => a.AttributeClass?.ToDisplayString() == GenerateAttributeInfo.MetadataName); + + if (hasServiceRegistrationsAttribute) + return null; + + var position = context.TargetNode.SpanStart; + var attributeData = context.Attributes.Select(a => AttributeModel.Create(a, method, context.SemanticModel)).ToArray(); + + foreach (var attribute in attributeData) + { + if (attribute.CustomHandler == null) + return Diagnostic.Create(MissingCustomHandlerOnGenerateServiceHandler, attribute.Location); + + if (!attribute.HasSearchCriteria) + return Diagnostic.Create(MissingSearchCriteria, attribute.Location); + + if (attribute.AssemblyOfTypeName != null && attribute.AssemblyNameFilter != null) + return Diagnostic.Create(CantUseBothFromAssemblyOfAndAssemblyNameFilter, attribute.Location); + + var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); + + if (customHandlerMethod != null) + { + 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 (attribute.HasErrors) + return null; + } + + if (!method.ReturnsVoid && + (method.Parameters.Length == 0 || !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, method.ReturnType))) + { + return Diagnostic.Create(WrongReturnTypeForCustomHandler, method.Locations[0]); + } + + var model = MethodModel.Create(method, context.TargetNode); + return new MethodWithAttributesModel(model, [.. attributeData]); + } } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs index f2970f4..f47000c 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs @@ -31,23 +31,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (context, ct) => FindServicesToRegister(context)); context.RegisterSourceOutput(methodImplementationsProvider, - static (context, src) => - { - if (src.Diagnostic != null) - context.ReportDiagnostic(src.Diagnostic); + static (context, src) => GenerateSource(context, src)); + + var handlerMethodProvider = context.SyntaxProvider.ForAttributeWithMetadataName( + GenerateAttributeInfo.HandlerMetadataName, + predicate: static (syntaxNode, ct) => syntaxNode is MethodDeclarationSyntax, + transform: static (context, ct) => ParseHandlerMethodModel(context)) + .Where(method => method != null); + + var combinedHandlerProvider = handlerMethodProvider.Combine(context.CompilationProvider) + .WithComparer(CombinedProviderComparer.Instance); + + var handlerImplementationsProvider = combinedHandlerProvider + .Select(static (context, ct) => FindServicesToRegister(context)); + + context.RegisterSourceOutput(handlerImplementationsProvider, + static (context, src) => GenerateSource(context, src)); + } + + private static void GenerateSource(SourceProductionContext context, DiagnosticModel src) + { + if (src.Diagnostic != null) + context.ReportDiagnostic(src.Diagnostic); - if (src.Model == null) - return; + if (src.Model == null) + return; - var (method, registrations, customHandling) = src.Model; - string source = registrations.Count > 0 - ? GenerateRegistrationsSource(method, registrations) - : GenerateCustomHandlingSource(method, customHandling); + var (method, registrations, customHandling) = src.Model; + string source = registrations.Count > 0 + ? GenerateRegistrationsSource(method, registrations) + : GenerateCustomHandlingSource(method, customHandling); - source = source.ReplaceLineEndings(); + source = source.ReplaceLineEndings(); - context.AddSource($"{method.TypeName}_{method.MethodName}.Generated.cs", SourceText.From(source, Encoding.UTF8)); - }); + context.AddSource($"{method.TypeName}_{method.MethodName}.Generated.cs", SourceText.From(source, Encoding.UTF8)); } private static string GenerateRegistrationsSource(MethodModel method, EquatableArray registrations) diff --git a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs index 36bdcef..459c24b 100644 --- a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs +++ b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs @@ -73,4 +73,18 @@ public static class DiagnosticDescriptors "Usage", DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor MissingCustomHandlerOnGenerateServiceHandler = new("DI0013", + "Missing CustomHandler", + "GenerateServiceHandlerAttribute must have CustomHandler specified", + "Usage", + DiagnosticSeverity.Error, + true); + + public static readonly DiagnosticDescriptor CantMixServiceRegistrationsAndServiceHandler = new("DI0014", + "Cannot mix GenerateServiceRegistrationsAttribute and GenerateServiceHandlerAttribute", + "It is not allowed to use both GenerateServiceRegistrationsAttribute and GenerateServiceHandlerAttribute on the same method", + "Usage", + DiagnosticSeverity.Error, + true); } diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs index 9fef6b0..af2f81f 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs @@ -3,6 +3,7 @@ internal static class GenerateAttributeInfo { public const string MetadataName = "ServiceScan.SourceGenerator.GenerateServiceRegistrationsAttribute"; + public const string HandlerMetadataName = "ServiceScan.SourceGenerator.GenerateServiceHandlerAttribute"; public const string Source = """ #nullable enable @@ -110,8 +111,78 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// This property is incompatible with , , , /// and properties. /// + /// This property is obsolete. Use instead. + [Obsolete("Use GenerateServiceHandlerAttribute instead.", error: false)] public string? CustomHandler { get; set; } } + + [Conditional("CODE_ANALYSIS")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + internal class GenerateServiceHandlerAttribute : 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. + /// - Static method name in found types. + /// + public string? CustomHandler { get; set; } + + /// + /// Sets the assembly containing the given type as the source of types to scan. + /// If not specified, the assembly containing the method with this attribute will be used. + /// + public Type? FromAssemblyOf { get; set; } + + /// + /// 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 a limited number of assemblies, as it can affect build and editor performance. + /// My.Product.* + public string? AssemblyNameFilter { get; set; } + + /// + /// Sets the type that the scanned types must be assignable to. + /// + public Type? AssignableTo { get; set; } + + /// + /// Sets the type that the scanned types must *not* be assignable to. + /// + public Type? ExcludeAssignableTo { get; set; } + + /// + /// Sets this value to filter the types by their full name. + /// You can use '*' wildcards. + /// You can also use ',' to separate multiple filters. + /// + /// Namespace.With.Services.* + /// *Service,*Factory + public string? TypeNameFilter { get; set; } + + /// + /// Filters types by the specified attribute type being present. + /// + public Type? AttributeFilter { get; set; } + + /// + /// Sets this value to exclude types by their full name. + /// You can use '*' wildcards. + /// You can also use ',' to separate multiple filters. + /// + /// Namespace.With.Services.* + /// *Service,*Factory + public string? ExcludeByTypeName { get; set; } + + /// + /// Excludes matching types by the specified attribute type being present. + /// + public Type? ExcludeByAttribute { get; set; } + } """; } From f138be06962d9efe5360739dd87072c83ec73951 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:54:08 +0000 Subject: [PATCH 3/5] Bump major version to 3.0 Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index c629a25..824037e 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.4", + "version": "3.0", "publicReleaseRefSpec": [ "^refs/heads/main", "^refs/heads/v\\d+(?:\\.\\d+)?$" From dfd498952ebcad80f2030e5ef50bda4b36859da6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:05:30 +0000 Subject: [PATCH 4/5] Rename GenerateServiceHandlerAttribute to ScanForTypesAttribute, CustomHandler to Handler Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- README.md | 16 ++++++------ .../CustomHandlerTests.cs | 26 +++++++++---------- ...encyInjectionGenerator.ParseMethodModel.cs | 2 +- .../DiagnosticDescriptors.cs | 8 +++--- .../GenerateAttributeInfo.cs | 10 +++---- .../Model/AttributeModel.cs | 3 ++- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 5d5b459..f6d4fdf 100644 --- a/README.md +++ b/README.md @@ -93,14 +93,14 @@ public class HelloWorldEndpoint : IEndpoint public static partial class ServiceCollectionExtensions { - [GenerateServiceHandler(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(IEndpoint.MapEndpoint))] + [ScanForTypes(AssignableTo = typeof(IEndpoint), Handler = nameof(IEndpoint.MapEndpoint))] public static partial IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints); } ``` ### Register Options types -Another example of `CustomHandler` is to register Options types. We can define custom `OptionAttribute`, which allows to specify configuration section key. -And then read that value in our `CustomHandler`: +Another example of `Handler` is to register Options types. We can define custom `OptionAttribute`, which allows to specify configuration section key. +And then read that value in our `Handler`: ```csharp [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class OptionAttribute(string? section = null) : Attribute @@ -116,7 +116,7 @@ public record SectionOption { } public static partial class ServiceCollectionExtensions { - [GenerateServiceHandler(AttributeFilter = typeof(OptionAttribute), CustomHandler = nameof(AddOption))] + [ScanForTypes(AttributeFilter = typeof(OptionAttribute), Handler = nameof(AddOption))] public static partial IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration); private static void AddOption(IServiceCollection services, IConfiguration configuration) where T : class @@ -133,7 +133,7 @@ public static partial class ServiceCollectionExtensions ```csharp public static partial class ModelBuilderExtensions { - [GenerateServiceHandler(AssignableTo = typeof(IEntityTypeConfiguration<>), CustomHandler = nameof(ApplyConfiguration))] + [ScanForTypes(AssignableTo = typeof(IEntityTypeConfiguration<>), Handler = nameof(ApplyConfiguration))] public static partial ModelBuilder ApplyEntityConfigurations(this ModelBuilder modelBuilder); private static void ApplyConfiguration(ModelBuilder modelBuilder) @@ -164,12 +164,12 @@ public static partial class ModelBuilderExtensions | **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** | *(Obsolete — use `GenerateServiceHandler` instead.)* Sets this property to invoke a custom method for each type found instead of regular registration logic. | +| **CustomHandler** | *(Obsolete — use `ScanForTypes` instead.)* Sets this property to invoke a custom method for each type found instead of regular registration logic. | -`GenerateServiceHandler` attribute is used to invoke a custom method for each matched type. It has the same filtering properties as `GenerateServiceRegistrations`, but without the registration-specific ones (`Lifetime`, `AsImplementedInterfaces`, `AsSelf`, `KeySelector`): +`ScanForTypes` attribute is used to invoke a custom method for each matched type. It has the same filtering properties as `GenerateServiceRegistrations`, but without the registration-specific ones (`Lifetime`, `AsImplementedInterfaces`, `AsSelf`, `KeySelector`): | Property | Description | | --- | --- | -| **CustomHandler** | 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.
- Static method name in found types.
**Note:** Types are automatically filtered by the generic constraints defined on the method's type parameters (e.g., `class`, `struct`, `new()`, interface constraints). | +| **Handler** | 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.
- Static method name in found types.
**Note:** Types are automatically filtered by the generic constraints defined on the method's type parameters (e.g., `class`, `struct`, `new()`, interface constraints). | | **FromAssemblyOf** | Sets the assembly containing the given type as the source of types to scan. 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. This option is incompatible with `FromAssemblyOf`. You can use '*' wildcards. You can also use ',' to separate multiple filters. | | **AssignableTo** | Sets the type that the scanned types must be assignable to. | diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index 356ca51..d002109 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1101,7 +1101,7 @@ private static Compilation CreateCompilation(params string[] source) } [Fact] - public void GenerateServiceHandlerAttribute_WithNoParameters() + public void ScanForTypesAttribute_WithNoParameters() { var source = $$""" using ServiceScan.SourceGenerator; @@ -1110,7 +1110,7 @@ namespace GeneratorTests; public static partial class ServicesExtensions { - [GenerateServiceHandler(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(HandleType))] public static partial void ProcessServices(); private static void HandleType() => System.Console.WriteLine(typeof(T).Name); @@ -1149,7 +1149,7 @@ public static partial void ProcessServices() } [Fact] - public void GenerateServiceHandlerAttribute_WithParameters() + public void ScanForTypesAttribute_WithParameters() { var source = $$""" using ServiceScan.SourceGenerator; @@ -1158,7 +1158,7 @@ namespace GeneratorTests; public static partial class ServicesExtensions { - [GenerateServiceHandler(TypeNameFilter = "*Service", CustomHandler = nameof(HandleType))] + [ScanForTypes(TypeNameFilter = "*Service", Handler = nameof(HandleType))] public static partial void ProcessServices(string value); private static void HandleType(string value) => System.Console.WriteLine(value + typeof(T).Name); @@ -1197,7 +1197,7 @@ public static partial void ProcessServices( string value) } [Fact] - public void GenerateServiceHandlerAttribute_MultipleAttributes() + public void ScanForTypesAttribute_MultipleAttributes() { var source = $$""" using ServiceScan.SourceGenerator; @@ -1206,8 +1206,8 @@ namespace GeneratorTests; public static partial class ServicesExtensions { - [GenerateServiceHandler(AssignableTo = typeof(IFirstService), CustomHandler = nameof(HandleFirstType))] - [GenerateServiceHandler(AssignableTo = typeof(ISecondService), CustomHandler = nameof(HandleSecondType))] + [ScanForTypes(AssignableTo = typeof(IFirstService), Handler = nameof(HandleFirstType))] + [ScanForTypes(AssignableTo = typeof(ISecondService), Handler = nameof(HandleSecondType))] public static partial void ProcessServices(); private static void HandleFirstType() => System.Console.WriteLine("First:" + typeof(T).Name); @@ -1248,7 +1248,7 @@ public static partial void ProcessServices() } [Fact] - public void GenerateServiceHandlerAttribute_MissingCustomHandler_ReportsDiagnostic() + public void ScanForTypesAttribute_MissingHandler_ReportsDiagnostic() { var source = $$""" using ServiceScan.SourceGenerator; @@ -1257,7 +1257,7 @@ namespace GeneratorTests; public static partial class ServicesExtensions { - [GenerateServiceHandler(AssignableTo = typeof(IService))] + [ScanForTypes(AssignableTo = typeof(IService))] public static partial void ProcessServices(); } """; @@ -1281,7 +1281,7 @@ public class MyService : IService { } } [Fact] - public void GenerateServiceHandlerAttribute_MissingSearchCriteria_ReportsDiagnostic() + public void ScanForTypesAttribute_MissingSearchCriteria_ReportsDiagnostic() { var source = $$""" using ServiceScan.SourceGenerator; @@ -1290,7 +1290,7 @@ namespace GeneratorTests; public static partial class ServicesExtensions { - [GenerateServiceHandler(CustomHandler = nameof(HandleType))] + [ScanForTypes(Handler = nameof(HandleType))] public static partial void ProcessServices(); private static void HandleType() { } @@ -1308,7 +1308,7 @@ private static void HandleType() { } } [Fact] - public void MixingGenerateServiceRegistrationsAndGenerateServiceHandler_ReportsDiagnostic() + public void MixingGenerateServiceRegistrationsAndScanForTypes_ReportsDiagnostic() { var source = $$""" using ServiceScan.SourceGenerator; @@ -1319,7 +1319,7 @@ namespace GeneratorTests; public static partial class ServicesExtensions { [GenerateServiceRegistrations(AssignableTo = typeof(IService))] - [GenerateServiceHandler(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] + [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(HandleType))] public static partial IServiceCollection ProcessServices(this IServiceCollection services); private static void HandleType() { } diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index adcf20c..c699244 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -17,7 +17,7 @@ public partial class DependencyInjectionGenerator if (!method.IsPartialDefinition) return Diagnostic.Create(NotPartialDefinition, method.Locations[0]); - // Check if GenerateServiceHandlerAttribute is also on this method - that's not allowed + // Check if ScanForTypesAttribute is also on this method - that's not allowed var hasServiceHandlerAttribute = method.GetAttributes() .Any(a => a.AttributeClass?.ToDisplayString() == GenerateAttributeInfo.HandlerMetadataName); diff --git a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs index 459c24b..54c9d78 100644 --- a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs +++ b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs @@ -75,15 +75,15 @@ public static class DiagnosticDescriptors true); public static readonly DiagnosticDescriptor MissingCustomHandlerOnGenerateServiceHandler = new("DI0013", - "Missing CustomHandler", - "GenerateServiceHandlerAttribute must have CustomHandler specified", + "Missing Handler", + "ScanForTypesAttribute must have Handler specified", "Usage", DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor CantMixServiceRegistrationsAndServiceHandler = new("DI0014", - "Cannot mix GenerateServiceRegistrationsAttribute and GenerateServiceHandlerAttribute", - "It is not allowed to use both GenerateServiceRegistrationsAttribute and GenerateServiceHandlerAttribute on the same method", + "Cannot mix GenerateServiceRegistrationsAttribute and ScanForTypesAttribute", + "It is not allowed to use both GenerateServiceRegistrationsAttribute and ScanForTypesAttribute on the same method", "Usage", DiagnosticSeverity.Error, true); diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs index af2f81f..edf452b 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs @@ -3,7 +3,7 @@ internal static class GenerateAttributeInfo { public const string MetadataName = "ServiceScan.SourceGenerator.GenerateServiceRegistrationsAttribute"; - public const string HandlerMetadataName = "ServiceScan.SourceGenerator.GenerateServiceHandlerAttribute"; + public const string HandlerMetadataName = "ServiceScan.SourceGenerator.ScanForTypesAttribute"; public const string Source = """ #nullable enable @@ -111,14 +111,14 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// This property is incompatible with , , , /// and properties. /// - /// This property is obsolete. Use instead. - [Obsolete("Use GenerateServiceHandlerAttribute instead.", error: false)] + /// This property is obsolete. Use instead. + [Obsolete("Use ScanForTypesAttribute instead.", error: false)] public string? CustomHandler { get; set; } } [Conditional("CODE_ANALYSIS")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - internal class GenerateServiceHandlerAttribute : Attribute + internal class ScanForTypesAttribute : Attribute { /// /// Sets this property to invoke a custom method for each type found. @@ -126,7 +126,7 @@ internal class GenerateServiceHandlerAttribute : Attribute /// - Name of a generic method in the current type. /// - Static method name in found types. /// - public string? CustomHandler { get; set; } + public string? Handler { get; set; } /// /// Sets the assembly containing the given type as the source of types to scan. diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index 60d8b83..cf38995 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -47,7 +47,8 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho var excludeByTypeName = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByTypeName").Value.Value as string; var excludeAssignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeAssignableTo").Value.Value as INamedTypeSymbol; var keySelector = attribute.NamedArguments.FirstOrDefault(a => a.Key == "KeySelector").Value.Value as string; - var customHandler = attribute.NamedArguments.FirstOrDefault(a => a.Key == "CustomHandler").Value.Value as string; + var customHandler = (attribute.NamedArguments.FirstOrDefault(a => a.Key == "Handler").Value.Value + ?? attribute.NamedArguments.FirstOrDefault(a => a.Key == "CustomHandler").Value.Value) as string; var assignableToTypeParametersCount = assignableTo?.TypeParameters.Length ?? 0; From 2291b64b348525308bc3972f51de3f9b3ce1c3df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:21:11 +0000 Subject: [PATCH 5/5] Merge main and fix ScanForTypesAttribute to use [Embedded] instead of [Conditional] Co-authored-by: Dreamescaper <17177729+Dreamescaper@users.noreply.github.com> --- ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs | 6 +++--- ServiceScan.SourceGenerator/GenerateAttributeInfo.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs index 0194817..4a027dd 100644 --- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs +++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs @@ -1145,7 +1145,7 @@ public static partial void ProcessServices() } } """; - Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + Assert.Equal(expected, results.GeneratedTrees[2].ToString()); } [Fact] @@ -1193,7 +1193,7 @@ public static partial void ProcessServices( string value) } } """; - Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + Assert.Equal(expected, results.GeneratedTrees[2].ToString()); } [Fact] @@ -1244,7 +1244,7 @@ public static partial void ProcessServices() } } """; - Assert.Equal(expected, results.GeneratedTrees[1].ToString()); + Assert.Equal(expected, results.GeneratedTrees[2].ToString()); } [Fact] diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs index e77d076..26f9b8b 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs @@ -116,7 +116,7 @@ internal class GenerateServiceRegistrationsAttribute : Attribute public string? CustomHandler { get; set; } } - [Conditional("CODE_ANALYSIS")] + [Embedded] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] internal class ScanForTypesAttribute : Attribute {