| | | 1 | | using Microsoft.Extensions.DependencyInjection; |
| | | 2 | | using Microsoft.Extensions.Hosting; |
| | | 3 | | |
| | | 4 | | using System.Diagnostics.CodeAnalysis; |
| | | 5 | | using System.Reflection; |
| | | 6 | | |
| | | 7 | | namespace 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 |
| | | 17 | | public sealed class ReflectionTypeRegistrar : ITypeRegistrar |
| | | 18 | | { |
| | | 19 | | /// <inheritdoc /> |
| | | 20 | | public void RegisterTypesFromAssemblies( |
| | | 21 | | IServiceCollection services, |
| | | 22 | | ITypeFilterer typeFilterer, |
| | | 23 | | IReadOnlyList<Assembly> assemblies) |
| | | 24 | | { |
| | 279 | 25 | | var allTypes = assemblies |
| | 333 | 26 | | .SelectMany(assembly => GetAllTypes(assembly)) |
| | 105338 | 27 | | .Where(type => type is not null) |
| | 105338 | 28 | | .Where(type => type.IsClass && !type.IsAbstract) // Only concrete classes |
| | 77023 | 29 | | .Where(type => !HasDoNotAutoRegisterAttribute(type)) |
| | 279 | 30 | | .ToList(); |
| | | 31 | | |
| | 136210 | 32 | | foreach (var type in allTypes) |
| | | 33 | | { |
| | | 34 | | // Check if type is excluded via Except<T>() or Except(predicate) |
| | 67826 | 35 | | if (typeFilterer.IsTypeExcluded(type)) |
| | | 36 | | { |
| | | 37 | | continue; |
| | | 38 | | } |
| | | 39 | | |
| | 67801 | 40 | | if (typeFilterer.IsInjectableSingletonType(type)) |
| | | 41 | | { |
| | 34838 | 42 | | RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Singleton); |
| | | 43 | | } |
| | 32963 | 44 | | else if (typeFilterer.IsInjectableTransientType(type)) |
| | | 45 | | { |
| | 625 | 46 | | RegisterTypeAsSelfWithInterfaces(services, type, ServiceLifetime.Transient); |
| | | 47 | | } |
| | 32338 | 48 | | else if (typeFilterer.IsInjectableScopedType(type)) |
| | | 49 | | { |
| | 615 | 50 | | 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 |
| | 279 | 56 | | RegisterHostedServices(services, allTypes); |
| | | 57 | | |
| | | 58 | | // Apply decorators discovered via [DecoratorFor<T>] attributes |
| | 279 | 59 | | ApplyDecoratorForAttributes(services, allTypes); |
| | 279 | 60 | | } |
| | | 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 | | { |
| | 279 | 68 | | var hostedServiceTypes = allTypes |
| | 279 | 69 | | .Where(IsHostedServiceType) |
| | 279 | 70 | | .ToList(); |
| | | 71 | | |
| | 1542 | 72 | | foreach (var type in hostedServiceTypes) |
| | | 73 | | { |
| | | 74 | | // Skip if already registered as concrete type (from regular registration) |
| | 31758 | 75 | | var existingRegistration = services.FirstOrDefault(d => d.ServiceType == type); |
| | 492 | 76 | | if (existingRegistration is null) |
| | | 77 | | { |
| | | 78 | | // Register concrete type as singleton |
| | 0 | 79 | | services.AddSingleton(type); |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | // Register as IHostedService forwarding to concrete type |
| | 528 | 83 | | services.AddSingleton<IHostedService>(sp => (IHostedService)sp.GetRequiredService(type)); |
| | | 84 | | } |
| | 279 | 85 | | } |
| | | 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 | | { |
| | 67826 | 93 | | if (!type.IsClass || type.IsAbstract) |
| | 0 | 94 | | return false; |
| | | 95 | | |
| | | 96 | | // Skip types that are decorators for IHostedService |
| | 67826 | 97 | | if (IsDecoratorForHostedService(type)) |
| | 615 | 98 | | return false; |
| | | 99 | | |
| | | 100 | | // Check if inherits from BackgroundService |
| | 67211 | 101 | | var baseType = type.BaseType; |
| | 136230 | 102 | | while (baseType is not null) |
| | | 103 | | { |
| | 69511 | 104 | | if (baseType.FullName == "Microsoft.Extensions.Hosting.BackgroundService") |
| | 492 | 105 | | return true; |
| | 69019 | 106 | | baseType = baseType.BaseType; |
| | | 107 | | } |
| | | 108 | | |
| | | 109 | | // Check if directly implements IHostedService |
| | 102049 | 110 | | return type.GetInterfaces().Any(i => i.FullName == "Microsoft.Extensions.Hosting.IHostedService"); |
| | | 111 | | } |
| | | 112 | | |
| | | 113 | | /// <summary> |
| | | 114 | | /// Checks if a type has [DecoratorFor<IHostedService>] attribute. |
| | | 115 | | /// </summary> |
| | | 116 | | private static bool IsDecoratorForHostedService(Type type) |
| | | 117 | | { |
| | 314253 | 118 | | foreach (var attribute in type.GetCustomAttributes(inherit: false)) |
| | | 119 | | { |
| | 89608 | 120 | | var attrType = attribute.GetType(); |
| | 89608 | 121 | | if (!attrType.IsGenericType) |
| | | 122 | | continue; |
| | | 123 | | |
| | 3075 | 124 | | var genericTypeDef = attrType.GetGenericTypeDefinition(); |
| | 3075 | 125 | | if (genericTypeDef.FullName?.StartsWith("NexusLabs.Needlr.DecoratorForAttribute`1", StringComparison.Ordinal |
| | | 126 | | continue; |
| | | 127 | | |
| | | 128 | | // Get the type argument |
| | 1230 | 129 | | var typeArgs = attrType.GetGenericArguments(); |
| | 1230 | 130 | | if (typeArgs.Length == 1 && typeArgs[0].FullName == "Microsoft.Extensions.Hosting.IHostedService") |
| | 615 | 131 | | return true; |
| | | 132 | | } |
| | 67211 | 133 | | return false; |
| | | 134 | | } |
| | | 135 | | |
| | | 136 | | /// <summary> |
| | | 137 | | /// Discovers and applies decorators marked with [DecoratorFor<T>] attributes. |
| | | 138 | | /// </summary> |
| | | 139 | | private static void ApplyDecoratorForAttributes(IServiceCollection services, List<Type> allTypes) |
| | | 140 | | { |
| | 279 | 141 | | var decoratorInfos = new List<(Type DecoratorType, Type ServiceType, int Order)>(); |
| | | 142 | | |
| | 136210 | 143 | | foreach (var type in allTypes) |
| | | 144 | | { |
| | 314868 | 145 | | foreach (var attribute in type.GetCustomAttributes(inherit: false)) |
| | | 146 | | { |
| | 89608 | 147 | | var attrType = attribute.GetType(); |
| | 89608 | 148 | | if (!attrType.IsGenericType) |
| | | 149 | | continue; |
| | | 150 | | |
| | 3075 | 151 | | var genericTypeDef = attrType.GetGenericTypeDefinition(); |
| | 3075 | 152 | | if (genericTypeDef.FullName?.StartsWith("NexusLabs.Needlr.DecoratorForAttribute`1", StringComparison.Ord |
| | | 153 | | continue; |
| | | 154 | | |
| | | 155 | | // Get the service type from the generic type argument |
| | 1230 | 156 | | var serviceType = attrType.GetGenericArguments()[0]; |
| | | 157 | | |
| | | 158 | | // Get the Order property value |
| | 1230 | 159 | | var orderProperty = attrType.GetProperty("Order"); |
| | 1230 | 160 | | var order = orderProperty?.GetValue(attribute) as int? ?? 0; |
| | | 161 | | |
| | 1230 | 162 | | decoratorInfos.Add((type, serviceType, order)); |
| | | 163 | | } |
| | | 164 | | } |
| | | 165 | | |
| | | 166 | | // Group by service type and apply in order |
| | 279 | 167 | | var decoratorsByService = decoratorInfos |
| | 1230 | 168 | | .GroupBy(d => d.ServiceType) |
| | 689 | 169 | | .OrderBy(g => g.Key.FullName); |
| | | 170 | | |
| | 1378 | 171 | | foreach (var serviceGroup in decoratorsByService) |
| | | 172 | | { |
| | 4510 | 173 | | foreach (var decorator in serviceGroup.OrderBy(d => d.Order)) |
| | | 174 | | { |
| | | 175 | | try |
| | | 176 | | { |
| | 1230 | 177 | | services.AddDecorator(decorator.ServiceType, decorator.DecoratorType); |
| | 1230 | 178 | | } |
| | 0 | 179 | | catch (InvalidOperationException) |
| | | 180 | | { |
| | | 181 | | // Service not registered - skip this decorator |
| | | 182 | | // This can happen if the base service wasn't registered (e.g., excluded) |
| | 0 | 183 | | } |
| | | 184 | | } |
| | | 185 | | } |
| | 279 | 186 | | } |
| | | 187 | | |
| | | 188 | | private static Type[] GetAllTypes(Assembly assembly) |
| | | 189 | | { |
| | | 190 | | try |
| | | 191 | | { |
| | 333 | 192 | | return assembly.GetTypes(); |
| | | 193 | | } |
| | | 194 | | catch (ReflectionTypeLoadException ex) |
| | | 195 | | { |
| | | 196 | | // Return only the types that loaded successfully |
| | 0 | 197 | | return ex.Types.Where(t => t is not null).ToArray()!; |
| | | 198 | | } |
| | 0 | 199 | | catch |
| | | 200 | | { |
| | | 201 | | // If we can't load types from the assembly, skip it |
| | 0 | 202 | | return []; |
| | | 203 | | } |
| | 333 | 204 | | } |
| | | 205 | | |
| | | 206 | | private static bool HasDoNotAutoRegisterAttribute(Type type) |
| | | 207 | | { |
| | 136408 | 208 | | return type.GetCustomAttribute<DoNotAutoRegisterAttribute>(inherit: true) is not null || |
| | 195793 | 209 | | 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 |
| | 36078 | 217 | | services.Add(new ServiceDescriptor(type, type, lifetime)); |
| | | 218 | | |
| | | 219 | | // Get constructor parameter types to detect decorator pattern |
| | 36078 | 220 | | var constructorParamTypes = GetConstructorParameterTypes(type); |
| | | 221 | | |
| | | 222 | | // Check for [RegisterAs<T>] attributes - if present, only register as those interfaces |
| | 36078 | 223 | | var registerAsInterfaces = GetRegisterAsInterfaces(type); |
| | | 224 | | |
| | | 225 | | List<Type> interfaces; |
| | 36078 | 226 | | if (registerAsInterfaces.Count > 0) |
| | | 227 | | { |
| | | 228 | | // Only register as explicitly specified interfaces via [RegisterAs<T>] |
| | 615 | 229 | | interfaces = registerAsInterfaces; |
| | | 230 | | } |
| | | 231 | | else |
| | | 232 | | { |
| | | 233 | | // Register as interfaces (excluding system interfaces, generic type definitions, decorator interfaces, and |
| | 35463 | 234 | | interfaces = type.GetInterfaces() |
| | 20134 | 235 | | .Where(i => !i.IsGenericTypeDefinition) |
| | 20134 | 236 | | .Where(i => i.Assembly != typeof(object).Assembly) // Skip system interfaces |
| | 19421 | 237 | | .Where(i => !i.Name.StartsWith("System.")) // Additional system interface filtering |
| | 19421 | 238 | | .Where(i => !IsDecoratorInterface(i, constructorParamTypes)) // Skip decorator pattern interfaces |
| | 17765 | 239 | | .Where(i => i.FullName != "Microsoft.Extensions.Hosting.IHostedService") // Hosted services registered s |
| | 35463 | 240 | | .ToList(); |
| | | 241 | | } |
| | | 242 | | |
| | 108342 | 243 | | foreach (var interfaceType in interfaces) |
| | | 244 | | { |
| | | 245 | | // For singletons, register interfaces as factory delegates to ensure same instance |
| | 18093 | 246 | | if (lifetime == ServiceLifetime.Singleton) |
| | | 247 | | { |
| | 16853 | 248 | | services.Add(new ServiceDescriptor( |
| | 16853 | 249 | | interfaceType, |
| | 67 | 250 | | serviceProvider => serviceProvider.GetRequiredService(type), |
| | 16853 | 251 | | lifetime)); |
| | | 252 | | } |
| | | 253 | | else |
| | | 254 | | { |
| | | 255 | | // For transient services, direct registration is fine |
| | 1240 | 256 | | services.Add(new ServiceDescriptor(interfaceType, type, lifetime)); |
| | | 257 | | } |
| | | 258 | | } |
| | 36078 | 259 | | } |
| | | 260 | | |
| | | 261 | | /// <summary> |
| | | 262 | | /// Gets interface types specified by [RegisterAs<T>] attributes on the type. |
| | | 263 | | /// </summary> |
| | | 264 | | private static List<Type> GetRegisterAsInterfaces(Type type) |
| | | 265 | | { |
| | 36078 | 266 | | var interfaces = new List<Type>(); |
| | | 267 | | |
| | 155864 | 268 | | foreach (var attribute in type.GetCustomAttributes(inherit: false)) |
| | | 269 | | { |
| | 41854 | 270 | | var attrType = attribute.GetType(); |
| | 41854 | 271 | | if (!attrType.IsGenericType) |
| | | 272 | | continue; |
| | | 273 | | |
| | 2870 | 274 | | var genericTypeDef = attrType.GetGenericTypeDefinition(); |
| | 2870 | 275 | | if (genericTypeDef.FullName?.StartsWith("NexusLabs.Needlr.RegisterAsAttribute`1", StringComparison.Ordinal) |
| | | 276 | | continue; |
| | | 277 | | |
| | | 278 | | // Get the interface type from the generic type argument |
| | 820 | 279 | | var interfaceType = attrType.GetGenericArguments()[0]; |
| | 820 | 280 | | interfaces.Add(interfaceType); |
| | | 281 | | } |
| | | 282 | | |
| | 36078 | 283 | | 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 | | { |
| | 36078 | 291 | | var paramTypes = new HashSet<Type>(); |
| | | 292 | | |
| | 144312 | 293 | | foreach (var ctor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)) |
| | | 294 | | { |
| | 91606 | 295 | | foreach (var param in ctor.GetParameters()) |
| | | 296 | | { |
| | 9725 | 297 | | paramTypes.Add(param.ParameterType); |
| | | 298 | | } |
| | | 299 | | } |
| | | 300 | | |
| | 36078 | 301 | | 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 | | { |
| | 19421 | 311 | | return constructorParamTypes.Contains(interfaceType); |
| | | 312 | | } |
| | | 313 | | } |