< Summary

Information
Class: NexusLabs.Needlr.ServiceCollectionExtensions
Assembly: NexusLabs.Needlr
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr/ServiceCollectionExtensions.cs
Line coverage
85%
Covered lines: 72
Uncovered lines: 12
Coverable lines: 84
Total lines: 286
Line coverage: 85.7%
Branch coverage
73%
Covered branches: 22
Total branches: 30
Branch coverage: 73.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddDecorator(...)100%88100%
CreateDecoratedDescriptor(...)75%4485.71%
AddDecorator(...)83.33%6686.95%
GetServiceRegistrations(...)100%11100%
GetServiceRegistrations(...)100%11100%
IsRegistered(...)100%11100%
IsRegistered(...)100%11100%
CreateOriginalService(...)83.33%6685.71%
CreateOriginalService(...)16.66%19628.57%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr/ServiceCollectionExtensions.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2
 3using Microsoft.Extensions.DependencyInjection;
 4
 5namespace NexusLabs.Needlr;
 6
 7/// <summary>
 8/// Extension methods for <see cref="IServiceCollection"/> that add decorator wiring, service inspection,
 9/// and registration-check utilities to the standard Microsoft DI container.
 10/// </summary>
 11/// <remarks>
 12/// <para>
 13/// The primary extension in this class is <c>AddDecorator</c>, which wraps an already-registered
 14/// service with a decorator while preserving the original service's lifetime. This complements
 15/// the attribute-based <see cref="DecoratorForAttribute{TService}"/> used with Needlr's
 16/// source generation and reflection scanning.
 17/// </para>
 18/// </remarks>
 19public static class ServiceCollectionExtensions
 20{
 21    /// <summary>
 22    /// Decorates an existing service registration with a decorator type, preserving the original service's lifetime.
 23    /// The decorator must implement the service interface and take the service interface as a constructor parameter.
 24    /// Works with both interfaces and class types.
 25    /// </summary>
 26    /// <typeparam name="TService">The service type (interface or class) to decorate.</typeparam>
 27    /// <typeparam name="TDecorator">The decorator type that implements TService.</typeparam>
 28    /// <param name="services">The service collection to modify.</param>
 29    /// <returns>The service collection for method chaining.</returns>
 30    /// <exception cref="ArgumentNullException">Thrown when services is null.</exception>
 31    /// <exception cref="InvalidOperationException">Thrown when no service registration is found for TService.</exceptio
 32    /// <example>
 33    /// <code>
 34    /// // Register the original service
 35    /// services.AddScoped&lt;IMyService, MyService&gt;();
 36    ///
 37    /// // Decorate it while preserving the scoped lifetime
 38    /// services.AddDecorator&lt;IMyService, MyServiceDecorator&gt;();
 39    /// </code>
 40    /// </example>
 41    public static IServiceCollection AddDecorator<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.P
 42        where TDecorator : class, TService
 43    {
 244344        ArgumentNullException.ThrowIfNull(services);
 45
 46        // Find ALL existing service registrations for this type
 244247        var existingDescriptors = services
 58010148            .Where(d => d.ServiceType == typeof(TService))
 244249            .ToList();
 50
 244251        if (existingDescriptors.Count == 0)
 52        {
 153            throw new InvalidOperationException(
 154                $"No service registration found for type {typeof(TService).Name}. " +
 155                $"Please register the service before decorating it.");
 56        }
 57
 58        // Remove all existing registrations
 1124859        foreach (var descriptor in existingDescriptors)
 60        {
 318361            services.Remove(descriptor);
 62        }
 63
 64        // Create decorated registrations for each, preserving order and lifetime
 1124865        foreach (var existingDescriptor in existingDescriptors)
 66        {
 318367            var decoratedDescriptor = CreateDecoratedDescriptor<TService, TDecorator>(existingDescriptor);
 318368            services.Add(decoratedDescriptor);
 69        }
 70
 244171        return services;
 72    }
 73
 74    private static ServiceDescriptor CreateDecoratedDescriptor<TService, [DynamicallyAccessedMembers(DynamicallyAccessed
 75        ServiceDescriptor existingDescriptor)
 76        where TDecorator : class, TService
 77    {
 318378        return existingDescriptor.Lifetime switch
 318379        {
 317380            ServiceLifetime.Singleton => new ServiceDescriptor(typeof(TService), provider =>
 317381            {
 13882                var originalService = CreateOriginalService<TService>(provider, existingDescriptor);
 13883                return ActivatorUtilities.CreateInstance<TDecorator>(provider, originalService!);
 317384            }, ServiceLifetime.Singleton),
 785            ServiceLifetime.Scoped => new ServiceDescriptor(typeof(TService), provider =>
 786            {
 1387                var originalService = CreateOriginalService<TService>(provider, existingDescriptor);
 1388                return ActivatorUtilities.CreateInstance<TDecorator>(provider, originalService!);
 789            }, ServiceLifetime.Scoped),
 390            ServiceLifetime.Transient => new ServiceDescriptor(typeof(TService), provider =>
 391            {
 592                var originalService = CreateOriginalService<TService>(provider, existingDescriptor);
 593                return ActivatorUtilities.CreateInstance<TDecorator>(provider, originalService!);
 394            }, ServiceLifetime.Transient),
 095            _ => throw new InvalidOperationException(
 096                $"Unsupported service lifetime '{existingDescriptor.Lifetime}' " +
 097                $"for '{typeof(TService)}'.")
 318398        };
 99    }
 100
 101    /// <summary>
 102    /// Decorates an existing service registration with a decorator type, preserving the original service's lifetime.
 103    /// The decorator must implement the service interface and take the service interface as a constructor parameter.
 104    /// Works with both interfaces and class types.
 105    /// </summary>
 106    /// <param name="services">The service collection to modify.</param>
 107    /// <param name="serviceType">The service type (interface or class) to decorate.</param>
 108    /// <param name="decoratorType">The decorator type that implements the service type.</param>
 109    /// <returns>The service collection for method chaining.</returns>
 110    /// <exception cref="ArgumentNullException">Thrown when services, serviceType, or decoratorType is null.</exception>
 111    /// <exception cref="InvalidOperationException">Thrown when no service registration is found for the service type.</
 112    /// <example>
 113    /// <code>
 114    /// // Register the original service
 115    /// services.AddScoped&lt;IMyService, MyService&gt;();
 116    ///
 117    /// // Decorate it while preserving the scoped lifetime
 118    /// services.AddDecorator(typeof(IMyService), typeof(MyServiceDecorator));
 119    /// </code>
 120    /// </example>
 121    public static IServiceCollection AddDecorator(
 122        this IServiceCollection services,
 123        Type serviceType,
 124        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type decoratorType)
 125    {
 1230126        ArgumentNullException.ThrowIfNull(services);
 1230127        ArgumentNullException.ThrowIfNull(serviceType);
 1230128        ArgumentNullException.ThrowIfNull(decoratorType);
 129
 130        // Find ALL existing service registrations for this type
 1230131        var existingDescriptors = services
 327744132            .Where(d => d.ServiceType == serviceType)
 1230133            .ToList();
 134
 1230135        if (existingDescriptors.Count == 0)
 136        {
 0137            throw new InvalidOperationException(
 0138                $"No service registration found for type {serviceType.Name}. " +
 0139                $"Please register the service before decorating it.");
 140        }
 141
 142        // Remove all existing registrations
 6150143        foreach (var descriptor in existingDescriptors)
 144        {
 1845145            services.Remove(descriptor);
 146        }
 147
 148        // Create decorated registrations for each, preserving order and lifetime
 6150149        foreach (var existingDescriptor in existingDescriptors)
 150        {
 1845151            var decoratedDescriptor = new ServiceDescriptor(
 1845152                serviceType,
 1845153                provider =>
 1845154                {
 81155                    var originalService = CreateOriginalService(provider, existingDescriptor, serviceType);
 81156                    return ActivatorUtilities.CreateInstance(provider, decoratorType, originalService!);
 1845157                },
 1845158                existingDescriptor.Lifetime);
 1845159            services.Add(decoratedDescriptor);
 160        }
 161
 1230162        return services;
 163    }
 164
 165    /// <summary>
 166    /// Gets detailed information about all registered services.
 167    /// </summary>
 168    /// <param name="serviceCollection">The service provider to inspect.</param>
 169    /// <returns>A read-only list of service registration information.</returns>
 170    /// <exception cref="ArgumentNullException">Thrown when serviceCollection is null.</exception>
 171    /// <example>
 172    /// <code>
 173    /// // Get all singleton services
 174    /// var singletons = serviceCollection.GetServiceRegistrations(
 175    ///     descriptor => descriptor.Lifetime == ServiceLifetime.Singleton);
 176    ///
 177    /// // Get all services with a specific implementation type
 178    /// var specificImpls = serviceCollection.GetServiceRegistrations(
 179    ///     descriptor => descriptor.ImplementationType == typeof(MyService));
 180    /// </code>
 181    /// </example>
 182    public static IReadOnlyList<ServiceRegistrationInfo> GetServiceRegistrations(
 183        this IServiceCollection serviceCollection)
 184    {
 6185        ArgumentNullException.ThrowIfNull(serviceCollection);
 186
 11187        return serviceCollection.GetServiceRegistrations(_ => true);
 188    }
 189
 190    /// <summary>
 191    /// Gets detailed information about all registered services that match the specified predicate.
 192    /// </summary>
 193    /// <param name="serviceCollection">The service provider to inspect.</param>
 194    /// <param name="predicate">A function to filter the service descriptors.</param>
 195    /// <returns>A read-only list of service registration information.</returns>
 196    /// <exception cref="ArgumentNullException">Thrown when serviceCollection or predicate is null.</exception>
 197    /// <example>
 198    /// <code>
 199    /// // Get all singleton services
 200    /// var singletons = serviceCollection.GetServiceRegistrations(
 201    ///     descriptor => descriptor.Lifetime == ServiceLifetime.Singleton);
 202    ///
 203    /// // Get all services with a specific implementation type
 204    /// var specificImpls = serviceCollection.GetServiceRegistrations(
 205    ///     descriptor => descriptor.ImplementationType == typeof(MyService));
 206    /// </code>
 207    /// </example>
 208    public static IReadOnlyList<ServiceRegistrationInfo> GetServiceRegistrations(
 209        this IServiceCollection serviceCollection,
 210        Func<ServiceDescriptor, bool> predicate)
 211    {
 65212        ArgumentNullException.ThrowIfNull(serviceCollection);
 64213        ArgumentNullException.ThrowIfNull(predicate);
 214
 63215        return serviceCollection
 63216            .Where(predicate)
 2349217            .Select(descriptor => new ServiceRegistrationInfo(descriptor))
 63218            .ToArray();
 219    }
 220
 221    /// <summary>
 222    /// Determines whether a service of the specified type is registered in the service collection.
 223    /// </summary>
 224    /// <typeparam name="TService">The service type to check.</typeparam>
 225    /// <param name="services">The service collection to check.</param>
 226    /// <returns>True if the service is registered; otherwise, false.</returns>
 227    public static bool IsRegistered<TService>(this IServiceCollection services)
 228    {
 5229        ArgumentNullException.ThrowIfNull(services);
 4230        return services.IsRegistered(typeof(TService));
 231    }
 232
 233    /// <summary>
 234    /// Determines whether a service of the specified type is registered in the service collection.
 235    /// </summary>
 236    /// <param name="services">The service collection to check.</param>
 237    /// <param name="serviceType">The service type to check.</param>
 238    /// <returns>True if the service is registered; otherwise, false.</returns>
 239    public static bool IsRegistered(
 240        this IServiceCollection services,
 241        Type serviceType)
 242    {
 12243        ArgumentNullException.ThrowIfNull(services);
 23244        return services.Any(d => d.ServiceType == serviceType);
 245    }
 246
 247    private static TService CreateOriginalService<TService>(IServiceProvider provider, ServiceDescriptor originalDescrip
 248    {
 156249        if (originalDescriptor.ImplementationFactory is not null)
 250        {
 102251            return (TService)originalDescriptor.ImplementationFactory(provider);
 252        }
 253
 54254        if (originalDescriptor.ImplementationInstance is not null)
 255        {
 1256            return (TService)originalDescriptor.ImplementationInstance;
 257        }
 258
 53259        if (originalDescriptor.ImplementationType is not null)
 260        {
 53261            return (TService)ActivatorUtilities.CreateInstance(provider, originalDescriptor.ImplementationType);
 262        }
 263
 0264        throw new InvalidOperationException($"Unable to create instance of service {typeof(TService).Name} from the orig
 265    }
 266
 267    private static object CreateOriginalService(IServiceProvider provider, ServiceDescriptor originalDescriptor, Type se
 268    {
 81269        if (originalDescriptor.ImplementationFactory is not null)
 270        {
 81271            return originalDescriptor.ImplementationFactory(provider);
 272        }
 273
 0274        if (originalDescriptor.ImplementationInstance is not null)
 275        {
 0276            return originalDescriptor.ImplementationInstance;
 277        }
 278
 0279        if (originalDescriptor.ImplementationType is not null)
 280        {
 0281            return ActivatorUtilities.CreateInstance(provider, originalDescriptor.ImplementationType);
 282        }
 283
 0284        throw new InvalidOperationException($"Unable to create instance of service {serviceType.Name} from the original 
 285    }
 286}