Skip to content

Commit

Permalink
SourceGenerator
Browse files Browse the repository at this point in the history
- Registration - adding generic attributes
- Registration - adding Analyzer to make sure the generic ServiceType is implemented by the class
  • Loading branch information
futurum-dev committed Apr 17, 2023
1 parent d35e7be commit 89dcf2c
Show file tree
Hide file tree
Showing 19 changed files with 249 additions and 26 deletions.
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,21 @@ var serviceProvider = await services.BuildServiceProviderWithStartablesAsync();
## Attribute based registration
You can also register services using attributes.

There are generic attributes available allowing you to specify the ServiceType, if your service implements multiple interfaces.

### RegisterAsSingleton attribute
```csharp
[RegisterAsSingleton]
public class Service : IService
{
}
```
```csharp
[RegisterAsSingleton<IService2>]
public class Service : IService1, IService2
{
}
```

### RegisterAsScoped attribute
```csharp
Expand All @@ -158,6 +166,12 @@ public class Service : IService
{
}
```
```csharp
[RegisterAsScoped<IService2>]
public class Service : IService1, IService2
{
}
```

### RegisterAsTransient attribute
```csharp
Expand All @@ -166,6 +180,12 @@ public class Service : IService
{
}
```
```csharp
[RegisterAsTransient<IService2>]
public class Service : IService1, IService2
{
}
```

### DuplicateRegistrationStrategy
- Try - Adds the new registration, if the service hasn't already been registered
Expand Down Expand Up @@ -201,4 +221,5 @@ public class Service : IService
- FMEDI0003 - Non empty constructor found on Module
- FMEDI0004 - Non empty constructor found on Startable
- FMEDI0005 - Non async method found on Startable
- FMEDI0006 - Non void method found on Module
- FMEDI0006 - Non void method found on Module
- FMEDI0007 - Register ServiceType not implemented by class
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ public interface IService3
{
}

public interface IService4
{
}

[RegisterAsTransient(InterfaceRegistrationStrategy = InterfaceRegistrationStrategy.Self)]
public class TransientService_Self : IService1
{
Expand Down Expand Up @@ -72,7 +76,7 @@ public class SingletonService_Add : IService1
{
}

[RegisterAsScoped(ServiceType = typeof(IService2), DuplicateRegistrationStrategy = DuplicateRegistrationStrategy.Add)]
[RegisterAsScoped<IService2>(DuplicateRegistrationStrategy = DuplicateRegistrationStrategy.Add)]
public class ServiceMultipleInterfaces : IService1, IService2
{
}
Expand All @@ -81,12 +85,12 @@ public interface IService_OpenGeneric<T>
{
}

[RegisterAsSingleton(ImplementationType = typeof(TransientServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))]
[RegisterAsTransient(ImplementationType = typeof(TransientServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))]
public class TransientServiceOpenGeneric<T> : IService_OpenGeneric<T>
{
}

[RegisterAsSingleton(ImplementationType = typeof(ScopedServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))]
[RegisterAsScoped(ImplementationType = typeof(ScopedServiceOpenGeneric<>), ServiceType = typeof(IService_OpenGeneric<>))]
public class ScopedServiceOpenGeneric<T> : IService_OpenGeneric<T>
{
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

[RegisterAsScoped<IService2>]
public class AutomaticScopedGenericService : IService1, IService2
{
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

[RegisterAsScoped]
public class AutomaticScopedService : IService
public class AutomaticScopedService : IService1
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

[RegisterAsSingleton<IService2>]
public class AutomaticSingletonGenericService : IService1, IService2
{
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

[RegisterAsSingleton]
public class AutomaticSingletonService : IService
public class AutomaticSingletonService : IService1
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

[RegisterAsTransient<IService2>]
public class AutomaticTransientGenericService : IService1, IService2
{
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

[RegisterAsTransient]
public class AutomaticTransientService : IService
public class AutomaticTransientService : IService1
{
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

public interface IService
public interface IService1
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

public interface IService2
{
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace Futurum.Microsoft.Extensions.DependencyInjection.Sample;

public class ManualService : IService
public class ManualService : IService1
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
serviceCollection.AddStartable<ManualStartable>();
serviceCollection.AddSingleton<IService, ManualService>();
serviceCollection.AddSingleton<IService1, ManualService>();
});

var host = builder.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,12 @@ public static class DiagnosticDescriptors
"Futurum.Microsoft.Extensions.DependencyInjection.Generator",
DiagnosticSeverity.Error,
true);

public static readonly DiagnosticDescriptor RegistrationServiceTypeNotImplementedByClass = new(
"FMEDI0007",
"Register ServiceType not implemented by class",
$"Class '{{0}}' does not implement ServiceType '{{1}}'.{Environment.NewLine} Class must implement the ServiceType.",
"Futurum.Microsoft.Extensions.DependencyInjection.Generator",
DiagnosticSeverity.Error,
true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,73 @@ namespace Futurum.Microsoft.Extensions.DependencyInjection.Generator;

public static class Diagnostics
{
public static class Registration
{
public static IEnumerable<AttributeData> HasAttribute(INamedTypeSymbol classSymbol)
{
return classSymbol.GetAttributes().Where(IsRegistrationAttribute);

static bool IsRegistrationAttribute(AttributeData attribute)
{
var attributeClass = attribute.AttributeClass;

if (attributeClass == null)
return false;

return attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.StartsWith("global::Futurum.Microsoft.Extensions.DependencyInjection.RegisterAsTransientAttribute") ||
attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.StartsWith("global::Futurum.Microsoft.Extensions.DependencyInjection.RegisterAsScopedAttribute") ||
attributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.StartsWith("global::Futurum.Microsoft.Extensions.DependencyInjection.RegisterAsSingletonAttribute");
}
}

public static class ServiceTypeNotImplementedByClass
{
public static IEnumerable<Diagnostic> Check(INamedTypeSymbol classSymbol, AttributeData attributeData)
{
var serviceType = GetServiceTypeFromAttribute(attributeData);

if (string.IsNullOrEmpty(serviceType))
yield break;

var classImplementsServiceType = classSymbol.AllInterfaces.Any(x => x.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == serviceType);

if (!classImplementsServiceType)
yield return Diagnostic.Create(DiagnosticDescriptors.RegistrationServiceTypeNotImplementedByClass,
attributeData.ApplicationSyntaxReference?.GetSyntax().GetLocation(),
classSymbol.Name,
serviceType);
}

private static string? GetServiceTypeFromAttribute(AttributeData attribute)
{
var attributeClass = attribute.AttributeClass;

string? serviceType = null;

if (attributeClass?.IsGenericType == true && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length)
{
for (var index = 0; index < attributeClass.TypeParameters.Length; index++)
{
var typeParameter = attributeClass.TypeParameters[index];
var typeArgument = attributeClass.TypeArguments[index];

switch (typeParameter.Name)
{
case "TService":
serviceType = typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
break;
}
}
}

return serviceType;
}
}
}

public static class Module
{
public static bool HasAttribute(IMethodSymbol methodSymbol)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Futurum.Microsoft.Extensions.DependencyInjection.Generator;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RegistrationServiceTypeNotImplementedByClassAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
= ImmutableArray.Create(DiagnosticDescriptors.RegistrationServiceTypeNotImplementedByClass);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSymbolAction(Execute, SymbolKind.NamedType);
}

private static void Execute(SymbolAnalysisContext context)
{
if (context.Symbol is not INamedTypeSymbol classSymbol)
return;

var attributes = Diagnostics.Registration.HasAttribute(classSymbol);
if (!attributes.Any())
return;

foreach (var attribute in attributes)
{
var diagnostics = Diagnostics.Registration.ServiceTypeNotImplementedByClass.Check(classSymbol, attribute);

foreach (var diagnostic in diagnostics)
{
context.ReportDiagnostic(diagnostic);
}
}
}
}
Loading

0 comments on commit 89dcf2c

Please sign in to comment.