From b626a04fa8be4101349beed96569bc1083eb568b Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Wed, 23 Apr 2025 17:15:40 +0300 Subject: [PATCH 1/4] Add ability to exclude by type name or attribute --- README.md | 12 ++- .../AddServicesTests.cs | 102 ++++++++++++++++++ ...ependencyInjectionGenerator.FilterTypes.cs | 30 +++++- .../GenerateAttributeSource.cs | 42 +++++--- .../Model/AttributeModel.cs | 16 ++- 5 files changed, 177 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 2725923..40c2242 100644 --- a/README.md +++ b/README.md @@ -138,12 +138,14 @@ 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, the assembly containing the method with this attribute will be used. | +| **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. | | **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. | -| **AsSelf** | If true, types will be registered with their actual type. It can be combined with `AsImplementedInterfaces`. In that case implemented interfaces will be "forwarded" to an actual implementation type | +| **AsSelf** | If true, types will be registered with their actual type. It can be combined with `AsImplementedInterfaces`. In that case, implemented interfaces will be "forwarded" to an actual implementation type. | | **TypeNameFilter** | Set this value to filter the types to register by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | -| **AttributeFilter** | Filter types by specified attribute type present. | -| **KeySelector** | Set this property to add types as keyed services. This property should point to one of the following:
- Name of the static method in the current type with string return type. Method should be either generic, or have a single parameter of type `Type`.
- Const field or static property in the implementation type. | -| **CustomHandler** | Set this property to a static generic method name in the current class. This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, `KeySelector` properties. | \ No newline at end of file +| **AttributeFilter** | Filter types by the specified attribute type present. | +| **ExcludeByTypeName** | Set this value to exclude types from being registered by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | +| **ExcludeByAttribute** | Exclude matching types by the specified attribute type present. | +| **KeySelector** | Set this property to add types as keyed services. This property should point to one of the following:
- Name of the static method in the current type with a string return type. The method should be either generic or have a single parameter of type `Type`.
- Const field or static property in the implementation type. | +| **CustomHandler** | Set this property to a static generic method name in the current class. This method will be invoked for each type found by the filter instead of the regular registration logic. This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, and `KeySelector` properties. | \ No newline at end of file diff --git a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs index a3e941f..dbbdcdf 100644 --- a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs +++ b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs @@ -467,6 +467,108 @@ public class ServiceWithNonMatchingName {} Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); } + [Fact] + public void AddServices_ExcludeByTypeName() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeByTypeName = "*Second*")]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public class MyFirstService {} + public class MySecondService {} + public class ThirdService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void AddServices_ExcludeByAttribute() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeByAttribute = typeof(ExcludeAttribute))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + using System; + + namespace GeneratorTests; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class ExcludeAttribute : Attribute; + + public class MyFirstService {} + + [Exclude] + public class MySecondService {} + + public class ThirdService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void AddServices_ExcludeByTypeNameAndAttribute() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeByTypeName = "*Third*", ExcludeByAttribute = typeof(ExcludeAttribute))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + using System; + + namespace GeneratorTests; + + [AttributeUsage(AttributeTargets.Class)] + public sealed class ExcludeAttribute : Attribute; + + public class MyFirstService {} + + [Exclude] + public class MySecondService {} + + public class ThirdService {} + + public class FourthService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + [Fact] public void AddServicesWithTypeNameFilterAsImplementedInterfaces() { diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index 095832c..01d0743 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -24,6 +24,13 @@ public partial class DependencyInjectionGenerator ? null : compilation.GetTypeByMetadataName(attribute.AttributeFilterTypeName); + var excludeByAttributeType = attribute.ExcludeByAttributeTypeName is null + ? null + : compilation.GetTypeByMetadataName(attribute.ExcludeByAttributeTypeName); + + var typeNameFilterRegex = BuildWildcardRegex(attribute.TypeNameFilter); + var excludeByTypeNameRegex = BuildWildcardRegex(attribute.ExcludeByTypeName); + if (assignableToType != null && attribute.AssignableToGenericArguments != null) { var typeArguments = attribute.AssignableToGenericArguments.Value.Select(t => compilation.GetTypeByMetadataName(t)).ToArray(); @@ -41,11 +48,21 @@ public partial class DependencyInjectionGenerator continue; } - if (attribute.TypeNameFilter != null) + if (excludeByAttributeType != null) + { + if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, excludeByAttributeType))) + continue; + } + + if (typeNameFilterRegex != null) { - var regex = $"^({Regex.Escape(attribute.TypeNameFilter).Replace(@"\*", ".*").Replace(",", "|")})$"; + if (!typeNameFilterRegex.IsMatch(type.ToDisplayString())) + continue; + } - if (!Regex.IsMatch(type.ToDisplayString(), regex)) + if (excludeByTypeNameRegex != null) + { + if (excludeByTypeNameRegex.IsMatch(type.ToDisplayString())) continue; } @@ -135,4 +152,11 @@ static IEnumerable GetTypesFromNamespaceOrType(INamespaceOrTyp } } } + + private static Regex? BuildWildcardRegex(string? wildcard) + { + return wildcard is null + ? null + : new Regex($"^({Regex.Escape(wildcard).Replace(@"\*", ".*").Replace(",", "|")})$"); + } } diff --git a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs index b6f5eb6..0ca75c8 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs @@ -27,11 +27,6 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// unless or is set. /// public Type? AssignableTo { get; set; } - - /// - /// Filter types by specified attribute type present. - /// - public Type? AttributeFilter { get; set; } /// /// Set the lifetime of the registered services. @@ -40,14 +35,14 @@ internal class GenerateServiceRegistrationsAttribute : Attribute public ServiceLifetime Lifetime { get; set; } /// - /// If set to true, types will be registered as implemented interfaces instead of their actual type. + /// If set to true, types will be registered as their implemented interfaces instead of their actual type. /// public bool AsImplementedInterfaces { get; set; } /// /// If set to true, types will be registered with their actual type. - /// It can be combined with , in that case implemented interfaces will be - /// "forwarded" to "self" implementation. + /// It can be combined with . In this case, implemented interfaces will be + /// "forwarded" to the "self" implementation. /// public bool AsSelf { get; set; } @@ -60,23 +55,42 @@ internal class GenerateServiceRegistrationsAttribute : Attribute /// *Service,*Factory public string? TypeNameFilter { get; set; } + /// + /// Filter types by the specified attribute type present. + /// + public Type? AttributeFilter { get; set; } + + /// + /// Set this value to exclude types from being registered by their full name. + /// You can use '*' wildcards. + /// You can also use ',' to separate multiple filters. + /// + /// Namespace.With.Services.* + /// *Service,*Factory + public string? ExcludeByTypeName { get; set; } + + /// + /// Exclude matching types by the specified attribute type present. + /// + public Type? ExcludeByAttribute { get; set; } + /// /// Set this property to add types as keyed services. /// This property should point to one of the following: - /// - Name of the static method in the current type with string return type. - /// Method should be either generic, or have a single parameter of type . - /// - Const field or static property in the implementation type. + /// - The name of a static method in the current type with a string return type. + /// The method should be either generic or have a single parameter of type . + /// - A constant field or static property in the implementation type. /// /// nameof(GetKey) public string? KeySelector { get; set; } /// /// Set this property to a static generic method name in the current class. - /// This method will be invoked for each type found by the filter instead of regular registration logic. + /// This method will be invoked for each type found by the filter instead of the regular registration logic. /// This property is incompatible with , , , - /// properties. + /// and properties. /// public string? CustomHandler { get; set; } } """; -} \ No newline at end of file +} diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index bbbf8cc..ea325c6 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -9,9 +9,11 @@ record AttributeModel( string? AssignableToTypeName, EquatableArray? AssignableToGenericArguments, string? AssemblyOfTypeName, - string? AttributeFilterTypeName, string Lifetime, + string? AttributeFilterTypeName, string? TypeNameFilter, + string? ExcludeByAttributeTypeName, + string? ExcludeByTypeName, string? KeySelector, KeySelectorType? KeySelectorType, string? CustomHandler, @@ -26,10 +28,12 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho { var assemblyType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "FromAssemblyOf").Value.Value as INamedTypeSymbol; var assignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssignableTo").Value.Value as INamedTypeSymbol; - var attributeFilterType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AttributeFilter").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; + var attributeFilterType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AttributeFilter").Value.Value as INamedTypeSymbol; var typeNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "TypeNameFilter").Value.Value as string; + var excludeByAttributeType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByAttribute").Value.Value as INamedTypeSymbol; + var excludeByTypeName = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByTypeName").Value.Value as string; var keySelector = attribute.NamedArguments.FirstOrDefault(a => a.Key == "KeySelector").Value.Value as string; var customHandler = attribute.NamedArguments.FirstOrDefault(a => a.Key == "CustomHandler").Value.Value as string; @@ -53,7 +57,11 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho if (string.IsNullOrWhiteSpace(typeNameFilter)) typeNameFilter = null; + if (string.IsNullOrWhiteSpace(excludeByTypeName)) + excludeByTypeName = null; + var attributeFilterTypeName = attributeFilterType?.ToFullMetadataName(); + var excludeByAttributeTypeName = excludeByAttributeType?.ToFullMetadataName(); var assemblyOfTypeName = assemblyType?.ToFullMetadataName(); var assignableToTypeName = assignableTo?.ToFullMetadataName(); EquatableArray? assignableToGenericArguments = assignableTo != null && assignableTo.IsGenericType && !assignableTo.IsUnboundGenericType @@ -79,9 +87,11 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho assignableToTypeName, assignableToGenericArguments, assemblyOfTypeName, - attributeFilterTypeName, lifetime, + attributeFilterTypeName, typeNameFilter, + excludeByAttributeTypeName, + excludeByTypeName, keySelector, keySelectorType, customHandler, From 014b53d97f9e19eda14effa86d019d96082b9533 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 3 May 2025 14:16:02 +0300 Subject: [PATCH 2/4] Allow excluding types assignable to certain type. --- .gitignore | 4 +- README.md | 1 + .../AddServicesTests.cs | 134 ++++++++++++++++++ ...ependencyInjectionGenerator.FilterTypes.cs | 29 ++-- ServiceScan.SourceGenerator/EquatableArray.cs | 2 +- .../GenerateAttributeSource.cs | 6 + .../Model/AttributeModel.cs | 11 +- 7 files changed, 173 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 9491a2f..95c5523 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,6 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 40c2242..a655af3 100644 --- a/README.md +++ b/README.md @@ -147,5 +147,6 @@ public static partial class ServiceCollectionExtensions | **AttributeFilter** | Filter types by the specified attribute type present. | | **ExcludeByTypeName** | Set this value to exclude types from being registered by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | | **ExcludeByAttribute** | Exclude matching types by the specified attribute type present. | +| **ExcludeAssignableTo** | Set the type that the registered types must not be assignable to. | | **KeySelector** | Set this property to add types as keyed services. This property should point to one of the following:
- Name of the static method in the current type with a string return type. The method should be either generic or have a single parameter of type `Type`.
- Const field or static property in the implementation type. | | **CustomHandler** | Set this property to a static generic method name in the current class. This method will be invoked for each type found by the filter instead of the regular registration logic. This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, and `KeySelector` properties. | \ No newline at end of file diff --git a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs index dbbdcdf..3e93266 100644 --- a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs +++ b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs @@ -569,6 +569,139 @@ public class FourthService {} Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); } + [Fact] + public void AddServices_ExcludeAssignableTo_Interface() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public interface IExclude {} + + public class MyFirstService {} + + public class MySecondService : IExclude {} + + public class ThirdService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void AddServices_ExcludeAssignableTo_AbstractClass() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(ExcludeBase))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public abstract class ExcludeBase {} + + public class MyFirstService {} + + public class MySecondService : ExcludeBase {} + + public class ThirdService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void AddServices_ExcludeAssignableTo_OpenGenericInterface() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude<>))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public interface IExclude {} + + public class MyFirstService {} + + public class MySecondService : IExclude {} + + public class ThirdService : IExclude {} + + public class FourthService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + + [Fact] + public void AddServices_ExcludeAssignableTo_ClosedGenericInterface() + { + var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public interface IExclude {} + + public class MyFirstService {} + + public class MySecondService : IExclude {} + + public class ThirdService : IExclude {} + + public class FourthService {} + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } + [Fact] public void AddServicesWithTypeNameFilterAsImplementedInterfaces() { @@ -804,3 +937,4 @@ private static Compilation CreateCompilation(params string[] source) new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } } + diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index 01d0743..e6dc357 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -20,6 +20,10 @@ public partial class DependencyInjectionGenerator ? null : compilation.GetTypeByMetadataName(attribute.AssignableToTypeName); + var excludeAssignableToType = attribute.ExcludeAssignableToTypeName is null + ? null + : compilation.GetTypeByMetadataName(attribute.ExcludeAssignableToTypeName); + var attributeFilterType = attribute.AttributeFilterTypeName is null ? null : compilation.GetTypeByMetadataName(attribute.AttributeFilterTypeName); @@ -37,6 +41,12 @@ public partial class DependencyInjectionGenerator assignableToType = assignableToType.Construct(typeArguments); } + if (excludeAssignableToType != null && attribute.ExcludeAssignableToGenericArguments != null) + { + var typeArguments = attribute.ExcludeAssignableToGenericArguments.Value.Select(t => compilation.GetTypeByMetadataName(t)).ToArray(); + excludeAssignableToType = excludeAssignableToType.Construct(typeArguments); + } + foreach (var type in GetTypesFromAssembly(assembly)) { if (type.IsAbstract || type.IsStatic || !type.CanBeReferencedByName || type.TypeKind != TypeKind.Class) @@ -54,17 +64,14 @@ public partial class DependencyInjectionGenerator continue; } - if (typeNameFilterRegex != null) - { - if (!typeNameFilterRegex.IsMatch(type.ToDisplayString())) - continue; - } + if (typeNameFilterRegex != null && !typeNameFilterRegex.IsMatch(type.ToDisplayString())) + continue; - if (excludeByTypeNameRegex != null) - { - if (excludeByTypeNameRegex.IsMatch(type.ToDisplayString())) - continue; - } + if (excludeByTypeNameRegex != null && excludeByTypeNameRegex.IsMatch(type.ToDisplayString())) + continue; + + if (excludeAssignableToType != null && IsAssignableTo(type, assignableToType, out _)) + continue; INamedTypeSymbol matchedType = null; if (assignableToType != null && !IsAssignableTo(type, assignableToType, out matchedType)) @@ -74,7 +81,7 @@ public partial class DependencyInjectionGenerator } } - private static bool IsAssignableTo(INamedTypeSymbol type, INamedTypeSymbol assignableTo, out INamedTypeSymbol matchedType) + private static bool IsAssignableTo(INamedTypeSymbol type, INamedTypeSymbol assignableTo, out INamedTypeSymbol? matchedType) { if (SymbolEqualityComparer.Default.Equals(type, assignableTo)) { diff --git a/ServiceScan.SourceGenerator/EquatableArray.cs b/ServiceScan.SourceGenerator/EquatableArray.cs index 753a55b..c10d6e7 100644 --- a/ServiceScan.SourceGenerator/EquatableArray.cs +++ b/ServiceScan.SourceGenerator/EquatableArray.cs @@ -84,7 +84,7 @@ IEnumerator IEnumerable.GetEnumerator() } } -file static class EquatableArrayBuilder +internal static class EquatableArrayBuilder { public static EquatableArray Create(ReadOnlySpan values) where T : IEquatable => new(values.ToArray()); } \ No newline at end of file diff --git a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs index 0ca75c8..05ffe81 100644 --- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs +++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs @@ -28,6 +28,11 @@ internal class GenerateServiceRegistrationsAttribute : Attribute ///
public Type? AssignableTo { get; set; } + /// + /// Set the type that the registered types must *not* be assignable to. + /// + public Type? ExcludeAssignableTo { get; set; } + /// /// Set the lifetime of the registered services. /// is used if not specified. @@ -94,3 +99,4 @@ internal class GenerateServiceRegistrationsAttribute : Attribute } """; } + diff --git a/ServiceScan.SourceGenerator/Model/AttributeModel.cs b/ServiceScan.SourceGenerator/Model/AttributeModel.cs index ea325c6..50998b1 100644 --- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs +++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs @@ -14,6 +14,8 @@ record AttributeModel( string? TypeNameFilter, string? ExcludeByAttributeTypeName, string? ExcludeByTypeName, + string? ExcludeAssignableToTypeName, + EquatableArray? ExcludeAssignableToGenericArguments, string? KeySelector, KeySelectorType? KeySelectorType, string? CustomHandler, @@ -34,6 +36,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho var typeNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "TypeNameFilter").Value.Value as string; var excludeByAttributeType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByAttribute").Value.Value as INamedTypeSymbol; var excludeByTypeName = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByTypeName").Value.Value as string; + var excludeAssignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeAssignableTo").Value.Value as INamedTypeSymbol; var keySelector = attribute.NamedArguments.FirstOrDefault(a => a.Key == "KeySelector").Value.Value as string; var customHandler = attribute.NamedArguments.FirstOrDefault(a => a.Key == "CustomHandler").Value.Value as string; @@ -64,11 +67,15 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho var excludeByAttributeTypeName = excludeByAttributeType?.ToFullMetadataName(); var assemblyOfTypeName = assemblyType?.ToFullMetadataName(); var assignableToTypeName = assignableTo?.ToFullMetadataName(); + var excludeAssignableToTypeName = excludeAssignableTo?.ToFullMetadataName(); EquatableArray? assignableToGenericArguments = assignableTo != null && assignableTo.IsGenericType && !assignableTo.IsUnboundGenericType ? [.. assignableTo?.TypeArguments.Select(t => t.ToFullMetadataName())] : null; + EquatableArray? excludeAssignableToGenericArguments = excludeAssignableTo != null && excludeAssignableTo.IsGenericType && !excludeAssignableTo.IsUnboundGenericType + ? [.. excludeAssignableTo?.TypeArguments.Select(t => t.ToFullMetadataName())] + : null; - var lifetime = attribute.NamedArguments.FirstOrDefault(a => a.Key == "Lifetime").Value.Value as int? switch + var lifetime = (attribute.NamedArguments.FirstOrDefault(a => a.Key == "Lifetime").Value.Value as int?) switch { 0 => "Singleton", 1 => "Scoped", @@ -92,6 +99,8 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho typeNameFilter, excludeByAttributeTypeName, excludeByTypeName, + excludeAssignableToTypeName, + excludeAssignableToGenericArguments, keySelector, keySelectorType, customHandler, From 2036d4f93a3883092bf2fa0c2d4ca0420e92aba9 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 3 May 2025 14:22:22 +0300 Subject: [PATCH 3/4] Fixes --- .../AddServicesTests.cs | 31 +++++++++++++++++++ ...ependencyInjectionGenerator.FilterTypes.cs | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs index 3e93266..be42f8a 100644 --- a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs +++ b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs @@ -701,6 +701,37 @@ public class FourthService {} """; Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); } + + [Fact] + public void AddServices_AssignableToAndExcludeAssignableTo() + { + var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(IService), ExcludeAssignableTo = typeof(IExclude))]"""; + + var compilation = CreateCompilation( + Sources.MethodWithAttribute(attribute), + """ + namespace GeneratorTests; + + public interface IService { } + public interface IExclude { } + + public class MyFirstService : IService { } + public class MySecondService : IService, IExclude { } + public class MyThirdService : IService { } + """); + + var results = CSharpGeneratorDriver + .Create(_generator) + .RunGenerators(compilation) + .GetRunResult(); + + var registrations = $""" + return services + .AddTransient() + .AddTransient(); + """; + Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); + } [Fact] public void AddServicesWithTypeNameFilterAsImplementedInterfaces() diff --git a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs index e6dc357..a79dad1 100644 --- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs +++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs @@ -70,7 +70,7 @@ public partial class DependencyInjectionGenerator if (excludeByTypeNameRegex != null && excludeByTypeNameRegex.IsMatch(type.ToDisplayString())) continue; - if (excludeAssignableToType != null && IsAssignableTo(type, assignableToType, out _)) + if (excludeAssignableToType != null && IsAssignableTo(type, excludeAssignableToType, out _)) continue; INamedTypeSymbol matchedType = null; From 65517e20bf097b1bc09e481bc69c90b515eb50cc Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 3 May 2025 14:27:34 +0300 Subject: [PATCH 4/4] return readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a655af3..534369a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ServiceScan.SourceGenerator [![NuGet Version](https://img.shields.io/nuget/v/ServiceScan.SourceGenerator)](https://www.nuget.org/packages/ServiceScan.SourceGenerator/) +[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) Source generator for services registrations inspired by [Scrutor](https://github.com/khellang/Scrutor/). Code generation allows to have AOT-compatible code, without an additional hit on startup performance due to runtime assembly scanning.