< Summary

Information
Class: NexusLabs.Needlr.ServiceCollectionExtensions
Assembly: NexusLabs.Needlr
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr/ServiceCollectionExtensions.cs
Line coverage
98%
Covered lines: 51
Uncovered lines: 1
Coverable lines: 52
Total lines: 234
Line coverage: 98%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddDecorator(...)100%11100%
AddDecorator(...)100%1212100%
GetServiceRegistrations(...)100%11100%
GetServiceRegistrations(...)100%11100%
IsRegistered(...)100%11100%
IsRegistered(...)100%11100%
CreateOriginalService(...)83.33%6685.71%

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">
 32    /// Thrown when no service registration is found for TService, or when an existing
 33    /// registration uses an unsupported lifetime.
 34    /// </exception>
 35    /// <example>
 36    /// <code>
 37    /// // Register the original service
 38    /// services.AddScoped&lt;IMyService, MyService&gt;();
 39    ///
 40    /// // Decorate it while preserving the scoped lifetime
 41    /// services.AddDecorator&lt;IMyService, MyServiceDecorator&gt;();
 42    /// </code>
 43    /// </example>
 44    public static IServiceCollection AddDecorator<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.P
 45        where TDecorator : class, TService
 46    {
 300847        ArgumentNullException.ThrowIfNull(services);
 48
 300749        return services.AddDecorator(typeof(TService), typeof(TDecorator));
 50    }
 51
 52    /// <summary>
 53    /// Decorates an existing service registration with a decorator type, preserving the original service's lifetime.
 54    /// The decorator must implement the service interface and take the service interface as a constructor parameter.
 55    /// Works with both interfaces and class types.
 56    /// </summary>
 57    /// <param name="services">The service collection to modify.</param>
 58    /// <param name="serviceType">The service type (interface or class) to decorate.</param>
 59    /// <param name="decoratorType">The decorator type that implements the service type.</param>
 60    /// <returns>The service collection for method chaining.</returns>
 61    /// <exception cref="ArgumentNullException">Thrown when services, serviceType, or decoratorType is null.</exception>
 62    /// <exception cref="InvalidOperationException">
 63    /// Thrown when no service registration is found for the service type, or when an existing
 64    /// registration uses an unsupported lifetime.
 65    /// </exception>
 66    /// <example>
 67    /// <code>
 68    /// // Register the original service
 69    /// services.AddScoped&lt;IMyService, MyService&gt;();
 70    ///
 71    /// // Decorate it while preserving the scoped lifetime
 72    /// services.AddDecorator(typeof(IMyService), typeof(MyServiceDecorator));
 73    /// </code>
 74    /// </example>
 75    public static IServiceCollection AddDecorator(
 76        this IServiceCollection services,
 77        Type serviceType,
 78        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type decoratorType)
 79    {
 425880        ArgumentNullException.ThrowIfNull(services);
 425781        ArgumentNullException.ThrowIfNull(serviceType);
 425682        ArgumentNullException.ThrowIfNull(decoratorType);
 83
 84        // Find ALL existing service registrations for this type
 425585        var existingDescriptors = services
 126849286            .Where(d => d.ServiceType == serviceType)
 425587            .ToList();
 88
 425589        if (existingDescriptors.Count == 0)
 90        {
 391            throw new InvalidOperationException(
 392                $"No service registration found for type {serviceType.Name}. " +
 393                $"Please register the service before decorating it.");
 94        }
 95
 96        // Validate before mutating so an unsupported lifetime leaves the collection untouched
 2006497        foreach (var descriptor in existingDescriptors)
 98        {
 578199            if (descriptor.Lifetime is not (
 5781100                ServiceLifetime.Singleton or
 5781101                ServiceLifetime.Scoped or
 5781102                ServiceLifetime.Transient))
 103            {
 2104                throw new InvalidOperationException(
 2105                    $"Unsupported service lifetime '{descriptor.Lifetime}' " +
 2106                    $"for '{serviceType}'.");
 107            }
 108        }
 109
 110        // Remove all existing registrations
 20058111        foreach (var descriptor in existingDescriptors)
 112        {
 5779113            services.Remove(descriptor);
 114        }
 115
 116        // Create decorated registrations for each, preserving order and lifetime
 20058117        foreach (var existingDescriptor in existingDescriptors)
 118        {
 5779119            var decoratedDescriptor = new ServiceDescriptor(
 5779120                serviceType,
 5779121                provider =>
 5779122                {
 275123                    var originalService = CreateOriginalService(provider, existingDescriptor, serviceType);
 275124                    return ActivatorUtilities.CreateInstance(provider, decoratorType, originalService!);
 5779125                },
 5779126                existingDescriptor.Lifetime);
 5779127            services.Add(decoratedDescriptor);
 128        }
 129
 4250130        return services;
 131    }
 132
 133    /// <summary>
 134    /// Gets detailed information about all registered services.
 135    /// </summary>
 136    /// <param name="serviceCollection">The service provider to inspect.</param>
 137    /// <returns>A read-only list of service registration information.</returns>
 138    /// <exception cref="ArgumentNullException">Thrown when serviceCollection is null.</exception>
 139    /// <example>
 140    /// <code>
 141    /// // Get all singleton services
 142    /// var singletons = serviceCollection.GetServiceRegistrations(
 143    ///     descriptor => descriptor.Lifetime == ServiceLifetime.Singleton);
 144    ///
 145    /// // Get all services with a specific implementation type
 146    /// var specificImpls = serviceCollection.GetServiceRegistrations(
 147    ///     descriptor => descriptor.ImplementationType == typeof(MyService));
 148    /// </code>
 149    /// </example>
 150    public static IReadOnlyList<ServiceRegistrationInfo> GetServiceRegistrations(
 151        this IServiceCollection serviceCollection)
 152    {
 6153        ArgumentNullException.ThrowIfNull(serviceCollection);
 154
 11155        return serviceCollection.GetServiceRegistrations(_ => true);
 156    }
 157
 158    /// <summary>
 159    /// Gets detailed information about all registered services that match the specified predicate.
 160    /// </summary>
 161    /// <param name="serviceCollection">The service provider to inspect.</param>
 162    /// <param name="predicate">A function to filter the service descriptors.</param>
 163    /// <returns>A read-only list of service registration information.</returns>
 164    /// <exception cref="ArgumentNullException">Thrown when serviceCollection or predicate is null.</exception>
 165    /// <example>
 166    /// <code>
 167    /// // Get all singleton services
 168    /// var singletons = serviceCollection.GetServiceRegistrations(
 169    ///     descriptor => descriptor.Lifetime == ServiceLifetime.Singleton);
 170    ///
 171    /// // Get all services with a specific implementation type
 172    /// var specificImpls = serviceCollection.GetServiceRegistrations(
 173    ///     descriptor => descriptor.ImplementationType == typeof(MyService));
 174    /// </code>
 175    /// </example>
 176    public static IReadOnlyList<ServiceRegistrationInfo> GetServiceRegistrations(
 177        this IServiceCollection serviceCollection,
 178        Func<ServiceDescriptor, bool> predicate)
 179    {
 34180        ArgumentNullException.ThrowIfNull(serviceCollection);
 33181        ArgumentNullException.ThrowIfNull(predicate);
 182
 32183        return serviceCollection
 32184            .Where(predicate)
 1557185            .Select(descriptor => new ServiceRegistrationInfo(descriptor))
 32186            .ToArray();
 187    }
 188
 189    /// <summary>
 190    /// Determines whether a service of the specified type is registered in the service collection.
 191    /// </summary>
 192    /// <typeparam name="TService">The service type to check.</typeparam>
 193    /// <param name="services">The service collection to check.</param>
 194    /// <returns>True if the service is registered; otherwise, false.</returns>
 195    public static bool IsRegistered<TService>(this IServiceCollection services)
 196    {
 5197        ArgumentNullException.ThrowIfNull(services);
 4198        return services.IsRegistered(typeof(TService));
 199    }
 200
 201    /// <summary>
 202    /// Determines whether a service of the specified type is registered in the service collection.
 203    /// </summary>
 204    /// <param name="services">The service collection to check.</param>
 205    /// <param name="serviceType">The service type to check.</param>
 206    /// <returns>True if the service is registered; otherwise, false.</returns>
 207    public static bool IsRegistered(
 208        this IServiceCollection services,
 209        Type serviceType)
 210    {
 12211        ArgumentNullException.ThrowIfNull(services);
 23212        return services.Any(d => d.ServiceType == serviceType);
 213    }
 214
 215    private static object CreateOriginalService(IServiceProvider provider, ServiceDescriptor originalDescriptor, Type se
 216    {
 275217        if (originalDescriptor.ImplementationFactory is not null)
 218        {
 202219            return originalDescriptor.ImplementationFactory(provider);
 220        }
 221
 73222        if (originalDescriptor.ImplementationInstance is not null)
 223        {
 3224            return originalDescriptor.ImplementationInstance;
 225        }
 226
 70227        if (originalDescriptor.ImplementationType is not null)
 228        {
 70229            return ActivatorUtilities.CreateInstance(provider, originalDescriptor.ImplementationType);
 230        }
 231
 0232        throw new InvalidOperationException($"Unable to create instance of service {serviceType.Name} from the original 
 233    }
 234}