diff --git a/README.md b/README.md
index e0e6dd6..ddc105e 100644
--- a/README.md
+++ b/README.md
@@ -170,6 +170,47 @@ public static partial class TypeDiscovery
}
```
+### Use a template expression for each matched type
+
+When `Handler` is not flexible enough — for example when you need to pass extra arguments, call static methods with multiple parameters, or perform `typeof` manipulations — use `HandlerTemplate`.
+The placeholder `T` is replaced with the fully-qualified name of each matched type at code-generation time.
+
+Instantiate types with constructor arguments:
+```csharp
+public static partial class Factory
+{
+ [ScanForTypes(AssignableTo = typeof(IPlugin), HandlerTemplate = "new T(options)")]
+ public static partial IPlugin[] CreatePlugins(PluginOptions options);
+}
+```
+
+Call a static factory method that takes multiple parameters:
+```csharp
+public static partial class PipelineBuilder
+{
+ [ScanForTypes(AssignableTo = typeof(IPipelineStep), HandlerTemplate = "T.Create(context, logger)")]
+ public static partial IPipelineStep[] BuildSteps(PipelineContext context, ILogger logger);
+}
+```
+
+Build descriptor objects using `typeof(T)` and additional context:
+```csharp
+public static partial class HandlerRegistry
+{
+ [ScanForTypes(AssignableTo = typeof(ICommandHandler), HandlerTemplate = "new HandlerDescriptor(typeof(T), category)")]
+ public static partial HandlerDescriptor[] GetDescriptors(string category);
+}
+```
+
+`HandlerTemplate` works equally well with void methods, where each expanded expression becomes a statement:
+```csharp
+public static partial class PluginLoader
+{
+ [ScanForTypes(AssignableTo = typeof(IPlugin), HandlerTemplate = "registry.Add(new T(options))")]
+ public static partial void RegisterPlugins(PluginRegistry registry, PluginOptions options);
+}
+```
+
## Parameters
@@ -195,6 +236,7 @@ public static partial class TypeDiscovery
| Property | Description |
| --- | --- |
| **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). |
+| **HandlerTemplate** | Sets an expression template to evaluate for each type found. Use `T` as a placeholder for the fully-qualified name of each matched type. For collection-returning methods the template is the element expression; for `void` methods it becomes a statement (a `;` is appended automatically if absent). Incompatible with `Handler`. |
| **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 85c7d57..8772f6b 100644
--- a/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs
+++ b/ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs
@@ -1674,4 +1674,179 @@ public class MyService : IService { }
Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.MissingCustomHandlerOnGenerateServiceHandler);
}
+
+ [Fact]
+ public void ScanForTypesAttribute_HandlerTemplate_ReturnsCollection()
+ {
+ var source = """
+ using ServiceScan.SourceGenerator;
+
+ namespace GeneratorTests;
+
+ public static partial class ServicesExtensions
+ {
+ [ScanForTypes(AssignableTo = typeof(IService), HandlerTemplate = "new T(argument)")]
+ public static partial IService[] GetServiceInstances(string argument);
+ }
+ """;
+
+ var services =
+ """
+ namespace GeneratorTests;
+
+ public interface IService { }
+ public class MyService1 : IService { public MyService1(string x) { } }
+ public class MyService2 : IService { public MyService2(string x) { } }
+ """;
+
+ 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::GeneratorTests.IService[] GetServiceInstances( string argument)
+ {
+ return [
+ new global::GeneratorTests.MyService1(argument),
+ new global::GeneratorTests.MyService2(argument)
+ ];
+ }
+ }
+ """;
+ Assert.Equal(expected, results.GeneratedTrees[2].ToString());
+ }
+
+ [Fact]
+ public void ScanForTypesAttribute_HandlerTemplate_VoidMethod()
+ {
+ var source = """
+ using ServiceScan.SourceGenerator;
+
+ namespace GeneratorTests;
+
+ public static partial class ServicesExtensions
+ {
+ [ScanForTypes(AssignableTo = typeof(IService), HandlerTemplate = "registry.Add(new T(argument))")]
+ public static partial void RegisterServices(ServiceRegistry registry, string argument);
+ }
+ """;
+
+ var services =
+ """
+ namespace GeneratorTests;
+
+ public interface IService { }
+ public class MyService1 : IService { public MyService1(string x) { } }
+ public class MyService2 : IService { public MyService2(string x) { } }
+ public class ServiceRegistry { public void Add(IService s) { } }
+ """;
+
+ 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 RegisterServices( global::GeneratorTests.ServiceRegistry registry, string argument)
+ {
+ registry.Add(new global::GeneratorTests.MyService1(argument));
+ registry.Add(new global::GeneratorTests.MyService2(argument));
+ }
+ }
+ """;
+ Assert.Equal(expected, results.GeneratedTrees[2].ToString());
+ }
+
+ [Fact]
+ public void ScanForTypesAttribute_HandlerTemplate_StatementWithSemicolon()
+ {
+ var source = """
+ using ServiceScan.SourceGenerator;
+
+ namespace GeneratorTests;
+
+ public static partial class ServicesExtensions
+ {
+ [ScanForTypes(AssignableTo = typeof(IService), HandlerTemplate = "registry.Add(new T(argument));")]
+ public static partial void RegisterServices(ServiceRegistry registry, string argument);
+ }
+ """;
+
+ var services =
+ """
+ namespace GeneratorTests;
+
+ public interface IService { }
+ public class MyService1 : IService { public MyService1(string x) { } }
+ public class ServiceRegistry { public void Add(IService s) { } }
+ """;
+
+ 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 RegisterServices( global::GeneratorTests.ServiceRegistry registry, string argument)
+ {
+ registry.Add(new global::GeneratorTests.MyService1(argument));
+ }
+ }
+ """;
+ Assert.Equal(expected, results.GeneratedTrees[2].ToString());
+ }
+
+ [Fact]
+ public void ScanForTypesAttribute_BothHandlerAndHandlerTemplate_ReportsDiagnostic()
+ {
+ var source = """
+ using ServiceScan.SourceGenerator;
+
+ namespace GeneratorTests;
+
+ public static partial class ServicesExtensions
+ {
+ [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(GetServiceInfo), HandlerTemplate = "new T()")]
+ public static partial IService[] GetServiceInstances();
+
+ private static IService GetServiceInfo() where T : IService, new() => new T();
+ }
+ """;
+
+ 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.CantUseBothHandlerAndHandlerTemplate);
+ }
}
diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs
index fbd50f6..c866dee 100644
--- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs
+++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using ServiceScan.SourceGenerator.Model;
using static ServiceScan.SourceGenerator.DiagnosticDescriptors;
@@ -13,6 +14,8 @@ public partial class DependencyInjectionGenerator
"System.IAsyncDisposable"
];
+ private static readonly Regex TypePlaceholderRegex = new(@"\bT\b", RegexOptions.Compiled);
+
private static DiagnosticModel FindServicesToRegister((DiagnosticModel, Compilation) context)
{
var (diagnosticModel, compilation) = context;
@@ -40,6 +43,8 @@ private static DiagnosticModel FindServicesToRegister
AddCollectionItems(implementationType, matchedTypes, attribute, method, collectionItems);
else if (attribute.CustomHandler != null)
AddCustomHandlerItems(implementationType, matchedTypes, attribute, customHandlers);
+ else if (attribute.HandlerTemplate != null)
+ AddTemplateStatementItem(implementationType, attribute, customHandlers);
else
{
var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
@@ -106,10 +111,14 @@ private static void AddCollectionItems(
{
var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
- if (attribute.CustomHandler == null)
+ if (attribute.CustomHandler == null && attribute.HandlerTemplate == null)
{
collectionItems.Add($"typeof({implementationTypeName})");
}
+ else if (attribute.HandlerTemplate != null)
+ {
+ collectionItems.Add(ExpandTemplate(attribute.HandlerTemplate, implementationTypeName));
+ }
else
{
var arguments = string.Join(", ", method.Parameters.Select(p => p.Name));
@@ -172,6 +181,28 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F
}
}
+ private static void AddTemplateStatementItem(
+ INamedTypeSymbol implementationType,
+ AttributeModel attribute,
+ List customHandlers)
+ {
+ var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var statement = ExpandTemplate(attribute.HandlerTemplate!, implementationTypeName);
+ if (!statement.EndsWith(";") && !statement.TrimEnd().EndsWith(";"))
+ statement += ";";
+
+ customHandlers.Add(new CustomHandlerModel(
+ Model.CustomHandlerType.Template,
+ statement,
+ implementationTypeName,
+ []));
+ }
+
+ private static string ExpandTemplate(string template, string typeName)
+ {
+ return TypePlaceholderRegex.Replace(template, typeName);
+ }
+
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 99738a3..6d13549 100644
--- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs
+++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs
@@ -129,13 +129,17 @@ public partial class DependencyInjectionGenerator
foreach (var attribute in attributeData)
{
- if (attribute.CustomHandler == null)
+ if (attribute.CustomHandler != null && attribute.HandlerTemplate != null)
{
- // Without a Handler, the method must return Type[] or IEnumerable
+ return Diagnostic.Create(CantUseBothHandlerAndHandlerTemplate, attribute.Location);
+ }
+ else if (attribute.CustomHandler == null && attribute.HandlerTemplate == null)
+ {
+ // Without a Handler or HandlerTemplate, the method must return Type[] or IEnumerable
if (!isTypeCollection)
return Diagnostic.Create(MissingCustomHandlerOnGenerateServiceHandler, attribute.Location);
}
- else
+ else if (attribute.CustomHandler != null)
{
var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position);
diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs
index 36bce65..f5be124 100644
--- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs
+++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs
@@ -158,7 +158,11 @@ private static string GenerateCustomHandlingSource(MethodModel method, Equatable
{
var invocations = string.Join("\n", customHandlers.Select(h =>
{
- if (h.CustomHandlerType == CustomHandlerType.Method)
+ if (h.CustomHandlerType == CustomHandlerType.Template)
+ {
+ return $" {h.HandlerMethodName}";
+ }
+ else if (h.CustomHandlerType == CustomHandlerType.Method)
{
var genericArguments = string.Join(", ", h.TypeArguments);
var arguments = string.Join(", ", method.Parameters.Select(p => p.Name));
diff --git a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs
index 69b00c8..dfe0a5f 100644
--- a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs
+++ b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs
@@ -94,4 +94,11 @@ public static class DiagnosticDescriptors
"Usage",
DiagnosticSeverity.Error,
true);
+
+ public static readonly DiagnosticDescriptor CantUseBothHandlerAndHandlerTemplate = new("DI0016",
+ "Cannot use both Handler and HandlerTemplate",
+ "It is not allowed to use both Handler and HandlerTemplate in the same attribute",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
}
diff --git a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs
index 26f9b8b..da25145 100644
--- a/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs
+++ b/ServiceScan.SourceGenerator/GenerateAttributeInfo.cs
@@ -125,9 +125,22 @@ internal class ScanForTypesAttribute : Attribute
/// 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 .
///
public string? Handler { get; set; }
+ ///
+ /// Sets an expression template to evaluate for each type found.
+ /// Use T as a placeholder for the full name of each found type.
+ /// For void methods (or methods returning their first argument), the template is used as a statement;
+ /// a semicolon is appended automatically if not present.
+ /// For methods returning a collection, the template is used as the expression for each collection element.
+ /// This property is incompatible with .
+ ///
+ /// new T(argument)
+ /// registry.Add(new T(argument))
+ public string? HandlerTemplate { 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.
diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs
index cf38995..6d51878 100644
--- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs
+++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs
@@ -5,7 +5,7 @@
namespace ServiceScan.SourceGenerator.Model;
enum KeySelectorType { Method, GenericMethod, TypeMember };
-enum CustomHandlerType { Method, TypeMethod };
+enum CustomHandlerType { Method, TypeMethod, Template };
record AttributeModel(
string? AssignableToTypeName,
@@ -28,7 +28,8 @@ record AttributeModel(
bool AsImplementedInterfaces,
bool AsSelf,
Location Location,
- bool HasErrors)
+ bool HasErrors,
+ string? HandlerTemplate)
{
public bool HasSearchCriteria => TypeNameFilter != null || AssignableToTypeName != null || AttributeFilterTypeName != null;
@@ -49,6 +50,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
var keySelector = attribute.NamedArguments.FirstOrDefault(a => a.Key == "KeySelector").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 handlerTemplate = attribute.NamedArguments.FirstOrDefault(a => a.Key == "HandlerTemplate").Value.Value as string;
var assignableToTypeParametersCount = assignableTo?.TypeParameters.Length ?? 0;
@@ -86,6 +88,9 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
if (string.IsNullOrWhiteSpace(assemblyNameFilter))
assemblyNameFilter = null;
+ if (string.IsNullOrWhiteSpace(handlerTemplate))
+ handlerTemplate = null;
+
var attributeFilterTypeName = attributeFilterType?.ToFullMetadataName();
var excludeByAttributeTypeName = excludeByAttributeType?.ToFullMetadataName();
var assemblyOfTypeName = assemblyType?.ToFullMetadataName();
@@ -134,6 +139,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
asImplementedInterfaces,
asSelf,
location,
- hasError);
+ hasError,
+ handlerTemplate);
}
}
\ No newline at end of file