< 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
96%
Covered lines: 119
Uncovered lines: 4
Coverable lines: 123
Total lines: 342
Line coverage: 96.7%
Branch coverage
95%
Covered branches: 59
Total branches: 62
Branch coverage: 95.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{
 192631    internal ITypeRegistrar? TypeRegistrar { get; init; }
 191832    internal ITypeFilterer? TypeFilterer { get; init; }
 189833    internal IPluginFactory? PluginFactory { get; init; }
 123834    internal Func<ITypeRegistrar, ITypeFilterer, IPluginFactory, IServiceCollectionPopulator>? ServiceCollectionPopulato
 194035    internal IAssemblyProvider? AssemblyProvider { get; init; }
 132236    internal AssemblyOrdering.AssemblyOrderBuilder? AssemblyOrder { get; init; }
 125837    internal IReadOnlyList<Assembly>? AdditionalAssemblies { get; init; }
 120838    internal IReadOnlyList<Action<IServiceCollection>>? PreRegistrationCallbacks { get; init; }
 149639    internal IReadOnlyList<Action<IServiceCollection>>? PostPluginRegistrationCallbacks { get; init; }
 118140    internal VerificationOptions? VerificationOptions { get; init; }
 41
 42    /// <summary>
 43    /// Factory for creating <see cref="IServiceProviderBuilder"/> instances.
 44    /// </summary>
 189745    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>
 64651    internal ConfiguredSyringe(Syringe source)
 52    {
 64653        ArgumentNullException.ThrowIfNull(source);
 64654        TypeRegistrar = source.TypeRegistrar;
 64655        TypeFilterer = source.TypeFilterer;
 64656        PluginFactory = source.PluginFactory;
 64657        ServiceCollectionPopulatorFactory = source.ServiceCollectionPopulatorFactory;
 64658        AssemblyProvider = source.AssemblyProvider;
 64659        AssemblyOrder = source.AssemblyOrder;
 64660        AdditionalAssemblies = source.AdditionalAssemblies;
 64661        PreRegistrationCallbacks = source.PreRegistrationCallbacks;
 64662        PostPluginRegistrationCallbacks = source.PostPluginRegistrationCallbacks;
 64663        VerificationOptions = source.VerificationOptions;
 64664        ServiceProviderBuilderFactory = source.ServiceProviderBuilderFactory;
 64665    }
 66
 67    /// <summary>
 68    /// Default constructor for record initialization syntax.
 69    /// Internal to prevent direct construction - use strategy methods like UsingReflection().
 70    /// </summary>
 4671    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    {
 524101        var typeRegistrar = GetOrCreateTypeRegistrar();
 524102        var typeFilterer = GetOrCreateTypeFilterer();
 524103        var pluginFactory = GetOrCreatePluginFactory();
 524104        var serviceCollectionPopulator = GetOrCreateServiceCollectionPopulator(typeRegistrar, typeFilterer, pluginFactor
 524105        var assemblyProvider = GetOrCreateAssemblyProvider();
 524106        var additionalAssemblies = AdditionalAssemblies ?? [];
 524107        var preCallbacks = PreRegistrationCallbacks ?? [];
 524108        var postCallbacks = PostPluginRegistrationCallbacks ?? [];
 524109        var verificationOptions = VerificationOptions ?? Needlr.VerificationOptions.Default;
 110
 524111        var callbacksWithExtras = ComposePostPluginRegistrationCallbacks(
 524112            postCallbacks,
 1006113            _ => config);
 114
 115        // Add verification as the final callback
 1048116        callbacksWithExtras.Add(services => RunVerification(services, verificationOptions));
 117
 524118        var serviceProviderBuilder = GetOrCreateServiceProviderBuilder(
 524119            serviceCollectionPopulator,
 524120            assemblyProvider,
 524121            additionalAssemblies);
 122
 524123        return serviceProviderBuilder.Build(
 524124            services: new ServiceCollection(),
 524125            config: config,
 524126            preRegistrationCallbacks: [.. preCallbacks],
 524127            postPluginRegistrationCallbacks: callbacksWithExtras);
 128    }
 129
 130    internal static List<Action<IServiceCollection>> ComposePostPluginRegistrationCallbacks(
 131        IReadOnlyList<Action<IServiceCollection>> callbacks,
 132        Func<IServiceCollection, IConfiguration> configurationResolver)
 133    {
 579134        ArgumentNullException.ThrowIfNull(callbacks);
 579135        ArgumentNullException.ThrowIfNull(configurationResolver);
 136
 579137        var callbacksWithRegistrars = new List<Action<IServiceCollection>>(callbacks);
 138
 579139        if (SourceGenRegistry.TryGetOptionsRegistrar(out var optionsRegistrar) &&
 579140            optionsRegistrar is not null)
 141        {
 493142            callbacksWithRegistrars.Add(
 986143                services => optionsRegistrar(
 986144                    services,
 986145                    configurationResolver(services)));
 146        }
 147
 579148        if (SourceGenRegistry.TryGetExtensionRegistrar(out var extensionRegistrar) &&
 579149            extensionRegistrar is not null)
 150        {
 6151            callbacksWithRegistrars.Add(
 12152                services => extensionRegistrar(
 12153                    services,
 12154                    configurationResolver(services)));
 155        }
 156
 579157        return callbacksWithRegistrars;
 158    }
 159
 160    internal static IConfiguration GetRequiredRegisteredConfiguration(
 161        IServiceCollection services,
 162        string compositionPath)
 163    {
 17164        ArgumentNullException.ThrowIfNull(services);
 17165        ArgumentException.ThrowIfNullOrWhiteSpace(compositionPath);
 166
 14654167        for (var index = services.Count - 1; index >= 0; index--)
 168        {
 7310169            var descriptor = services[index];
 7310170            if (descriptor.ServiceType == typeof(IConfiguration) &&
 7310171                descriptor.ImplementationInstance is IConfiguration configuration)
 172            {
 0173                return configuration;
 174            }
 175        }
 176
 17177        using var provider = services.BuildServiceProvider();
 17178        return provider.GetService<IConfiguration>()
 17179            ?? throw new InvalidOperationException(
 17180                $"{compositionPath} could not locate an IConfiguration registration " +
 17181                "when invoking source-generated registrars.");
 17182    }
 183
 184    private static void RunVerification(IServiceCollection services, VerificationOptions options)
 185    {
 524186        var issues = new List<VerificationIssue>();
 187
 188        // Check for lifetime mismatches
 524189        if (options.LifetimeMismatchBehavior != VerificationBehavior.Silent)
 190        {
 522191            var mismatches = services.DetectLifetimeMismatches();
 1058192            foreach (var mismatch in mismatches)
 193            {
 7194                issues.Add(new VerificationIssue(
 7195                    Type: VerificationIssueType.LifetimeMismatch,
 7196                    Message: $"Lifetime mismatch: {mismatch.ConsumerServiceType.Name} ({mismatch.ConsumerLifetime}) depe
 7197                    DetailedMessage: mismatch.ToDetailedString(),
 7198                    ConfiguredBehavior: options.LifetimeMismatchBehavior)
 7199                {
 7200                    InvolvedTypes = [mismatch.ConsumerServiceType, mismatch.DependencyServiceType]
 7201                });
 202            }
 203        }
 204
 205        // Process issues based on configured behavior
 531206        var issuesByBehavior = issues.GroupBy(i => i.ConfiguredBehavior);
 207
 1059208        foreach (var group in issuesByBehavior)
 209        {
 7210            switch (group.Key)
 211            {
 212                case VerificationBehavior.Warn:
 16213                    foreach (var issue in group)
 214                    {
 4215                        if (options.IssueReporter is not null)
 216                        {
 4217                            options.IssueReporter(issue);
 218                        }
 219                        else
 220                        {
 0221                            Console.Error.WriteLine($"[Needlr Warning] {issue.Message}");
 0222                            Console.Error.WriteLine(issue.DetailedMessage);
 0223                            Console.Error.WriteLine();
 224                        }
 225                    }
 226                    break;
 227
 228                case VerificationBehavior.Throw:
 3229                    var throwableIssues = group.ToList();
 3230                    if (throwableIssues.Count > 0)
 231                    {
 3232                        throw new ContainerVerificationException(throwableIssues);
 233                    }
 234                    break;
 235            }
 236        }
 521237    }
 238
 239    /// <summary>
 240    /// Gets the configured type registrar.
 241    /// </summary>
 242    /// <exception cref="InvalidOperationException">
 243    /// Thrown if no type registrar is configured. This should not happen if the syringe was created
 244    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 245    /// </exception>
 246    public ITypeRegistrar GetOrCreateTypeRegistrar()
 247    {
 598248        return TypeRegistrar ?? throw new InvalidOperationException(
 598249            "No TypeRegistrar configured. This ConfiguredSyringe was not properly initialized. " +
 598250            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 251    }
 252
 253    /// <summary>
 254    /// Gets the configured type filterer or creates an empty one.
 255    /// </summary>
 256    public ITypeFilterer GetOrCreateTypeFilterer()
 257    {
 598258        return TypeFilterer ?? new EmptyTypeFilterer();
 259    }
 260
 261    /// <summary>
 262    /// Gets the configured plugin factory.
 263    /// </summary>
 264    /// <exception cref="InvalidOperationException">
 265    /// Thrown if no plugin factory is configured. This should not happen if the syringe was created
 266    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 267    /// </exception>
 268    public IPluginFactory GetOrCreatePluginFactory()
 269    {
 598270        return PluginFactory ?? throw new InvalidOperationException(
 598271            "No PluginFactory configured. This ConfiguredSyringe was not properly initialized. " +
 598272            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 273    }
 274
 275    /// <summary>
 276    /// Gets the configured service collection populator or creates a default one.
 277    /// </summary>
 278    public IServiceCollectionPopulator GetOrCreateServiceCollectionPopulator(
 279        ITypeRegistrar typeRegistrar,
 280        ITypeFilterer typeFilterer,
 281        IPluginFactory pluginFactory)
 282    {
 591283        return ServiceCollectionPopulatorFactory?.Invoke(typeRegistrar, typeFilterer, pluginFactory)
 591284            ?? new ServiceCollectionPopulator(typeRegistrar, typeFilterer, pluginFactory);
 285    }
 286
 287    /// <summary>
 288    /// Gets the configured service provider builder or throws if not configured.
 289    /// </summary>
 290    /// <exception cref="InvalidOperationException">
 291    /// Thrown if no service provider builder factory is configured. This should not happen if the syringe was created
 292    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 293    /// </exception>
 294    public IServiceProviderBuilder GetOrCreateServiceProviderBuilder(
 295        IServiceCollectionPopulator serviceCollectionPopulator,
 296        IAssemblyProvider assemblyProvider,
 297        IReadOnlyList<Assembly> additionalAssemblies)
 298    {
 591299        return ServiceProviderBuilderFactory?.Invoke(serviceCollectionPopulator, assemblyProvider, additionalAssemblies)
 591300            ?? throw new InvalidOperationException(
 591301                "No ServiceProviderBuilderFactory configured. This ConfiguredSyringe was not properly initialized. " +
 591302                "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 303    }
 304
 305    /// <summary>
 306    /// Gets the configured assembly provider, with ordering applied if configured.
 307    /// </summary>
 308    /// <exception cref="InvalidOperationException">
 309    /// Thrown if no assembly provider is configured. This should not happen if the syringe was created
 310    /// via UsingReflection(), UsingSourceGen(), or UsingAutoConfiguration().
 311    /// </exception>
 312    public IAssemblyProvider GetOrCreateAssemblyProvider()
 313    {
 617314        var provider = AssemblyProvider ?? throw new InvalidOperationException(
 617315            "No AssemblyProvider configured. This ConfiguredSyringe was not properly initialized. " +
 617316            "Use new Syringe().UsingSourceGen(), .UsingReflection(), or .UsingAutoConfiguration().");
 317
 318        // Apply ordering if configured
 616319        if (AssemblyOrder != null)
 320        {
 30321            return new OrderedAssemblyProvider(provider, AssemblyOrder);
 322        }
 323
 586324        return provider;
 325    }
 326
 327    /// <summary>
 328    /// Gets the configured additional assemblies.
 329    /// </summary>
 330    public IReadOnlyList<Assembly> GetAdditionalAssemblies()
 331    {
 67332        return AdditionalAssemblies ?? [];
 333    }
 334
 335    /// <summary>
 336    /// Gets the configured post-plugin registration callbacks.
 337    /// </summary>
 338    public IReadOnlyList<Action<IServiceCollection>> GetPostPluginRegistrationCallbacks()
 339    {
 60340        return PostPluginRegistrationCallbacks ?? [];
 341    }
 342}