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 fede0fb..534369a 100644
--- a/README.md
+++ b/README.md
@@ -139,12 +139,15 @@ 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. |
+| **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 a3e941f..be42f8a 100644
--- a/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs
+++ b/ServiceScan.SourceGenerator.Tests/AddServicesTests.cs
@@ -467,6 +467,272 @@ 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 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 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()
{
@@ -702,3 +968,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 095832c..a79dad1 100644
--- a/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs
+++ b/ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs
@@ -20,16 +20,33 @@ 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);
+ 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();
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)
@@ -41,14 +58,21 @@ public partial class DependencyInjectionGenerator
continue;
}
- if (attribute.TypeNameFilter != null)
+ if (excludeByAttributeType != null)
{
- var regex = $"^({Regex.Escape(attribute.TypeNameFilter).Replace(@"\*", ".*").Replace(",", "|")})$";
-
- if (!Regex.IsMatch(type.ToDisplayString(), regex))
+ if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, excludeByAttributeType)))
continue;
}
+ if (typeNameFilterRegex != null && !typeNameFilterRegex.IsMatch(type.ToDisplayString()))
+ continue;
+
+ if (excludeByTypeNameRegex != null && excludeByTypeNameRegex.IsMatch(type.ToDisplayString()))
+ continue;
+
+ if (excludeAssignableToType != null && IsAssignableTo(type, excludeAssignableToType, out _))
+ continue;
+
INamedTypeSymbol matchedType = null;
if (assignableToType != null && !IsAssignableTo(type, assignableToType, out matchedType))
continue;
@@ -57,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))
{
@@ -135,4 +159,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/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 b6f5eb6..05ffe81 100644
--- a/ServiceScan.SourceGenerator/GenerateAttributeSource.cs
+++ b/ServiceScan.SourceGenerator/GenerateAttributeSource.cs
@@ -27,11 +27,11 @@ internal class GenerateServiceRegistrationsAttribute : Attribute
/// unless or is set.
///
public Type? AssignableTo { get; set; }
-
+
///
- /// Filter types by specified attribute type present.
+ /// Set the type that the registered types must *not* be assignable to.
///
- public Type? AttributeFilter { get; set; }
+ public Type? ExcludeAssignableTo { get; set; }
///
/// Set the lifetime of the registered services.
@@ -40,14 +40,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 +60,43 @@ 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..50998b1 100644
--- a/ServiceScan.SourceGenerator/Model/AttributeModel.cs
+++ b/ServiceScan.SourceGenerator/Model/AttributeModel.cs
@@ -9,9 +9,13 @@ record AttributeModel(
string? AssignableToTypeName,
EquatableArray? AssignableToGenericArguments,
string? AssemblyOfTypeName,
- string? AttributeFilterTypeName,
string Lifetime,
+ string? AttributeFilterTypeName,
string? TypeNameFilter,
+ string? ExcludeByAttributeTypeName,
+ string? ExcludeByTypeName,
+ string? ExcludeAssignableToTypeName,
+ EquatableArray? ExcludeAssignableToGenericArguments,
string? KeySelector,
KeySelectorType? KeySelectorType,
string? CustomHandler,
@@ -26,10 +30,13 @@ 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 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;
@@ -53,14 +60,22 @@ 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();
+ 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",
@@ -79,9 +94,13 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
assignableToTypeName,
assignableToGenericArguments,
assemblyOfTypeName,
- attributeFilterTypeName,
lifetime,
+ attributeFilterTypeName,
typeNameFilter,
+ excludeByAttributeTypeName,
+ excludeByTypeName,
+ excludeAssignableToTypeName,
+ excludeAssignableToGenericArguments,
keySelector,
keySelectorType,
customHandler,