Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: <br>- Name of a generic method in the current type. <br>- Static method name in found types. <br>**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. |
Expand Down
175 changes: 175 additions & 0 deletions ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>() 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +14,8 @@ public partial class DependencyInjectionGenerator
"System.IAsyncDisposable"
];

private static readonly Regex TypePlaceholderRegex = new(@"\bT\b", RegexOptions.Compiled);

private static DiagnosticModel<MethodImplementationModel> FindServicesToRegister((DiagnosticModel<MethodWithAttributesModel>, Compilation) context)
{
var (diagnosticModel, compilation) = context;
Expand Down Expand Up @@ -40,6 +43,8 @@ private static DiagnosticModel<MethodImplementationModel> 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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -172,6 +181,28 @@ .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.F
}
}

private static void AddTemplateStatementItem(
INamedTypeSymbol implementationType,
AttributeModel attribute,
List<CustomHandlerModel> 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<INamedTypeSymbol> GetSuitableInterfaces(ITypeSymbol type)
{
return type.AllInterfaces.Where(x => !ExcludedInterfaces.Contains(x.ToDisplayString()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type>
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<Type>
if (!isTypeCollection)
return Diagnostic.Create(MissingCustomHandlerOnGenerateServiceHandler, attribute.Location);
}
else
else if (attribute.CustomHandler != null)
{
var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position);

Expand Down
6 changes: 5 additions & 1 deletion ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
7 changes: 7 additions & 0 deletions ServiceScan.SourceGenerator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,63 @@

public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor NotPartialDefinition = new("DI0001",

Check warning on line 7 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Method is not partial",
"Method with GenerateServiceRegistrations attribute must have partial modifier",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor WrongReturnType = new("DI0002",

Check warning on line 14 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Wrong return type",
"Method with GenerateServiceRegistrations attribute must return void or IServiceCollection",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor WrongMethodParameters = new("DI0003",

Check warning on line 21 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Wrong method parameters",
"Method with GenerateServiceRegistrations attribute must have a single IServiceCollection parameter",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor MissingSearchCriteria = new("DI0004",

Check warning on line 28 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0004' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Missing search criteria",
"GenerateServiceRegistrations must have at least one search criteria",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor NoMatchingTypesFound = new("DI0005",

Check warning on line 35 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0005' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"No matching types found",
"There are no types matching attribute's search criteria",
"Usage",
DiagnosticSeverity.Warning,
true);

public static readonly DiagnosticDescriptor KeySelectorMethodHasIncorrectSignature = new("DI0007",

Check warning on line 42 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0007' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Provided KeySelector method has incorrect signature",
"KeySelector should have non-void return type, and either be generic with no parameters, or non-generic with a single Type parameter",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor CantMixRegularAndCustomHandlerRegistrations = new("DI0008",

Check warning on line 49 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0008' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"It's not allowed to mix GenerateServiceRegistrations attributes with and without CustomHandler on the same method",
"It's not allowed to mix GenerateServiceRegistrations attributes with and without CustomHandler on the same method",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor WrongReturnTypeForCustomHandler = new("DI0009",

Check warning on line 56 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0009' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Wrong return type",
"Method with CustomHandler must return void or the type of its first parameter",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor CustomHandlerMethodHasIncorrectSignature = new("DI0011",

Check warning on line 63 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0011' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Provided CustomHandler method has incorrect signature",
"CustomHandler method must be generic, and must have the same parameters as the method with the attribute",
"Usage",
Expand All @@ -74,7 +74,7 @@
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor MissingCustomHandlerOnGenerateServiceHandler = new("DI0013",

Check warning on line 77 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0013' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Missing Handler",
"ScanForTypesAttribute must have Handler specified",
"Usage",
Expand All @@ -94,4 +94,11 @@
"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);
}
13 changes: 13 additions & 0 deletions ServiceScan.SourceGenerator/GenerateAttributeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="HandlerTemplate"/>.
/// </summary>
public string? Handler { get; set; }

/// <summary>
/// Sets an expression template to evaluate for each type found.
/// Use <c>T</c> 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 <see cref="Handler"/>.
/// </summary>
/// <example>new T(argument)</example>
/// <example>registry.Add(new T(argument))</example>
public string? HandlerTemplate { get; set; }

/// <summary>
/// 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.
Expand Down
Loading
Loading