< 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    {
 27925        var allTypes = assemblies
 33326            .SelectMany(assembly => GetAllTypes(assembly))
 10533827            .Where(type => type is not null)
 10533828            .Where(type => type.IsClass && !type.IsAbstract) // Only concrete classes
 7702329            .Where(type => !HasDoNotAutoRegisterAttribute(type))
 27930            .ToList();
 31
 13621032        foreach (var type in allTypes)
 33        {
 34            // Check if type is excluded via Except<T>() or Except(predicate)
 6782635            if (typeFilterer.IsTypeExcluded(type))
 36            {
 37                continue;
 38            }
 39
 6780140            if (typeFilterer.IsInjectableSingletonType(type))
 41            {
 3483842                RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Singleton);
 43            }
 3296344            else if (typeFilterer.IsInjectableTransientType(type))
 45            {
 62546                RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Transient);
 47            }
 3233848            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
 27956        RegisterHostedServices(services, allTypes);
 57
 58        // Apply decorators discovered via [DecoratorFor<T>] attributes
 27959        ApplyDecoratorForAttributes(services, allTypes);
 27960    }
 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    {
 27968        var hostedServiceTypes = allTypes
 27969            .Where(IsHostedServiceType)
 27970            .ToList();
 71
 154272        foreach (var type in hostedServiceTypes)
 73        {
 74            // Skip if already registered as concrete type (from regular registration)
 3175875            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        }
 27985    }
 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    {
 6782693        if (!type.IsClass || type.IsAbstract)
 094            return false;
 95
 96        // Skip types that are decorators for IHostedService
 6782697        if (IsDecoratorForHostedService(type))
 61598            return false;
 99
 100        // Check if inherits from BackgroundService
 67211101        var baseType = type.BaseType;
 136230102        while (baseType is not null)
 103        {
 69511104            if (baseType.FullName == "Microsoft.Extensions.Hosting.BackgroundService")
 492105                return true;
 69019106            baseType = baseType.BaseType;
 107        }
 108
 109        // Check if directly implements IHostedService
 102049110        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    {
 314253118        foreach (var attribute in type.GetCustomAttributes(inherit: false))
 119        {
 89608120            var attrType = attribute.GetType();
 89608121            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        }
 67211133        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    {
 279141        var decoratorInfos = new List<(Type DecoratorType, Type ServiceType, int Order)>();
 142
 136210143        foreach (var type in allTypes)
 144        {
 314868145            foreach (var attribute in type.GetCustomAttributes(inherit: false))
 146            {
 89608147                var attrType = attribute.GetType();
 89608148                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
 279167        var decoratorsByService = decoratorInfos
 1230168            .GroupBy(d => d.ServiceType)
 689169            .OrderBy(g => g.Key.FullName);
 170
 1378171        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        }
 279186    }
 187
 188    private static Type[] GetAllTypes(Assembly assembly)
 189    {
 190        try
 191        {
 333192            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        }
 333204    }
 205
 206    private static bool HasDoNotAutoRegisterAttribute(Type type)
 207    {
 136408208        return type.GetCustomAttribute<DoNotAutoRegisterAttribute>(inherit: true) is not null ||
 195793209            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
 36078217        services.Add(new ServiceDescriptor(type, type, lifetime));
 218
 219        // Get constructor parameter types to detect decorator pattern
 36078220        var constructorParamTypes = GetConstructorParameterTypes(type);
 221
 222        // Check for [RegisterAs<T>] attributes - if present, only register as those interfaces
 36078223        var registerAsInterfaces = GetRegisterAsInterfaces(type);
 224
 225        List<Type> interfaces;
 36078226        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 
 35463234            interfaces = type.GetInterfaces()
 20134235                .Where(i => !i.IsGenericTypeDefinition)
 20134236                .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
 35463240                .ToList();
 241        }
 242
 108342243        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        }
 36078259    }
 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    {
 36078266        var interfaces = new List<Type>();
 267
 155864268        foreach (var attribute in type.GetCustomAttributes(inherit: false))
 269        {
 41854270            var attrType = attribute.GetType();
 41854271            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
 36078283        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    {
 36078291        var paramTypes = new HashSet<Type>();
 292
 144312293        foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance))
 294        {
 91606295            foreach (var param in ctor.GetParameters())
 296            {
 9725297                paramTypes.Add(param.ParameterType);
 298            }
 299        }
 300
 36078301        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}