< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Providers.TieredProviderSelectorServiceCollectionExtensions
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Providers/TieredProviderSelectorServiceCollectionExtensions.cs
Line coverage
100%
Covered lines: 35
Uncovered lines: 0
Coverable lines: 35
Total lines: 192
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddTieredProviderSelector(...)100%11100%
AddTieredProviderSelector(...)100%11100%
AddTieredProviderSelector(...)100%11100%
AddTieredProviderSelectorCore(...)100%44100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/Providers/TieredProviderSelectorServiceCollectionExtensions.cs

#LineLine coverage
 1using Microsoft.Extensions.DependencyInjection;
 2using Microsoft.Extensions.DependencyInjection.Extensions;
 3
 4using NexusLabs.Needlr.AgentFramework.Context;
 5
 6namespace NexusLabs.Needlr.AgentFramework.Providers;
 7
 8/// <summary>
 9/// Extension methods that register an
 10/// <see cref="ITieredProviderSelector{TQuery, TResult}"/> with optional consumer-supplied
 11/// <see cref="TieredProviderSelectorOptions"/> in the DI container.
 12/// </summary>
 13/// <remarks>
 14/// <para>
 15/// Wraps the boilerplate of resolving providers, the quota gate, the execution-context
 16/// accessor, and <see cref="TimeProvider"/> from the container so consumers do not have
 17/// to hand-roll the same factory in every host.
 18/// </para>
 19/// <para>
 20/// <b>Registration semantics: last-wins (override-friendly).</b> The extension uses
 21/// <c>AddSingleton</c> (not <c>TryAddSingleton</c>). If the same <c>(TQuery, TResult)</c>
 22/// pair is registered multiple times — for example, by two plugins —
 23/// <see cref="System.IServiceProvider.GetService"/> resolves the LAST descriptor added.
 24/// This is the intentional convention for consumer-supplied configuration: a
 25/// downstream plugin or test harness can override an upstream plugin's selector
 26/// registration without removing it first. (The <see cref="TimeProvider"/> dependency
 27/// is registered with <c>TryAddSingleton</c> because it IS framework infrastructure —
 28/// first-wins is correct for that.)
 29/// </para>
 30/// <para>
 31/// <b>Lifetime: Singleton.</b> The selector's per-instance skip cache is the whole point
 32/// of registering it as a long-lived service — registering as Scoped would reset the
 33/// cache per request, defeating the purpose. If you need a different lifetime,
 34/// hand-roll the registration.
 35/// </para>
 36/// <para>
 37/// <b>Configure delegate evaluation.</b> Both <c>configure</c> overloads invoke the
 38/// supplied delegate INSIDE the singleton factory at first resolution, with a real
 39/// <see cref="System.IServiceProvider"/> in scope. This means OnHit callbacks can
 40/// resolve other services (loggers, options monitors, telemetry sinks) from the
 41/// container — register them BEFORE calling
 42/// <see cref="ITieredProviderSelector{TQuery, TResult}"/>.GetRequiredService for the
 43/// first time. The delegate runs exactly once per Singleton lifetime; throw an
 44/// <see cref="System.InvalidOperationException"/> if it returns <see langword="null"/>.
 45/// </para>
 46/// </remarks>
 47public static class TieredProviderSelectorServiceCollectionExtensions
 48{
 49    /// <summary>
 50    /// Registers an <see cref="ITieredProviderSelector{TQuery, TResult}"/> as a singleton
 51    /// using <see cref="TieredProviderSelectorOptions.Default"/> (PUE-only fall-through,
 52    /// no skip, no callback).
 53    /// </summary>
 54    /// <typeparam name="TQuery">Query type for the selector.</typeparam>
 55    /// <typeparam name="TResult">Result type for the selector.</typeparam>
 56    /// <param name="services">Service collection to add the registration to.</param>
 57    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
 58    /// <exception cref="ArgumentNullException">
 59    /// <paramref name="services"/> is <see langword="null"/>.
 60    /// </exception>
 61    public static IServiceCollection AddTieredProviderSelector<TQuery, TResult>(
 62        this IServiceCollection services)
 63    {
 564        return AddTieredProviderSelectorCore<TQuery, TResult>(services, optionsFactory: null);
 65    }
 66
 67    /// <summary>
 68    /// Registers an <see cref="ITieredProviderSelector{TQuery, TResult}"/> as a singleton
 69    /// with consumer-supplied options derived from
 70    /// <see cref="TieredProviderSelectorOptions.Default"/>.
 71    /// </summary>
 72    /// <typeparam name="TQuery">Query type for the selector.</typeparam>
 73    /// <typeparam name="TResult">Result type for the selector.</typeparam>
 74    /// <param name="services">Service collection to add the registration to.</param>
 75    /// <param name="configure">
 76    /// Delegate that receives <see cref="TieredProviderSelectorOptions.Default"/> and
 77    /// returns the (possibly mutated) options to use. Runs lazily inside the singleton
 78    /// factory at first resolution. Must not return <see langword="null"/>.
 79    /// </param>
 80    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
 81    /// <exception cref="ArgumentNullException">
 82    /// <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.
 83    /// </exception>
 84    /// <remarks>
 85    /// Use this overload when your <see cref="ProviderFailurePolicy.OnHit"/> callbacks
 86    /// (and any other policy fields) do not need access to other DI services. If you
 87    /// need an <c>ILogger</c>, <c>IOptionsMonitor</c>, or any other container-resolved
 88    /// service inside your callbacks, use the
 89    /// <see cref="AddTieredProviderSelector{TQuery, TResult}(IServiceCollection, Func{IServiceProvider, TieredProviderS
 90    /// overload instead.
 91    /// </remarks>
 92    public static IServiceCollection AddTieredProviderSelector<TQuery, TResult>(
 93        this IServiceCollection services,
 94        Func<TieredProviderSelectorOptions, TieredProviderSelectorOptions> configure)
 95    {
 1496        ArgumentNullException.ThrowIfNull(configure);
 1397        return AddTieredProviderSelectorCore<TQuery, TResult>(
 1398            services,
 2299            optionsFactory: _ => configure(TieredProviderSelectorOptions.Default));
 100    }
 101
 102    /// <summary>
 103    /// Registers an <see cref="ITieredProviderSelector{TQuery, TResult}"/> as a singleton
 104    /// with consumer-supplied options derived from
 105    /// <see cref="TieredProviderSelectorOptions.Default"/>, with access to the
 106    /// <see cref="System.IServiceProvider"/> so policy callbacks can resolve other
 107    /// container services.
 108    /// </summary>
 109    /// <typeparam name="TQuery">Query type for the selector.</typeparam>
 110    /// <typeparam name="TResult">Result type for the selector.</typeparam>
 111    /// <param name="services">Service collection to add the registration to.</param>
 112    /// <param name="configure">
 113    /// Delegate that receives the <see cref="System.IServiceProvider"/> and
 114    /// <see cref="TieredProviderSelectorOptions.Default"/> and returns the (possibly
 115    /// mutated) options to use. Runs lazily inside the singleton factory at first
 116    /// resolution. Must not return <see langword="null"/>.
 117    /// </param>
 118    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
 119    /// <exception cref="ArgumentNullException">
 120    /// <paramref name="services"/> or <paramref name="configure"/> is <see langword="null"/>.
 121    /// </exception>
 122    /// <example>
 123    /// <code>
 124    /// services.AddTieredProviderSelector&lt;WebSearchQuery, IReadOnlyList&lt;WebSearchResult&gt;&gt;(
 125    ///     (sp, opts) =>
 126    ///     {
 127    ///         var logger = sp.GetRequiredService&lt;ILogger&lt;CopilotWebSearchProvider&gt;&gt;();
 128    ///         return opts with
 129    ///         {
 130    ///             FailurePolicies =
 131    ///             [
 132    ///                 .. opts.FailurePolicies,
 133    ///                 new ProviderFailurePolicy(
 134    ///                     Match: ex => ex is CopilotAuthException,
 135    ///                     SkipDuration: TimeSpan.FromMinutes(5),
 136    ///                     OnHit: ctx =>
 137    ///                     {
 138    ///                         logger.LogWarning(ctx.Exception,
 139    ///                             "Provider {Provider} skipped until {Until}",
 140    ///                             ctx.ProviderName, ctx.SkipUntil);
 141    ///                         return ValueTask.CompletedTask;
 142    ///                     }),
 143    ///             ],
 144    ///         };
 145    ///     });
 146    /// </code>
 147    /// </example>
 148    public static IServiceCollection AddTieredProviderSelector<TQuery, TResult>(
 149        this IServiceCollection services,
 150        Func<IServiceProvider, TieredProviderSelectorOptions, TieredProviderSelectorOptions> configure)
 151    {
 3152        ArgumentNullException.ThrowIfNull(configure);
 2153        return AddTieredProviderSelectorCore<TQuery, TResult>(
 2154            services,
 3155            optionsFactory: sp => configure(sp, TieredProviderSelectorOptions.Default));
 156    }
 157
 158    private static IServiceCollection AddTieredProviderSelectorCore<TQuery, TResult>(
 159        IServiceCollection services,
 160        Func<IServiceProvider, TieredProviderSelectorOptions>? optionsFactory)
 161    {
 20162        ArgumentNullException.ThrowIfNull(services);
 163
 17164        services.TryAddSingleton(TimeProvider.System);
 165
 17166        services.AddSingleton<ITieredProviderSelector<TQuery, TResult>>(sp =>
 17167        {
 17168            TieredProviderSelectorOptions resolved;
 12169            if (optionsFactory is null)
 17170            {
 2171                resolved = TieredProviderSelectorOptions.Default;
 17172            }
 17173            else
 17174            {
 10175                resolved = optionsFactory(sp)
 10176                    ?? throw new InvalidOperationException(
 10177                        "The configure delegate passed to AddTieredProviderSelector returned null. " +
 10178                        "Return TieredProviderSelectorOptions.Default if you want the framework defaults.");
 17179            }
 17180
 11181            return new TieredProviderSelector<TQuery, TResult>(
 11182                sp.GetServices<ITieredProvider<TQuery, TResult>>(),
 11183                sp.GetRequiredService<IQuotaGate>(),
 11184                sp.GetRequiredService<IAgentExecutionContextAccessor>(),
 11185                partitionSelector: null,
 11186                options: resolved,
 11187                timeProvider: sp.GetRequiredService<TimeProvider>());
 17188        });
 189
 17190        return services;
 191    }
 192}