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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,5 @@ public static partial class ServiceCollectionExtensions
| **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. |
| **WithAttribute** | Filter types by specified attribute type present. |
| **KeySelector** | Set this value to a static method name returning string. Returned value will be used as a key for the registration. Method should either be generic, or have a single parameter of type `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. |
| **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. |
36 changes: 36 additions & 0 deletions ServiceScan.SourceGenerator.Tests/AddServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,42 @@ public class MyService2 : IService { }
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString());
}

[Fact]
public void AddAsKeyedServices_ConstantFieldInType()
{
var attribute = @"[GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = ""Key"")]";

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

public interface IService { }

public class MyService1 : IService
{
public const string Key = "MSR1";
}

public class MyService2 : IService
{
public const string Key = "MSR2";
}
""");

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

var registrations = $"""
return services
.AddKeyedTransient<GeneratorTests.IService, GeneratorTests.MyService1>(GeneratorTests.MyService1.Key)
.AddKeyedTransient<GeneratorTests.IService, GeneratorTests.MyService2>(GeneratorTests.MyService2.Key);
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString());
}

[Fact]
public void DontGenerateAnythingIfTypeIsInvalid()
{
Expand Down
26 changes: 0 additions & 26 deletions ServiceScan.SourceGenerator.Tests/DiagnosticTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,32 +202,6 @@ public static partial class ServicesExtensions
Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.MissingSearchCriteria);
}

[Fact]
public void KeySelectorMethodDoesNotExist()
{
var attribute = @"
private static string GetName<T>() => typeof(T).Name.Replace(""Service"", """");

[GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = ""NoSuchMethodHere"")]";

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

public interface IService { }
public class MyService1 : IService { }
public class MyService2 : IService { }
""");

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

Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.KeySelectorMethodNotFound);
}

[Fact]
public void KeySelectorMethod_GenericButHasParameters()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private static DiagnosticModel<MethodImplementationModel> FindServicesToRegister
false,
true,
attribute.KeySelector,
attribute.KeySelectorGeneric);
attribute.KeySelectorType);

registrations.Add(registration);
}
Expand All @@ -75,7 +75,7 @@ private static DiagnosticModel<MethodImplementationModel> FindServicesToRegister
shouldResolve,
false,
attribute.KeySelector,
attribute.KeySelectorGeneric);
attribute.KeySelectorType);

registrations.Add(registration);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ private static DiagnosticModel<MethodWithAttributesModel> ParseRegisterMethodMod
var keySelectorMethod = method.ContainingType.GetMembers().OfType<IMethodSymbol>()
.FirstOrDefault(m => m.IsStatic && m.Name == attribute.KeySelector);

if (keySelectorMethod is null)
return Diagnostic.Create(KeySelectorMethodNotFound, attribute.Location);
if (keySelectorMethod is not null)
{
if (keySelectorMethod.ReturnsVoid)
return Diagnostic.Create(KeySelectorMethodHasIncorrectSignature, attribute.Location);

if (keySelectorMethod.ReturnsVoid)
return Diagnostic.Create(KeySelectorMethodHasIncorrectSignature, attribute.Location);
var validGenericKeySelector = keySelectorMethod.TypeArguments.Length == 1 && keySelectorMethod.Parameters.Length == 0;
var validNonGenericKeySelector = !keySelectorMethod.IsGenericMethod && keySelectorMethod.Parameters is [{ Type.Name: nameof(Type) }];

var validGenericKeySelector = keySelectorMethod.TypeArguments.Length == 1 && keySelectorMethod.Parameters.Length == 0;
var validNonGenericKeySelector = !keySelectorMethod.IsGenericMethod && keySelectorMethod.Parameters is [{ Type.Name: nameof(Type) }];

if (!validGenericKeySelector && !validNonGenericKeySelector)
return Diagnostic.Create(KeySelectorMethodHasIncorrectSignature, attribute.Location);
if (!validGenericKeySelector && !validNonGenericKeySelector)
return Diagnostic.Create(KeySelectorMethodHasIncorrectSignature, attribute.Location);
}
}

if (attribute.CustomHandler != null)
Expand Down
13 changes: 7 additions & 6 deletions ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,19 @@ private static string GenerateRegistrationsSource(MethodModel method, EquatableA
}
else
{
var addMethod = registration.KeySelectorMethodName != null
var addMethod = registration.KeySelector != null
? $"AddKeyed{registration.Lifetime}"
: $"Add{registration.Lifetime}";

var keyMethodInvocation = registration.KeySelectorMethodGeneric switch
var keySelectorInvocation = registration.KeySelectorType switch
{
true => $"{registration.KeySelectorMethodName}<{registration.ImplementationTypeName}>()",
false => $"{registration.KeySelectorMethodName}(typeof({registration.ImplementationTypeName}))",
null => null
KeySelectorType.GenericMethod => $"{registration.KeySelector}<{registration.ImplementationTypeName}>()",
KeySelectorType.Method => $"{registration.KeySelector}(typeof({registration.ImplementationTypeName}))",
KeySelectorType.TypeMember => $"{registration.ImplementationTypeName}.{registration.KeySelector}",
_ => null
};

return $" .{addMethod}<{registration.ServiceTypeName}, {registration.ImplementationTypeName}>({keyMethodInvocation})";
return $" .{addMethod}<{registration.ServiceTypeName}, {registration.ImplementationTypeName}>({keySelectorInvocation})";
}
}
}));
Expand Down
7 changes: 0 additions & 7 deletions ServiceScan.SourceGenerator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,70 @@

public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor NotPartialDefinition = new("DI0001",

Check warning on line 7 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0001' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Method is not partial",
"Method with GenerateServiceRegistrations attribute must have partial modifier",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor WrongReturnType = new("DI0002",

Check warning on line 14 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0002' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Wrong return type",
"Method with GenerateServiceRegistrations attribute must return void or IServiceCollection",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor WrongMethodParameters = new("DI0003",

Check warning on line 21 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0003' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Wrong method parameters",
"Method with GenerateServiceRegistrations attribute must have a single IServiceCollection parameter",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor MissingSearchCriteria = new("DI0004",

Check warning on line 28 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0004' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Missing search criteria",
"GenerateServiceRegistrations must have at least one search criteria",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor NoMatchingTypesFound = new("DI0005",

Check warning on line 35 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0005' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"No matching types found",
"There are no types matching attribute's search criteria",
"Usage",
DiagnosticSeverity.Warning,
true);

public static readonly DiagnosticDescriptor KeySelectorMethodNotFound = new("DI0006",
"Provided KeySelector method is not found",
"KeySelector parameter should point to a static method in the class",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor KeySelectorMethodHasIncorrectSignature = new("DI0007",

Check warning on line 42 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0007' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Provided KeySelector method has incorrect signature",
"KeySelector should have non-void return type, and either be generic with no parameters, or non-generic with a single Type parameter",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor OnlyOneCustomHandlerAllowed = new("DI0008",

Check warning on line 49 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0008' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Only one GenerateServiceRegistrations attribute is allowed when CustomHandler used",
"Only one GenerateServiceRegistrations attribute is allowed when CustomHandler used",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor WrongReturnTypeForCustomHandler = new("DI0009",

Check warning on line 56 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0009' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Wrong return type",
"Method with CustomHandler must return void or 'this' parameter type",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor CustomHandlerMethodNotFound = new("DI0012",

Check warning on line 63 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0012' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Provided CustomHandler method is not found",
"CustomHandler parameter should point to a static method in the class",
"Usage",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor CustomHandlerMethodHasIncorrectSignature = new("DI0011",

Check warning on line 70 in ServiceScan.SourceGenerator/DiagnosticDescriptors.cs

View workflow job for this annotation

GitHub Actions / build

Enable analyzer release tracking for the analyzer project containing rule 'DI0011' (https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md)
"Provided CustomHandler method has incorrect signature",
"CustomHandler method must be generic, and must have the same parameters as the method with an attribute",
"Usage",
Expand Down
8 changes: 5 additions & 3 deletions ServiceScan.SourceGenerator/GenerateAttributeSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ internal class GenerateServiceRegistrationsAttribute : Attribute
public string? TypeNameFilter { get; set; }

/// <summary>
/// Set this property to a static method name returning string.
/// Returned value will be used as a key for the registration.
/// Method should either be generic, or have a single parameter of type <see cref="Type"/>.
/// 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 <see cref="Type"/>.
/// - Const field or static property in the implementation type.
/// </summary>
/// <example>nameof(GetKey)</example>
public string? KeySelector { get; set; }
Expand Down
14 changes: 10 additions & 4 deletions ServiceScan.SourceGenerator/Model/AttributeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace ServiceScan.SourceGenerator.Model;

enum KeySelectorType { Method, GenericMethod, TypeMember };

record AttributeModel(
string? AssignableToTypeName,
EquatableArray<string>? AssignableToGenericArguments,
Expand All @@ -11,7 +13,7 @@ record AttributeModel(
string Lifetime,
string? TypeNameFilter,
string? KeySelector,
bool? KeySelectorGeneric,
KeySelectorType? KeySelectorType,
string? CustomHandler,
bool AsImplementedInterfaces,
bool AsSelf,
Expand All @@ -31,7 +33,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
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;

bool? keySelectorGeneric = null;
KeySelectorType? keySelectorType = null;
if (keySelector != null)
{
var keySelectorMethod = method.ContainingType.GetMembers()
Expand All @@ -40,7 +42,11 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho

if (keySelectorMethod != null)
{
keySelectorGeneric = keySelectorMethod.IsGenericMethod;
keySelectorType = keySelectorMethod.IsGenericMethod ? Model.KeySelectorType.GenericMethod : Model.KeySelectorType.Method;
}
else
{
keySelectorType = Model.KeySelectorType.TypeMember;
}
}

Expand Down Expand Up @@ -77,7 +83,7 @@ public static AttributeModel Create(AttributeData attribute, IMethodSymbol metho
lifetime,
typeNameFilter,
keySelector,
keySelectorGeneric,
keySelectorType,
customHandler,
asImplementedInterfaces,
asSelf,
Expand Down
4 changes: 2 additions & 2 deletions ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ record ServiceRegistrationModel(
string ImplementationTypeName,
bool ResolveImplementation,
bool IsOpenGeneric,
string? KeySelectorMethodName,
bool? KeySelectorMethodGeneric);
string? KeySelector,
KeySelectorType? KeySelectorType);

record CustomHandlerModel(
string HandlerMethodName,
Expand Down