< Summary

Information
Class: NexusLabs.Needlr.Injection.ConfiguredSyringe
Assembly: NexusLabs.Needlr.Injection
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Injection/ConfiguredSyringe.cs
Line coverage
95%
Covered lines: 92
Uncovered lines: 4
Coverable lines: 96
Total lines: 286
Line coverage: 95.8%
Branch coverage
85%
Covered branches: 46
Total branches: 54
Branch coverage: 85.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Injection/ConfiguredSyringe.cs

#LineLine coverage
 1using Microsoft.Extensions.Configuration;
 2using Microsoft.Extensions.DependencyInjection;
 3
 4using NexusLabs.Needlr.Injection.TypeFilterers;
 5
 6using System.Reflection;
 7
 8namespace NexusLabs.Needlr.Injection;
 9
 10/// <summary>
 11/// Represents a Syringe that has been configured with a strategy (UsingReflection, UsingSourceGen, or UsingAutoConfigur
 12/// This type has access to all configuration extension methods and can build a service provider.
 13/// </summary>
 14/// <remarks>
 15/// <para>
 16/// ConfiguredSyringe is created by calling one of the strategy methods on <see cref="Syringe"/>:
 17/// </para>
 18/// <list type="bullet">
 19/// <item><c>new Syringe().UsingReflection()</c> - uses reflection-based type discovery</item>
 20/// <item><c>new Syringe().UsingSourceGen()</c> - uses source-generated type discovery</item>
 21/// <item><c>new Syringe().UsingAutoConfiguration()</c> - automatically selects best available strategy</item>
 22/// </list>
 23/// <para>
 24/// Once configured, use extension methods to further customize the container, then call
 25/// <see cref="BuildServiceProvider(IConfiguration)"/> to create the service provider.
 26/// </para>
 27/// </remarks>
 28[DoNotAutoRegister]
 29public sealed record ConfiguredSyringe
 30{
 174931    internal ITypeRegistrar? TypeRegistrar { get; init; }
 174732    internal ITypeFilterer? TypeFilterer { get; init; }
 172133    internal IPluginFactory? PluginFactory { get; init; }
 112334    internal Func<ITypeRegistrar, ITypeFilterer, IPluginFactory, IServiceCollectionPopulator>? ServiceCollectionPopulato
 176035    internal IAssemblyProvider? AssemblyProvider { get; init; }
 120536    internal AssemblyOrdering.AssemblyOrderBuilder? AssemblyOrder { get; init; }
 113637    internal IReadOnlyList<Assembly>? AdditionalAssemblies { get; init; }
 111838    internal IReadOnlyList<Action<IServiceCollection>>? PreRegistrationCallbacks { get; init; }
 141439    internal IReadOnlyList<Action<IServiceCollection>>? PostPluginRegistrationCallbacks { get; init; }
 108940    internal VerificationOptions? VerificationOptions { get; init; }
 41
 42    /// <summary>
 43    /// Factory for creating <see cref="IServiceProviderBuilder"/> instances.
 44    /// </summary>
 171945    internal Func<IServiceCollectionPopulator, IAssemblyProvider, IReadOnlyList<Assembly>, IServiceProviderBuilder>? Ser
 46
 47    /// <summary>
 48    /// Creates a ConfiguredSyringe from a base Syringe, copying all properties.
 49    /// </summary>
 50    /// <param name="source">The source Syringe to copy from.</param>
 59051    internal ConfiguredSyringe(Syringe source)
 52    {
 59053        ArgumentNullException.ThrowIfNull(source);
 59054        TypeRegistrar = source.TypeRegistrar;
 59055        TypeFilterer = source.TypeFilterer;
 59056        PluginFactory = source.PluginFactory;
 59057        ServiceCollectionPopulatorFactory = source.ServiceCollectionPopulatorFactory;
 59058        AssemblyProvider = source.AssemblyProvider;
 59059        AssemblyOrder = source.AssemblyOrder;
 59060        AdditionalAssemblies = source.AdditionalAssemblies;
 59061        PreRegistrationCallbacks = source.PreRegistrationCallbacks;
 59062        PostPluginRegistrationCallbacks = source.PostPluginRegistrationCallbacks;
 59063        VerificationOptions = source.VerificationOptions;
 59064        ServiceProviderBuilderFactory = source.ServiceProviderBuilderFactory;
 59065    }
 66
 67    /// <summary>
 68    /// Default constructor for record initialization syntax.
 69    /// Internal to prevent direct construction - use strategy methods like UsingReflection().
 70    /// </summary>
 071    internal ConfiguredSyringe() { }
 72
 73    /// <summary>
 74    /// Builds a service provider with the configured settings.
 75    /// Automatically runs container verification based on <see cref="VerificationOptions"/>.
 76    /// </summary>
 77    /// <param name="config">The configuration to use for building the service provider.</param>
 78    /// <returns>The configured <see cref="IServiceProvider"/>.</returns>
 79    /// <exception cref="InvalidOperationException">
 80    /// Thrown if required components (TypeRegistrar, TypeFilterer, PluginFactory, AssemblyProvider) are not configured.
 81    /// </exception>
 82    /// <exception cref="ContainerVerificationException">
 83    /// Thrown if verification issues are detected and the configured behavior is <see cref="VerificationBehavior.Throw"
 84    /// </exception>
 85    public IServiceProvider BuildServiceProvider(
 86        IConfiguration config)
 87    {
 49288        var typeRegistrar = GetOrCreateTypeRegistrar();
 49289        var typeFilterer = GetOrCreateTypeFilterer();
 49290        var pluginFactory = GetOrCreatePluginFactory();
 49291        var serviceCollectionPopulator = GetOrCreateServiceCollectionPopulator(typeRegistrar, typeFilterer, pluginFactor
 49292        var assemblyProvider = GetOrCreateAssemblyProvider();
 49293        var additionalAssemblies = AdditionalAssemblies ?? [];
 49294        var preCallbacks = PreRegistrationCallbacks ?? [];
 49295        var postCallbacks = PostPluginRegistrationCallbacks ?? [];
 49296        var verificationOptions = VerificationOptions ?? Needlr.VerificationOptions.Default;
 97
 98        // Build the list of post-plugin callbacks
 49299        var callbacksWithExtras = new List<Action<IServiceCollection>>(postCallbacks);
 100
 101        // Auto-register options from source-generated bootstrap
 492102        if (SourceGenRegistry.TryGetOptionsRegistrar(out var optionsRegistrar) && optionsRegistrar != null)
 103        {
 872104            callbacksWithExtras.Add(services => optionsRegistrar(services, config));
 105        }
 106
 107        // Auto-register extensions (e.g., FluentValidation) from source-generated bootstrap
 492108        if (SourceGenRegistry.TryGetExtensionRegistrar(out var extensionRegistrar) && extensionRegistrar != null)
 109        {
 10110            callbacksWithExtras.Add(services => extensionRegistrar(services, config));
 111        }
 112
 113        // Add verification as the final callback
 984114        callbacksWithExtras.Add(services => RunVerification(services, verificationOptions));
 115
 492116        var serviceProviderBuilder = GetOrCreateServiceProviderBuilder(
 492117            serviceCollectionPopulator,
 492118            assemblyProvider,
 492119            additionalAssemblies);
 120
 492121        return serviceProviderBuilder.Build(
 492122            services: new ServiceCollection(),
 492123            config: config,
 492124            preRegistrationCallbacks: [.. preCallbacks],
 492125            postPluginRegistrationCallbacks: callbacksWithExtras);
 126    }
 127
 128    private static void RunVerification(IServiceCollection services, VerificationOptions options)
 129    {
 492130        var issues = new List<VerificationIssue>();
 131
 132        // Check for lifetime mismatches
 492133        if (options.LifetimeMismatchBehavior != VerificationBehavior.Silent)
 134        {
 491135            var mismatches = services.DetectLifetimeMismatches();
 990136            foreach (var mismatch in mismatches)
 137            {
 4138                issues.Add(new VerificationIssue(
 4139                    Type: VerificationIssueType.LifetimeMismatch,
 4140                    Message: $"Lifetime mismatch: {mismatch.ConsumerServiceType.Name} ({mismatch.ConsumerLifetime}) depe
 4141                    DetailedMessage: mismatch.ToDetailedString(),
 4142                    ConfiguredBehavior: options.LifetimeMismatchBehavior)
 4143                {
 4144                    InvolvedTypes = [mismatch.ConsumerServiceType, mismatch.DependencyServiceType]
 4145                });
 146            }
 147        }
 148
 149        // Process issues based on configured behavior
 496150        var issuesByBehavior = issues.GroupBy(i => i.ConfiguredBehavior);
 151
 990152        foreach (var group in issuesByBehavior)
 153        {
 4154            switch (group.Key)
 155            {
 156                case VerificationBehavior.Warn:
 8157                    foreach (var issue in group)
 158                    {
 2159                        if (options.IssueReporter is not null)
 160                        {
 2161                            options.IssueReporter(issue);
 162                        }
 163                        else
 164                        {
 0165                            Console.Error.WriteLine($"[Needlr Warning] {issue.Message}");
 0166                            Console.Error.WriteLine(issue.DetailedMessage);
 0167                            Console.Error.WriteLine();
 168                        }
 169                    }
 170                    break;
 171
 172                case VerificationBehavior.Throw:
 2173                    var throwableIssues = group.ToList();
 2174                    if (throwableIssues.Count > 0)
 175                    {
 2176                        throw new ContainerVerificationException(throwableIssues);
 177                    }
 178                    break;
 179            }
 180        }
 490181    }
 182
 183    /// <summary>
 184    /// Gets the configured type registrar.
 185    /// </summary>
 186    /// <exception cref="InvalidOperationException">
 187    /// Thrown if no type registrar is configured. This should not happen if the syringe was created
 188    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 189    /// </exception>
 190    public ITypeRegistrar GetOrCreateTypeRegistrar()
 191    {
 540192        return TypeRegistrar ?? throw new InvalidOperationException(
 540193            "No TypeRegistrar configured. This ConfiguredSyringe was not properly initialized. " +
 540194            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 195    }
 196
 197    /// <summary>
 198    /// Gets the configured type filterer or creates an empty one.
 199    /// </summary>
 200    public ITypeFilterer GetOrCreateTypeFilterer()
 201    {
 540202        return TypeFilterer ?? new EmptyTypeFilterer();
 203    }
 204
 205    /// <summary>
 206    /// Gets the configured plugin factory.
 207    /// </summary>
 208    /// <exception cref="InvalidOperationException">
 209    /// Thrown if no plugin factory is configured. This should not happen if the syringe was created
 210    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 211    /// </exception>
 212    public IPluginFactory GetOrCreatePluginFactory()
 213    {
 540214        return PluginFactory ?? throw new InvalidOperationException(
 540215            "No PluginFactory configured. This ConfiguredSyringe was not properly initialized. " +
 540216            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 217    }
 218
 219    /// <summary>
 220    /// Gets the configured service collection populator or creates a default one.
 221    /// </summary>
 222    public IServiceCollectionPopulator GetOrCreateServiceCollectionPopulator(
 223        ITypeRegistrar typeRegistrar,
 224        ITypeFilterer typeFilterer,
 225        IPluginFactory pluginFactory)
 226    {
 533227        return ServiceCollectionPopulatorFactory?.Invoke(typeRegistrar, typeFilterer, pluginFactory)
 533228            ?? new ServiceCollectionPopulator(typeRegistrar, typeFilterer, pluginFactory);
 229    }
 230
 231    /// <summary>
 232    /// Gets the configured service provider builder or throws if not configured.
 233    /// </summary>
 234    /// <exception cref="InvalidOperationException">
 235    /// Thrown if no service provider builder factory is configured. This should not happen if the syringe was created
 236    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 237    /// </exception>
 238    public IServiceProviderBuilder GetOrCreateServiceProviderBuilder(
 239        IServiceCollectionPopulator serviceCollectionPopulator,
 240        IAssemblyProvider assemblyProvider,
 241        IReadOnlyList<Assembly> additionalAssemblies)
 242    {
 533243        return ServiceProviderBuilderFactory?.Invoke(serviceCollectionPopulator, assemblyProvider, additionalAssemblies)
 533244            ?? throw new InvalidOperationException(
 533245                "No ServiceProviderBuilderFactory configured. This ConfiguredSyringe was not properly initialized. " +
 533246                "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 247    }
 248
 249    /// <summary>
 250    /// Gets the configured assembly provider, with ordering applied if configured.
 251    /// </summary>
 252    /// <exception cref="InvalidOperationException">
 253    /// Thrown if no assembly provider is configured. This should not happen if the syringe was created
 254    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 255    /// </exception>
 256    public IAssemblyProvider GetOrCreateAssemblyProvider()
 257    {
 557258        var provider = AssemblyProvider ?? throw new InvalidOperationException(
 557259            "No AssemblyProvider configured. This ConfiguredSyringe was not properly initialized. " +
 557260            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 261
 262        // Apply ordering if configured
 557263        if (AssemblyOrder != null)
 264        {
 29265            return new OrderedAssemblyProvider(provider, AssemblyOrder);
 266        }
 267
 528268        return provider;
 269    }
 270
 271    /// <summary>
 272    /// Gets the configured additional assemblies.
 273    /// </summary>
 274    public IReadOnlyList<Assembly> GetAdditionalAssemblies()
 275    {
 41276        return AdditionalAssemblies ?? [];
 277    }
 278
 279    /// <summary>
 280    /// Gets the configured post-plugin registration callbacks.
 281    /// </summary>
 282    public IReadOnlyList<Action<IServiceCollection>> GetPostPluginRegistrationCallbacks()
 283    {
 34284        return PostPluginRegistrationCallbacks ?? [];
 285    }
 286}