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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ public static partial class ServiceCollectionExtensions
`GenerateServiceRegistrations` attribute has the following properties:
| Property | Description |
| --- | --- |
| **FromAssemblyOf** | Set the assembly containing the given type as the source of types to register. If not specified, all referenced projects are scanned.|
| **FromAssemblyOf** | Set the assembly containing the given type as the source of types to register. If not specified, the assembly containing the method with this attribute will be used. |
| **AssemblyNameFilter** | Set this value to filter scanned assemblies by assembly name. It allows to apply an attribute to multiple assemblies. For example, this allows to scan all assemblies from your solution. You can use '\*' wildcards. You can also use ',' to separate multiple filters. *Be careful to include limited amount of assemblies, as it can affect build and editor performance.* |
| **AssignableTo** | Set the type that the registered types must be assignable to. Types will be registered with this type as the service type, unless `AsImplementedInterfaces` or `AsSelf` is set. |
| **Lifetime** | Set the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. |
| **AsImplementedInterfaces** | If true, the registered types will be registered as implemented interfaces instead of their actual type. |
Expand Down
10 changes: 5 additions & 5 deletions ServiceScan.SourceGenerator.Tests/AddServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,30 @@ public void AddServicesFromAnotherAssembly()
}

[Fact]
public void AddServicesFromReferencedCompilationsByDefault()
public void AddServicesFromReferencedAssembliesByAssemblyNameFilter()
{
var coreCompilation = CreateCompilation(
"""
namespace Core;
public interface IService { }
""")
.WithAssemblyName("Core");
.WithAssemblyName("MyProduct.Core");

var implementation1Compilation = CreateCompilation(["""
namespace Module1;
public class MyService1 : Core.IService { }
"""],
[coreCompilation])
.WithAssemblyName("Module1");
.WithAssemblyName("MyProduct.Module1");

var implementation2Compilation = CreateCompilation(["""
namespace Module2;
public class MyService2 : Core.IService { }
"""],
[coreCompilation])
.WithAssemblyName("Module2");
.WithAssemblyName("MyProduct.Module2");

var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(Core.IService), Lifetime = ServiceLifetime.Scoped)]";
var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(Core.IService), Lifetime = ServiceLifetime.Scoped, AssemblyNameFilter="MyProduct.*")]""";
var registrationsCompilation = CreateCompilation(
[Sources.MethodWithAttribute(attribute)],
[coreCompilation, implementation1Compilation, implementation2Compilation]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
Expand All @@ -12,13 +13,7 @@ public partial class DependencyInjectionGenerator
private static IEnumerable<(INamedTypeSymbol Type, INamedTypeSymbol? MatchedAssignableType)> FilterTypes
(Compilation compilation, AttributeModel attribute, INamedTypeSymbol containingType)
{
var assemblyOfType = attribute.AssemblyOfTypeName is null
? null
: compilation.GetTypeByMetadataName(attribute.AssemblyOfTypeName);

var assemblies = assemblyOfType is not null
? [assemblyOfType.ContainingAssembly]
: GetSolutionAssemblies(compilation);
var assemblies = GetAssembliesToScan(compilation, attribute, containingType);

var assignableToType = attribute.AssignableToTypeName is null
? null
Expand Down Expand Up @@ -139,6 +134,29 @@ private static bool IsAssignableTo(INamedTypeSymbol type, INamedTypeSymbol assig
return false;
}

private static IEnumerable<IAssemblySymbol> GetAssembliesToScan(Compilation compilation, AttributeModel attribute, INamedTypeSymbol containingType)
{
var assemblyOfType = attribute.AssemblyOfTypeName is null
? null
: compilation.GetTypeByMetadataName(attribute.AssemblyOfTypeName);

if (assemblyOfType is not null)
{
return [assemblyOfType.ContainingAssembly];
}

if (attribute.AssemblyNameFilter is not null)
{
var assemblyNameRegex = BuildWildcardRegex(attribute.AssemblyNameFilter);

return new[] { compilation.Assembly }
.Concat(compilation.SourceModule.ReferencedAssemblySymbols)
.Where(assembly => assemblyNameRegex.IsMatch(assembly.Name));
}

return [containingType.ContainingAssembly];
}

private static IEnumerable<IAssemblySymbol> GetSolutionAssemblies(Compilation compilation)
{
yield return compilation.Assembly;
Expand Down Expand Up @@ -177,6 +195,7 @@ static IEnumerable<INamedTypeSymbol> GetTypesFromNamespaceOrType(INamespaceOrTyp
}
}

[return: NotNullIfNotNull(nameof(wildcard))]
private static Regex? BuildWildcardRegex(string? wildcard)
{
return wildcard is null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public partial class DependencyInjectionGenerator
if (hasCustomHandlers && attribute.CustomHandler == null)
return Diagnostic.Create(CantMixRegularAndCustomHandlerRegistrations, attribute.Location);

if (attribute.AssemblyOfTypeName != null && attribute.AssemblyNameFilter != null)
return Diagnostic.Create(CantUseBothFromAssemblyOfAndAssemblyNameFilter, attribute.Location);

if (attribute.KeySelector != null)
{
var keySelectorMethod = method.ContainingType.GetMembers().OfType<IMethodSymbol>()
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,56 +4,56 @@

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",
Expand All @@ -67,10 +67,17 @@
DiagnosticSeverity.Error,
true);

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

Check warning on line 70 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",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor CantUseBothFromAssemblyOfAndAssemblyNameFilter = new("DI0012",

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 'DI0012' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Only one assembly selection criteria allowed",
"It is not allowed to use both FromAssemblyOf and AssemblyNameFilter in the same attribute",
"Usage",
DiagnosticSeverity.Error,
true);
}
13 changes: 12 additions & 1 deletion ServiceScan.SourceGenerator/GenerateAttributeSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,21 @@ internal class GenerateServiceRegistrationsAttribute : Attribute
{
/// <summary>
/// Set the assembly containing the given type as the source of types to register.
/// If not specified, all referenced projects are scanned.
/// If not specified, the assembly containing the method with this attribute will be used.
/// </summary>
public Type? FromAssemblyOf { get; set; }

/// <summary>
/// Set this value to filter scanned assemblies by assembly name.
/// It allows to apply an attribute to multiple assemblies.
/// For example, this allows to scan all assemblies from your solution.
/// This option is incompatible with <see cref="FromAssemblyOf"/>.
/// You can use '*' wildcards. You can also use ',' to separate multiple filters.
/// </summary>
/// <remarks>Be careful to include limited amount of assemblies, as it can affect build and editor performance.</remarks>
/// <example>My.Product.*</example>
public string? AssemblyNameFilter { get; set; }

/// <summary>
/// Set the type that the registered types must be assignable to.
/// Types will be registered with this type as the service type,
Expand Down
6 changes: 6 additions & 0 deletions ServiceScan.SourceGenerator/Model/AttributeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ enum KeySelectorType { Method, GenericMethod, TypeMember };

record AttributeModel(
string? AssignableToTypeName,
string? AssemblyNameFilter,
EquatableArray<string>? AssignableToGenericArguments,
string? AssemblyOfTypeName,
string Lifetime,
Expand All @@ -29,6 +30,7 @@ record AttributeModel(
public static AttributeModel Create(AttributeData attribute, IMethodSymbol method)
{
var assemblyType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "FromAssemblyOf").Value.Value as INamedTypeSymbol;
var assemblyNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssemblyNameFilter").Value.Value as string;
var assignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssignableTo").Value.Value as INamedTypeSymbol;
var asImplementedInterfaces = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsImplementedInterfaces").Value.Value is true;
var asSelf = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsSelf").Value.Value is true;
Expand Down Expand Up @@ -63,6 +65,9 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
if (string.IsNullOrWhiteSpace(excludeByTypeName))
excludeByTypeName = null;

if (string.IsNullOrWhiteSpace(assemblyNameFilter))
assemblyNameFilter = null;

var attributeFilterTypeName = attributeFilterType?.ToFullMetadataName();
var excludeByAttributeTypeName = excludeByAttributeType?.ToFullMetadataName();
var assemblyOfTypeName = assemblyType?.ToFullMetadataName();
Expand Down Expand Up @@ -92,6 +97,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho

return new(
assignableToTypeName,
assemblyNameFilter,
assignableToGenericArguments,
assemblyOfTypeName,
lifetime,
Expand Down