< Summary

Information
Class: NexusLabs.Needlr.Generators.NeedlrSourceGenBootstrap
Assembly: NexusLabs.Needlr.Generators.Attributes
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators.Attributes/NeedlrSourceGenBootstrap.cs
Line coverage
70%
Covered lines: 93
Uncovered lines: 39
Coverable lines: 132
Total lines: 406
Line coverage: 70.4%
Branch coverage
47%
Covered branches: 20
Total branches: 42
Branch coverage: 47.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_InjectableTypeProvider()100%11100%
get_PluginTypeProvider()100%11100%
get_DecoratorApplier()100%11100%
get_OptionsRegistrar()100%11100%
.cctor()100%11100%
RegisterPlugins(...)100%22100%
Register(...)100%11100%
Register(...)100%11100%
Register(...)50%44100%
RegisterExtension(...)0%620%
TryGetProviders(...)100%66100%
TryGetDecoratorApplier(...)66.66%6676.92%
TryGetOptionsRegistrar(...)0%4260%
TryGetExtensionRegistrar(...)0%2040%
ClearRegistrationsForTesting()100%11100%
BeginTestScope(...)50%44100%
.ctor(...)100%11100%
Dispose()100%11100%
Combine(...)50%8886.95%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators.Attributes/NeedlrSourceGenBootstrap.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Linq;
 4using System.Threading;
 5
 6namespace NexusLabs.Needlr.Generators;
 7
 8/// <summary>
 9/// Runtime bootstrap registry for source-generated Needlr components.
 10/// </summary>
 11/// <remarks>
 12/// The source generator emits a module initializer in the host assembly that calls
 13/// one of the Register overloads with the generated TypeRegistry providers.
 14/// Needlr runtime can then discover generated registries without any runtime reflection.
 15/// </remarks>
 16public static class NeedlrSourceGenBootstrap
 17{
 18    private sealed class Registration
 19    {
 9220        public Registration(
 9221            Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 9222            Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider,
 9223            Action<object>? decoratorApplier = null,
 9224            Action<object, object>? optionsRegistrar = null)
 25        {
 9226            InjectableTypeProvider = injectableTypeProvider;
 9227            PluginTypeProvider = pluginTypeProvider;
 9228            DecoratorApplier = decoratorApplier;
 9229            OptionsRegistrar = optionsRegistrar;
 9230        }
 31
 22432        public Func<IReadOnlyList<InjectableTypeInfo>> InjectableTypeProvider { get; }
 22433        public Func<IReadOnlyList<PluginTypeInfo>> PluginTypeProvider { get; }
 30634        public Action<object>? DecoratorApplier { get; }
 3135        public Action<object, object>? OptionsRegistrar { get; }
 36    }
 37
 2738    private static readonly object _gate = new object();
 2739    private static readonly List<Registration> _registrations = new List<Registration>();
 2740    private static readonly List<Action<object, object>> _extensionRegistrars = new List<Action<object, object>>();
 41
 2742    private static readonly AsyncLocal<Registration?> _asyncLocalOverride = new AsyncLocal<Registration?>();
 43
 44    private static Registration? _cachedCombined;
 45
 46    /// <summary>
 47    /// Registers plugin types that were emitted by another source generator and are therefore
 48    /// invisible to <c>TypeRegistryGenerator</c> at compile time.
 49    /// </summary>
 50    /// <param name="pluginTypeProvider">Provider for the generator-emitted plugin types.</param>
 51    /// <remarks>
 52    /// <para>
 53    /// Roslyn source generators run in isolation — each generator receives the original
 54    /// compilation and cannot see types emitted by other generators. This means
 55    /// <c>TypeRegistryGenerator</c> cannot discover types produced by a second generator
 56    /// (e.g., a <c>CacheProviderGenerator</c> emitting <c>*CacheConfiguration</c> records).
 57    /// </para>
 58    /// <para>
 59    /// The solution is a runtime registration: the second generator emits a
 60    /// <c>[ModuleInitializer]</c> that calls <c>RegisterPlugins()</c>. Module initializers run
 61    /// before any user code, so by the time the application calls
 62    /// <c>IPluginFactory.CreatePluginsFromAssemblies&lt;T&gt;()</c> all providers are combined.
 63    /// </para>
 64    /// <para>
 65    /// Example of what the second generator should emit:
 66    /// <code>
 67    /// [ModuleInitializer]
 68    /// internal static void Initialize()
 69    /// {
 70    ///     NeedlrSourceGenBootstrap.RegisterPlugins(() =>
 71    ///     [
 72    ///         new PluginTypeInfo(
 73    ///             typeof(MyCacheConfiguration),
 74    ///             [typeof(CacheConfiguration)],
 75    ///             static () => new MyCacheConfiguration(),
 76    ///             [])
 77    ///     ]);
 78    /// }
 79    /// </code>
 80    /// </para>
 81    /// </remarks>
 82    public static void RegisterPlugins(Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider)
 83    {
 1784        if (pluginTypeProvider is null) throw new ArgumentNullException(nameof(pluginTypeProvider));
 3385        Register(() => Array.Empty<InjectableTypeInfo>(), pluginTypeProvider);
 1586    }
 87
 88    /// <summary>
 89    /// Registers the generated type and plugin providers for this application.
 90    /// </summary>
 91    public static void Register(
 92        Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 93        Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider)
 94    {
 1695        Register(injectableTypeProvider, pluginTypeProvider, (Action<object>?)null);
 1696    }
 97
 98    /// <summary>
 99    /// Registers the generated type, plugin, and decorator providers for this application.
 100    /// </summary>
 101    /// <param name="injectableTypeProvider">Provider for injectable types.</param>
 102    /// <param name="pluginTypeProvider">Provider for plugin types.</param>
 103    /// <param name="decoratorApplier">
 104    /// Action that applies decorators to the service collection.
 105    /// The parameter is an IServiceCollection, but typed as object to avoid dependency on Microsoft.Extensions.Dependen
 106    /// </param>
 107    public static void Register(
 108        Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 109        Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider,
 110        Action<object>? decoratorApplier)
 111    {
 16112        Register(injectableTypeProvider, pluginTypeProvider, decoratorApplier, null);
 16113    }
 114
 115    /// <summary>
 116    /// Registers the generated type, plugin, decorator, and options providers for this application.
 117    /// </summary>
 118    /// <param name="injectableTypeProvider">Provider for injectable types.</param>
 119    /// <param name="pluginTypeProvider">Provider for plugin types.</param>
 120    /// <param name="decoratorApplier">
 121    /// Action that applies decorators to the service collection.
 122    /// The parameter is an IServiceCollection, but typed as object to avoid dependency on Microsoft.Extensions.Dependen
 123    /// </param>
 124    /// <param name="optionsRegistrar">
 125    /// Action that registers options with the service collection and configuration.
 126    /// Parameters are (IServiceCollection, IConfiguration), typed as object to avoid dependencies.
 127    /// </param>
 128    public static void Register(
 129        Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 130        Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider,
 131        Action<object>? decoratorApplier,
 132        Action<object, object>? optionsRegistrar)
 133    {
 69134        if (injectableTypeProvider is null) throw new ArgumentNullException(nameof(injectableTypeProvider));
 69135        if (pluginTypeProvider is null) throw new ArgumentNullException(nameof(pluginTypeProvider));
 136
 69137        lock (_gate)
 138        {
 69139            _registrations.Add(new Registration(injectableTypeProvider, pluginTypeProvider, decoratorApplier, optionsReg
 69140            _cachedCombined = null;
 69141        }
 69142    }
 143
 144    /// <summary>
 145    /// Registers an extension that provides additional service registrations.
 146    /// Extensions are invoked after the main options registrar during BuildServiceProvider.
 147    /// </summary>
 148    /// <param name="extensionRegistrar">
 149    /// Action that registers extension services with the service collection and configuration.
 150    /// Parameters are (IServiceCollection, IConfiguration), typed as object to avoid dependencies.
 151    /// </param>
 152    /// <remarks>
 153    /// Use this method from extension package module initializers to register additional services.
 154    /// For example, FluentValidation can register its validators without modifying core Needlr.
 155    /// </remarks>
 156    public static void RegisterExtension(Action<object, object> extensionRegistrar)
 157    {
 0158        if (extensionRegistrar is null) throw new ArgumentNullException(nameof(extensionRegistrar));
 159
 0160        lock (_gate)
 161        {
 0162            _extensionRegistrars.Add(extensionRegistrar);
 0163            _cachedCombined = null;
 0164        }
 0165    }
 166
 167    /// <summary>
 168    /// Gets the registered providers (if any).
 169    /// </summary>
 170    public static bool TryGetProviders(
 171        out Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 172        out Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider)
 173    {
 199174        var local = _asyncLocalOverride.Value;
 199175        if (local is not null)
 176        {
 9177            injectableTypeProvider = local.InjectableTypeProvider;
 9178            pluginTypeProvider = local.PluginTypeProvider;
 9179            return true;
 180        }
 181
 190182        lock (_gate)
 183        {
 190184            if (_registrations.Count == 0)
 185            {
 4186                injectableTypeProvider = null!;
 4187                pluginTypeProvider = null!;
 4188                return false;
 189            }
 190
 186191            if (_cachedCombined is null)
 192            {
 13193                _cachedCombined = Combine(_registrations);
 194            }
 195
 186196            injectableTypeProvider = _cachedCombined.InjectableTypeProvider;
 186197            pluginTypeProvider = _cachedCombined.PluginTypeProvider;
 186198            return true;
 199        }
 190200    }
 201
 202    /// <summary>
 203    /// Gets the decorator applier (if any).
 204    /// </summary>
 205    /// <param name="decoratorApplier">
 206    /// Action that applies decorators to the service collection.
 207    /// The parameter is an IServiceCollection, but typed as object to avoid dependency on Microsoft.Extensions.Dependen
 208    /// </param>
 209    /// <returns>True if a decorator applier is registered.</returns>
 210    public static bool TryGetDecoratorApplier(out Action<object>? decoratorApplier)
 211    {
 262212        var local = _asyncLocalOverride.Value;
 262213        if (local is not null)
 214        {
 0215            decoratorApplier = local.DecoratorApplier;
 0216            return decoratorApplier is not null;
 217        }
 218
 262219        lock (_gate)
 220        {
 262221            if (_registrations.Count == 0)
 222            {
 3223                decoratorApplier = null;
 3224                return false;
 225            }
 226
 259227            if (_cachedCombined is null)
 228            {
 0229                _cachedCombined = Combine(_registrations);
 230            }
 231
 259232            decoratorApplier = _cachedCombined.DecoratorApplier;
 259233            return decoratorApplier is not null;
 234        }
 262235    }
 236
 237    /// <summary>
 238    /// Gets the options registrar (if any).
 239    /// </summary>
 240    /// <param name="optionsRegistrar">
 241    /// Action that registers options with the service collection and configuration.
 242    /// Parameters are (IServiceCollection, IConfiguration), typed as object to avoid dependencies.
 243    /// </param>
 244    /// <returns>True if an options registrar is registered.</returns>
 245    public static bool TryGetOptionsRegistrar(out Action<object, object>? optionsRegistrar)
 246    {
 0247        var local = _asyncLocalOverride.Value;
 0248        if (local is not null)
 249        {
 0250            optionsRegistrar = local.OptionsRegistrar;
 0251            return optionsRegistrar is not null;
 252        }
 253
 0254        lock (_gate)
 255        {
 0256            if (_registrations.Count == 0)
 257            {
 0258                optionsRegistrar = null;
 0259                return false;
 260            }
 261
 0262            if (_cachedCombined is null)
 263            {
 0264                _cachedCombined = Combine(_registrations);
 265            }
 266
 0267            optionsRegistrar = _cachedCombined.OptionsRegistrar;
 0268            return optionsRegistrar is not null;
 269        }
 0270    }
 271
 272    /// <summary>
 273    /// Gets the combined extension registrar (if any extensions are registered).
 274    /// </summary>
 275    /// <param name="extensionRegistrar">
 276    /// Combined action that invokes all registered extensions.
 277    /// Parameters are (IServiceCollection, IConfiguration), typed as object to avoid dependencies.
 278    /// </param>
 279    /// <returns>True if any extension registrars are registered.</returns>
 280    public static bool TryGetExtensionRegistrar(out Action<object, object>? extensionRegistrar)
 281    {
 0282        lock (_gate)
 283        {
 0284            if (_extensionRegistrars.Count == 0)
 285            {
 0286                extensionRegistrar = null;
 0287                return false;
 288            }
 289
 0290            var registrars = _extensionRegistrars.ToArray();
 0291            extensionRegistrar = (services, config) =>
 0292            {
 0293                foreach (var registrar in registrars)
 0294                {
 0295                    registrar(services, config);
 0296                }
 0297            };
 0298            return true;
 299        }
 0300    }
 301
 302    internal static void ClearRegistrationsForTesting()
 303    {
 14304        lock (_gate)
 305        {
 14306            _registrations.Clear();
 14307            _cachedCombined = null;
 14308        }
 14309    }
 310
 311    internal static IDisposable BeginTestScope(
 312        Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 313        Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider)
 314    {
 10315        if (injectableTypeProvider is null) throw new ArgumentNullException(nameof(injectableTypeProvider));
 10316        if (pluginTypeProvider is null) throw new ArgumentNullException(nameof(pluginTypeProvider));
 317
 10318        var prior = _asyncLocalOverride.Value;
 10319        _asyncLocalOverride.Value = new Registration(injectableTypeProvider, pluginTypeProvider);
 10320        return new Scope(prior);
 321    }
 322
 323    private sealed class Scope : IDisposable
 324    {
 325        private readonly Registration? _prior;
 326
 10327        public Scope(Registration? prior)
 328        {
 10329            _prior = prior;
 10330        }
 331
 332        public void Dispose()
 333        {
 10334            _asyncLocalOverride.Value = _prior;
 10335        }
 336    }
 337
 338    private static Registration Combine(IReadOnlyList<Registration> registrations)
 339    {
 340        // Snapshot the current registrations to avoid capturing a mutable List.
 42341        var injectableProviders = registrations.Select(r => r.InjectableTypeProvider).ToArray();
 42342        var pluginProviders = registrations.Select(r => r.PluginTypeProvider).ToArray();
 60343        var decoratorAppliers = registrations.Where(r => r.DecoratorApplier is not null).Select(r => r.DecoratorApplier!
 44344        var optionsRegistrars = registrations.Where(r => r.OptionsRegistrar is not null).Select(r => r.OptionsRegistrar!
 345
 346        IReadOnlyList<InjectableTypeInfo> GetInjectableTypes()
 347        {
 348            var result = new List<InjectableTypeInfo>();
 349            var seen = new HashSet<Type>();
 350
 351            foreach (var provider in injectableProviders)
 352            {
 353                foreach (var info in provider())
 354                {
 355                    if (seen.Add(info.Type))
 356                    {
 357                        result.Add(info);
 358                    }
 359                }
 360            }
 361
 362            return result;
 363        }
 364
 365        IReadOnlyList<PluginTypeInfo> GetPluginTypes()
 366        {
 367            var result = new List<PluginTypeInfo>();
 368            var seen = new HashSet<Type>();
 369
 370            foreach (var provider in pluginProviders)
 371            {
 372                foreach (var info in provider())
 373                {
 374                    if (seen.Add(info.PluginType))
 375                    {
 376                        result.Add(info);
 377                    }
 378                }
 379            }
 380
 381            return result;
 382        }
 383
 13384        Action<object>? combinedDecoratorApplier = decoratorAppliers.Length > 0
 13385            ? services =>
 13386            {
 1060387                foreach (var applier in decoratorAppliers)
 13388                {
 274389                    applier(services);
 13390                }
 256391            }
 13392            : null;
 393
 13394        Action<object, object>? combinedOptionsRegistrar = optionsRegistrars.Length > 0
 13395            ? (services, config) =>
 13396            {
 0397                foreach (var registrar in optionsRegistrars)
 13398                {
 0399                    registrar(services, config);
 13400                }
 0401            }
 13402            : null;
 403
 13404        return new Registration(GetInjectableTypes, GetPluginTypes, combinedDecoratorApplier, combinedOptionsRegistrar);
 405    }
 406}

Methods/Properties

.ctor(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.InjectableTypeInfo>>,System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>,System.Action`1<System.Object>,System.Action`2<System.Object,System.Object>)
get_InjectableTypeProvider()
get_PluginTypeProvider()
get_DecoratorApplier()
get_OptionsRegistrar()
.cctor()
RegisterPlugins(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>)
Register(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.InjectableTypeInfo>>,System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>)
Register(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.InjectableTypeInfo>>,System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>,System.Action`1<System.Object>)
Register(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.InjectableTypeInfo>>,System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>,System.Action`1<System.Object>,System.Action`2<System.Object,System.Object>)
RegisterExtension(System.Action`2<System.Object,System.Object>)
TryGetProviders(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.InjectableTypeInfo>>&,System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>&)
TryGetDecoratorApplier(System.Action`1<System.Object>&)
TryGetOptionsRegistrar(System.Action`2<System.Object,System.Object>&)
TryGetExtensionRegistrar(System.Action`2<System.Object,System.Object>&)
ClearRegistrationsForTesting()
BeginTestScope(System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.InjectableTypeInfo>>,System.Func`1<System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.PluginTypeInfo>>)
.ctor(NexusLabs.Needlr.Generators.NeedlrSourceGenBootstrap/Registration)
Dispose()
Combine(System.Collections.Generic.IReadOnlyList`1<NexusLabs.Needlr.Generators.NeedlrSourceGenBootstrap/Registration>)