< 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: 299
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{
 255631    internal ITypeRegistrar? TypeRegistrar { get; init; }
 255432    internal ITypeFilterer? TypeFilterer { get; init; }
 252833    internal IPluginFactory? PluginFactory { get; init; }
 165934    internal Func<ITypeRegistrar, ITypeFilterer, IPluginFactory, IServiceCollectionPopulator>? ServiceCollectionPopulato
 256735    internal IAssemblyProvider? AssemblyProvider { get; init; }
 174136    internal AssemblyOrdering.AssemblyOrderBuilder? AssemblyOrder { get; init; }
 167937    internal IReadOnlyList<Assembly>? AdditionalAssemblies { get; init; }
 164138    internal IReadOnlyList<Action<IServiceCollection>>? PreRegistrationCallbacks { get; init; }
 245839    internal IReadOnlyList<Action<IServiceCollection>>? PostPluginRegistrationCallbacks { get; init; }
 161040    internal VerificationOptions? VerificationOptions { get; init; }
 41
 42    /// <summary>
 43    /// Factory for creating <see cref="IServiceProviderBuilder"/> instances.
 44    /// </summary>
 252645    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>
 86151    internal ConfiguredSyringe(Syringe source)
 52    {
 86153        ArgumentNullException.ThrowIfNull(source);
 86154        TypeRegistrar = source.TypeRegistrar;
 86155        TypeFilterer = source.TypeFilterer;
 86156        PluginFactory = source.PluginFactory;
 86157        ServiceCollectionPopulatorFactory = source.ServiceCollectionPopulatorFactory;
 86158        AssemblyProvider = source.AssemblyProvider;
 86159        AssemblyOrder = source.AssemblyOrder;
 86160        AdditionalAssemblies = source.AdditionalAssemblies;
 86161        PreRegistrationCallbacks = source.PreRegistrationCallbacks;
 86162        PostPluginRegistrationCallbacks = source.PostPluginRegistrationCallbacks;
 86163        VerificationOptions = source.VerificationOptions;
 86164        ServiceProviderBuilderFactory = source.ServiceProviderBuilderFactory;
 86165    }
 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 supplied <see cref="IConfiguration"/>.
 75    /// </summary>
 76    /// <remarks>
 77    /// <para>
 78    /// The provided <paramref name="config"/> is registered as a singleton
 79    /// <see cref="IConfiguration"/> in the container. All <c>[Options]</c> and
 80    /// <c>[HttpClientOptions]</c> bindings resolve their configuration sections from
 81    /// this instance. If your application uses configuration-bound options, this is
 82    /// the overload you should call — pass an <see cref="IConfiguration"/> that includes
 83    /// <c>appsettings.json</c> and any other sources your options types depend on.
 84    /// </para>
 85    /// <para>
 86    /// Automatically runs container verification based on <see cref="VerificationOptions"/>.
 87    /// </para>
 88    /// </remarks>
 89    /// <param name="config">The configuration to use for building the service provider.
 90    /// This instance is registered in DI and used for all options binding.</param>
 91    /// <returns>The configured <see cref="IServiceProvider"/>.</returns>
 92    /// <exception cref="InvalidOperationException">
 93    /// Thrown if required components (TypeRegistrar, TypeFilterer, PluginFactory, AssemblyProvider) are not configured.
 94    /// </exception>
 95    /// <exception cref="ContainerVerificationException">
 96    /// Thrown if verification issues are detected and the configured behavior is <see cref="VerificationBehavior.Throw"
 97    /// </exception>
 98    public IServiceProvider BuildServiceProvider(
 99        IConfiguration config)
 100    {
 742101        var typeRegistrar = GetOrCreateTypeRegistrar();
 742102        var typeFilterer = GetOrCreateTypeFilterer();
 742103        var pluginFactory = GetOrCreatePluginFactory();
 742104        var serviceCollectionPopulator = GetOrCreateServiceCollectionPopulator(typeRegistrar, typeFilterer, pluginFactor
 742105        var assemblyProvider = GetOrCreateAssemblyProvider();
 742106        var additionalAssemblies = AdditionalAssemblies ?? [];
 742107        var preCallbacks = PreRegistrationCallbacks ?? [];
 742108        var postCallbacks = PostPluginRegistrationCallbacks ?? [];
 742109        var verificationOptions = VerificationOptions ?? Needlr.VerificationOptions.Default;
 110
 111        // Build the list of post-plugin callbacks
 742112        var callbacksWithExtras = new List<Action<IServiceCollection>>(postCallbacks);
 113
 114        // Auto-register options from source-generated bootstrap
 742115        if (SourceGenRegistry.TryGetOptionsRegistrar(out var optionsRegistrar) && optionsRegistrar != null)
 116        {
 912117            callbacksWithExtras.Add(services => optionsRegistrar(services, config));
 118        }
 119
 120        // Auto-register extensions (e.g., FluentValidation) from source-generated bootstrap
 742121        if (SourceGenRegistry.TryGetExtensionRegistrar(out var extensionRegistrar) && extensionRegistrar != null)
 122        {
 10123            callbacksWithExtras.Add(services => extensionRegistrar(services, config));
 124        }
 125
 126        // Add verification as the final callback
 1484127        callbacksWithExtras.Add(services => RunVerification(services, verificationOptions));
 128
 742129        var serviceProviderBuilder = GetOrCreateServiceProviderBuilder(
 742130            serviceCollectionPopulator,
 742131            assemblyProvider,
 742132            additionalAssemblies);
 133
 742134        return serviceProviderBuilder.Build(
 742135            services: new ServiceCollection(),
 742136            config: config,
 742137            preRegistrationCallbacks: [.. preCallbacks],
 742138            postPluginRegistrationCallbacks: callbacksWithExtras);
 139    }
 140
 141    private static void RunVerification(IServiceCollection services, VerificationOptions options)
 142    {
 742143        var issues = new List<VerificationIssue>();
 144
 145        // Check for lifetime mismatches
 742146        if (options.LifetimeMismatchBehavior != VerificationBehavior.Silent)
 147        {
 741148            var mismatches = services.DetectLifetimeMismatches();
 1490149            foreach (var mismatch in mismatches)
 150            {
 4151                issues.Add(new VerificationIssue(
 4152                    Type: VerificationIssueType.LifetimeMismatch,
 4153                    Message: $"Lifetime mismatch: {mismatch.ConsumerServiceType.Name} ({mismatch.ConsumerLifetime}) depe
 4154                    DetailedMessage: mismatch.ToDetailedString(),
 4155                    ConfiguredBehavior: options.LifetimeMismatchBehavior)
 4156                {
 4157                    InvolvedTypes = [mismatch.ConsumerServiceType, mismatch.DependencyServiceType]
 4158                });
 159            }
 160        }
 161
 162        // Process issues based on configured behavior
 746163        var issuesByBehavior = issues.GroupBy(i => i.ConfiguredBehavior);
 164
 1490165        foreach (var group in issuesByBehavior)
 166        {
 4167            switch (group.Key)
 168            {
 169                case VerificationBehavior.Warn:
 8170                    foreach (var issue in group)
 171                    {
 2172                        if (options.IssueReporter is not null)
 173                        {
 2174                            options.IssueReporter(issue);
 175                        }
 176                        else
 177                        {
 0178                            Console.Error.WriteLine($"[Needlr Warning] {issue.Message}");
 0179                            Console.Error.WriteLine(issue.DetailedMessage);
 0180                            Console.Error.WriteLine();
 181                        }
 182                    }
 183                    break;
 184
 185                case VerificationBehavior.Throw:
 2186                    var throwableIssues = group.ToList();
 2187                    if (throwableIssues.Count > 0)
 188                    {
 2189                        throw new ContainerVerificationException(throwableIssues);
 190                    }
 191                    break;
 192            }
 193        }
 740194    }
 195
 196    /// <summary>
 197    /// Gets the configured type registrar.
 198    /// </summary>
 199    /// <exception cref="InvalidOperationException">
 200    /// Thrown if no type registrar is configured. This should not happen if the syringe was created
 201    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 202    /// </exception>
 203    public ITypeRegistrar GetOrCreateTypeRegistrar()
 204    {
 805205        return TypeRegistrar ?? throw new InvalidOperationException(
 805206            "No TypeRegistrar configured. This ConfiguredSyringe was not properly initialized. " +
 805207            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 208    }
 209
 210    /// <summary>
 211    /// Gets the configured type filterer or creates an empty one.
 212    /// </summary>
 213    public ITypeFilterer GetOrCreateTypeFilterer()
 214    {
 805215        return TypeFilterer ?? new EmptyTypeFilterer();
 216    }
 217
 218    /// <summary>
 219    /// Gets the configured plugin factory.
 220    /// </summary>
 221    /// <exception cref="InvalidOperationException">
 222    /// Thrown if no plugin factory is configured. This should not happen if the syringe was created
 223    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 224    /// </exception>
 225    public IPluginFactory GetOrCreatePluginFactory()
 226    {
 805227        return PluginFactory ?? throw new InvalidOperationException(
 805228            "No PluginFactory configured. This ConfiguredSyringe was not properly initialized. " +
 805229            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 230    }
 231
 232    /// <summary>
 233    /// Gets the configured service collection populator or creates a default one.
 234    /// </summary>
 235    public IServiceCollectionPopulator GetOrCreateServiceCollectionPopulator(
 236        ITypeRegistrar typeRegistrar,
 237        ITypeFilterer typeFilterer,
 238        IPluginFactory pluginFactory)
 239    {
 798240        return ServiceCollectionPopulatorFactory?.Invoke(typeRegistrar, typeFilterer, pluginFactory)
 798241            ?? new ServiceCollectionPopulator(typeRegistrar, typeFilterer, pluginFactory);
 242    }
 243
 244    /// <summary>
 245    /// Gets the configured service provider builder or throws if not configured.
 246    /// </summary>
 247    /// <exception cref="InvalidOperationException">
 248    /// Thrown if no service provider builder factory is configured. This should not happen if the syringe was created
 249    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 250    /// </exception>
 251    public IServiceProviderBuilder GetOrCreateServiceProviderBuilder(
 252        IServiceCollectionPopulator serviceCollectionPopulator,
 253        IAssemblyProvider assemblyProvider,
 254        IReadOnlyList<Assembly> additionalAssemblies)
 255    {
 798256        return ServiceProviderBuilderFactory?.Invoke(serviceCollectionPopulator, assemblyProvider, additionalAssemblies)
 798257            ?? throw new InvalidOperationException(
 798258                "No ServiceProviderBuilderFactory configured. This ConfiguredSyringe was not properly initialized. " +
 798259                "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 260    }
 261
 262    /// <summary>
 263    /// Gets the configured assembly provider, with ordering applied if configured.
 264    /// </summary>
 265    /// <exception cref="InvalidOperationException">
 266    /// Thrown if no assembly provider is configured. This should not happen if the syringe was created
 267    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 268    /// </exception>
 269    public IAssemblyProvider GetOrCreateAssemblyProvider()
 270    {
 822271        var provider = AssemblyProvider ?? throw new InvalidOperationException(
 822272            "No AssemblyProvider configured. This ConfiguredSyringe was not properly initialized. " +
 822273            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 274
 275        // Apply ordering if configured
 822276        if (AssemblyOrder != null)
 277        {
 29278            return new OrderedAssemblyProvider(provider, AssemblyOrder);
 279        }
 280
 793281        return provider;
 282    }
 283
 284    /// <summary>
 285    /// Gets the configured additional assemblies.
 286    /// </summary>
 287    public IReadOnlyList<Assembly> GetAdditionalAssemblies()
 288    {
 56289        return AdditionalAssemblies ?? [];
 290    }
 291
 292    /// <summary>
 293    /// Gets the configured post-plugin registration callbacks.
 294    /// </summary>
 295    public IReadOnlyList<Action<IServiceCollection>> GetPostPluginRegistrationCallbacks()
 296    {
 49297        return PostPluginRegistrationCallbacks ?? [];
 298    }
 299}