From 8ee0005d0540688d90ac7feb6eba5ca0292397c3 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Thu, 29 May 2025 17:16:51 +0300 Subject: [PATCH 1/4] AssemblyNameFilter --- .../AddServicesTests.cs | 10 +++--- ...ependencyInjectionGenerator.FilterTypes.cs | 35 +++++++++++++++---- .../GenerateAttributeSource.cs | 5 +++ .../Model/AttributeModel.cs | 6 ++++ 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs index 17e8cff..8014d2d 100644 --- a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs +++ b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs @@ -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]); diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index defef0b..181375d 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; @@ -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 @@ -139,6 +134,31 @@ private static bool IsAssignableTo(INamedTypeSymbol type, INamedTypeSymbol assig return false; } + private static IEnumerable 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 compilation.Assembly.Modules + .SelectMany(m => m.ReferencedAssemblySymbols) + .Concat([compilation.Assembly]) + .Where(assembly => assemblyNameRegex.IsMatch(assembly.Name)) + .ToArray(); + } + + return [containingType.ContainingAssembly]; + } + private static IEnumerable GetSolutionAssemblies(Compilation compilation) { yield return compilation.Assembly; @@ -177,6 +197,7 @@ static IEnumerable GetTypesFromNamespaceOrType(INamespaceOrTyp } } + [return: NotNullIfNotNull(nameof(wildcard))] private static Regex? BuildWildcardRegex(string? wildcard) { return wildcard is null diff --git a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs index ce7f0f7..e2991bd 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs @@ -20,6 +20,11 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// If not specified, all referenced projects are scanned. /// public Type? FromAssemblyOf { get; set; } + + /// + /// TODO + /// + public string? AssemblyNameFilter { get; set; } /// /// Set the type that the registered types must be assignable to. diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index 50998b1..61c2c40 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -7,6 +7,7 @@ enum KeySelectorType { Method, GenericMethod, TypeMember }; record AttributeModel( string? AssignableToTypeName, + string? AssemblyNameFilter, EquatableArray? AssignableToGenericArguments, string? AssemblyOfTypeName, string Lifetime, @@ -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; @@ -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(); @@ -92,6 +97,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho return new( assignableToTypeName, + assemblyNameFilter, assignableToGenericArguments, assemblyOfTypeName, lifetime, From f23a75efd79a4e0aa32f10122ea98f15939a4688 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 31 May 2025 11:14:31 +0300 Subject: [PATCH 2/4] Update README --- README.md | 3 ++- .../GenerateAttributeSource.cs | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d54bd12..af0b05f 100644 --- a/README.md +++ b/README.md @@ -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. | diff --git a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs index e2991bd..4faed05 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs @@ -17,13 +17,18 @@ internal class GenerateServiceRegistrationsAttribute : Attribute { /// /// 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. /// public Type? FromAssemblyOf { get; set; } - + /// - /// TODO + /// 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. + /// My.Product.* public string? AssemblyNameFilter { get; set; } /// From bb47a607b20f504cd70b7c1828a710d240797f12 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 31 May 2025 11:27:21 +0300 Subject: [PATCH 3/4] Do not allow FromAssemblyOf and AssemblyNameFilter in the same attribute --- .../DependencyInjectionGenerator.ParseMethodModel.cs | 3 +++ ServiceScan.SourceGenerator/DiagnosticDescriptors.cs | 7 +++++++ ServiceScan.SourceGenerator/GenerateAttributeSource.cs | 1 + 3 files changed, 11 insertions(+) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs index 90d86ac..263b2a4 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs @@ -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() diff --git a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs index 87a89c0..db73d00 100644 --- a/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs +++ b/ServiceScan.SourceGenerator/DiagnosticDescriptors.cs @@ -73,4 +73,11 @@ public static class DiagnosticDescriptors "Usage", DiagnosticSeverity.Error, true); + + public static readonly DiagnosticDescriptor CantUseBothFromAssemblyOfAndAssemblyNameFilter = new("DI0012", + "Only one assembly selection criteria allowed", + "It is not allowed to use both FromAssemblyOf and AssemblyNameFilter in the same attribute", + "Usage", + DiagnosticSeverity.Error, + true); } diff --git a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs index 4faed05..d288f44 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs @@ -25,6 +25,7 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// 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 . /// 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. From 4c4992eacb8098a58f5ca5db099aa755cdd90166 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 31 May 2025 11:36:54 +0300 Subject: [PATCH 4/4] simplify ReferencedAssemblySymbols --- .../DependencyInjectionGenerator.FilterTypes.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index 181375d..f2a79a6 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -149,11 +149,9 @@ private static IEnumerable GetAssembliesToScan(Compilation comp { var assemblyNameRegex = BuildWildcardRegex(attribute.AssemblyNameFilter); - return compilation.Assembly.Modules - .SelectMany(m => m.ReferencedAssemblySymbols) - .Concat([compilation.Assembly]) - .Where(assembly => assemblyNameRegex.IsMatch(assembly.Name)) - .ToArray(); + return new[] { compilation.Assembly } + .Concat(compilation.SourceModule.ReferencedAssemblySymbols) + .Where(assembly => assemblyNameRegex.IsMatch(assembly.Name)); } return [containingType.ContainingAssembly];