< Summary

Information
Class: NexusLabs.Needlr.Injection.Reflection.TypeRegistrars.ReflectionTypeRegistrar
Assembly: NexusLabs.Needlr.Injection.Reflection
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Injection.Reflection/TypeRegistrars/ReflectionTypeRegistrar.cs
Line coverage
93%
Covered lines: 101
Uncovered lines: 7
Coverable lines: 108
Total lines: 313
Line coverage: 93.5%
Branch coverage
90%
Covered branches: 69
Total branches: 76
Branch coverage: 90.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Injection.Reflection/TypeRegistrars/ReflectionTypeRegistrar.cs

#LineLine coverage
 1using Microsoft.Extensions.DependencyInjection;
 2using Microsoft.Extensions.Hosting;
 3
 4using System.Diagnostics.CodeAnalysis;
 5using System.Reflection;
 6
 7namespace NexusLabs.Needlr.Injection.Reflection.TypeRegistrars;
 8
 9/// <summary>
 10/// Type registrar that uses runtime reflection to discover and register types.
 11/// </summary>
 12/// <remarks>
 13/// This registrar is not compatible with NativeAOT or trimming. For AOT scenarios,
 14/// use GeneratedTypeRegistrar from NexusLabs.Needlr.Injection.SourceGen with the Needlr source generator instead.
 15/// </remarks>
 16[RequiresUnreferencedCode("ReflectionTypeRegistrar uses reflection to discover types. Use GeneratedTypeRegistrar for AOT
 17public sealed class ReflectionTypeRegistrar : ITypeRegistrar
 18{
 19    /// <inheritdoc />
 20    public void RegisterTypesFromAssemblies(
 21        IServiceCollection services,
 22        ITypeFilterer typeFilterer,
 23        IReadOnlyList<Assembly> assemblies)
 24    {
 34425        var allTypes = assemblies
 39826            .SelectMany(assembly => GetAllTypes(assembly))
 11410027            .Where(type => type is not null)
 11410028            .Where(type => type.IsClass && !type.IsAbstract) // Only concrete classes
 8427429            .Where(type => !HasDoNotAutoRegisterAttribute(type))
 34430            .ToList();
 31
 15084232        foreach (var type in allTypes)
 33        {
 34            // Check if type is excluded via Except<T>() or Except(predicate)
 7507735            if (typeFilterer.IsTypeExcluded(type))
 36            {
 37                continue;
 38            }
 39
 7505240            if (typeFilterer.IsInjectableSingletonType(type))
 41            {
 3758442                RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Singleton);
 43            }
 3746844            else if (typeFilterer.IsInjectableTransientType(type))
 45            {
 62546                RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Transient);
 47            }
 3684348            else if (typeFilterer.IsInjectableScopedType(type))
 49            {
 61550                RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Scoped);
 51            }
 52        }
 53
 54        // Register hosted services discovered via BackgroundService/IHostedService
 55        // Must happen BEFORE decorators so that decorators can wrap the hosted services
 34456        RegisterHostedServices(services, allTypes);
 57
 58        // Apply decorators discovered via [DecoratorFor<T>] attributes
 34459        ApplyDecoratorForAttributes(services, allTypes);
 34460    }
 61
 62    /// <summary>
 63    /// Discovers and registers types that are hosted services (inherit from BackgroundService
 64    /// or implement IHostedService directly).
 65    /// </summary>
 66    private static void RegisterHostedServices(IServiceCollection services, List<Type> allTypes)
 67    {
 34468        var hostedServiceTypes = allTypes
 34469            .Where(IsHostedServiceType)
 34470            .ToList();
 71
 167272        foreach (var type in hostedServiceTypes)
 73        {
 74            // Skip if already registered as concrete type (from regular registration)
 3183075            var existingRegistration = services.FirstOrDefault(d => d.ServiceType == type);
 49276            if (existingRegistration is null)
 77            {
 78                // Register concrete type as singleton
 079                services.AddSingleton(type);
 80            }
 81
 82            // Register as IHostedService forwarding to concrete type
 52883            services.AddSingleton<IHostedService>(sp => (IHostedService)sp.GetRequiredService(type));
 84        }
 34485    }
 86
 87    /// <summary>
 88    /// Determines if a type is a hosted service (inherits from BackgroundService
 89    /// or implements IHostedService directly, excluding abstract classes and decorators).
 90    /// </summary>
 91    private static bool IsHostedServiceType(Type type)
 92    {
 7507793        if (!type.IsClass || type.IsAbstract)
 094            return false;
 95
 96        // Skip types that are decorators for IHostedService
 7507797        if (IsDecoratorForHostedService(type))
 61598            return false;
 99
 100        // Check if inherits from BackgroundService
 74462101        var baseType = type.BaseType;
 150732102        while (baseType is not null)
 103        {
 76762104            if (baseType.FullName == "Microsoft.Extensions.Hosting.BackgroundService")
 492105                return true;
 76270106            baseType = baseType.BaseType;
 107        }
 108
 109        // Check if directly implements IHostedService
 111368110        return type.GetInterfaces().Any(i => i.FullName == "Microsoft.Extensions.Hosting.IHostedService");
 111    }
 112
 113    /// <summary>
 114    /// Checks if a type has [DecoratorFor&lt;IHostedService&gt;] attribute.
 115    /// </summary>
 116    private static bool IsDecoratorForHostedService(Type type)
 117    {
 346079118        foreach (var attribute in type.GetCustomAttributes(inherit: false))
 119        {
 98270120            var attrType = attribute.GetType();
 98270121            if (!attrType.IsGenericType)
 122                continue;
 123
 3075124            var genericTypeDef = attrType.GetGenericTypeDefinition();
 3075125            if (genericTypeDef.FullName?.StartsWith("NexusLabs.Needlr.DecoratorForAttribute`1", StringComparison.Ordinal
 126                continue;
 127
 128            // Get the type argument
 1230129            var typeArgs = attrType.GetGenericArguments();
 1230130            if (typeArgs.Length == 1 && typeArgs[0].FullName == "Microsoft.Extensions.Hosting.IHostedService")
 615131                return true;
 132        }
 74462133        return false;
 134    }
 135
 136    /// <summary>
 137    /// Discovers and applies decorators marked with [DecoratorFor&lt;T&gt;] attributes.
 138    /// </summary>
 139    private static void ApplyDecoratorForAttributes(IServiceCollection services, List<Type> allTypes)
 140    {
 344141        var decoratorInfos = new List<(Type DecoratorType, Type ServiceType, int Order)>();
 142
 150842143        foreach (var type in allTypes)
 144        {
 346694145            foreach (var attribute in type.GetCustomAttributes(inherit: false))
 146            {
 98270147                var attrType = attribute.GetType();
 98270148                if (!attrType.IsGenericType)
 149                    continue;
 150
 3075151                var genericTypeDef = attrType.GetGenericTypeDefinition();
 3075152                if (genericTypeDef.FullName?.StartsWith("NexusLabs.Needlr.DecoratorForAttribute`1", StringComparison.Ord
 153                    continue;
 154
 155                // Get the service type from the generic type argument
 1230156                var serviceType = attrType.GetGenericArguments()[0];
 157
 158                // Get the Order property value
 1230159                var orderProperty = attrType.GetProperty("Order");
 1230160                var order = orderProperty?.GetValue(attribute) as int? ?? 0;
 161
 1230162                decoratorInfos.Add((type, serviceType, order));
 163            }
 164        }
 165
 166        // Group by service type and apply in order
 344167        var decoratorsByService = decoratorInfos
 1230168            .GroupBy(d => d.ServiceType)
 754169            .OrderBy(g => g.Key.FullName);
 170
 1508171        foreach (var serviceGroup in decoratorsByService)
 172        {
 4510173            foreach (var decorator in serviceGroup.OrderBy(d => d.Order))
 174            {
 175                try
 176                {
 1230177                    services.AddDecorator(decorator.ServiceType, decorator.DecoratorType);
 1230178                }
 0179                catch (InvalidOperationException)
 180                {
 181                    // Service not registered - skip this decorator
 182                    // This can happen if the base service wasn't registered (e.g., excluded)
 0183                }
 184            }
 185        }
 344186    }
 187
 188    private static Type[] GetAllTypes(Assembly assembly)
 189    {
 190        try
 191        {
 398192            return assembly.GetTypes();
 193        }
 194        catch (ReflectionTypeLoadException ex)
 195        {
 196            // Return only the types that loaded successfully
 0197            return ex.Types.Where(t => t is not null).ToArray()!;
 198        }
 0199        catch
 200        {
 201            // If we can't load types from the assembly, skip it
 0202            return [];
 203        }
 398204    }
 205
 206    private static bool HasDoNotAutoRegisterAttribute(Type type)
 207    {
 149237208        return type.GetCustomAttribute<DoNotAutoRegisterAttribute>(inherit: true) is not null ||
 214200209            type.GetInterfaces().Any(t => HasDoNotAutoRegisterAttribute(t));
 210    }
 211
 212    private static void RegisterTypeAsSelfWithInterfaces(
 213        IServiceCollection services,
 214        Type type, ServiceLifetime lifetime)
 215    {
 216        // Register as self
 38824217        services.Add(new ServiceDescriptor(type, type, lifetime));
 218
 219        // Get constructor parameter types to detect decorator pattern
 38824220        var constructorParamTypes = GetConstructorParameterTypes(type);
 221
 222        // Check for [RegisterAs<T>] attributes - if present, only register as those interfaces
 38824223        var registerAsInterfaces = GetRegisterAsInterfaces(type);
 224
 225        List<Type> interfaces;
 38824226        if (registerAsInterfaces.Count > 0)
 227        {
 228            // Only register as explicitly specified interfaces via [RegisterAs<T>]
 615229            interfaces = registerAsInterfaces;
 230        }
 231        else
 232        {
 233            // Register as interfaces (excluding system interfaces, generic type definitions, decorator interfaces, and 
 38209234            interfaces = type.GetInterfaces()
 20142235                .Where(i => !i.IsGenericTypeDefinition)
 20142236                .Where(i => i.Assembly != typeof(object).Assembly) // Skip system interfaces
 19421237                .Where(i => !i.Name.StartsWith("System.")) // Additional system interface filtering
 19421238                .Where(i => !IsDecoratorInterface(i, constructorParamTypes)) // Skip decorator pattern interfaces
 17765239                .Where(i => i.FullName != "Microsoft.Extensions.Hosting.IHostedService") // Hosted services registered s
 38209240                .ToList();
 241        }
 242
 113834243        foreach (var interfaceType in interfaces)
 244        {
 245            // For singletons, register interfaces as factory delegates to ensure same instance
 18093246            if (lifetime == ServiceLifetime.Singleton)
 247            {
 16853248                services.Add(new ServiceDescriptor(
 16853249                    interfaceType,
 67250                    serviceProvider => serviceProvider.GetRequiredService(type),
 16853251                    lifetime));
 252            }
 253            else
 254            {
 255                // For transient services, direct registration is fine
 1240256                services.Add(new ServiceDescriptor(interfaceType, type, lifetime));
 257            }
 258        }
 38824259    }
 260
 261    /// <summary>
 262    /// Gets interface types specified by [RegisterAs&lt;T&gt;] attributes on the type.
 263    /// </summary>
 264    private static List<Type> GetRegisterAsInterfaces(Type type)
 265    {
 38824266        var interfaces = new List<Type>();
 267
 167856268        foreach (var attribute in type.GetCustomAttributes(inherit: false))
 269        {
 45104270            var attrType = attribute.GetType();
 45104271            if (!attrType.IsGenericType)
 272                continue;
 273
 2870274            var genericTypeDef = attrType.GetGenericTypeDefinition();
 2870275            if (genericTypeDef.FullName?.StartsWith("NexusLabs.Needlr.RegisterAsAttribute`1", StringComparison.Ordinal) 
 276                continue;
 277
 278            // Get the interface type from the generic type argument
 820279            var interfaceType = attrType.GetGenericArguments()[0];
 820280            interfaces.Add(interfaceType);
 281        }
 282
 38824283        return interfaces;
 284    }
 285
 286    /// <summary>
 287    /// Gets all parameter types from all constructors of a type.
 288    /// </summary>
 289    private static HashSet<Type> GetConstructorParameterTypes(Type type)
 290    {
 38824291        var paramTypes = new HashSet<Type>();
 292
 155296293        foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance))
 294        {
 97098295            foreach (var param in ctor.GetParameters())
 296            {
 9725297                paramTypes.Add(param.ParameterType);
 298            }
 299        }
 300
 38824301        return paramTypes;
 302    }
 303
 304    /// <summary>
 305    /// Checks if an interface is a decorator interface (type implements it and also takes it as a constructor parameter
 306    /// A type that implements IFoo and takes IFoo in its constructor is likely a decorator
 307    /// and should not be auto-registered as IFoo to avoid circular dependencies.
 308    /// </summary>
 309    private static bool IsDecoratorInterface(Type interfaceType, HashSet<Type> constructorParamTypes)
 310    {
 19421311        return constructorParamTypes.Contains(interfaceType);
 312    }
 313}