Skip to content

Commit 7b6c502

Browse files
authored
Add AssemblyNameFilter to allow to scan multiple assemblies (#34)
* AssemblyNameFilter * Update README * Do not allow FromAssemblyOf and AssemblyNameFilter in the same attribute * simplify ReferencedAssemblySymbols
1 parent 4b57130 commit 7b6c502

File tree

7 files changed

+61
-14
lines changed

7 files changed

+61
-14
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ public static partial class ServiceCollectionExtensions
139139
`GenerateServiceRegistrations` attribute has the following properties:
140140
| Property | Description |
141141
| --- | --- |
142-
| **FromAssemblyOf** | Set the assembly containing the given type as the source of types to register. If not specified, all referenced projects are scanned.|
142+
| **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. |
143+
| **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.* |
143144
| **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. |
144145
| **Lifetime** | Set the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. |
145146
| **AsImplementedInterfaces** | If true, the registered types will be registered as implemented interfaces instead of their actual type. |

ServiceScan.SourceGenerator.Tests/AddServicesTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,30 @@ public void AddServicesFromAnotherAssembly()
6060
}
6161

6262
[Fact]
63-
public void AddServicesFromReferencedCompilationsByDefault()
63+
public void AddServicesFromReferencedAssembliesByAssemblyNameFilter()
6464
{
6565
var coreCompilation = CreateCompilation(
6666
"""
6767
namespace Core;
6868
public interface IService { }
6969
""")
70-
.WithAssemblyName("Core");
70+
.WithAssemblyName("MyProduct.Core");
7171

7272
var implementation1Compilation = CreateCompilation(["""
7373
namespace Module1;
7474
public class MyService1 : Core.IService { }
7575
"""],
7676
[coreCompilation])
77-
.WithAssemblyName("Module1");
77+
.WithAssemblyName("MyProduct.Module1");
7878

7979
var implementation2Compilation = CreateCompilation(["""
8080
namespace Module2;
8181
public class MyService2 : Core.IService { }
8282
"""],
8383
[coreCompilation])
84-
.WithAssemblyName("Module2");
84+
.WithAssemblyName("MyProduct.Module2");
8585

86-
var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(Core.IService), Lifetime = ServiceLifetime.Scoped)]";
86+
var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(Core.IService), Lifetime = ServiceLifetime.Scoped, AssemblyNameFilter="MyProduct.*")]""";
8787
var registrationsCompilation = CreateCompilation(
8888
[Sources.MethodWithAttribute(attribute)],
8989
[coreCompilation, implementation1Compilation, implementation2Compilation]);

ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45
using System.Text.RegularExpressions;
56
using Microsoft.CodeAnalysis;
@@ -12,13 +13,7 @@ public partial class DependencyInjectionGenerator
1213
private static IEnumerable<(INamedTypeSymbol Type, INamedTypeSymbol? MatchedAssignableType)> FilterTypes
1314
(Compilation compilation, AttributeModel attribute, INamedTypeSymbol containingType)
1415
{
15-
var assemblyOfType = attribute.AssemblyOfTypeName is null
16-
? null
17-
: compilation.GetTypeByMetadataName(attribute.AssemblyOfTypeName);
18-
19-
var assemblies = assemblyOfType is not null
20-
? [assemblyOfType.ContainingAssembly]
21-
: GetSolutionAssemblies(compilation);
16+
var assemblies = GetAssembliesToScan(compilation, attribute, containingType);
2217

2318
var assignableToType = attribute.AssignableToTypeName is null
2419
? null
@@ -139,6 +134,29 @@ private static bool IsAssignableTo(INamedTypeSymbol type, INamedTypeSymbol assig
139134
return false;
140135
}
141136

137+
private static IEnumerable<IAssemblySymbol> GetAssembliesToScan(Compilation compilation, AttributeModel attribute, INamedTypeSymbol containingType)
138+
{
139+
var assemblyOfType = attribute.AssemblyOfTypeName is null
140+
? null
141+
: compilation.GetTypeByMetadataName(attribute.AssemblyOfTypeName);
142+
143+
if (assemblyOfType is not null)
144+
{
145+
return [assemblyOfType.ContainingAssembly];
146+
}
147+
148+
if (attribute.AssemblyNameFilter is not null)
149+
{
150+
var assemblyNameRegex = BuildWildcardRegex(attribute.AssemblyNameFilter);
151+
152+
return new[] { compilation.Assembly }
153+
.Concat(compilation.SourceModule.ReferencedAssemblySymbols)
154+
.Where(assembly => assemblyNameRegex.IsMatch(assembly.Name));
155+
}
156+
157+
return [containingType.ContainingAssembly];
158+
}
159+
142160
private static IEnumerable<IAssemblySymbol> GetSolutionAssemblies(Compilation compilation)
143161
{
144162
yield return compilation.Assembly;
@@ -177,6 +195,7 @@ static IEnumerable<INamedTypeSymbol> GetTypesFromNamespaceOrType(INamespaceOrTyp
177195
}
178196
}
179197

198+
[return: NotNullIfNotNull(nameof(wildcard))]
180199
private static Regex? BuildWildcardRegex(string? wildcard)
181200
{
182201
return wildcard is null

ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public partial class DependencyInjectionGenerator
3030
if (hasCustomHandlers && attribute.CustomHandler == null)
3131
return Diagnostic.Create(CantMixRegularAndCustomHandlerRegistrations, attribute.Location);
3232

33+
if (attribute.AssemblyOfTypeName != null && attribute.AssemblyNameFilter != null)
34+
return Diagnostic.Create(CantUseBothFromAssemblyOfAndAssemblyNameFilter, attribute.Location);
35+
3336
if (attribute.KeySelector != null)
3437
{
3538
var keySelectorMethod = method.ContainingType.GetMembers().OfType<IMethodSymbol>()

ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,11 @@ public static class DiagnosticDescriptors
7373
"Usage",
7474
DiagnosticSeverity.Error,
7575
true);
76+
77+
public static readonly DiagnosticDescriptor CantUseBothFromAssemblyOfAndAssemblyNameFilter = new("DI0012",
78+
"Only one assembly selection criteria allowed",
79+
"It is not allowed to use both FromAssemblyOf and AssemblyNameFilter in the same attribute",
80+
"Usage",
81+
DiagnosticSeverity.Error,
82+
true);
7683
}

ServiceScan.SourceGenerator/GenerateAttributeSource.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,21 @@ internal class GenerateServiceRegistrationsAttribute : Attribute
1717
{
1818
/// <summary>
1919
/// Set the assembly containing the given type as the source of types to register.
20-
/// If not specified, all referenced projects are scanned.
20+
/// If not specified, the assembly containing the method with this attribute will be used.
2121
/// </summary>
2222
public Type? FromAssemblyOf { get; set; }
2323
24+
/// <summary>
25+
/// Set this value to filter scanned assemblies by assembly name.
26+
/// It allows to apply an attribute to multiple assemblies.
27+
/// For example, this allows to scan all assemblies from your solution.
28+
/// This option is incompatible with <see cref="FromAssemblyOf"/>.
29+
/// You can use '*' wildcards. You can also use ',' to separate multiple filters.
30+
/// </summary>
31+
/// <remarks>Be careful to include limited amount of assemblies, as it can affect build and editor performance.</remarks>
32+
/// <example>My.Product.*</example>
33+
public string? AssemblyNameFilter { get; set; }
34+
2435
/// <summary>
2536
/// Set the type that the registered types must be assignable to.
2637
/// Types will be registered with this type as the service type,

ServiceScan.SourceGenerator/Model/AttributeModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ enum KeySelectorType { Method, GenericMethod, TypeMember };
77

88
record AttributeModel(
99
string? AssignableToTypeName,
10+
string? AssemblyNameFilter,
1011
EquatableArray<string>? AssignableToGenericArguments,
1112
string? AssemblyOfTypeName,
1213
string Lifetime,
@@ -29,6 +30,7 @@ record AttributeModel(
2930
public static AttributeModel Create(AttributeData attribute, IMethodSymbol method)
3031
{
3132
var assemblyType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "FromAssemblyOf").Value.Value as INamedTypeSymbol;
33+
var assemblyNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssemblyNameFilter").Value.Value as string;
3234
var assignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssignableTo").Value.Value as INamedTypeSymbol;
3335
var asImplementedInterfaces = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsImplementedInterfaces").Value.Value is true;
3436
var asSelf = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsSelf").Value.Value is true;
@@ -63,6 +65,9 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
6365
if (string.IsNullOrWhiteSpace(excludeByTypeName))
6466
excludeByTypeName = null;
6567

68+
if (string.IsNullOrWhiteSpace(assemblyNameFilter))
69+
assemblyNameFilter = null;
70+
6671
var attributeFilterTypeName = attributeFilterType?.ToFullMetadataName();
6772
var excludeByAttributeTypeName = excludeByAttributeType?.ToFullMetadataName();
6873
var assemblyOfTypeName = assemblyType?.ToFullMetadataName();
@@ -92,6 +97,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
9297

9398
return new(
9499
assignableToTypeName,
100+
assemblyNameFilter,
95101
assignableToGenericArguments,
96102
assemblyOfTypeName,
97103
lifetime,

0 commit comments

Comments
 (0)