ServiceScan.SourceGenerator

repository·main·Indexed 41 days ago

https://github.com/dreamescaper/servicescan.sourcegenerator

A .NET source generator for AOT-compatible dependency injection registration and type discovery. It provides attributes like [GenerateServiceRegistrations] and [ScanForTypes] to replace runtime assembly scanning with compile-time generation, reducing startup performance overhead. Supports filtering by interface, type name, or attributes, and allows custom mapping via Handler and HandlerTemplate.

Tokens
5.6K
Snippets
15
Records
16
Agent score
50%

What's inside ServiceScan.SourceGenerator

  1. Use HandlerTemplate for complex type mapping

    main

    When a standard Handler is insufficient (e.g., you need to pass extra arguments, call static methods with multiple parameters, or perform typeof manipulations), use HandlerTemplate. The placeholder T is replaced with the fully-qualified name of each matched type at code-generation time.

    Examples of HandlerTemplate usage:

    Instantiate types with constructor arguments:

    [ScanForTypes(AssignableTo = typeof(IPlugin), HandlerTemplate = "new T(options)")]
    public static partial IPlugin[] CreatePlugins(PluginOptions options);

    Call a static factory method with multiple parameters:

    [ScanForTypes(AssignableTo = typeof(IPipelineStep), HandlerTemplate = "T.Create(context, logger)")]
    public static partial IPipelineStep[] BuildSteps(PipelineContext context, ILogger logger);

    Build descriptor objects using typeof(T):

    [ScanForTypes(AssignableTo = typeof(ICommandHandler), HandlerTemplate = "new HandlerDescriptor(typeof(T), category)")]
    public static partial HandlerDescriptor[] GetDescriptors(string category);

    Use with void methods (each expression becomes a statement):

    [ScanForTypes(AssignableTo = typeof(IPlugin), HandlerTemplate = "registry.Add(new T(options))")]
    public static partial void RegisterPlugins(PluginRegistry registry, PluginOptions options);
    // Instantiate types with constructor arguments
    [ScanForTypes(AssignableTo = typeof(IPlugin), HandlerTemplate = "new T(options)")]
    public static partial IPlugin[] CreatePlugins(PluginOptions options);
    
    // Call a static factory method that takes multiple parameters
    [ScanForTypes(AssignableTo = typeof(IPipelineStep), HandlerTemplate = "T.Create(context, logger)")]
    public static partial IPipelineStep[] BuildSteps(PipelineContext context, ILogger logger);
    
    // Build descriptor objects using typeof(T) and additional context
    [ScanForTypes(AssignableTo = typeof(ICommandHandler), HandlerTemplate = "new HandlerDescriptor(typeof(T), category)")]
    public static partial HandlerDescriptor[] GetDescriptors(string category);
    
    // Use with void methods (each expanded expression becomes a statement)
    [ScanForTypes(AssignableTo = typeof(IPlugin), HandlerTemplate = "registry.Add(new T(options))")]
    public static partial void RegisterPlugins(PluginRegistry registry, PluginOptions options);
  2. How GenerateServiceRegistrations works

    main

    The [GenerateServiceRegistrations] attribute is applied to a partial method that accepts an IServiceCollection parameter. The source generator implements this method by scanning for types that match your criteria and adding them to the service collection.

    Basic Usage

    To register implementations for a specific interface with a specific lifetime:

    public static partial class ServicesExtensions
    {
        [GenerateServiceRegistrations(AssignableTo = typeof(IMyService), Lifetime = ServiceLifetime.Scoped)]
        public static partial IServiceCollection AddServices(this IServiceCollection services);
    }
    
    // Usage in Program.cs or Startup.cs
    services.AddServices();
    public static partial class ServicesExtensions
    {
        [GenerateServiceRegistrations(AssignableTo = typeof(IMyService), Lifetime = ServiceLifetime.Scoped)]
        public static partial IServiceCollection AddServices(this IServiceCollection services);
    }
    
    // Usage
    services.AddServices();
  3. Understand the compilation invalidation tradeoff in ServiceScan

    main

    ServiceScan uses an incremental generator design that prioritizes IDE responsiveness over immediate consistency. To prevent the discovery scan from rerunning on every keystroke (which would happen if it depended on the full Roslyn Compilation), the generator uses a CombinedProviderComparer that ignores Compilation changes.

    Impact on Development

    • Stale IntelliSense: Editing a candidate type (e.g., adding a new implementation of a scanned interface) might not trigger an immediate update in the IDE's generated code view. The change may only appear after a full build or when the generator's cache is invalidated.
    • Cache Invalidation: The discovery pipeline is only rerun when the attributed method or its configuration (GenerateServiceRegistrations or ScanForTypes) is modified.
    • Build Consistency: While the IDE might show stale results due to caching, command-line builds or MSBuild compilations always create a fresh generator driver and scan the current compilation. This ensures that your actual build output is always accurate and not based on cached IDE state.
  4. Discover and map Minimal API endpoints with ScanForTypes

    main

    The [ScanForTypes] attribute allows you to discover types and execute a specific Handler method for each discovered type. This is useful for mapping endpoints.

    public interface IEndpoint
    {
        abstract static void MapEndpoint(IEndpointRouteBuilder endpoints);
    }
    
    public class HelloWorldEndpoint : IEndpoint
    {
        public static void MapEndpoint(IEndpointRouteBuilder endpoints)
        {
            endpoints.MapGet("/", () => "Hello World!");
        }
    }
    
    public static partial class ServiceCollectionExtensions
    {
        [ScanForTypes(AssignableTo = typeof(IEndpoint), Handler = nameof(IEndpoint.MapEndpoint))]
        public static partial IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints);
    }
  5. Get matched types as a collection using ScanForTypes

    main

    If the method decorated with [ScanForTypes] returns Type[] or IEnumerable<Type> and no Handler is specified, the generator will return a collection of all matched types.

    public static partial class TypeDiscovery
    {
        [ScanForTypes(AssignableTo = typeof(IService))]
        public static partial Type[] GetAllServiceTypes();
    }
  6. Add MediatR handlers

    main

    You can register multiple MediatR handler types (e.g., IRequestHandler<,> and IRequestHandler<>) by applying multiple [GenerateServiceRegistrations] attributes to the same partial method.

    public static IServiceCollection AddMediatR(this IServiceCollection services)
    {
        return services
            .AddTransient<IMediator, Mediator>()
            .AddMediatRHandlers();
    }
    
    [GenerateServiceRegistrations(AssignableTo = typeof(IRequestHandler<>), Lifetime = ServiceLifetime.Transient)]
    [GenerateServiceRegistrations(AssignableTo = typeof(IRequestHandler<,>), Lifetime = ServiceLifetime.Transient)]
    private static partial IServiceCollection AddMediatRHandlers(this IServiceCollection services);
  7. Register Options types using ScanForTypes and AttributeFilter

    main

    You can use [ScanForTypes] with an AttributeFilter to find types decorated with a specific attribute (e.g., a custom OptionAttribute) and pass them to a handler that performs the registration.

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class OptionAttribute(string? section = null) : Attribute
    {
        public string? Section { get; } = section;
    }
    
    [Option]
    public record RootSection { }
    
    [Option("SectionOption")]
    public record SectionOption { }
    
    public static partial class ServiceCollectionExtensions
    {
        [ScanForTypes(AttributeFilter = typeof(OptionAttribute), Handler = nameof(AddOption))]
        public static partial IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration);
    
        private static void AddOption<T>(IServiceCollection services, IConfiguration configuration) where T : class
        {
            var sectionKey = typeof(T).GetCustomAttribute<OptionAttribute>()?.Section;
            var section = sectionKey is null ? configuration : configuration.GetSection(sectionKey);
            services.Configure<T>(section);
        }
    }
  8. Map matched types to a custom result type using ScanForTypes

    main

    When the method returns TResponse[] or IEnumerable<TResponse>, you can specify a Handler that defines how each discovered type is mapped to the response type.

    public static partial class TypeDiscovery
    {
        [ScanForTypes(AssignableTo = typeof(IService), Handler = nameof(Describe))]
        public static partial ServiceDescriptor[] GetServiceDescriptors();
    
        private static ServiceDescriptor Describe<T>() where T : IService
            => ServiceDescriptor.Transient(typeof(IService), typeof(T));
    }
  9. Register repository types using a name filter

    main

    Use TypeNameFilter to match types by name (supporting wildcards) and AsImplementedInterfaces = true to register them as their implemented interfaces.

    [GenerateServiceRegistrations(
        TypeNameFilter = "*Repository",
        AsImplementedInterfaces = true,
        Lifetime = ServiceLifetime.Scoped)]
    public static partial IServiceCollection AddRepositories(this IServiceCollection services);
  10. Apply EF Core IEntityTypeConfiguration types

    main

    Use [ScanForTypes] to find all IEntityTypeConfiguration<T> implementations and apply them to the ModelBuilder.

    public static partial class ModelBuilderExtensions
    {
        [ScanForTypes(AssignableTo = typeof(IEntityTypeConfiguration<>), Handler = nameof(ApplyConfiguration))]
        public static partial ModelBuilder ApplyEntityConfigurations(this ModelBuilder modelBuilder);
    
        private static void ApplyConfiguration<T, TEntity>(ModelBuilder modelBuilder)
            where T : IEntityTypeConfiguration<TEntity>, new()
            where TEntity : class
        {
            modelBuilder.ApplyConfiguration(new T());
        }
    }
  11. Register FluentValidation validators

    main

    Register all classes implementing IValidator<> as singletons. This approach is AOT-compatible and avoids the startup performance penalty of runtime assembly scanning.

    [GenerateServiceRegistrations(AssignableTo = typeof(IValidator<>), Lifetime = ServiceLifetime.Singleton)]
    public static partial IServiceCollection AddValidators(this IServiceCollection services);