< Summary

Information
Class: NexusLabs.Needlr.Generators.NeedlrSourceGenBootstrap
Assembly: NexusLabs.Needlr.Generators.Attributes
File(s): /actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators.Attributes/NeedlrSourceGenBootstrap.cs
Line coverage
100%
Covered lines: 133
Uncovered lines: 0
Coverable lines: 133
Total lines: 433
Line coverage: 100%
Branch coverage
100%
Covered branches: 42
Total branches: 42
Branch coverage: 100%
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(...)100%44100%
RegisterExtension(...)100%22100%
TryGetProviders(...)100%66100%
TryGetDecoratorApplier(...)100%66100%
TryGetOptionsRegistrar(...)100%66100%
TryGetExtensionRegistrar(...)100%44100%
ClearRegistrationsForTesting()100%11100%
BeginTestScope(...)100%44100%
.ctor(...)100%11100%
Dispose()100%11100%
Combine(...)100%88100%

File(s)

/actions-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    {
 26520        public Registration(
 26521            Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 26522            Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider,
 26523            Action<object>? decoratorApplier = null,
 26524            Action<object, object>? optionsRegistrar = null)
 25        {
 26526            InjectableTypeProvider = injectableTypeProvider;
 26527            PluginTypeProvider = pluginTypeProvider;
 26528            DecoratorApplier = decoratorApplier;
 26529            OptionsRegistrar = optionsRegistrar;
 26530        }
 31
 235432        public Func<IReadOnlyList<InjectableTypeInfo>> InjectableTypeProvider { get; }
 235433        public Func<IReadOnlyList<PluginTypeInfo>> PluginTypeProvider { get; }
 240434        public Action<object>? DecoratorApplier { get; }
 207035        public Action<object, object>? OptionsRegistrar { get; }
 36    }
 37
 2938    private static readonly object _gate = new object();
 2939    private static readonly List<Registration> _registrations = new List<Registration>();
 2940    private static readonly List<Action<object, object>> _extensionRegistrars = new List<Action<object, object>>();
 41
 2942    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    {
 1684        if (pluginTypeProvider is null) throw new ArgumentNullException(nameof(pluginTypeProvider));
 3285        Register(() => Array.Empty<InjectableTypeInfo>(), pluginTypeProvider);
 1486    }
 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    {
 9695        Register(injectableTypeProvider, pluginTypeProvider, (Action<object>?)null);
 9496    }
 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    {
 101112        Register(injectableTypeProvider, pluginTypeProvider, decoratorApplier, null);
 99113    }
 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    {
 164134        if (injectableTypeProvider is null) throw new ArgumentNullException(nameof(injectableTypeProvider));
 163135        if (pluginTypeProvider is null) throw new ArgumentNullException(nameof(pluginTypeProvider));
 136
 161137        lock (_gate)
 138        {
 161139            _registrations.Add(new Registration(injectableTypeProvider, pluginTypeProvider, decoratorApplier, optionsReg
 161140            _cachedCombined = null;
 161141        }
 161142    }
 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    /// <para>
 154    /// Use this method from extension package module initializers to register additional services.
 155    /// For example, FluentValidation can register its validators without modifying core Needlr.
 156    /// </para>
 157    /// <para>
 158    /// Needlr's own runtime composition (<c>ConfiguredSyringe</c>, <c>WebApplicationSyringe</c>,
 159    /// and <c>MauiSyringe</c>) reads options and extension registrars from
 160    /// <c>NexusLabs.Needlr.SourceGenRegistry</c> so that <c>NexusLabs.Needlr.Injection</c> does not
 161    /// need a dependency on this assembly. Extension packages that already depend on
 162    /// <c>NexusLabs.Needlr.Generators.Attributes</c> may keep using this registry; extension
 163    /// packages that only depend on <c>NexusLabs.Needlr</c> should use
 164    /// <c>SourceGenRegistry.RegisterExtension</c> instead.
 165    /// </para>
 166    /// </remarks>
 167    public static void RegisterExtension(Action<object, object> extensionRegistrar)
 168    {
 11169        if (extensionRegistrar is null) throw new ArgumentNullException(nameof(extensionRegistrar));
 170
 9171        lock (_gate)
 172        {
 9173            _extensionRegistrars.Add(extensionRegistrar);
 9174            _cachedCombined = null;
 9175        }
 9176    }
 177
 178    /// <summary>
 179    /// Gets the registered providers (if any).
 180    /// </summary>
 181    public static bool TryGetProviders(
 182        out Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 183        out Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider)
 184    {
 300185        var local = _asyncLocalOverride.Value;
 300186        if (local is not null)
 187        {
 13188            injectableTypeProvider = local.InjectableTypeProvider;
 13189            pluginTypeProvider = local.PluginTypeProvider;
 13190            return true;
 191        }
 192
 287193        lock (_gate)
 194        {
 287195            if (_registrations.Count == 0)
 196            {
 7197                injectableTypeProvider = null!;
 7198                pluginTypeProvider = null!;
 7199                return false;
 200            }
 201
 280202            if (_cachedCombined is null)
 203            {
 82204                _cachedCombined = Combine(_registrations);
 205            }
 206
 280207            injectableTypeProvider = _cachedCombined.InjectableTypeProvider;
 280208            pluginTypeProvider = _cachedCombined.PluginTypeProvider;
 280209            return true;
 210        }
 287211    }
 212
 213    /// <summary>
 214    /// Gets the decorator applier (if any).
 215    /// </summary>
 216    /// <param name="decoratorApplier">
 217    /// Action that applies decorators to the service collection.
 218    /// The parameter is an IServiceCollection, but typed as object to avoid dependency on Microsoft.Extensions.Dependen
 219    /// </param>
 220    /// <returns>True if a decorator applier is registered.</returns>
 221    public static bool TryGetDecoratorApplier(out Action<object>? decoratorApplier)
 222    {
 326223        var local = _asyncLocalOverride.Value;
 326224        if (local is not null)
 225        {
 2226            decoratorApplier = local.DecoratorApplier;
 2227            return decoratorApplier is not null;
 228        }
 229
 324230        lock (_gate)
 231        {
 324232            if (_registrations.Count == 0)
 233            {
 8234                decoratorApplier = null;
 8235                return false;
 236            }
 237
 316238            if (_cachedCombined is null)
 239            {
 3240                _cachedCombined = Combine(_registrations);
 241            }
 242
 316243            decoratorApplier = _cachedCombined.DecoratorApplier;
 316244            return decoratorApplier is not null;
 245        }
 324246    }
 247
 248    /// <summary>
 249    /// Gets the options registrar (if any).
 250    /// </summary>
 251    /// <param name="optionsRegistrar">
 252    /// Action that registers options with the service collection and configuration.
 253    /// Parameters are (IServiceCollection, IConfiguration), typed as object to avoid dependencies.
 254    /// </param>
 255    /// <returns>True if an options registrar is registered.</returns>
 256    /// <remarks>
 257    /// The options registrar is supplied by the generated module initializer through the
 258    /// four-argument <see cref="Register(Func{IReadOnlyList{InjectableTypeInfo}}, Func{IReadOnlyList{PluginTypeInfo}}, 
 259    /// overload. Needlr's own runtime composition reads options registrars from
 260    /// <c>NexusLabs.Needlr.SourceGenRegistry</c>; this accessor exists for hosts that compose
 261    /// generated registrations directly against this assembly.
 262    /// </remarks>
 263    public static bool TryGetOptionsRegistrar(out Action<object, object>? optionsRegistrar)
 264    {
 6265        var local = _asyncLocalOverride.Value;
 6266        if (local is not null)
 267        {
 1268            optionsRegistrar = local.OptionsRegistrar;
 1269            return optionsRegistrar is not null;
 270        }
 271
 5272        lock (_gate)
 273        {
 5274            if (_registrations.Count == 0)
 275            {
 2276                optionsRegistrar = null;
 2277                return false;
 278            }
 279
 3280            if (_cachedCombined is null)
 281            {
 3282                _cachedCombined = Combine(_registrations);
 283            }
 284
 3285            optionsRegistrar = _cachedCombined.OptionsRegistrar;
 3286            return optionsRegistrar is not null;
 287        }
 5288    }
 289
 290    /// <summary>
 291    /// Gets the combined extension registrar (if any extensions are registered).
 292    /// </summary>
 293    /// <param name="extensionRegistrar">
 294    /// Combined action that invokes all registered extensions.
 295    /// Parameters are (IServiceCollection, IConfiguration), typed as object to avoid dependencies.
 296    /// </param>
 297    /// <returns>True if any extension registrars are registered.</returns>
 298    /// <remarks>
 299    /// The returned action invokes every registrar in registration order. Unlike
 300    /// <see cref="TryGetProviders"/>, extension registrars are not affected by test scopes.
 301    /// </remarks>
 302    public static bool TryGetExtensionRegistrar(out Action<object, object>? extensionRegistrar)
 303    {
 7304        lock (_gate)
 305        {
 7306            if (_extensionRegistrars.Count == 0)
 307            {
 2308                extensionRegistrar = null;
 2309                return false;
 310            }
 311
 5312            var registrars = _extensionRegistrars.ToArray();
 5313            extensionRegistrar = (services, config) =>
 5314            {
 20315                foreach (var registrar in registrars)
 5316                {
 6317                    registrar(services, config);
 5318                }
 9319            };
 5320            return true;
 321        }
 7322    }
 323
 324    /// <summary>
 325    /// Clears every global registration, including extension registrars and the cached
 326    /// combined registration. For testing purposes only.
 327    /// </summary>
 328    internal static void ClearRegistrationsForTesting()
 329    {
 94330        lock (_gate)
 331        {
 94332            _registrations.Clear();
 94333            _extensionRegistrars.Clear();
 94334            _cachedCombined = null;
 94335        }
 94336    }
 337
 338    internal static IDisposable BeginTestScope(
 339        Func<IReadOnlyList<InjectableTypeInfo>> injectableTypeProvider,
 340        Func<IReadOnlyList<PluginTypeInfo>> pluginTypeProvider)
 341    {
 19342        if (injectableTypeProvider is null) throw new ArgumentNullException(nameof(injectableTypeProvider));
 18343        if (pluginTypeProvider is null) throw new ArgumentNullException(nameof(pluginTypeProvider));
 344
 16345        var prior = _asyncLocalOverride.Value;
 16346        _asyncLocalOverride.Value = new Registration(injectableTypeProvider, pluginTypeProvider);
 16347        return new Scope(prior);
 348    }
 349
 350    private sealed class Scope : IDisposable
 351    {
 352        private readonly Registration? _prior;
 353
 16354        public Scope(Registration? prior)
 355        {
 16356            _prior = prior;
 16357        }
 358
 359        public void Dispose()
 360        {
 16361            _asyncLocalOverride.Value = _prior;
 16362        }
 363    }
 364
 365    private static Registration Combine(IReadOnlyList<Registration> registrations)
 366    {
 367        // Snapshot the current registrations to avoid capturing a mutable List.
 2149368        var injectableProviders = registrations.Select(r => r.InjectableTypeProvider).ToArray();
 2149369        var pluginProviders = registrations.Select(r => r.PluginTypeProvider).ToArray();
 2174370        var decoratorAppliers = registrations.Where(r => r.DecoratorApplier is not null).Select(r => r.DecoratorApplier!
 2154371        var optionsRegistrars = registrations.Where(r => r.OptionsRegistrar is not null).Select(r => r.OptionsRegistrar!
 372
 373        IReadOnlyList<InjectableTypeInfo> GetInjectableTypes()
 374        {
 375            var result = new List<InjectableTypeInfo>();
 376            var seen = new HashSet<Type>();
 377
 378            foreach (var provider in injectableProviders)
 379            {
 380                foreach (var info in provider())
 381                {
 382                    if (seen.Add(info.Type))
 383                    {
 384                        result.Add(info);
 385                    }
 386                }
 387            }
 388
 389            return result;
 390        }
 391
 392        IReadOnlyList<PluginTypeInfo> GetPluginTypes()
 393        {
 394            var result = new List<PluginTypeInfo>();
 395            var seen = new HashSet<Type>();
 396
 397            foreach (var provider in pluginProviders)
 398            {
 399                foreach (var info in provider())
 400                {
 401                    if (seen.Add(info.PluginType))
 402                    {
 403                        result.Add(info);
 404                    }
 405                }
 406            }
 407
 408            return result;
 409        }
 410
 88411        Action<object>? combinedDecoratorApplier = decoratorAppliers.Length > 0
 88412            ? services =>
 88413            {
 1290414                foreach (var applier in decoratorAppliers)
 88415                {
 330416                    applier(services);
 88417                }
 315418            }
 88419            : null;
 420
 88421        Action<object, object>? combinedOptionsRegistrar = optionsRegistrars.Length > 0
 88422            ? (services, config) =>
 88423            {
 10424                foreach (var registrar in optionsRegistrars)
 88425                {
 3426                    registrar(services, config);
 88427                }
 2428            }
 88429            : null;
 430
 88431        return new Registration(GetInjectableTypes, GetPluginTypes, combinedDecoratorApplier, combinedOptionsRegistrar);
 432    }
 433}

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>)