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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,6 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

.idea/
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <br>- 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`. <br>- 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: <br>- 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`. <br>- 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. |
267 changes: 267 additions & 0 deletions ServiceScan.SourceGenerator.Tests/AddServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.ThirdService, global::GeneratorTests.ThirdService>();
""";
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<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.ThirdService, global::GeneratorTests.ThirdService>();
""";
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<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.FourthService, global::GeneratorTests.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<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.ThirdService, global::GeneratorTests.ThirdService>();
""";
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<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.ThirdService, global::GeneratorTests.ThirdService>();
""";
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<T> {}

public class MyFirstService {}

public class MySecondService : IExclude<int> {}

public class ThirdService : IExclude<string> {}

public class FourthService {}
""");

var results = CSharpGeneratorDriver
.Create(_generator)
.RunGenerators(compilation)
.GetRunResult();

var registrations = $"""
return services
.AddTransient<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.FourthService, global::GeneratorTests.FourthService>();
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString());
}

[Fact]
public void AddServices_ExcludeAssignableTo_ClosedGenericInterface()
{
var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude<int>))]""";

var compilation = CreateCompilation(
Sources.MethodWithAttribute(attribute),
"""
namespace GeneratorTests;

public interface IExclude<T> {}

public class MyFirstService {}

public class MySecondService : IExclude<int> {}

public class ThirdService : IExclude<string> {}

public class FourthService {}
""");

var results = CSharpGeneratorDriver
.Create(_generator)
.RunGenerators(compilation)
.GetRunResult();

var registrations = $"""
return services
.AddTransient<global::GeneratorTests.MyFirstService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.ThirdService, global::GeneratorTests.ThirdService>()
.AddTransient<global::GeneratorTests.FourthService, global::GeneratorTests.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<global::GeneratorTests.IService, global::GeneratorTests.MyFirstService>()
.AddTransient<global::GeneratorTests.IService, global::GeneratorTests.MyThirdService>();
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString());
}

[Fact]
public void AddServicesWithTypeNameFilterAsImplementedInterfaces()
{
Expand Down Expand Up @@ -702,3 +968,4 @@ private static Compilation CreateCompilation(params string[] source)
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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))
{
Expand Down Expand Up @@ -135,4 +159,11 @@ static IEnumerable<INamedTypeSymbol> GetTypesFromNamespaceOrType(INamespaceOrTyp
}
}
}

private static Regex? BuildWildcardRegex(string? wildcard)
{
return wildcard is null
? null
: new Regex($"^({Regex.Escape(wildcard).Replace(@"\*", ".*").Replace(",", "|")})$");
}
}
Loading
Loading