I like the idea of resolving services in the IoC using attributes. I wanted to replace System.Reflection
with source generation, so it improves startup time.
The code snippet below is also on GitHub.
#1 System.Reflection
[Service(typeof(IListenerService), ServiceLifetime.Scoped)]
public class ListenerService : IListenerService
{
...
}
public static class Bootstrapper
{
public static void ConfigureServices(IServiceCollection serviceCollection)
{
IEnumerable<Assembly> assemblies = GetAssemblies();
List<KeyValuePair<Type, ServiceAttribute>> serviceAttributes = assemblies
.SelectMany(x => x.DefinedTypes)
.SelectMany(x =>
x.GetCustomAttributes<ServiceAttribute>(false)
.Select(y => new KeyValuePair<Type, ServiceAttribute>(x, y)))
.ToList();
foreach ((Type type, ServiceAttribute serviceAttribute) in serviceAttributes)
{
serviceCollection.Add(new ServiceDescriptor(serviceAttribute.Type, type,
serviceAttribute.ServiceLifetime));
}
}
private static IEnumerable<Assembly> GetAssemblies()
{
List<Assembly> assemblies = GetAssemblies(Assembly.GetEntryAssembly()).Distinct().ToList();
return assemblies;
}
private static IEnumerable<Assembly> GetAssemblies(Assembly assembly)
{
string namespacePrefix = typeof(Bootstrapper).Namespace?.Split(".")[0];
foreach (Assembly referencedAssembly in assembly.GetReferencedAssemblies()
.Where(x => x.Name != null && x.Name.StartsWith(namespacePrefix ?? Empty)).Select(Assembly.Load)
.SelectMany(GetAssemblies))
{
yield return referencedAssembly;
}
yield return assembly;
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ServiceAttribute : Attribute
{
public ServiceAttribute(Type type)
{
Type = type;
ServiceLifetime = ServiceLifetime.Scoped;
}
public ServiceAttribute(Type type, ServiceLifetime serviceLifetime)
{
Type = type;
ServiceLifetime = serviceLifetime;
}
public Type Type { get; }
public ServiceLifetime ServiceLifetime { get; }
}
#2 Source Generation
I made a Class Library called Enliven.SourceGenerator
and put the following classes:
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Enliven.SourceGenerator;
[Generator]
public class ServiceRegistrationGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
var serviceTypes = context.Compilation.SyntaxTrees
.SelectMany(tree => tree.GetRoot().DescendantNodes())
.OfType<ClassDeclarationSyntax>()
.Where(classDecl => classDecl.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString() == nameof(ServiceAttribute)))
.ToList();
if (serviceTypes.Count == 0)
{
return;
}
var code = new StringBuilder();
code.AppendLine("using Microsoft.Extensions.DependencyInjection;");
code.AppendLine("public static class Bootstrapper {");
code.AppendLine(" public static void ConfigureServices(IServiceCollection serviceCollection) {");
foreach (var serviceType in serviceTypes)
{
var serviceAttribute = serviceType.AttributeLists
.SelectMany(al => al.Attributes)
.First(a => a.Name.ToString() == nameof(ServiceAttribute));
var serviceTypeName = serviceType.Identifier.Text;
var serviceLifetime = serviceAttribute.ArgumentList!.Arguments[1].Expression.ToString();
code.AppendLine($" serviceCollection.Add(new ServiceDescriptor({serviceAttribute.ArgumentList.Arguments[0].Expression}, typeof({serviceTypeName}), ServiceLifetime.{serviceLifetime}));");
}
code.AppendLine(" }");
code.AppendLine("}");
context.AddSource("Bootstrapper", SourceText.From(code.ToString(), Encoding.UTF8));
}
public void Initialize(GeneratorInitializationContext context)
{
}
}
using Microsoft.Extensions.DependencyInjection;
namespace Enliven.SourceGenerator;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ServiceAttribute : Attribute
{
public ServiceAttribute(Type type, ServiceLifetime serviceLifetime)
{
Type = type;
ServiceLifetime = serviceLifetime;
}
public Type Type { get; }
public ServiceLifetime ServiceLifetime { get; }
}
The issue
I referenced the library in the Enliven.Host
and did the following. The issue is it doesn't find the generated class Bootstrapper. Why?
using Enliven.Observability;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
var host = new HostBuilder()
.ConfigureAppConfiguration(c => c
.AddEnvironmentVariables()
.AddJsonFile($"appsettings.{environment}.json"))
.ConfigureServices((context, services) =>
{
// Configure services
Bootstrapper.ConfigureServices(services);
})
.AddEnlivenLogging()
.UseConsoleLifetime()
.Build();
await host.RunAsync();