| | | 1 | | using Microsoft.CodeAnalysis; |
| | | 2 | | using Microsoft.CodeAnalysis.Text; |
| | | 3 | | using NexusLabs.Needlr.Generators.Helpers; |
| | | 4 | | using NexusLabs.Needlr.Generators.Models; |
| | | 5 | | using System.Text; |
| | | 6 | | |
| | | 7 | | namespace NexusLabs.Needlr.Generators; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Incremental source generator that produces a compile-time type registry |
| | | 11 | | /// for dependency injection, eliminating runtime reflection. |
| | | 12 | | /// </summary> |
| | | 13 | | [Generator(LanguageNames.CSharp)] |
| | | 14 | | public sealed class TypeRegistryGenerator : IIncrementalGenerator |
| | | 15 | | { |
| | | 16 | | private const string GenerateTypeRegistryAttributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute" |
| | | 17 | | |
| | | 18 | | public void Initialize(IncrementalGeneratorInitializationContext context) |
| | | 19 | | { |
| | | 20 | | // Combine compilation with analyzer config options to read MSBuild properties |
| | 434 | 21 | | var compilationAndOptions = context.CompilationProvider |
| | 434 | 22 | | .Combine(context.AnalyzerConfigOptionsProvider); |
| | | 23 | | |
| | | 24 | | // ForAttributeWithMetadataName doesn't work for assembly-level attributes. |
| | | 25 | | // Instead, we register directly on the compilation provider and check |
| | | 26 | | // compilation.Assembly.GetAttributes() for [GenerateTypeRegistry]. |
| | 434 | 27 | | context.RegisterSourceOutput(compilationAndOptions, static (spc, source) => |
| | 434 | 28 | | { |
| | 434 | 29 | | var (compilation, configOptions) = source; |
| | 434 | 30 | | |
| | 434 | 31 | | var attributeInfo = GetAttributeInfoFromCompilation(compilation); |
| | 434 | 32 | | if (attributeInfo == null) |
| | 0 | 33 | | return; |
| | 434 | 34 | | |
| | 434 | 35 | | var info = attributeInfo.Value; |
| | 434 | 36 | | var assemblyName = compilation.AssemblyName ?? "Generated"; |
| | 434 | 37 | | |
| | 434 | 38 | | // Read breadcrumb level from MSBuild property |
| | 434 | 39 | | var breadcrumbLevel = GetBreadcrumbLevel(configOptions); |
| | 434 | 40 | | var projectDirectory = GetProjectDirectory(configOptions); |
| | 434 | 41 | | var breadcrumbs = new BreadcrumbWriter(breadcrumbLevel); |
| | 434 | 42 | | |
| | 434 | 43 | | // Check if this is an AOT project |
| | 434 | 44 | | var isAotProject = IsAotProject(configOptions); |
| | 434 | 45 | | |
| | 434 | 46 | | var discoveryResult = DiscoverTypes( |
| | 434 | 47 | | compilation, |
| | 434 | 48 | | info.NamespacePrefixes, |
| | 434 | 49 | | info.IncludeSelf); |
| | 434 | 50 | | |
| | 434 | 51 | | // Report errors for inaccessible internal types in referenced assemblies |
| | 5400 | 52 | | foreach (var inaccessibleType in discoveryResult.InaccessibleTypes) |
| | 434 | 53 | | { |
| | 2266 | 54 | | spc.ReportDiagnostic(Diagnostic.Create( |
| | 2266 | 55 | | DiagnosticDescriptors.InaccessibleInternalType, |
| | 2266 | 56 | | Location.None, |
| | 2266 | 57 | | inaccessibleType.TypeName, |
| | 2266 | 58 | | inaccessibleType.AssemblyName)); |
| | 434 | 59 | | } |
| | 434 | 60 | | |
| | 434 | 61 | | // Report errors for referenced assemblies with internal plugin types but no [GenerateTypeRegistry] |
| | 870 | 62 | | foreach (var missingPlugin in discoveryResult.MissingTypeRegistryPlugins) |
| | 434 | 63 | | { |
| | 1 | 64 | | spc.ReportDiagnostic(Diagnostic.Create( |
| | 1 | 65 | | DiagnosticDescriptors.MissingGenerateTypeRegistryAttribute, |
| | 1 | 66 | | Location.None, |
| | 1 | 67 | | missingPlugin.AssemblyName, |
| | 1 | 68 | | missingPlugin.TypeName)); |
| | 434 | 69 | | } |
| | 434 | 70 | | |
| | 434 | 71 | | // NDLRGEN020: Previously reported error if [Options] used in AOT project |
| | 434 | 72 | | // Now removed for parity - we generate best-effort code and let unsupported |
| | 434 | 73 | | // types fail at runtime (matching non-AOT ConfigurationBinder behavior) |
| | 434 | 74 | | |
| | 434 | 75 | | // NDLRGEN021: Report warning for non-partial positional records |
| | 1037 | 76 | | foreach (var opt in discoveryResult.Options.Where(o => o.IsNonPartialPositionalRecord)) |
| | 434 | 77 | | { |
| | 2 | 78 | | spc.ReportDiagnostic(Diagnostic.Create( |
| | 2 | 79 | | DiagnosticDescriptors.PositionalRecordMustBePartial, |
| | 2 | 80 | | Location.None, |
| | 2 | 81 | | opt.TypeName)); |
| | 434 | 82 | | } |
| | 434 | 83 | | |
| | 434 | 84 | | // NDLRGEN022: Detect disposable captive dependencies using inferred lifetimes |
| | 434 | 85 | | ReportDisposableCaptiveDependencies(spc, discoveryResult); |
| | 434 | 86 | | |
| | 434 | 87 | | var sourceText = GenerateTypeRegistrySource(discoveryResult, assemblyName, breadcrumbs, projectDirectory, is |
| | 434 | 88 | | spc.AddSource("TypeRegistry.g.cs", SourceText.From(sourceText, Encoding.UTF8)); |
| | 434 | 89 | | |
| | 434 | 90 | | // Discover referenced assemblies with [GenerateTypeRegistry] for forced loading |
| | 434 | 91 | | // Note: Order of force-loading doesn't matter; ordering is applied at service registration time |
| | 434 | 92 | | var referencedAssemblies = DiscoverReferencedAssembliesWithTypeRegistry(compilation) |
| | 2 | 93 | | .OrderBy(a => a, StringComparer.OrdinalIgnoreCase) |
| | 434 | 94 | | .ToList(); |
| | 434 | 95 | | |
| | 434 | 96 | | var bootstrapText = GenerateModuleInitializerBootstrapSource(assemblyName, referencedAssemblies, breadcrumbs |
| | 434 | 97 | | spc.AddSource("NeedlrSourceGenBootstrap.g.cs", SourceText.From(bootstrapText, Encoding.UTF8)); |
| | 434 | 98 | | |
| | 434 | 99 | | // Generate interceptor proxy classes if any were discovered |
| | 434 | 100 | | if (discoveryResult.InterceptedServices.Count > 0) |
| | 434 | 101 | | { |
| | 15 | 102 | | var interceptorProxiesText = GenerateInterceptorProxiesSource(discoveryResult.InterceptedServices, assem |
| | 15 | 103 | | spc.AddSource("InterceptorProxies.g.cs", SourceText.From(interceptorProxiesText, Encoding.UTF8)); |
| | 434 | 104 | | } |
| | 434 | 105 | | |
| | 434 | 106 | | // Generate factory classes if any were discovered |
| | 434 | 107 | | if (discoveryResult.Factories.Count > 0) |
| | 434 | 108 | | { |
| | 26 | 109 | | var factoriesText = GenerateFactoriesSource(discoveryResult.Factories, assemblyName, breadcrumbs, projec |
| | 26 | 110 | | spc.AddSource("Factories.g.cs", SourceText.From(factoriesText, Encoding.UTF8)); |
| | 434 | 111 | | } |
| | 434 | 112 | | |
| | 434 | 113 | | // Generate provider classes if any were discovered |
| | 434 | 114 | | if (discoveryResult.Providers.Count > 0) |
| | 434 | 115 | | { |
| | 434 | 116 | | // Interface-based providers go in the Generated namespace |
| | 35 | 117 | | var interfaceProviders = discoveryResult.Providers.Where(p => p.IsInterface).ToList(); |
| | 17 | 118 | | if (interfaceProviders.Count > 0) |
| | 434 | 119 | | { |
| | 11 | 120 | | var providersText = GenerateProvidersSource(interfaceProviders, assemblyName, breadcrumbs, projectDi |
| | 11 | 121 | | spc.AddSource("Providers.g.cs", SourceText.From(providersText, Encoding.UTF8)); |
| | 434 | 122 | | } |
| | 434 | 123 | | |
| | 434 | 124 | | // Shorthand class providers need to be generated in their original namespace |
| | 35 | 125 | | var classProviders = discoveryResult.Providers.Where(p => !p.IsInterface && p.IsPartial).ToList(); |
| | 46 | 126 | | foreach (var provider in classProviders) |
| | 434 | 127 | | { |
| | 6 | 128 | | var providerText = GenerateShorthandProviderSource(provider, assemblyName, breadcrumbs, projectDirec |
| | 6 | 129 | | spc.AddSource($"Provider.{provider.SimpleTypeName}.g.cs", SourceText.From(providerText, Encoding.UTF |
| | 434 | 130 | | } |
| | 434 | 131 | | } |
| | 434 | 132 | | |
| | 434 | 133 | | // Generate options validator classes if any have validation methods |
| | 599 | 134 | | var optionsWithValidators = discoveryResult.Options.Where(o => o.HasValidatorMethod).ToList(); |
| | 434 | 135 | | if (optionsWithValidators.Count > 0) |
| | 434 | 136 | | { |
| | 18 | 137 | | var validatorsText = CodeGen.OptionsCodeGenerator.GenerateOptionsValidatorsSource(optionsWithValidators, |
| | 18 | 138 | | spc.AddSource("OptionsValidators.g.cs", SourceText.From(validatorsText, Encoding.UTF8)); |
| | 434 | 139 | | } |
| | 434 | 140 | | |
| | 434 | 141 | | // Generate DataAnnotations validator classes if any have DataAnnotation attributes |
| | 599 | 142 | | var optionsWithDataAnnotations = discoveryResult.Options.Where(o => o.HasDataAnnotations).ToList(); |
| | 434 | 143 | | if (optionsWithDataAnnotations.Count > 0) |
| | 434 | 144 | | { |
| | 18 | 145 | | var dataAnnotationsValidatorsText = CodeGen.OptionsCodeGenerator.GenerateDataAnnotationsValidatorsSource |
| | 18 | 146 | | spc.AddSource("OptionsDataAnnotationsValidators.g.cs", SourceText.From(dataAnnotationsValidatorsText, En |
| | 434 | 147 | | } |
| | 434 | 148 | | |
| | 434 | 149 | | // Generate parameterless constructors for partial positional records with [Options] |
| | 599 | 150 | | var optionsNeedingConstructors = discoveryResult.Options.Where(o => o.NeedsGeneratedConstructor).ToList(); |
| | 434 | 151 | | if (optionsNeedingConstructors.Count > 0) |
| | 434 | 152 | | { |
| | 7 | 153 | | var constructorsText = CodeGen.OptionsCodeGenerator.GeneratePositionalRecordConstructorsSource(optionsNe |
| | 7 | 154 | | spc.AddSource("OptionsConstructors.g.cs", SourceText.From(constructorsText, Encoding.UTF8)); |
| | 434 | 155 | | } |
| | 434 | 156 | | |
| | 434 | 157 | | // Generate ServiceCatalog for runtime introspection |
| | 434 | 158 | | var catalogText = CodeGen.ServiceCatalogCodeGenerator.GenerateServiceCatalogSource(discoveryResult, assembly |
| | 434 | 159 | | spc.AddSource("ServiceCatalog.g.cs", SourceText.From(catalogText, Encoding.UTF8)); |
| | 434 | 160 | | |
| | 434 | 161 | | // Generate diagnostic output files if configured |
| | 434 | 162 | | var diagnosticOptions = GetDiagnosticOptions(configOptions); |
| | 434 | 163 | | if (diagnosticOptions.Enabled) |
| | 434 | 164 | | { |
| | 95 | 165 | | var referencedAssemblyTypes = DiscoverReferencedAssemblyTypesForDiagnostics(compilation); |
| | 95 | 166 | | var diagnosticsText = DiagnosticsGenerator.GenerateDiagnosticsSource(discoveryResult, assemblyName, proj |
| | 95 | 167 | | spc.AddSource("NeedlrDiagnostics.g.cs", SourceText.From(diagnosticsText, Encoding.UTF8)); |
| | 434 | 168 | | } |
| | 868 | 169 | | }); |
| | 434 | 170 | | } |
| | | 171 | | |
| | | 172 | | /// <summary> |
| | | 173 | | /// Detects disposable captive dependencies using inferred lifetimes from DiscoveryResult. |
| | | 174 | | /// Reports NDLRGEN022 when a longer-lived service depends on a shorter-lived disposable. |
| | | 175 | | /// </summary> |
| | | 176 | | private static void ReportDisposableCaptiveDependencies(SourceProductionContext spc, DiscoveryResult discoveryResult |
| | | 177 | | { |
| | | 178 | | // Build lookup from type name to DiscoveredType for O(1) lifetime lookups |
| | 434 | 179 | | var typeLookup = new Dictionary<string, DiscoveredType>(); |
| | 457032 | 180 | | foreach (var type in discoveryResult.InjectableTypes) |
| | | 181 | | { |
| | 228082 | 182 | | typeLookup[type.TypeName] = type; |
| | | 183 | | // Also map by interfaces so we can look up dependencies by interface |
| | 456656 | 184 | | foreach (var iface in type.InterfaceNames) |
| | | 185 | | { |
| | | 186 | | // Only add if not already present (first registration wins for interface resolution) |
| | 246 | 187 | | if (!typeLookup.ContainsKey(iface)) |
| | | 188 | | { |
| | 240 | 189 | | typeLookup[iface] = type; |
| | | 190 | | } |
| | | 191 | | } |
| | | 192 | | } |
| | | 193 | | |
| | | 194 | | // Check each injectable type for captive dependencies |
| | 457032 | 195 | | foreach (var type in discoveryResult.InjectableTypes) |
| | | 196 | | { |
| | 228082 | 197 | | CheckForCaptiveDependencies(spc, type, typeLookup); |
| | | 198 | | } |
| | 434 | 199 | | } |
| | | 200 | | |
| | | 201 | | /// <summary> |
| | | 202 | | /// Checks a single type for captive dependency issues. |
| | | 203 | | /// </summary> |
| | | 204 | | private static void CheckForCaptiveDependencies( |
| | | 205 | | SourceProductionContext spc, |
| | | 206 | | DiscoveredType type, |
| | | 207 | | Dictionary<string, DiscoveredType> typeLookup) |
| | | 208 | | { |
| | | 209 | | // Skip types with transient lifetime - they can't capture shorter-lived dependencies |
| | 228082 | 210 | | if (type.Lifetime == GeneratorLifetime.Transient) |
| | 4 | 211 | | return; |
| | | 212 | | |
| | 564086 | 213 | | foreach (var param in type.ConstructorParameters) |
| | | 214 | | { |
| | | 215 | | // Skip factory patterns that create new instances on demand |
| | 53965 | 216 | | if (IsFactoryPattern(param.TypeName)) |
| | | 217 | | continue; |
| | | 218 | | |
| | | 219 | | // Try to find the dependency in our discovered types |
| | 53965 | 220 | | if (!typeLookup.TryGetValue(param.TypeName, out var dependency)) |
| | | 221 | | continue; |
| | | 222 | | |
| | | 223 | | // Check if the dependency is shorter-lived |
| | 17765 | 224 | | if (!IsShorterLifetime(type.Lifetime, dependency.Lifetime)) |
| | | 225 | | continue; |
| | | 226 | | |
| | | 227 | | // Check if the dependency is disposable |
| | 6 | 228 | | if (!dependency.IsDisposable) |
| | | 229 | | continue; |
| | | 230 | | |
| | | 231 | | // Report the captive dependency |
| | 5 | 232 | | spc.ReportDiagnostic(Diagnostic.Create( |
| | 5 | 233 | | DiagnosticDescriptors.DisposableCaptiveDependency, |
| | 5 | 234 | | Location.None, |
| | 5 | 235 | | type.TypeName, |
| | 5 | 236 | | GetLifetimeName(type.Lifetime), |
| | 5 | 237 | | dependency.TypeName, |
| | 5 | 238 | | GetLifetimeName(dependency.Lifetime))); |
| | | 239 | | } |
| | 228078 | 240 | | } |
| | | 241 | | |
| | | 242 | | /// <summary> |
| | | 243 | | /// Checks if a type name represents a factory pattern that creates new instances on demand. |
| | | 244 | | /// </summary> |
| | | 245 | | private static bool IsFactoryPattern(string typeName) |
| | | 246 | | { |
| | | 247 | | // Func<T> - factory delegate |
| | 53965 | 248 | | if (typeName.StartsWith("System.Func<", StringComparison.Ordinal)) |
| | 0 | 249 | | return true; |
| | | 250 | | |
| | | 251 | | // Lazy<T> - deferred creation |
| | 53965 | 252 | | if (typeName.StartsWith("System.Lazy<", StringComparison.Ordinal)) |
| | 0 | 253 | | return true; |
| | | 254 | | |
| | | 255 | | // IServiceScopeFactory - creates new scopes |
| | 53965 | 256 | | if (typeName == "Microsoft.Extensions.DependencyInjection.IServiceScopeFactory") |
| | 0 | 257 | | return true; |
| | | 258 | | |
| | | 259 | | // IServiceProvider - resolves services dynamically |
| | 53965 | 260 | | if (typeName == "System.IServiceProvider") |
| | 0 | 261 | | return true; |
| | | 262 | | |
| | 53965 | 263 | | return false; |
| | | 264 | | } |
| | | 265 | | |
| | | 266 | | /// <summary> |
| | | 267 | | /// Checks if dependency lifetime is shorter than consumer lifetime. |
| | | 268 | | /// </summary> |
| | | 269 | | private static bool IsShorterLifetime(GeneratorLifetime consumer, GeneratorLifetime dependency) |
| | | 270 | | { |
| | | 271 | | // Singleton > Scoped > Transient (in terms of lifetime duration) |
| | | 272 | | // A shorter lifetime means the dependency will be disposed sooner |
| | 17765 | 273 | | return (consumer, dependency) switch |
| | 17765 | 274 | | { |
| | 4 | 275 | | (GeneratorLifetime.Singleton, GeneratorLifetime.Scoped) => true, |
| | 1 | 276 | | (GeneratorLifetime.Singleton, GeneratorLifetime.Transient) => true, |
| | 1 | 277 | | (GeneratorLifetime.Scoped, GeneratorLifetime.Transient) => true, |
| | 17759 | 278 | | _ => false |
| | 17765 | 279 | | }; |
| | | 280 | | } |
| | | 281 | | |
| | | 282 | | /// <summary> |
| | | 283 | | /// Gets the human-readable name for a lifetime. |
| | | 284 | | /// </summary> |
| | 10 | 285 | | private static string GetLifetimeName(GeneratorLifetime lifetime) => lifetime switch |
| | 10 | 286 | | { |
| | 4 | 287 | | GeneratorLifetime.Singleton => "Singleton", |
| | 4 | 288 | | GeneratorLifetime.Scoped => "Scoped", |
| | 2 | 289 | | GeneratorLifetime.Transient => "Transient", |
| | 0 | 290 | | _ => lifetime.ToString() |
| | 10 | 291 | | }; |
| | | 292 | | |
| | | 293 | | private static BreadcrumbLevel GetBreadcrumbLevel(Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider c |
| | | 294 | | { |
| | 434 | 295 | | if (configOptions.GlobalOptions.TryGetValue("build_property.NeedlrBreadcrumbLevel", out var levelStr) && |
| | 434 | 296 | | !string.IsNullOrWhiteSpace(levelStr)) |
| | | 297 | | { |
| | 259 | 298 | | if (levelStr.Equals("None", StringComparison.OrdinalIgnoreCase)) |
| | 17 | 299 | | return BreadcrumbLevel.None; |
| | 242 | 300 | | if (levelStr.Equals("Verbose", StringComparison.OrdinalIgnoreCase)) |
| | 28 | 301 | | return BreadcrumbLevel.Verbose; |
| | | 302 | | } |
| | | 303 | | |
| | | 304 | | // Default to Minimal |
| | 389 | 305 | | return BreadcrumbLevel.Minimal; |
| | | 306 | | } |
| | | 307 | | |
| | | 308 | | private static string? GetProjectDirectory(Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider configOp |
| | | 309 | | { |
| | | 310 | | // Try to get the project directory from MSBuild properties |
| | 434 | 311 | | if (configOptions.GlobalOptions.TryGetValue("build_property.ProjectDir", out var projectDir) && |
| | 434 | 312 | | !string.IsNullOrWhiteSpace(projectDir)) |
| | | 313 | | { |
| | 0 | 314 | | return projectDir.TrimEnd('/', '\\'); |
| | | 315 | | } |
| | | 316 | | |
| | 434 | 317 | | return null; |
| | | 318 | | } |
| | | 319 | | |
| | | 320 | | private static DiagnosticOptions GetDiagnosticOptions(Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvid |
| | | 321 | | { |
| | 434 | 322 | | configOptions.GlobalOptions.TryGetValue("build_property.NeedlrDiagnostics", out var enabled); |
| | 434 | 323 | | configOptions.GlobalOptions.TryGetValue("build_property.NeedlrDiagnosticsPath", out var outputPath); |
| | 434 | 324 | | configOptions.GlobalOptions.TryGetValue("build_property.NeedlrDiagnosticsFilter", out var filter); |
| | | 325 | | |
| | 434 | 326 | | return DiagnosticOptions.Parse(enabled, outputPath, filter); |
| | | 327 | | } |
| | | 328 | | |
| | | 329 | | /// <summary> |
| | | 330 | | /// Checks if the project is configured for AOT compilation. |
| | | 331 | | /// Returns true if either PublishAot or IsAotCompatible is set to true. |
| | | 332 | | /// </summary> |
| | | 333 | | private static bool IsAotProject(Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider configOptions) |
| | | 334 | | { |
| | 434 | 335 | | if (configOptions.GlobalOptions.TryGetValue("build_property.PublishAot", out var publishAot) && |
| | 434 | 336 | | publishAot.Equals("true", StringComparison.OrdinalIgnoreCase)) |
| | | 337 | | { |
| | 79 | 338 | | return true; |
| | | 339 | | } |
| | | 340 | | |
| | 355 | 341 | | if (configOptions.GlobalOptions.TryGetValue("build_property.IsAotCompatible", out var isAotCompatible) && |
| | 355 | 342 | | isAotCompatible.Equals("true", StringComparison.OrdinalIgnoreCase)) |
| | | 343 | | { |
| | 1 | 344 | | return true; |
| | | 345 | | } |
| | | 346 | | |
| | 354 | 347 | | return false; |
| | | 348 | | } |
| | | 349 | | |
| | | 350 | | /// <summary> |
| | | 351 | | /// Detects if a type is a positional record (record with primary constructor parameters). |
| | | 352 | | /// Returns null if not a positional record, or PositionalRecordInfo if it is. |
| | | 353 | | /// </summary> |
| | | 354 | | private static PositionalRecordInfo? DetectPositionalRecord(INamedTypeSymbol typeSymbol) |
| | | 355 | | { |
| | | 356 | | // Must be a record |
| | 167 | 357 | | if (!typeSymbol.IsRecord) |
| | 155 | 358 | | return null; |
| | | 359 | | |
| | | 360 | | // Check for primary constructor with parameters |
| | | 361 | | // Records with positional parameters have a primary constructor generated from the record declaration |
| | 12 | 362 | | var primaryCtor = typeSymbol.InstanceConstructors |
| | 27 | 363 | | .FirstOrDefault(c => c.Parameters.Length > 0 && IsPrimaryConstructor(c, typeSymbol)); |
| | | 364 | | |
| | 12 | 365 | | if (primaryCtor == null) |
| | 3 | 366 | | return null; |
| | | 367 | | |
| | | 368 | | // Check if the record has a parameterless constructor already |
| | | 369 | | // (user-defined or from record with init-only properties) |
| | 9 | 370 | | var hasParameterlessCtor = typeSymbol.InstanceConstructors |
| | 27 | 371 | | .Any(c => c.Parameters.Length == 0 && !c.IsImplicitlyDeclared); |
| | | 372 | | |
| | 9 | 373 | | if (hasParameterlessCtor) |
| | 0 | 374 | | return null; // Doesn't need generated constructor |
| | | 375 | | |
| | | 376 | | // Check if partial |
| | 9 | 377 | | var isPartial = typeSymbol.DeclaringSyntaxReferences |
| | 9 | 378 | | .Select(r => r.GetSyntax()) |
| | 9 | 379 | | .OfType<Microsoft.CodeAnalysis.CSharp.Syntax.TypeDeclarationSyntax>() |
| | 34 | 380 | | .Any(s => s.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword))); |
| | | 381 | | |
| | | 382 | | // Extract constructor parameters |
| | 9 | 383 | | var parameters = primaryCtor.Parameters |
| | 23 | 384 | | .Select(p => new PositionalRecordParameter(p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualified |
| | 9 | 385 | | .ToList(); |
| | | 386 | | |
| | | 387 | | // Get namespace |
| | 9 | 388 | | var containingNamespace = typeSymbol.ContainingNamespace.IsGlobalNamespace |
| | 9 | 389 | | ? "" |
| | 9 | 390 | | : typeSymbol.ContainingNamespace.ToDisplayString(); |
| | | 391 | | |
| | 9 | 392 | | return new PositionalRecordInfo( |
| | 9 | 393 | | typeSymbol.Name, |
| | 9 | 394 | | containingNamespace, |
| | 9 | 395 | | isPartial, |
| | 9 | 396 | | parameters); |
| | | 397 | | } |
| | | 398 | | |
| | | 399 | | /// <summary> |
| | | 400 | | /// Determines if a constructor is the primary constructor of a record. |
| | | 401 | | /// Primary constructors for positional records are synthesized and have matching properties. |
| | | 402 | | /// </summary> |
| | | 403 | | private static bool IsPrimaryConstructor(IMethodSymbol ctor, INamedTypeSymbol recordType) |
| | | 404 | | { |
| | | 405 | | // For positional records, the primary constructor parameters correspond to auto-properties |
| | | 406 | | // Check if each parameter has a matching property |
| | 73 | 407 | | foreach (var param in ctor.Parameters) |
| | | 408 | | { |
| | 26 | 409 | | var hasMatchingProperty = recordType.GetMembers() |
| | 26 | 410 | | .OfType<IPropertySymbol>() |
| | 101 | 411 | | .Any(p => p.Name.Equals(param.Name, StringComparison.Ordinal) && |
| | 101 | 412 | | SymbolEqualityComparer.Default.Equals(p.Type, param.Type)); |
| | | 413 | | |
| | 26 | 414 | | if (!hasMatchingProperty) |
| | 3 | 415 | | return false; |
| | | 416 | | } |
| | | 417 | | |
| | 9 | 418 | | return true; |
| | | 419 | | } |
| | | 420 | | |
| | | 421 | | /// <summary> |
| | | 422 | | /// Extracts bindable properties from an options type for AOT code generation. |
| | | 423 | | /// </summary> |
| | | 424 | | private static IReadOnlyList<OptionsPropertyInfo> ExtractBindableProperties(INamedTypeSymbol typeSymbol, HashSet<str |
| | | 425 | | { |
| | 195 | 426 | | var properties = new List<OptionsPropertyInfo>(); |
| | 195 | 427 | | visitedTypes ??= new HashSet<string>(); |
| | | 428 | | |
| | | 429 | | // Prevent infinite recursion for circular references |
| | 195 | 430 | | var typeFullName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | 195 | 431 | | if (!visitedTypes.Add(typeFullName)) |
| | | 432 | | { |
| | 6 | 433 | | return properties; // Already visited - circular reference |
| | | 434 | | } |
| | | 435 | | |
| | 3294 | 436 | | foreach (var member in typeSymbol.GetMembers()) |
| | | 437 | | { |
| | 1458 | 438 | | if (member is not IPropertySymbol property) |
| | | 439 | | continue; |
| | | 440 | | |
| | | 441 | | // Skip static, indexers, readonly properties without init |
| | 291 | 442 | | if (property.IsStatic || property.IsIndexer) |
| | | 443 | | continue; |
| | | 444 | | |
| | | 445 | | // Must have a setter (set or init) |
| | 291 | 446 | | if (property.SetMethod == null) |
| | | 447 | | continue; |
| | | 448 | | |
| | | 449 | | // Check if it's init-only |
| | 279 | 450 | | var isInitOnly = property.SetMethod.IsInitOnly; |
| | | 451 | | |
| | | 452 | | // Get nullability info |
| | 279 | 453 | | var isNullable = property.NullableAnnotation == NullableAnnotation.Annotated || |
| | 279 | 454 | | (property.Type is INamedTypeSymbol namedType && |
| | 279 | 455 | | namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T); |
| | | 456 | | |
| | 279 | 457 | | var typeName = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | | 458 | | |
| | | 459 | | // Check if it's an enum type |
| | 279 | 460 | | var isEnum = false; |
| | 279 | 461 | | string? enumTypeName = null; |
| | 279 | 462 | | var actualType = property.Type; |
| | | 463 | | |
| | | 464 | | // For nullable types, get the underlying type |
| | 279 | 465 | | if (actualType is INamedTypeSymbol nullableType && |
| | 279 | 466 | | nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && |
| | 279 | 467 | | nullableType.TypeArguments.Length == 1) |
| | | 468 | | { |
| | 4 | 469 | | actualType = nullableType.TypeArguments[0]; |
| | | 470 | | } |
| | | 471 | | |
| | 279 | 472 | | if (actualType.TypeKind == TypeKind.Enum) |
| | | 473 | | { |
| | 15 | 474 | | isEnum = true; |
| | 15 | 475 | | enumTypeName = actualType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | | 476 | | } |
| | | 477 | | |
| | | 478 | | // Detect complex types |
| | 279 | 479 | | var (complexKind, elementTypeName, nestedProps) = AnalyzeComplexType(property.Type, visitedTypes); |
| | | 480 | | |
| | | 481 | | // Extract DataAnnotation attributes |
| | 279 | 482 | | var dataAnnotations = ExtractDataAnnotations(property); |
| | | 483 | | |
| | 279 | 484 | | properties.Add(new OptionsPropertyInfo( |
| | 279 | 485 | | property.Name, |
| | 279 | 486 | | typeName, |
| | 279 | 487 | | isNullable, |
| | 279 | 488 | | isInitOnly, |
| | 279 | 489 | | isEnum, |
| | 279 | 490 | | enumTypeName, |
| | 279 | 491 | | complexKind, |
| | 279 | 492 | | elementTypeName, |
| | 279 | 493 | | nestedProps, |
| | 279 | 494 | | dataAnnotations)); |
| | | 495 | | } |
| | | 496 | | |
| | 189 | 497 | | return properties; |
| | | 498 | | } |
| | | 499 | | |
| | | 500 | | private static IReadOnlyList<DataAnnotationInfo> ExtractDataAnnotations(IPropertySymbol property) |
| | | 501 | | { |
| | 279 | 502 | | var annotations = new List<DataAnnotationInfo>(); |
| | | 503 | | |
| | 604 | 504 | | foreach (var attr in property.GetAttributes()) |
| | | 505 | | { |
| | 23 | 506 | | var attrClass = attr.AttributeClass; |
| | 23 | 507 | | if (attrClass == null) continue; |
| | | 508 | | |
| | | 509 | | // Get the attribute type name - use ContainingNamespace + Name for reliable matching |
| | 23 | 510 | | var attrNamespace = attrClass.ContainingNamespace?.ToDisplayString() ?? ""; |
| | 23 | 511 | | var attrTypeName = attrClass.Name; |
| | | 512 | | |
| | | 513 | | // Only process System.ComponentModel.DataAnnotations attributes |
| | 23 | 514 | | if (attrNamespace != "System.ComponentModel.DataAnnotations") |
| | | 515 | | continue; |
| | | 516 | | |
| | | 517 | | // Extract error message if present |
| | 23 | 518 | | string? errorMessage = null; |
| | 51 | 519 | | foreach (var namedArg in attr.NamedArguments) |
| | | 520 | | { |
| | 3 | 521 | | if (namedArg.Key == "ErrorMessage" && namedArg.Value.Value is string msg) |
| | | 522 | | { |
| | 1 | 523 | | errorMessage = msg; |
| | 1 | 524 | | break; |
| | | 525 | | } |
| | | 526 | | } |
| | | 527 | | |
| | | 528 | | // Check for known DataAnnotation attributes |
| | 23 | 529 | | if (attrTypeName == "RequiredAttribute") |
| | | 530 | | { |
| | 12 | 531 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.Required, errorMessage)); |
| | | 532 | | } |
| | 11 | 533 | | else if (attrTypeName == "RangeAttribute") |
| | | 534 | | { |
| | 12 | 535 | | object? min = null, max = null; |
| | 6 | 536 | | if (attr.ConstructorArguments.Length >= 2) |
| | | 537 | | { |
| | 6 | 538 | | min = attr.ConstructorArguments[0].Value; |
| | 6 | 539 | | max = attr.ConstructorArguments[1].Value; |
| | | 540 | | } |
| | 6 | 541 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.Range, errorMessage, min, max)); |
| | | 542 | | } |
| | 5 | 543 | | else if (attrTypeName == "StringLengthAttribute") |
| | | 544 | | { |
| | 2 | 545 | | object? maxLen = null; |
| | 2 | 546 | | int? minLen = null; |
| | 2 | 547 | | if (attr.ConstructorArguments.Length >= 1) |
| | | 548 | | { |
| | 2 | 549 | | maxLen = attr.ConstructorArguments[0].Value; |
| | | 550 | | } |
| | 8 | 551 | | foreach (var namedArg in attr.NamedArguments) |
| | | 552 | | { |
| | 2 | 553 | | if (namedArg.Key == "MinimumLength" && namedArg.Value.Value is int ml) |
| | | 554 | | { |
| | 2 | 555 | | minLen = ml; |
| | | 556 | | } |
| | | 557 | | } |
| | 2 | 558 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.StringLength, errorMessage, null, maxLen, null |
| | | 559 | | } |
| | 3 | 560 | | else if (attrTypeName == "MinLengthAttribute") |
| | | 561 | | { |
| | 1 | 562 | | int? minLen = null; |
| | 1 | 563 | | if (attr.ConstructorArguments.Length >= 1 && attr.ConstructorArguments[0].Value is int ml) |
| | | 564 | | { |
| | 1 | 565 | | minLen = ml; |
| | | 566 | | } |
| | 1 | 567 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.MinLength, errorMessage, null, null, null, min |
| | | 568 | | } |
| | 2 | 569 | | else if (attrTypeName == "MaxLengthAttribute") |
| | | 570 | | { |
| | 1 | 571 | | object? maxLen = null; |
| | 1 | 572 | | if (attr.ConstructorArguments.Length >= 1) |
| | | 573 | | { |
| | 1 | 574 | | maxLen = attr.ConstructorArguments[0].Value; |
| | | 575 | | } |
| | 1 | 576 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.MaxLength, errorMessage, null, maxLen)); |
| | | 577 | | } |
| | 1 | 578 | | else if (attrTypeName == "RegularExpressionAttribute") |
| | | 579 | | { |
| | 1 | 580 | | string? pattern = null; |
| | 1 | 581 | | if (attr.ConstructorArguments.Length >= 1 && attr.ConstructorArguments[0].Value is string p) |
| | | 582 | | { |
| | 1 | 583 | | pattern = p; |
| | | 584 | | } |
| | 1 | 585 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.RegularExpression, errorMessage, null, null, p |
| | | 586 | | } |
| | 0 | 587 | | else if (attrTypeName == "EmailAddressAttribute") |
| | | 588 | | { |
| | 0 | 589 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.EmailAddress, errorMessage)); |
| | | 590 | | } |
| | 0 | 591 | | else if (attrTypeName == "PhoneAttribute") |
| | | 592 | | { |
| | 0 | 593 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.Phone, errorMessage)); |
| | | 594 | | } |
| | 0 | 595 | | else if (attrTypeName == "UrlAttribute") |
| | | 596 | | { |
| | 0 | 597 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.Url, errorMessage)); |
| | | 598 | | } |
| | 0 | 599 | | else if (IsValidationAttribute(attrClass)) |
| | | 600 | | { |
| | | 601 | | // Unsupported validation attribute |
| | 0 | 602 | | annotations.Add(new DataAnnotationInfo(DataAnnotationKind.Unsupported, errorMessage)); |
| | | 603 | | } |
| | | 604 | | } |
| | | 605 | | |
| | 279 | 606 | | return annotations; |
| | | 607 | | } |
| | | 608 | | |
| | | 609 | | private static bool IsValidationAttribute(INamedTypeSymbol attrClass) |
| | | 610 | | { |
| | | 611 | | // Check if this inherits from ValidationAttribute |
| | 0 | 612 | | var current = attrClass.BaseType; |
| | 0 | 613 | | while (current != null) |
| | | 614 | | { |
| | 0 | 615 | | if (current.ToDisplayString() == "System.ComponentModel.DataAnnotations.ValidationAttribute") |
| | 0 | 616 | | return true; |
| | 0 | 617 | | current = current.BaseType; |
| | | 618 | | } |
| | 0 | 619 | | return false; |
| | | 620 | | } |
| | | 621 | | |
| | | 622 | | private static (ComplexTypeKind Kind, string? ElementTypeName, IReadOnlyList<OptionsPropertyInfo>? NestedProperties) |
| | | 623 | | ITypeSymbol typeSymbol, |
| | | 624 | | HashSet<string> visitedTypes) |
| | | 625 | | { |
| | | 626 | | // Check for array |
| | 279 | 627 | | if (typeSymbol is IArrayTypeSymbol arrayType) |
| | | 628 | | { |
| | 3 | 629 | | var elementType = arrayType.ElementType; |
| | 3 | 630 | | var elementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | 3 | 631 | | var nestedProps = TryGetNestedProperties(elementType, visitedTypes); |
| | 3 | 632 | | return (ComplexTypeKind.Array, elementTypeName, nestedProps); |
| | | 633 | | } |
| | | 634 | | |
| | 276 | 635 | | if (typeSymbol is not INamedTypeSymbol namedType) |
| | | 636 | | { |
| | 0 | 637 | | return (ComplexTypeKind.None, null, null); |
| | | 638 | | } |
| | | 639 | | |
| | | 640 | | // Check for Dictionary<string, T> |
| | 276 | 641 | | if (IsDictionaryType(namedType)) |
| | | 642 | | { |
| | 4 | 643 | | var valueType = namedType.TypeArguments[1]; |
| | 4 | 644 | | var valueTypeName = valueType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | 4 | 645 | | var nestedProps = TryGetNestedProperties(valueType, visitedTypes); |
| | 4 | 646 | | return (ComplexTypeKind.Dictionary, valueTypeName, nestedProps); |
| | | 647 | | } |
| | | 648 | | |
| | | 649 | | // Check for List<T>, IList<T>, ICollection<T>, IEnumerable<T> |
| | 272 | 650 | | if (IsListType(namedType)) |
| | | 651 | | { |
| | 7 | 652 | | var elementType = namedType.TypeArguments[0]; |
| | 7 | 653 | | var elementTypeName = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | 7 | 654 | | var nestedProps = TryGetNestedProperties(elementType, visitedTypes); |
| | 7 | 655 | | return (ComplexTypeKind.List, elementTypeName, nestedProps); |
| | | 656 | | } |
| | | 657 | | |
| | | 658 | | // Check for nested object (class with bindable properties) |
| | 265 | 659 | | if (IsBindableClass(namedType)) |
| | | 660 | | { |
| | 25 | 661 | | var nestedProps = ExtractBindableProperties(namedType, visitedTypes); |
| | 25 | 662 | | if (nestedProps.Count > 0) |
| | | 663 | | { |
| | 19 | 664 | | return (ComplexTypeKind.NestedObject, null, nestedProps); |
| | | 665 | | } |
| | | 666 | | } |
| | | 667 | | |
| | 246 | 668 | | return (ComplexTypeKind.None, null, null); |
| | | 669 | | } |
| | | 670 | | |
| | | 671 | | private static IReadOnlyList<OptionsPropertyInfo>? TryGetNestedProperties(ITypeSymbol elementType, HashSet<string> v |
| | | 672 | | { |
| | 14 | 673 | | if (elementType is INamedTypeSymbol namedElement && IsBindableClass(namedElement)) |
| | | 674 | | { |
| | 3 | 675 | | var props = ExtractBindableProperties(namedElement, visitedTypes); |
| | 3 | 676 | | return props.Count > 0 ? props : null; |
| | | 677 | | } |
| | 11 | 678 | | return null; |
| | | 679 | | } |
| | | 680 | | |
| | | 681 | | private static bool IsDictionaryType(INamedTypeSymbol type) |
| | | 682 | | { |
| | | 683 | | // Check for Dictionary<TKey, TValue> or IDictionary<TKey, TValue> |
| | 276 | 684 | | if (type.TypeArguments.Length != 2) |
| | 272 | 685 | | return false; |
| | | 686 | | |
| | 4 | 687 | | var typeName = type.OriginalDefinition.ToDisplayString(); |
| | 4 | 688 | | return typeName == "System.Collections.Generic.Dictionary<TKey, TValue>" || |
| | 4 | 689 | | typeName == "System.Collections.Generic.IDictionary<TKey, TValue>"; |
| | | 690 | | } |
| | | 691 | | |
| | | 692 | | private static bool IsListType(INamedTypeSymbol type) |
| | | 693 | | { |
| | 272 | 694 | | if (type.TypeArguments.Length != 1) |
| | 261 | 695 | | return false; |
| | | 696 | | |
| | 11 | 697 | | var typeName = type.OriginalDefinition.ToDisplayString(); |
| | 11 | 698 | | return typeName == "System.Collections.Generic.List<T>" || |
| | 11 | 699 | | typeName == "System.Collections.Generic.IList<T>" || |
| | 11 | 700 | | typeName == "System.Collections.Generic.ICollection<T>" || |
| | 11 | 701 | | typeName == "System.Collections.Generic.IEnumerable<T>"; |
| | | 702 | | } |
| | | 703 | | |
| | | 704 | | private static bool IsBindableClass(INamedTypeSymbol type) |
| | | 705 | | { |
| | | 706 | | // Must be a class or struct, not abstract, not a system type |
| | 279 | 707 | | if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct) |
| | 17 | 708 | | return false; |
| | | 709 | | |
| | 262 | 710 | | if (type.IsAbstract) |
| | 4 | 711 | | return false; |
| | | 712 | | |
| | | 713 | | // Skip system types and primitives |
| | 258 | 714 | | var ns = type.ContainingNamespace?.ToDisplayString() ?? ""; |
| | 258 | 715 | | if (ns.StartsWith("System")) |
| | | 716 | | { |
| | | 717 | | // Skip known non-bindable System namespaces |
| | 230 | 718 | | if (ns == "System" || ns.StartsWith("System.Collections") || ns.StartsWith("System.Threading")) |
| | 230 | 719 | | return false; |
| | | 720 | | } |
| | | 721 | | |
| | | 722 | | // Must have a parameterless constructor (explicit or implicit) |
| | | 723 | | // Note: Classes without any explicit constructors have an implicit parameterless constructor |
| | 56 | 724 | | var hasExplicitConstructors = type.InstanceConstructors.Any(c => !c.IsImplicitlyDeclared); |
| | 28 | 725 | | if (hasExplicitConstructors) |
| | | 726 | | { |
| | 0 | 727 | | var hasParameterlessCtor = type.InstanceConstructors |
| | 0 | 728 | | .Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public); |
| | 0 | 729 | | return hasParameterlessCtor; |
| | | 730 | | } |
| | | 731 | | |
| | | 732 | | // No explicit constructors means implicit parameterless constructor exists |
| | 28 | 733 | | return true; |
| | | 734 | | } |
| | | 735 | | |
| | | 736 | | private static AttributeInfo? GetAttributeInfoFromCompilation(Compilation compilation) |
| | | 737 | | { |
| | | 738 | | // Get assembly-level attributes directly from the compilation |
| | 1302 | 739 | | foreach (var attribute in compilation.Assembly.GetAttributes()) |
| | | 740 | | { |
| | 434 | 741 | | var attrClassName = attribute.AttributeClass?.ToDisplayString(); |
| | | 742 | | |
| | | 743 | | // Check if this is our attribute (various name format possibilities) |
| | 434 | 744 | | if (attrClassName != GenerateTypeRegistryAttributeName) |
| | | 745 | | continue; |
| | | 746 | | |
| | 434 | 747 | | string[]? namespacePrefixes = null; |
| | 434 | 748 | | var includeSelf = true; |
| | | 749 | | |
| | 1002 | 750 | | foreach (var namedArg in attribute.NamedArguments) |
| | | 751 | | { |
| | 67 | 752 | | switch (namedArg.Key) |
| | | 753 | | { |
| | | 754 | | case "IncludeNamespacePrefixes": |
| | 57 | 755 | | if (!namedArg.Value.IsNull && namedArg.Value.Values.Length > 0) |
| | | 756 | | { |
| | 57 | 757 | | namespacePrefixes = namedArg.Value.Values |
| | 58 | 758 | | .Where(v => v.Value is string) |
| | 58 | 759 | | .Select(v => (string)v.Value!) |
| | 57 | 760 | | .ToArray(); |
| | | 761 | | } |
| | 57 | 762 | | break; |
| | | 763 | | |
| | | 764 | | case "IncludeSelf": |
| | 10 | 765 | | if (namedArg.Value.Value is bool selfValue) |
| | | 766 | | { |
| | 10 | 767 | | includeSelf = selfValue; |
| | | 768 | | } |
| | | 769 | | break; |
| | | 770 | | } |
| | | 771 | | } |
| | | 772 | | |
| | 434 | 773 | | return new AttributeInfo(namespacePrefixes, includeSelf); |
| | | 774 | | } |
| | | 775 | | |
| | 0 | 776 | | return null; |
| | | 777 | | } |
| | | 778 | | |
| | | 779 | | private static DiscoveryResult DiscoverTypes( |
| | | 780 | | Compilation compilation, |
| | | 781 | | string[]? namespacePrefixes, |
| | | 782 | | bool includeSelf) |
| | | 783 | | { |
| | 434 | 784 | | var injectableTypes = new List<DiscoveredType>(); |
| | 434 | 785 | | var pluginTypes = new List<DiscoveredPlugin>(); |
| | 434 | 786 | | var decorators = new List<DiscoveredDecorator>(); |
| | 434 | 787 | | var openDecorators = new List<DiscoveredOpenDecorator>(); |
| | 434 | 788 | | var interceptedServices = new List<DiscoveredInterceptedService>(); |
| | 434 | 789 | | var factories = new List<DiscoveredFactory>(); |
| | 434 | 790 | | var options = new List<DiscoveredOptions>(); |
| | 434 | 791 | | var hostedServices = new List<DiscoveredHostedService>(); |
| | 434 | 792 | | var providers = new List<DiscoveredProvider>(); |
| | 434 | 793 | | var inaccessibleTypes = new List<InaccessibleType>(); |
| | 434 | 794 | | var prefixList = namespacePrefixes?.ToList(); |
| | | 795 | | |
| | | 796 | | // Compute the generated namespace for the current assembly |
| | 434 | 797 | | var currentAssemblyName = compilation.Assembly.Name; |
| | 434 | 798 | | var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(currentAssemblyName); |
| | 434 | 799 | | var generatedNamespace = $"{safeAssemblyName}.Generated"; |
| | | 800 | | |
| | | 801 | | // Collect types from the current compilation if includeSelf is true |
| | 434 | 802 | | if (includeSelf) |
| | | 803 | | { |
| | 433 | 804 | | CollectTypesFromAssembly(compilation.Assembly, prefixList, injectableTypes, pluginTypes, decorators, openDec |
| | | 805 | | } |
| | | 806 | | |
| | | 807 | | // Collect types from all referenced assemblies |
| | 147466 | 808 | | foreach (var reference in compilation.References) |
| | | 809 | | { |
| | 73299 | 810 | | if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol) |
| | | 811 | | { |
| | | 812 | | // For referenced assemblies, they use their own generated namespace |
| | 73112 | 813 | | var refSafeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblySymbol.Name); |
| | 73112 | 814 | | var refGeneratedNamespace = $"{refSafeAssemblyName}.Generated"; |
| | 73112 | 815 | | CollectTypesFromAssembly(assemblySymbol, prefixList, injectableTypes, pluginTypes, decorators, openDecor |
| | | 816 | | } |
| | | 817 | | } |
| | | 818 | | |
| | | 819 | | // Expand open generic decorators into closed decorator registrations |
| | 434 | 820 | | if (openDecorators.Count > 0) |
| | | 821 | | { |
| | 6 | 822 | | ExpandOpenDecorators(injectableTypes, openDecorators, decorators); |
| | | 823 | | } |
| | | 824 | | |
| | | 825 | | // Filter out nested options types (types used as properties in other options types) |
| | 434 | 826 | | if (options.Count > 1) |
| | | 827 | | { |
| | 19 | 828 | | options = FilterNestedOptions(options, compilation); |
| | | 829 | | } |
| | | 830 | | |
| | | 831 | | // Check for referenced assemblies with internal plugin types but no [GenerateTypeRegistry] |
| | 434 | 832 | | var missingTypeRegistryPlugins = new List<MissingTypeRegistryPlugin>(); |
| | 147466 | 833 | | foreach (var reference in compilation.References) |
| | | 834 | | { |
| | 73299 | 835 | | if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol) |
| | | 836 | | { |
| | | 837 | | // Skip assemblies that already have [GenerateTypeRegistry] |
| | 73112 | 838 | | if (TypeDiscoveryHelper.HasGenerateTypeRegistryAttribute(assemblySymbol)) |
| | | 839 | | continue; |
| | | 840 | | |
| | | 841 | | // Look for internal types that implement Needlr plugin interfaces |
| | 3575976 | 842 | | foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assemblySymbol.GlobalNamespace)) |
| | | 843 | | { |
| | 1714893 | 844 | | if (!TypeDiscoveryHelper.IsInternalOrLessAccessible(typeSymbol)) |
| | | 845 | | continue; |
| | | 846 | | |
| | 81920 | 847 | | if (!TypeDiscoveryHelper.ImplementsNeedlrPluginInterface(typeSymbol)) |
| | | 848 | | continue; |
| | | 849 | | |
| | | 850 | | // This is an internal plugin type in an assembly without [GenerateTypeRegistry] |
| | 1 | 851 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 1 | 852 | | missingTypeRegistryPlugins.Add(new MissingTypeRegistryPlugin(typeName, assemblySymbol.Name)); |
| | | 853 | | } |
| | | 854 | | } |
| | | 855 | | } |
| | | 856 | | |
| | 434 | 857 | | return new DiscoveryResult(injectableTypes, pluginTypes, decorators, inaccessibleTypes, missingTypeRegistryPlugi |
| | | 858 | | } |
| | | 859 | | |
| | | 860 | | private static void CollectTypesFromAssembly( |
| | | 861 | | IAssemblySymbol assembly, |
| | | 862 | | IReadOnlyList<string>? namespacePrefixes, |
| | | 863 | | List<DiscoveredType> injectableTypes, |
| | | 864 | | List<DiscoveredPlugin> pluginTypes, |
| | | 865 | | List<DiscoveredDecorator> decorators, |
| | | 866 | | List<DiscoveredOpenDecorator> openDecorators, |
| | | 867 | | List<DiscoveredInterceptedService> interceptedServices, |
| | | 868 | | List<DiscoveredFactory> factories, |
| | | 869 | | List<DiscoveredOptions> options, |
| | | 870 | | List<DiscoveredHostedService> hostedServices, |
| | | 871 | | List<DiscoveredProvider> providers, |
| | | 872 | | List<InaccessibleType> inaccessibleTypes, |
| | | 873 | | Compilation compilation, |
| | | 874 | | bool isCurrentAssembly, |
| | | 875 | | string generatedNamespace) |
| | | 876 | | { |
| | 3579478 | 877 | | foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assembly.GlobalNamespace)) |
| | | 878 | | { |
| | 1716194 | 879 | | if (!TypeDiscoveryHelper.MatchesNamespacePrefix(typeSymbol, namespacePrefixes)) |
| | | 880 | | continue; |
| | | 881 | | |
| | | 882 | | // For referenced assemblies, check if the type would be registerable but is inaccessible |
| | 1492084 | 883 | | if (!isCurrentAssembly && TypeDiscoveryHelper.IsInternalOrLessAccessible(typeSymbol)) |
| | | 884 | | { |
| | | 885 | | // Check if this type would have been registered if it were accessible |
| | 71804 | 886 | | if (TypeDiscoveryHelper.WouldBeInjectableIgnoringAccessibility(typeSymbol) || |
| | 71804 | 887 | | TypeDiscoveryHelper.WouldBePluginIgnoringAccessibility(typeSymbol)) |
| | | 888 | | { |
| | 2266 | 889 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 2266 | 890 | | inaccessibleTypes.Add(new InaccessibleType(typeName, assembly.Name)); |
| | | 891 | | } |
| | 2266 | 892 | | continue; // Skip further processing for inaccessible types |
| | | 893 | | } |
| | | 894 | | |
| | | 895 | | // Check for [Options] attribute |
| | 1420280 | 896 | | if (OptionsAttributeHelper.HasOptionsAttribute(typeSymbol)) |
| | | 897 | | { |
| | 167 | 898 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 167 | 899 | | var optionsAttrs = OptionsAttributeHelper.GetOptionsAttributes(typeSymbol); |
| | 167 | 900 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | | 901 | | |
| | | 902 | | // Detect positional record (record with primary constructor parameters) |
| | 167 | 903 | | var positionalRecordInfo = DetectPositionalRecord(typeSymbol); |
| | | 904 | | |
| | | 905 | | // Extract bindable properties for AOT code generation |
| | 167 | 906 | | var properties = ExtractBindableProperties(typeSymbol); |
| | | 907 | | |
| | 680 | 908 | | foreach (var optionsAttr in optionsAttrs) |
| | | 909 | | { |
| | | 910 | | // Determine validator type and method |
| | 173 | 911 | | var validatorTypeSymbol = optionsAttr.ValidatorType; |
| | 173 | 912 | | var targetType = validatorTypeSymbol ?? typeSymbol; // Look for method on options class or external |
| | 173 | 913 | | var methodName = optionsAttr.ValidateMethod ?? "Validate"; // Convention: "Validate" |
| | | 914 | | |
| | | 915 | | // Find validation method using convention-based discovery |
| | 173 | 916 | | var validatorMethodInfo = OptionsAttributeHelper.FindValidationMethod(targetType, methodName); |
| | 173 | 917 | | OptionsValidatorInfo? validatorInfo = validatorMethodInfo.HasValue |
| | 173 | 918 | | ? new OptionsValidatorInfo(validatorMethodInfo.Value.MethodName, validatorMethodInfo.Value.IsSta |
| | 173 | 919 | | : null; |
| | | 920 | | |
| | | 921 | | // Infer section name if not provided |
| | 173 | 922 | | var sectionName = optionsAttr.SectionName |
| | 173 | 923 | | ?? Helpers.OptionsNamingHelper.InferSectionName(typeSymbol.Name); |
| | | 924 | | |
| | 173 | 925 | | var validatorTypeName = validatorTypeSymbol != null |
| | 173 | 926 | | ? TypeDiscoveryHelper.GetFullyQualifiedName(validatorTypeSymbol) |
| | 173 | 927 | | : null; |
| | | 928 | | |
| | 173 | 929 | | options.Add(new DiscoveredOptions( |
| | 173 | 930 | | typeName, |
| | 173 | 931 | | sectionName, |
| | 173 | 932 | | optionsAttr.Name, |
| | 173 | 933 | | optionsAttr.ValidateOnStart, |
| | 173 | 934 | | assembly.Name, |
| | 173 | 935 | | sourceFilePath, |
| | 173 | 936 | | validatorInfo, |
| | 173 | 937 | | optionsAttr.ValidateMethod, |
| | 173 | 938 | | validatorTypeName, |
| | 173 | 939 | | positionalRecordInfo, |
| | 173 | 940 | | properties)); |
| | | 941 | | } |
| | | 942 | | } |
| | | 943 | | |
| | | 944 | | // Check for [GenerateFactory] attribute - these types get factories instead of direct registration |
| | 1420280 | 945 | | if (FactoryDiscoveryHelper.HasGenerateFactoryAttribute(typeSymbol)) |
| | | 946 | | { |
| | 27 | 947 | | var factoryConstructors = FactoryDiscoveryHelper.GetFactoryConstructors(typeSymbol); |
| | 27 | 948 | | if (factoryConstructors.Count > 0) |
| | | 949 | | { |
| | | 950 | | // Has at least one constructor with runtime params - generate factory |
| | 26 | 951 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 26 | 952 | | var interfaces = TypeDiscoveryHelper.GetRegisterableInterfaces(typeSymbol); |
| | 35 | 953 | | var interfaceNames = interfaces.Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i)).ToArray(); |
| | 26 | 954 | | var generationMode = FactoryDiscoveryHelper.GetFactoryGenerationMode(typeSymbol); |
| | 26 | 955 | | var returnTypeOverride = FactoryDiscoveryHelper.GetFactoryReturnInterfaceType(typeSymbol); |
| | 26 | 956 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | | 957 | | |
| | 26 | 958 | | factories.Add(new DiscoveredFactory( |
| | 26 | 959 | | typeName, |
| | 26 | 960 | | interfaceNames, |
| | 26 | 961 | | assembly.Name, |
| | 26 | 962 | | generationMode, |
| | 26 | 963 | | factoryConstructors.ToArray(), |
| | 26 | 964 | | returnTypeOverride, |
| | 26 | 965 | | sourceFilePath)); |
| | | 966 | | |
| | 26 | 967 | | continue; // Don't add to injectable types - factory handles registration |
| | | 968 | | } |
| | | 969 | | // If no runtime params, fall through to normal registration (with warning in future analyzer) |
| | | 970 | | } |
| | | 971 | | |
| | | 972 | | // Check for DecoratorFor<T> attributes |
| | 1420254 | 973 | | var decoratorInfos = TypeDiscoveryHelper.GetDecoratorForAttributes(typeSymbol); |
| | 2840548 | 974 | | foreach (var decoratorInfo in decoratorInfos) |
| | | 975 | | { |
| | 20 | 976 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | 20 | 977 | | decorators.Add(new DiscoveredDecorator( |
| | 20 | 978 | | decoratorInfo.DecoratorTypeName, |
| | 20 | 979 | | decoratorInfo.ServiceTypeName, |
| | 20 | 980 | | decoratorInfo.Order, |
| | 20 | 981 | | assembly.Name, |
| | 20 | 982 | | sourceFilePath)); |
| | | 983 | | } |
| | | 984 | | |
| | | 985 | | // Check for OpenDecoratorFor attributes (source-gen only open generic decorators) |
| | 1420254 | 986 | | var openDecoratorInfos = OpenDecoratorDiscoveryHelper.GetOpenDecoratorForAttributes(typeSymbol); |
| | 2840522 | 987 | | foreach (var openDecoratorInfo in openDecoratorInfos) |
| | | 988 | | { |
| | 7 | 989 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | 7 | 990 | | openDecorators.Add(new DiscoveredOpenDecorator( |
| | 7 | 991 | | openDecoratorInfo.DecoratorType, |
| | 7 | 992 | | openDecoratorInfo.OpenGenericInterface, |
| | 7 | 993 | | openDecoratorInfo.Order, |
| | 7 | 994 | | assembly.Name, |
| | 7 | 995 | | sourceFilePath)); |
| | | 996 | | } |
| | | 997 | | |
| | | 998 | | // Check for Intercept attributes and collect intercepted services |
| | 1420254 | 999 | | if (InterceptorDiscoveryHelper.HasInterceptAttributes(typeSymbol)) |
| | | 1000 | | { |
| | 15 | 1001 | | var lifetime = TypeDiscoveryHelper.DetermineLifetime(typeSymbol); |
| | 15 | 1002 | | if (lifetime.HasValue) |
| | | 1003 | | { |
| | 15 | 1004 | | var classLevelInterceptors = InterceptorDiscoveryHelper.GetInterceptAttributes(typeSymbol); |
| | 15 | 1005 | | var methodLevelInterceptors = InterceptorDiscoveryHelper.GetMethodLevelInterceptAttributes(typeSymbo |
| | 15 | 1006 | | var methods = InterceptorDiscoveryHelper.GetInterceptedMethods(typeSymbol, classLevelInterceptors, m |
| | | 1007 | | |
| | 15 | 1008 | | if (methods.Count > 0) |
| | | 1009 | | { |
| | 15 | 1010 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 15 | 1011 | | var interfaces = TypeDiscoveryHelper.GetRegisterableInterfaces(typeSymbol); |
| | 30 | 1012 | | var interfaceNames = interfaces.Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i)).ToArra |
| | | 1013 | | |
| | | 1014 | | // Collect all unique interceptor types |
| | 15 | 1015 | | var allInterceptorTypes = classLevelInterceptors |
| | 15 | 1016 | | .Concat(methodLevelInterceptors) |
| | 18 | 1017 | | .Select(i => i.InterceptorTypeName) |
| | 15 | 1018 | | .Distinct() |
| | 15 | 1019 | | .ToArray(); |
| | | 1020 | | |
| | 15 | 1021 | | var interceptedSourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | | 1022 | | |
| | 15 | 1023 | | interceptedServices.Add(new DiscoveredInterceptedService( |
| | 15 | 1024 | | typeName, |
| | 15 | 1025 | | interfaceNames, |
| | 15 | 1026 | | assembly.Name, |
| | 15 | 1027 | | lifetime.Value, |
| | 15 | 1028 | | methods.ToArray(), |
| | 15 | 1029 | | allInterceptorTypes, |
| | 15 | 1030 | | interceptedSourceFilePath)); |
| | | 1031 | | } |
| | | 1032 | | } |
| | | 1033 | | } |
| | | 1034 | | |
| | | 1035 | | // Check for injectable types (but skip types that are providers, which are handled separately) |
| | 1420254 | 1036 | | if (TypeDiscoveryHelper.IsInjectableType(typeSymbol, isCurrentAssembly) && !ProviderDiscoveryHelper.HasProvi |
| | | 1037 | | { |
| | | 1038 | | // Determine lifetime first - only include types that are actually injectable |
| | 400190 | 1039 | | var lifetime = TypeDiscoveryHelper.DetermineLifetime(typeSymbol); |
| | 400190 | 1040 | | if (lifetime.HasValue) |
| | | 1041 | | { |
| | 228082 | 1042 | | var interfaces = TypeDiscoveryHelper.GetRegisterableInterfaces(typeSymbol); |
| | 228082 | 1043 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 228328 | 1044 | | var interfaceNames = interfaces.Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i)).ToArray(); |
| | | 1045 | | |
| | | 1046 | | // Check for [DeferToContainer] attribute - use declared types instead of discovered constructors |
| | 228082 | 1047 | | var deferredParams = TypeDiscoveryHelper.GetDeferToContainerParameterTypes(typeSymbol); |
| | | 1048 | | TypeDiscoveryHelper.ConstructorParameterInfo[] constructorParams; |
| | 228082 | 1049 | | if (deferredParams != null) |
| | | 1050 | | { |
| | | 1051 | | // DeferToContainer doesn't support keyed services - convert to simple params |
| | 10 | 1052 | | constructorParams = deferredParams.Select(t => new TypeDiscoveryHelper.ConstructorParameterInfo( |
| | | 1053 | | } |
| | | 1054 | | else |
| | | 1055 | | { |
| | 228077 | 1056 | | constructorParams = TypeDiscoveryHelper.GetBestConstructorParametersWithKeys(typeSymbol)?.ToArra |
| | | 1057 | | } |
| | | 1058 | | |
| | | 1059 | | // Get source file path for breadcrumbs (null for external assemblies) |
| | 228082 | 1060 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | | 1061 | | |
| | | 1062 | | // Get [Keyed] attribute keys |
| | 228082 | 1063 | | var serviceKeys = TypeDiscoveryHelper.GetKeyedServiceKeys(typeSymbol); |
| | | 1064 | | |
| | | 1065 | | // Check if this type implements IDisposable or IAsyncDisposable |
| | 228082 | 1066 | | var isDisposable = TypeDiscoveryHelper.IsDisposableType(typeSymbol); |
| | | 1067 | | |
| | 228082 | 1068 | | injectableTypes.Add(new DiscoveredType(typeName, interfaceNames, assembly.Name, lifetime.Value, cons |
| | | 1069 | | } |
| | | 1070 | | } |
| | | 1071 | | |
| | | 1072 | | // Check for hosted service types (BackgroundService or IHostedService implementations) |
| | 1420254 | 1073 | | if (TypeDiscoveryHelper.IsHostedServiceType(typeSymbol, isCurrentAssembly)) |
| | | 1074 | | { |
| | 7 | 1075 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 7 | 1076 | | var constructorParams = TypeDiscoveryHelper.GetBestConstructorParametersWithKeys(typeSymbol)?.ToArray() |
| | 7 | 1077 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | | 1078 | | |
| | 7 | 1079 | | hostedServices.Add(new DiscoveredHostedService( |
| | 7 | 1080 | | typeName, |
| | 7 | 1081 | | assembly.Name, |
| | 7 | 1082 | | GeneratorLifetime.Singleton, // Hosted services are always singleton |
| | 7 | 1083 | | constructorParams, |
| | 7 | 1084 | | sourceFilePath)); |
| | | 1085 | | } |
| | | 1086 | | |
| | | 1087 | | // Check for [Provider] attribute |
| | 1420254 | 1088 | | if (ProviderDiscoveryHelper.HasProviderAttribute(typeSymbol)) |
| | | 1089 | | { |
| | 18 | 1090 | | var discoveredProvider = ProviderDiscoveryHelper.DiscoverProvider(typeSymbol, assembly.Name, generatedNa |
| | 18 | 1091 | | if (discoveredProvider.HasValue) |
| | | 1092 | | { |
| | 18 | 1093 | | providers.Add(discoveredProvider.Value); |
| | | 1094 | | } |
| | | 1095 | | } |
| | | 1096 | | |
| | | 1097 | | // Check for plugin types (concrete class with parameterless ctor and interfaces) |
| | 1420254 | 1098 | | if (TypeDiscoveryHelper.IsPluginType(typeSymbol, isCurrentAssembly)) |
| | | 1099 | | { |
| | 395294 | 1100 | | var pluginInterfaces = TypeDiscoveryHelper.GetPluginInterfaces(typeSymbol); |
| | 395294 | 1101 | | if (pluginInterfaces.Count > 0) |
| | | 1102 | | { |
| | 1379 | 1103 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 2768 | 1104 | | var interfaceNames = pluginInterfaces.Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i)).ToAr |
| | 1379 | 1105 | | var attributeNames = TypeDiscoveryHelper.GetPluginAttributes(typeSymbol).ToArray(); |
| | 1379 | 1106 | | var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; |
| | 1379 | 1107 | | var order = PluginOrderHelper.GetPluginOrder(typeSymbol); |
| | | 1108 | | |
| | 1379 | 1109 | | pluginTypes.Add(new DiscoveredPlugin(typeName, interfaceNames, assembly.Name, attributeNames, source |
| | | 1110 | | } |
| | | 1111 | | } |
| | | 1112 | | |
| | | 1113 | | // Check for IHubRegistrationPlugin implementations |
| | | 1114 | | // NOTE: SignalR hub discovery is now handled by NexusLabs.Needlr.SignalR.Generators |
| | | 1115 | | |
| | | 1116 | | // Check for SemanticKernel plugin types (classes/statics with [KernelFunction] methods) |
| | | 1117 | | // NOTE: SemanticKernel plugin discovery is now handled by NexusLabs.Needlr.SemanticKernel.Generators |
| | | 1118 | | } |
| | 73545 | 1119 | | } |
| | | 1120 | | |
| | | 1121 | | private static string GenerateTypeRegistrySource(DiscoveryResult discoveryResult, string assemblyName, BreadcrumbWri |
| | | 1122 | | { |
| | 434 | 1123 | | var builder = new StringBuilder(); |
| | 434 | 1124 | | var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName); |
| | 434 | 1125 | | var hasOptions = discoveryResult.Options.Count > 0; |
| | | 1126 | | |
| | 434 | 1127 | | breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Type Registry"); |
| | 434 | 1128 | | builder.AppendLine("#nullable enable"); |
| | 434 | 1129 | | builder.AppendLine(); |
| | 434 | 1130 | | builder.AppendLine("using System;"); |
| | 434 | 1131 | | builder.AppendLine("using System.Collections.Generic;"); |
| | 434 | 1132 | | builder.AppendLine(); |
| | 434 | 1133 | | if (hasOptions) |
| | | 1134 | | { |
| | 146 | 1135 | | builder.AppendLine("using Microsoft.Extensions.Configuration;"); |
| | 146 | 1136 | | if (isAotProject) |
| | | 1137 | | { |
| | 78 | 1138 | | builder.AppendLine("using Microsoft.Extensions.Options;"); |
| | | 1139 | | } |
| | | 1140 | | } |
| | 434 | 1141 | | builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); |
| | 434 | 1142 | | builder.AppendLine(); |
| | 434 | 1143 | | builder.AppendLine("using NexusLabs.Needlr;"); |
| | 434 | 1144 | | builder.AppendLine("using NexusLabs.Needlr.Generators;"); |
| | 434 | 1145 | | builder.AppendLine(); |
| | 434 | 1146 | | builder.AppendLine($"namespace {safeAssemblyName}.Generated;"); |
| | 434 | 1147 | | builder.AppendLine(); |
| | 434 | 1148 | | builder.AppendLine("/// <summary>"); |
| | 434 | 1149 | | builder.AppendLine("/// Compile-time generated registry of injectable types and plugins."); |
| | 434 | 1150 | | builder.AppendLine("/// This eliminates the need for runtime reflection-based type discovery."); |
| | 434 | 1151 | | builder.AppendLine("/// </summary>"); |
| | 434 | 1152 | | builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1 |
| | 434 | 1153 | | builder.AppendLine("public static class TypeRegistry"); |
| | 434 | 1154 | | builder.AppendLine("{"); |
| | | 1155 | | |
| | 434 | 1156 | | GenerateInjectableTypesArray(builder, discoveryResult.InjectableTypes, breadcrumbs, projectDirectory); |
| | 434 | 1157 | | builder.AppendLine(); |
| | 434 | 1158 | | GeneratePluginTypesArray(builder, discoveryResult.PluginTypes, breadcrumbs, projectDirectory); |
| | | 1159 | | |
| | 434 | 1160 | | builder.AppendLine(); |
| | 434 | 1161 | | builder.AppendLine(" /// <summary>"); |
| | 434 | 1162 | | builder.AppendLine(" /// Gets all injectable types discovered at compile time."); |
| | 434 | 1163 | | builder.AppendLine(" /// </summary>"); |
| | 434 | 1164 | | builder.AppendLine(" /// <returns>A read-only list of injectable type information.</returns>"); |
| | 434 | 1165 | | builder.AppendLine(" public static IReadOnlyList<InjectableTypeInfo> GetInjectableTypes() => _types;"); |
| | 434 | 1166 | | builder.AppendLine(); |
| | 434 | 1167 | | builder.AppendLine(" /// <summary>"); |
| | 434 | 1168 | | builder.AppendLine(" /// Gets all plugin types discovered at compile time."); |
| | 434 | 1169 | | builder.AppendLine(" /// </summary>"); |
| | 434 | 1170 | | builder.AppendLine(" /// <returns>A read-only list of plugin type information.</returns>"); |
| | 434 | 1171 | | builder.AppendLine(" public static IReadOnlyList<PluginTypeInfo> GetPluginTypes() => _plugins;"); |
| | | 1172 | | |
| | 434 | 1173 | | if (hasOptions) |
| | | 1174 | | { |
| | 146 | 1175 | | builder.AppendLine(); |
| | 146 | 1176 | | GenerateRegisterOptionsMethod(builder, discoveryResult.Options, safeAssemblyName, breadcrumbs, projectDirect |
| | | 1177 | | } |
| | | 1178 | | |
| | 434 | 1179 | | if (discoveryResult.Providers.Count > 0) |
| | | 1180 | | { |
| | 17 | 1181 | | builder.AppendLine(); |
| | 17 | 1182 | | GenerateRegisterProvidersMethod(builder, discoveryResult.Providers, safeAssemblyName, breadcrumbs, projectDi |
| | | 1183 | | } |
| | | 1184 | | |
| | 434 | 1185 | | builder.AppendLine(); |
| | 434 | 1186 | | GenerateApplyDecoratorsMethod(builder, discoveryResult.Decorators, discoveryResult.InterceptedServices.Count > 0 |
| | | 1187 | | |
| | 434 | 1188 | | if (discoveryResult.HostedServices.Count > 0) |
| | | 1189 | | { |
| | 6 | 1190 | | builder.AppendLine(); |
| | 6 | 1191 | | GenerateRegisterHostedServicesMethod(builder, discoveryResult.HostedServices, breadcrumbs, projectDirectory) |
| | | 1192 | | } |
| | | 1193 | | |
| | 434 | 1194 | | builder.AppendLine("}"); |
| | | 1195 | | |
| | 434 | 1196 | | return builder.ToString(); |
| | | 1197 | | } |
| | | 1198 | | |
| | | 1199 | | private static string GenerateModuleInitializerBootstrapSource(string assemblyName, IReadOnlyList<string> referenced |
| | | 1200 | | { |
| | 434 | 1201 | | var builder = new StringBuilder(); |
| | 434 | 1202 | | var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName); |
| | | 1203 | | |
| | 434 | 1204 | | breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Source-Gen Bootstrap"); |
| | 434 | 1205 | | builder.AppendLine("#nullable enable"); |
| | 434 | 1206 | | builder.AppendLine(); |
| | 434 | 1207 | | builder.AppendLine("using System.Runtime.CompilerServices;"); |
| | 434 | 1208 | | builder.AppendLine(); |
| | 434 | 1209 | | builder.AppendLine("using Microsoft.Extensions.Configuration;"); |
| | 434 | 1210 | | builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); |
| | 434 | 1211 | | builder.AppendLine(); |
| | 434 | 1212 | | builder.AppendLine($"namespace {safeAssemblyName}.Generated;"); |
| | 434 | 1213 | | builder.AppendLine(); |
| | 434 | 1214 | | builder.AppendLine("internal static class NeedlrSourceGenModuleInitializer"); |
| | 434 | 1215 | | builder.AppendLine("{"); |
| | 434 | 1216 | | builder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]"); |
| | 434 | 1217 | | builder.AppendLine(" internal static void Initialize()"); |
| | 434 | 1218 | | builder.AppendLine(" {"); |
| | | 1219 | | |
| | | 1220 | | // Generate ForceLoadAssemblies call if there are referenced assemblies with [GenerateTypeRegistry] |
| | 434 | 1221 | | if (referencedAssemblies.Count > 0) |
| | | 1222 | | { |
| | 16 | 1223 | | builder.AppendLine(" // Force-load referenced assemblies to ensure their module initializers run"); |
| | 16 | 1224 | | builder.AppendLine(" ForceLoadReferencedAssemblies();"); |
| | 16 | 1225 | | builder.AppendLine(); |
| | | 1226 | | } |
| | | 1227 | | |
| | 434 | 1228 | | builder.AppendLine(" global::NexusLabs.Needlr.Generators.NeedlrSourceGenBootstrap.Register("); |
| | 434 | 1229 | | builder.AppendLine($" global::{safeAssemblyName}.Generated.TypeRegistry.GetInjectableTypes,"); |
| | 434 | 1230 | | builder.AppendLine($" global::{safeAssemblyName}.Generated.TypeRegistry.GetPluginTypes,"); |
| | | 1231 | | |
| | | 1232 | | // Generate the decorator/factory/provider applier lambda |
| | 434 | 1233 | | if (hasFactories || hasProviders) |
| | | 1234 | | { |
| | 43 | 1235 | | builder.AppendLine(" services =>"); |
| | 43 | 1236 | | builder.AppendLine(" {"); |
| | 43 | 1237 | | builder.AppendLine($" global::{safeAssemblyName}.Generated.TypeRegistry.ApplyDecorators((ISer |
| | 43 | 1238 | | if (hasFactories) |
| | | 1239 | | { |
| | 26 | 1240 | | builder.AppendLine($" global::{safeAssemblyName}.Generated.FactoryRegistrations.RegisterF |
| | | 1241 | | } |
| | 43 | 1242 | | if (hasProviders) |
| | | 1243 | | { |
| | 17 | 1244 | | builder.AppendLine($" global::{safeAssemblyName}.Generated.TypeRegistry.RegisterProviders |
| | | 1245 | | } |
| | 43 | 1246 | | builder.AppendLine(" },"); |
| | | 1247 | | } |
| | | 1248 | | else |
| | | 1249 | | { |
| | 391 | 1250 | | builder.AppendLine($" services => global::{safeAssemblyName}.Generated.TypeRegistry.ApplyDecorato |
| | | 1251 | | } |
| | | 1252 | | |
| | | 1253 | | // Generate the options registrar lambda for NeedlrSourceGenBootstrap (for backward compat) |
| | 434 | 1254 | | if (hasOptions) |
| | | 1255 | | { |
| | 146 | 1256 | | builder.AppendLine($" (services, config) => global::{safeAssemblyName}.Generated.TypeRegistry.Reg |
| | | 1257 | | } |
| | | 1258 | | else |
| | | 1259 | | { |
| | 288 | 1260 | | builder.AppendLine(" null);"); |
| | | 1261 | | } |
| | | 1262 | | |
| | | 1263 | | // Also register with SourceGenRegistry (for ConfiguredSyringe without Generators.Attributes dependency) |
| | 434 | 1264 | | if (hasOptions) |
| | | 1265 | | { |
| | 146 | 1266 | | builder.AppendLine(); |
| | 146 | 1267 | | builder.AppendLine(" // Register options with core SourceGenRegistry for ConfiguredSyringe"); |
| | 146 | 1268 | | builder.AppendLine($" global::NexusLabs.Needlr.SourceGenRegistry.RegisterOptionsRegistrar("); |
| | 146 | 1269 | | builder.AppendLine($" (services, config) => global::{safeAssemblyName}.Generated.TypeRegistry.Reg |
| | | 1270 | | } |
| | | 1271 | | |
| | 434 | 1272 | | builder.AppendLine(" }"); |
| | | 1273 | | |
| | | 1274 | | // Generate ForceLoadReferencedAssemblies method if needed |
| | 434 | 1275 | | if (referencedAssemblies.Count > 0) |
| | | 1276 | | { |
| | 16 | 1277 | | builder.AppendLine(); |
| | 16 | 1278 | | builder.AppendLine(" /// <summary>"); |
| | 16 | 1279 | | builder.AppendLine(" /// Forces referenced assemblies with [GenerateTypeRegistry] to load,"); |
| | 16 | 1280 | | builder.AppendLine(" /// ensuring their module initializers execute and register their types."); |
| | 16 | 1281 | | builder.AppendLine(" /// </summary>"); |
| | 16 | 1282 | | builder.AppendLine(" /// <remarks>"); |
| | 16 | 1283 | | builder.AppendLine(" /// Without this, transitive dependencies that are never directly referenced"); |
| | 16 | 1284 | | builder.AppendLine(" /// in code would not be loaded by the CLR, and their plugins would not be discovere |
| | 16 | 1285 | | builder.AppendLine(" /// </remarks>"); |
| | 16 | 1286 | | builder.AppendLine(" [MethodImpl(MethodImplOptions.NoInlining)]"); |
| | 16 | 1287 | | builder.AppendLine(" private static void ForceLoadReferencedAssemblies()"); |
| | 16 | 1288 | | builder.AppendLine(" {"); |
| | | 1289 | | |
| | 66 | 1290 | | foreach (var referencedAssembly in referencedAssemblies) |
| | | 1291 | | { |
| | 17 | 1292 | | var safeRefAssemblyName = GeneratorHelpers.SanitizeIdentifier(referencedAssembly); |
| | 17 | 1293 | | builder.AppendLine($" _ = typeof(global::{safeRefAssemblyName}.Generated.TypeRegistry).Assembly;" |
| | | 1294 | | } |
| | | 1295 | | |
| | 16 | 1296 | | builder.AppendLine(" }"); |
| | | 1297 | | } |
| | | 1298 | | |
| | 434 | 1299 | | builder.AppendLine("}"); |
| | | 1300 | | |
| | 434 | 1301 | | return builder.ToString(); |
| | | 1302 | | } |
| | | 1303 | | |
| | | 1304 | | private static void GenerateInjectableTypesArray(StringBuilder builder, IReadOnlyList<DiscoveredType> types, Breadcr |
| | | 1305 | | { |
| | 434 | 1306 | | builder.AppendLine(" private static readonly InjectableTypeInfo[] _types ="); |
| | 434 | 1307 | | builder.AppendLine(" ["); |
| | | 1308 | | |
| | 256258 | 1309 | | var typesByAssembly = types.GroupBy(t => t.AssemblyName).OrderBy(g => g.Key); |
| | | 1310 | | |
| | 56352 | 1311 | | foreach (var group in typesByAssembly) |
| | | 1312 | | { |
| | 27742 | 1313 | | breadcrumbs.WriteInlineComment(builder, " ", $"From {group.Key}"); |
| | | 1314 | | |
| | 739730 | 1315 | | foreach (var type in group.OrderBy(t => t.TypeName)) |
| | | 1316 | | { |
| | | 1317 | | // Write breadcrumb for this type |
| | 228082 | 1318 | | if (breadcrumbs.Level == BreadcrumbLevel.Verbose) |
| | | 1319 | | { |
| | 15132 | 1320 | | var sourcePath = type.SourceFilePath != null |
| | 15132 | 1321 | | ? BreadcrumbWriter.GetRelativeSourcePath(type.SourceFilePath, projectDirectory) |
| | 15132 | 1322 | | : $"[{type.AssemblyName}]"; |
| | 15132 | 1323 | | var interfaces = type.InterfaceNames.Length > 0 |
| | 11 | 1324 | | ? string.Join(", ", type.InterfaceNames.Select(i => i.Split('.').Last())) |
| | 15132 | 1325 | | : "none"; |
| | 15132 | 1326 | | var keysInfo = type.ServiceKeys.Length > 0 |
| | 0 | 1327 | | ? $"Keys: {string.Join(", ", type.ServiceKeys.Select(k => $"\"{k}\""))}" |
| | 15132 | 1328 | | : null; |
| | | 1329 | | |
| | 15132 | 1330 | | if (keysInfo != null) |
| | | 1331 | | { |
| | 0 | 1332 | | breadcrumbs.WriteVerboseBox(builder, " ", |
| | 0 | 1333 | | $"{type.TypeName.Split('.').Last()} → {interfaces}", |
| | 0 | 1334 | | $"Source: {sourcePath}", |
| | 0 | 1335 | | $"Lifetime: {type.Lifetime}", |
| | 0 | 1336 | | keysInfo); |
| | | 1337 | | } |
| | | 1338 | | else |
| | | 1339 | | { |
| | 15132 | 1340 | | breadcrumbs.WriteVerboseBox(builder, " ", |
| | 15132 | 1341 | | $"{type.TypeName.Split('.').Last()} → {interfaces}", |
| | 15132 | 1342 | | $"Source: {sourcePath}", |
| | 15132 | 1343 | | $"Lifetime: {type.Lifetime}"); |
| | | 1344 | | } |
| | | 1345 | | } |
| | | 1346 | | |
| | 228082 | 1347 | | builder.Append($" new(typeof({type.TypeName}), "); |
| | | 1348 | | |
| | | 1349 | | // Interfaces |
| | 228082 | 1350 | | if (type.InterfaceNames.Length == 0) |
| | | 1351 | | { |
| | 227841 | 1352 | | builder.Append("Array.Empty<Type>(), "); |
| | | 1353 | | } |
| | | 1354 | | else |
| | | 1355 | | { |
| | 241 | 1356 | | builder.Append("["); |
| | 487 | 1357 | | builder.Append(string.Join(", ", type.InterfaceNames.Select(i => $"typeof({i})"))); |
| | 241 | 1358 | | builder.Append("], "); |
| | | 1359 | | } |
| | | 1360 | | |
| | | 1361 | | // Lifetime |
| | 228082 | 1362 | | builder.Append($"InjectableLifetime.{type.Lifetime}, "); |
| | | 1363 | | |
| | | 1364 | | // Factory lambda - resolves dependencies and creates instance without reflection |
| | 228082 | 1365 | | builder.Append("sp => new "); |
| | 228082 | 1366 | | builder.Append(type.TypeName); |
| | 228082 | 1367 | | builder.Append("("); |
| | 228082 | 1368 | | if (type.ConstructorParameters.Length > 0) |
| | | 1369 | | { |
| | 45668 | 1370 | | var parameterExpressions = type.ConstructorParameters |
| | 99634 | 1371 | | .Select(p => p.IsKeyed |
| | 99634 | 1372 | | ? $"sp.GetRequiredKeyedService<{p.TypeName}>(\"{GeneratorHelpers.EscapeStringLiteral(p.Servi |
| | 99634 | 1373 | | : $"sp.GetRequiredService<{p.TypeName}>()"); |
| | 45668 | 1374 | | builder.Append(string.Join(", ", parameterExpressions)); |
| | | 1375 | | } |
| | 228082 | 1376 | | builder.Append("), "); |
| | | 1377 | | |
| | | 1378 | | // Service keys from [Keyed] attributes |
| | 228082 | 1379 | | if (type.ServiceKeys.Length == 0) |
| | | 1380 | | { |
| | 228077 | 1381 | | builder.AppendLine("Array.Empty<string>()),"); |
| | | 1382 | | } |
| | | 1383 | | else |
| | | 1384 | | { |
| | 5 | 1385 | | builder.Append("["); |
| | 10 | 1386 | | builder.Append(string.Join(", ", type.ServiceKeys.Select(k => $"\"{GeneratorHelpers.EscapeStringLite |
| | 5 | 1387 | | builder.AppendLine("]),"); |
| | | 1388 | | } |
| | | 1389 | | } |
| | | 1390 | | } |
| | | 1391 | | |
| | 434 | 1392 | | builder.AppendLine(" ];"); |
| | 434 | 1393 | | } |
| | | 1394 | | |
| | | 1395 | | private static void GeneratePluginTypesArray(StringBuilder builder, IReadOnlyList<DiscoveredPlugin> plugins, Breadcr |
| | | 1396 | | { |
| | 434 | 1397 | | builder.AppendLine(" private static readonly PluginTypeInfo[] _plugins ="); |
| | 434 | 1398 | | builder.AppendLine(" ["); |
| | | 1399 | | |
| | | 1400 | | // Sort plugins by Order first, then by TypeName for determinism |
| | 434 | 1401 | | var sortedPlugins = plugins |
| | 1361 | 1402 | | .OrderBy(p => p.Order) |
| | 1361 | 1403 | | .ThenBy(p => p.TypeName, StringComparer.Ordinal) |
| | 434 | 1404 | | .ToList(); |
| | | 1405 | | |
| | | 1406 | | // Group for breadcrumb display, but maintain the sorted order |
| | 2367 | 1407 | | var pluginsByAssembly = sortedPlugins.GroupBy(p => p.AssemblyName).OrderBy(g => g.Key); |
| | | 1408 | | |
| | 1976 | 1409 | | foreach (var group in pluginsByAssembly) |
| | | 1410 | | { |
| | 554 | 1411 | | breadcrumbs.WriteInlineComment(builder, " ", $"From {group.Key}"); |
| | | 1412 | | |
| | | 1413 | | // Maintain order within assembly group |
| | 6624 | 1414 | | foreach (var plugin in group.OrderBy(p => p.Order).ThenBy(p => p.TypeName, StringComparer.Ordinal)) |
| | | 1415 | | { |
| | | 1416 | | // Write verbose breadcrumb for this plugin |
| | 1379 | 1417 | | if (breadcrumbs.Level == BreadcrumbLevel.Verbose) |
| | | 1418 | | { |
| | 91 | 1419 | | var sourcePath = plugin.SourceFilePath != null |
| | 91 | 1420 | | ? BreadcrumbWriter.GetRelativeSourcePath(plugin.SourceFilePath, projectDirectory) |
| | 91 | 1421 | | : $"[{plugin.AssemblyName}]"; |
| | 91 | 1422 | | var interfaces = plugin.InterfaceNames.Length > 0 |
| | 91 | 1423 | | ? string.Join(", ", plugin.InterfaceNames.Select(i => i.Split('.').Last())) |
| | 91 | 1424 | | : "none"; |
| | 91 | 1425 | | var orderInfo = plugin.Order != 0 ? $"Order: {plugin.Order}" : "Order: 0 (default)"; |
| | | 1426 | | |
| | 91 | 1427 | | breadcrumbs.WriteVerboseBox(builder, " ", |
| | 91 | 1428 | | $"Plugin: {plugin.TypeName.Split('.').Last()}", |
| | 91 | 1429 | | $"Source: {sourcePath}", |
| | 91 | 1430 | | $"Implements: {interfaces}", |
| | 91 | 1431 | | orderInfo); |
| | | 1432 | | } |
| | 1288 | 1433 | | else if (breadcrumbs.Level == BreadcrumbLevel.Minimal && plugin.Order != 0) |
| | | 1434 | | { |
| | | 1435 | | // Show order in minimal mode only if non-default |
| | 0 | 1436 | | breadcrumbs.WriteInlineComment(builder, " ", $"{plugin.TypeName.Split('.').Last()} (Order: {p |
| | | 1437 | | } |
| | | 1438 | | |
| | 1379 | 1439 | | builder.Append($" new(typeof({plugin.TypeName}), "); |
| | | 1440 | | |
| | | 1441 | | // Interfaces |
| | 1379 | 1442 | | if (plugin.InterfaceNames.Length == 0) |
| | | 1443 | | { |
| | 0 | 1444 | | builder.Append("Array.Empty<Type>(), "); |
| | | 1445 | | } |
| | | 1446 | | else |
| | | 1447 | | { |
| | 1379 | 1448 | | builder.Append("["); |
| | 2768 | 1449 | | builder.Append(string.Join(", ", plugin.InterfaceNames.Select(i => $"typeof({i})"))); |
| | 1379 | 1450 | | builder.Append("], "); |
| | | 1451 | | } |
| | | 1452 | | |
| | | 1453 | | // Factory lambda - no Activator.CreateInstance needed |
| | 1379 | 1454 | | builder.Append($"() => new {plugin.TypeName}(), "); |
| | | 1455 | | |
| | | 1456 | | // Attributes |
| | 1379 | 1457 | | if (plugin.AttributeNames.Length == 0) |
| | | 1458 | | { |
| | 1339 | 1459 | | builder.Append("Array.Empty<Type>(), "); |
| | | 1460 | | } |
| | | 1461 | | else |
| | | 1462 | | { |
| | 40 | 1463 | | builder.Append("["); |
| | 96 | 1464 | | builder.Append(string.Join(", ", plugin.AttributeNames.Select(a => $"typeof({a})"))); |
| | 40 | 1465 | | builder.Append("], "); |
| | | 1466 | | } |
| | | 1467 | | |
| | | 1468 | | // Order |
| | 1379 | 1469 | | builder.AppendLine($"{plugin.Order}),"); |
| | | 1470 | | } |
| | | 1471 | | } |
| | | 1472 | | |
| | 434 | 1473 | | builder.AppendLine(" ];"); |
| | 434 | 1474 | | } |
| | | 1475 | | |
| | | 1476 | | private static void GenerateRegisterOptionsMethod(StringBuilder builder, IReadOnlyList<DiscoveredOptions> options, s |
| | | 1477 | | { |
| | 146 | 1478 | | builder.AppendLine(" /// <summary>"); |
| | 146 | 1479 | | builder.AppendLine(" /// Registers all discovered options types with the service collection."); |
| | 146 | 1480 | | builder.AppendLine(" /// This binds configuration sections to strongly-typed options classes."); |
| | 146 | 1481 | | builder.AppendLine(" /// </summary>"); |
| | 146 | 1482 | | builder.AppendLine(" /// <param name=\"services\">The service collection to configure.</param>"); |
| | 146 | 1483 | | builder.AppendLine(" /// <param name=\"configuration\">The configuration root to bind options from.</param>") |
| | 146 | 1484 | | builder.AppendLine(" public static void RegisterOptions(IServiceCollection services, IConfiguration configura |
| | 146 | 1485 | | builder.AppendLine(" {"); |
| | | 1486 | | |
| | 146 | 1487 | | if (options.Count == 0) |
| | | 1488 | | { |
| | 0 | 1489 | | breadcrumbs.WriteInlineComment(builder, " ", "No options types discovered"); |
| | | 1490 | | } |
| | 146 | 1491 | | else if (isAotProject) |
| | | 1492 | | { |
| | 78 | 1493 | | GenerateAotOptionsRegistration(builder, options, safeAssemblyName, breadcrumbs, projectDirectory); |
| | | 1494 | | } |
| | | 1495 | | else |
| | | 1496 | | { |
| | 68 | 1497 | | GenerateReflectionOptionsRegistration(builder, options, safeAssemblyName, breadcrumbs); |
| | | 1498 | | } |
| | | 1499 | | |
| | 146 | 1500 | | builder.AppendLine(" }"); |
| | 146 | 1501 | | } |
| | | 1502 | | |
| | | 1503 | | private static void GenerateReflectionOptionsRegistration(StringBuilder builder, IReadOnlyList<DiscoveredOptions> op |
| | | 1504 | | { |
| | | 1505 | | // Track external validators to register (avoid duplicates) |
| | 68 | 1506 | | var externalValidatorsToRegister = new HashSet<string>(); |
| | | 1507 | | |
| | 292 | 1508 | | foreach (var opt in options) |
| | | 1509 | | { |
| | 78 | 1510 | | var typeName = opt.TypeName; |
| | | 1511 | | |
| | 78 | 1512 | | if (opt.ValidateOnStart) |
| | | 1513 | | { |
| | | 1514 | | // Use AddOptions pattern for validation support |
| | | 1515 | | // services.AddOptions<T>().BindConfiguration("Section").ValidateDataAnnotations().ValidateOnStart(); |
| | 23 | 1516 | | builder.Append($" services.AddOptions<{typeName}>"); |
| | | 1517 | | |
| | 23 | 1518 | | if (opt.IsNamed) |
| | | 1519 | | { |
| | 1 | 1520 | | builder.Append($"(\"{opt.Name}\")"); |
| | | 1521 | | } |
| | | 1522 | | else |
| | | 1523 | | { |
| | 22 | 1524 | | builder.Append("()"); |
| | | 1525 | | } |
| | | 1526 | | |
| | 23 | 1527 | | builder.Append($".BindConfiguration(\"{opt.SectionName}\")"); |
| | 23 | 1528 | | builder.Append(".ValidateDataAnnotations()"); |
| | 23 | 1529 | | builder.AppendLine(".ValidateOnStart();"); |
| | | 1530 | | |
| | | 1531 | | // Register source-generated DataAnnotations validator if present |
| | | 1532 | | // This runs alongside .ValidateDataAnnotations() - source-gen handles supported attributes, |
| | | 1533 | | // reflection fallback handles unsupported attributes (like [CustomValidation]) |
| | 23 | 1534 | | if (opt.HasDataAnnotations) |
| | | 1535 | | { |
| | 2 | 1536 | | var shortTypeName = GeneratorHelpers.GetShortTypeName(typeName); |
| | 2 | 1537 | | var dataAnnotationsValidatorClassName = $"global::{safeAssemblyName}.Generated.{shortTypeName}DataAn |
| | 2 | 1538 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOpt |
| | | 1539 | | } |
| | | 1540 | | |
| | | 1541 | | // If there's a custom validator method, register the generated validator |
| | 23 | 1542 | | if (opt.HasValidatorMethod) |
| | | 1543 | | { |
| | 12 | 1544 | | var shortTypeName = GeneratorHelpers.GetShortTypeName(typeName); |
| | 12 | 1545 | | var validatorClassName = $"global::{safeAssemblyName}.Generated.{shortTypeName}Validator"; |
| | 12 | 1546 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOpt |
| | | 1547 | | |
| | | 1548 | | // If external validator with instance method, register it too |
| | 12 | 1549 | | if (opt.HasExternalValidator && opt.ValidatorMethod != null && !opt.ValidatorMethod.Value.IsStatic) |
| | | 1550 | | { |
| | 3 | 1551 | | externalValidatorsToRegister.Add(opt.ValidatorTypeName!); |
| | | 1552 | | } |
| | | 1553 | | } |
| | | 1554 | | } |
| | 55 | 1555 | | else if (opt.IsNamed) |
| | | 1556 | | { |
| | | 1557 | | // Named options: OptionsConfigurationServiceCollectionExtensions.Configure<T>(services, "name", section |
| | 8 | 1558 | | builder.AppendLine($" global::Microsoft.Extensions.DependencyInjection.OptionsConfigurationServic |
| | | 1559 | | } |
| | | 1560 | | else |
| | | 1561 | | { |
| | | 1562 | | // Default options: OptionsConfigurationServiceCollectionExtensions.Configure<T>(services, section) |
| | 47 | 1563 | | builder.AppendLine($" global::Microsoft.Extensions.DependencyInjection.OptionsConfigurationServic |
| | | 1564 | | } |
| | | 1565 | | } |
| | | 1566 | | |
| | | 1567 | | // Register external validators that have instance methods |
| | 142 | 1568 | | foreach (var validatorType in externalValidatorsToRegister) |
| | | 1569 | | { |
| | 3 | 1570 | | builder.AppendLine($" services.AddSingleton<{validatorType}>();"); |
| | | 1571 | | } |
| | 68 | 1572 | | } |
| | | 1573 | | |
| | | 1574 | | private static void GenerateAotOptionsRegistration(StringBuilder builder, IReadOnlyList<DiscoveredOptions> options, |
| | | 1575 | | { |
| | 78 | 1576 | | breadcrumbs.WriteInlineComment(builder, " ", "AOT-compatible options binding (no reflection)"); |
| | | 1577 | | |
| | 78 | 1578 | | var externalValidatorsToRegister = new HashSet<string>(); |
| | | 1579 | | |
| | 330 | 1580 | | foreach (var opt in options) |
| | | 1581 | | { |
| | 87 | 1582 | | var typeName = opt.TypeName; |
| | 87 | 1583 | | builder.AppendLine(); |
| | 87 | 1584 | | builder.AppendLine($" // Bind {opt.SectionName} section to {GeneratorHelpers.GetShortTypeName(typeNam |
| | | 1585 | | |
| | | 1586 | | // Choose binding pattern based on type characteristics |
| | 87 | 1587 | | if (opt.IsPositionalRecord) |
| | | 1588 | | { |
| | | 1589 | | // Positional records: Use constructor binding with Options.Create |
| | 5 | 1590 | | GeneratePositionalRecordBinding(builder, opt, safeAssemblyName, externalValidatorsToRegister); |
| | | 1591 | | } |
| | 82 | 1592 | | else if (opt.HasInitOnlyProperties) |
| | | 1593 | | { |
| | | 1594 | | // Classes/records with init-only properties: Use object initializer with Options.Create |
| | 7 | 1595 | | GenerateInitOnlyBinding(builder, opt, safeAssemblyName, externalValidatorsToRegister); |
| | | 1596 | | } |
| | | 1597 | | else |
| | | 1598 | | { |
| | | 1599 | | // Regular classes with setters: Use Configure delegate pattern |
| | 75 | 1600 | | GenerateConfigureBinding(builder, opt, safeAssemblyName, externalValidatorsToRegister); |
| | | 1601 | | } |
| | | 1602 | | } |
| | | 1603 | | |
| | | 1604 | | // Register external validators that have instance methods |
| | 158 | 1605 | | foreach (var validatorType in externalValidatorsToRegister) |
| | | 1606 | | { |
| | 1 | 1607 | | builder.AppendLine($" services.AddSingleton<{validatorType}>();"); |
| | | 1608 | | } |
| | 78 | 1609 | | } |
| | | 1610 | | |
| | | 1611 | | private static void GenerateConfigureBinding(StringBuilder builder, DiscoveredOptions opt, string safeAssemblyName, |
| | | 1612 | | { |
| | 75 | 1613 | | var typeName = opt.TypeName; |
| | | 1614 | | |
| | 75 | 1615 | | if (opt.IsNamed) |
| | | 1616 | | { |
| | 13 | 1617 | | builder.AppendLine($" services.AddOptions<{typeName}>(\"{opt.Name}\")"); |
| | | 1618 | | } |
| | | 1619 | | else |
| | | 1620 | | { |
| | 62 | 1621 | | builder.AppendLine($" services.AddOptions<{typeName}>()"); |
| | | 1622 | | } |
| | | 1623 | | |
| | 75 | 1624 | | builder.AppendLine(" .Configure<IConfiguration>((options, config) =>"); |
| | 75 | 1625 | | builder.AppendLine(" {"); |
| | 75 | 1626 | | builder.AppendLine($" var section = config.GetSection(\"{opt.SectionName}\");"); |
| | | 1627 | | |
| | | 1628 | | // Generate property binding for each property |
| | 75 | 1629 | | var propIndex = 0; |
| | 390 | 1630 | | foreach (var prop in opt.Properties) |
| | | 1631 | | { |
| | 120 | 1632 | | GeneratePropertyBinding(builder, prop, propIndex); |
| | 120 | 1633 | | propIndex++; |
| | | 1634 | | } |
| | | 1635 | | |
| | 75 | 1636 | | builder.Append(" })"); |
| | | 1637 | | |
| | | 1638 | | // Add validation chain if ValidateOnStart is enabled |
| | 75 | 1639 | | if (opt.ValidateOnStart) |
| | | 1640 | | { |
| | 20 | 1641 | | builder.AppendLine(); |
| | 20 | 1642 | | builder.Append(" .ValidateOnStart()"); |
| | | 1643 | | } |
| | | 1644 | | |
| | 75 | 1645 | | builder.AppendLine(";"); |
| | | 1646 | | |
| | 75 | 1647 | | RegisterValidator(builder, opt, safeAssemblyName, externalValidatorsToRegister); |
| | 75 | 1648 | | } |
| | | 1649 | | |
| | | 1650 | | private static void GenerateInitOnlyBinding(StringBuilder builder, DiscoveredOptions opt, string safeAssemblyName, H |
| | | 1651 | | { |
| | 7 | 1652 | | var typeName = opt.TypeName; |
| | | 1653 | | |
| | | 1654 | | // Use AddSingleton with IOptions<T> factory pattern for init-only |
| | 7 | 1655 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IOptions<{typeName}>>(sp |
| | 7 | 1656 | | builder.AppendLine(" {"); |
| | 7 | 1657 | | builder.AppendLine($" var config = sp.GetRequiredService<IConfiguration>();"); |
| | 7 | 1658 | | builder.AppendLine($" var section = config.GetSection(\"{opt.SectionName}\");"); |
| | | 1659 | | |
| | | 1660 | | // Generate parsing variables first |
| | 7 | 1661 | | var propIndex = 0; |
| | 44 | 1662 | | foreach (var prop in opt.Properties) |
| | | 1663 | | { |
| | 15 | 1664 | | GeneratePropertyParseVariable(builder, prop, propIndex); |
| | 15 | 1665 | | propIndex++; |
| | | 1666 | | } |
| | | 1667 | | |
| | | 1668 | | // Create object with initializer |
| | 7 | 1669 | | builder.AppendLine($" return global::Microsoft.Extensions.Options.Options.Create(new {typeName}"); |
| | 7 | 1670 | | builder.AppendLine(" {"); |
| | | 1671 | | |
| | 7 | 1672 | | propIndex = 0; |
| | 44 | 1673 | | foreach (var prop in opt.Properties) |
| | | 1674 | | { |
| | 15 | 1675 | | var comma = propIndex < opt.Properties.Count - 1 ? "," : ""; |
| | 15 | 1676 | | GeneratePropertyInitializer(builder, prop, propIndex, comma); |
| | 15 | 1677 | | propIndex++; |
| | | 1678 | | } |
| | | 1679 | | |
| | 7 | 1680 | | builder.AppendLine(" });"); |
| | 7 | 1681 | | builder.AppendLine(" });"); |
| | | 1682 | | |
| | | 1683 | | // For validation with factory pattern, we need to register the validator separately |
| | 7 | 1684 | | if (opt.ValidateOnStart) |
| | | 1685 | | { |
| | 1 | 1686 | | RegisterValidatorForFactory(builder, opt, safeAssemblyName, externalValidatorsToRegister); |
| | | 1687 | | } |
| | 7 | 1688 | | } |
| | | 1689 | | |
| | | 1690 | | private static void GeneratePositionalRecordBinding(StringBuilder builder, DiscoveredOptions opt, string safeAssembl |
| | | 1691 | | { |
| | 5 | 1692 | | var typeName = opt.TypeName; |
| | 5 | 1693 | | var recordInfo = opt.PositionalRecordInfo!.Value; |
| | | 1694 | | |
| | | 1695 | | // Use AddSingleton with IOptions<T> factory pattern for positional records |
| | 5 | 1696 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IOptions<{typeName}>>(sp |
| | 5 | 1697 | | builder.AppendLine(" {"); |
| | 5 | 1698 | | builder.AppendLine($" var config = sp.GetRequiredService<IConfiguration>();"); |
| | 5 | 1699 | | builder.AppendLine($" var section = config.GetSection(\"{opt.SectionName}\");"); |
| | | 1700 | | |
| | | 1701 | | // Generate parsing variables for each constructor parameter |
| | 5 | 1702 | | var paramIndex = 0; |
| | 36 | 1703 | | foreach (var param in recordInfo.Parameters) |
| | | 1704 | | { |
| | 13 | 1705 | | GenerateParameterParseVariable(builder, param, paramIndex); |
| | 13 | 1706 | | paramIndex++; |
| | | 1707 | | } |
| | | 1708 | | |
| | | 1709 | | // Create record with constructor |
| | 5 | 1710 | | builder.Append($" return global::Microsoft.Extensions.Options.Options.Create(new {typeName}("); |
| | | 1711 | | |
| | 5 | 1712 | | paramIndex = 0; |
| | 36 | 1713 | | foreach (var param in recordInfo.Parameters) |
| | | 1714 | | { |
| | 21 | 1715 | | if (paramIndex > 0) builder.Append(", "); |
| | 13 | 1716 | | builder.Append($"p{paramIndex}"); |
| | 13 | 1717 | | paramIndex++; |
| | | 1718 | | } |
| | | 1719 | | |
| | 5 | 1720 | | builder.AppendLine("));"); |
| | 5 | 1721 | | builder.AppendLine(" });"); |
| | | 1722 | | |
| | | 1723 | | // For validation with factory pattern, we need to register the validator separately |
| | 5 | 1724 | | if (opt.ValidateOnStart) |
| | | 1725 | | { |
| | 0 | 1726 | | RegisterValidatorForFactory(builder, opt, safeAssemblyName, externalValidatorsToRegister); |
| | | 1727 | | } |
| | 5 | 1728 | | } |
| | | 1729 | | |
| | | 1730 | | private static void GeneratePropertyParseVariable(StringBuilder builder, OptionsPropertyInfo prop, int index) |
| | | 1731 | | { |
| | 15 | 1732 | | var varName = $"p{index}"; |
| | 15 | 1733 | | var typeName = prop.TypeName; |
| | 15 | 1734 | | var baseTypeName = GetBaseTypeName(typeName); |
| | | 1735 | | |
| | | 1736 | | // Handle complex types |
| | 15 | 1737 | | if (prop.ComplexTypeKind != ComplexTypeKind.None) |
| | | 1738 | | { |
| | 2 | 1739 | | GenerateComplexTypeParseVariable(builder, prop, index); |
| | 2 | 1740 | | return; |
| | | 1741 | | } |
| | | 1742 | | |
| | | 1743 | | // Handle enums |
| | 13 | 1744 | | if (prop.IsEnum && prop.EnumTypeName != null) |
| | | 1745 | | { |
| | 0 | 1746 | | var defaultVal = prop.IsNullable ? "null" : "default"; |
| | 0 | 1747 | | builder.AppendLine($" var {varName} = section[\"{prop.Name}\"] is {{ }} v{index} && global::Syste |
| | 0 | 1748 | | return; |
| | | 1749 | | } |
| | | 1750 | | |
| | | 1751 | | // Handle primitives |
| | 13 | 1752 | | if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 1753 | | { |
| | 7 | 1754 | | var defaultVal = prop.IsNullable ? "null" : "\"\""; |
| | 7 | 1755 | | builder.AppendLine($" var {varName} = section[\"{prop.Name}\"] ?? {defaultVal};"); |
| | | 1756 | | } |
| | 6 | 1757 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 1758 | | { |
| | 5 | 1759 | | var defaultVal = prop.IsNullable ? "null" : "0"; |
| | 5 | 1760 | | builder.AppendLine($" var {varName} = section[\"{prop.Name}\"] is {{ }} v{index} && int.TryParse( |
| | | 1761 | | } |
| | 1 | 1762 | | else if (baseTypeName == "bool" || baseTypeName == "global::System.Boolean") |
| | | 1763 | | { |
| | 1 | 1764 | | var defaultVal = prop.IsNullable ? "null" : "false"; |
| | 1 | 1765 | | builder.AppendLine($" var {varName} = section[\"{prop.Name}\"] is {{ }} v{index} && bool.TryParse |
| | | 1766 | | } |
| | 0 | 1767 | | else if (baseTypeName == "double" || baseTypeName == "global::System.Double") |
| | | 1768 | | { |
| | 0 | 1769 | | var defaultVal = prop.IsNullable ? "null" : "0.0"; |
| | 0 | 1770 | | builder.AppendLine($" var {varName} = section[\"{prop.Name}\"] is {{ }} v{index} && double.TryPar |
| | | 1771 | | } |
| | | 1772 | | else |
| | | 1773 | | { |
| | | 1774 | | // Default to default value for unsupported types |
| | 0 | 1775 | | builder.AppendLine($" var {varName} = default({typeName}); // Unsupported type"); |
| | | 1776 | | } |
| | 0 | 1777 | | } |
| | | 1778 | | |
| | | 1779 | | private static void GenerateComplexTypeParseVariable(StringBuilder builder, OptionsPropertyInfo prop, int index) |
| | | 1780 | | { |
| | 2 | 1781 | | var varName = $"p{index}"; |
| | 2 | 1782 | | var sectionVar = $"sec{index}"; |
| | | 1783 | | |
| | 2 | 1784 | | switch (prop.ComplexTypeKind) |
| | | 1785 | | { |
| | | 1786 | | case ComplexTypeKind.NestedObject: |
| | 1 | 1787 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | 1 | 1788 | | builder.AppendLine($" var {varName} = new {GetNonNullableTypeName(prop.TypeName)}();"); |
| | 1 | 1789 | | if (prop.NestedProperties != null) |
| | | 1790 | | { |
| | 1 | 1791 | | var nestedIndex = index * 100; |
| | 6 | 1792 | | foreach (var nested in prop.NestedProperties) |
| | | 1793 | | { |
| | 2 | 1794 | | GenerateNestedPropertyAssignment(builder, nested, nestedIndex, varName, sectionVar); |
| | 2 | 1795 | | nestedIndex++; |
| | | 1796 | | } |
| | | 1797 | | } |
| | | 1798 | | break; |
| | | 1799 | | |
| | | 1800 | | case ComplexTypeKind.List: |
| | 1 | 1801 | | var listElemType = prop.ElementTypeName ?? "string"; |
| | 1 | 1802 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | 1 | 1803 | | builder.AppendLine($" var {varName} = new global::System.Collections.Generic.List<{listElemTy |
| | 1 | 1804 | | builder.AppendLine($" foreach (var child in {sectionVar}.GetChildren())"); |
| | 1 | 1805 | | builder.AppendLine(" {"); |
| | 1 | 1806 | | if (prop.NestedProperties != null && prop.NestedProperties.Count > 0) |
| | | 1807 | | { |
| | 0 | 1808 | | builder.AppendLine($" var item = new {listElemType}();"); |
| | 0 | 1809 | | var ni = index * 100; |
| | 0 | 1810 | | foreach (var np in prop.NestedProperties) |
| | | 1811 | | { |
| | 0 | 1812 | | GenerateChildPropertyAssignment(builder, np, ni, "item", "child"); |
| | 0 | 1813 | | ni++; |
| | | 1814 | | } |
| | 0 | 1815 | | builder.AppendLine($" {varName}.Add(item);"); |
| | | 1816 | | } |
| | | 1817 | | else |
| | | 1818 | | { |
| | 1 | 1819 | | builder.AppendLine($" if (child.Value is {{ }} val) {varName}.Add(val);"); |
| | | 1820 | | } |
| | 1 | 1821 | | builder.AppendLine(" }"); |
| | 1 | 1822 | | break; |
| | | 1823 | | |
| | | 1824 | | case ComplexTypeKind.Dictionary: |
| | 0 | 1825 | | var dictValType = prop.ElementTypeName ?? "string"; |
| | 0 | 1826 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | 0 | 1827 | | builder.AppendLine($" var {varName} = new global::System.Collections.Generic.Dictionary<strin |
| | 0 | 1828 | | builder.AppendLine($" foreach (var child in {sectionVar}.GetChildren())"); |
| | 0 | 1829 | | builder.AppendLine(" {"); |
| | 0 | 1830 | | if (prop.NestedProperties != null && prop.NestedProperties.Count > 0) |
| | | 1831 | | { |
| | 0 | 1832 | | builder.AppendLine($" var item = new {dictValType}();"); |
| | 0 | 1833 | | var ni = index * 100; |
| | 0 | 1834 | | foreach (var np in prop.NestedProperties) |
| | | 1835 | | { |
| | 0 | 1836 | | GenerateChildPropertyAssignment(builder, np, ni, "item", "child"); |
| | 0 | 1837 | | ni++; |
| | | 1838 | | } |
| | 0 | 1839 | | builder.AppendLine($" {varName}[child.Key] = item;"); |
| | | 1840 | | } |
| | 0 | 1841 | | else if (dictValType == "int" || dictValType == "global::System.Int32") |
| | | 1842 | | { |
| | 0 | 1843 | | builder.AppendLine($" if (child.Value is {{ }} val && int.TryParse(val, out var iv)) |
| | | 1844 | | } |
| | | 1845 | | else |
| | | 1846 | | { |
| | 0 | 1847 | | builder.AppendLine($" if (child.Value is {{ }} val) {varName}[child.Key] = val;"); |
| | | 1848 | | } |
| | 0 | 1849 | | builder.AppendLine(" }"); |
| | 0 | 1850 | | break; |
| | | 1851 | | |
| | | 1852 | | default: |
| | 0 | 1853 | | builder.AppendLine($" var {varName} = default({prop.TypeName}); // Complex type"); |
| | | 1854 | | break; |
| | | 1855 | | } |
| | 1 | 1856 | | } |
| | | 1857 | | |
| | | 1858 | | private static void GenerateNestedPropertyAssignment(StringBuilder builder, OptionsPropertyInfo prop, int index, str |
| | | 1859 | | { |
| | 2 | 1860 | | var varName = $"nv{index}"; |
| | 2 | 1861 | | var baseTypeName = GetBaseTypeName(prop.TypeName); |
| | | 1862 | | |
| | 2 | 1863 | | if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 1864 | | { |
| | 1 | 1865 | | builder.AppendLine($" if ({sectionVar}[\"{prop.Name}\"] is {{ }} {varName}) {targetVar}.{prop.Nam |
| | | 1866 | | } |
| | 1 | 1867 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 1868 | | { |
| | 1 | 1869 | | builder.AppendLine($" if ({sectionVar}[\"{prop.Name}\"] is {{ }} {varName} && int.TryParse({varNa |
| | | 1870 | | } |
| | 0 | 1871 | | else if (baseTypeName == "bool" || baseTypeName == "global::System.Boolean") |
| | | 1872 | | { |
| | 0 | 1873 | | builder.AppendLine($" if ({sectionVar}[\"{prop.Name}\"] is {{ }} {varName} && bool.TryParse({varN |
| | | 1874 | | } |
| | 0 | 1875 | | } |
| | | 1876 | | |
| | | 1877 | | private static void GenerateChildPropertyAssignment(StringBuilder builder, OptionsPropertyInfo prop, int index, stri |
| | | 1878 | | { |
| | 0 | 1879 | | var varName = $"cv{index}"; |
| | 0 | 1880 | | var baseTypeName = GetBaseTypeName(prop.TypeName); |
| | | 1881 | | |
| | 0 | 1882 | | if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 1883 | | { |
| | 0 | 1884 | | builder.AppendLine($" if ({sectionVar}[\"{prop.Name}\"] is {{ }} {varName}) {targetVar}.{prop |
| | | 1885 | | } |
| | 0 | 1886 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 1887 | | { |
| | 0 | 1888 | | builder.AppendLine($" if ({sectionVar}[\"{prop.Name}\"] is {{ }} {varName} && int.TryParse({v |
| | | 1889 | | } |
| | 0 | 1890 | | } |
| | | 1891 | | |
| | | 1892 | | private static void GeneratePropertyInitializer(StringBuilder builder, OptionsPropertyInfo prop, int index, string c |
| | | 1893 | | { |
| | 15 | 1894 | | builder.AppendLine($" {prop.Name} = p{index}{comma}"); |
| | 15 | 1895 | | } |
| | | 1896 | | |
| | | 1897 | | private static void GenerateParameterParseVariable(StringBuilder builder, PositionalRecordParameter param, int index |
| | | 1898 | | { |
| | 13 | 1899 | | var varName = $"p{index}"; |
| | 13 | 1900 | | var typeName = param.TypeName; |
| | 13 | 1901 | | var baseTypeName = GetBaseTypeName(typeName); |
| | | 1902 | | |
| | | 1903 | | // Check if it's an enum |
| | | 1904 | | // For simplicity, check if it's a known primitive, otherwise assume it could be an enum |
| | 13 | 1905 | | if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 1906 | | { |
| | 5 | 1907 | | builder.AppendLine($" var {varName} = section[\"{param.Name}\"] ?? \"\";"); |
| | | 1908 | | } |
| | 8 | 1909 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 1910 | | { |
| | 4 | 1911 | | builder.AppendLine($" var {varName} = section[\"{param.Name}\"] is {{ }} v{index} && int.TryParse |
| | | 1912 | | } |
| | 4 | 1913 | | else if (baseTypeName == "bool" || baseTypeName == "global::System.Boolean") |
| | | 1914 | | { |
| | 3 | 1915 | | builder.AppendLine($" var {varName} = section[\"{param.Name}\"] is {{ }} v{index} && bool.TryPars |
| | | 1916 | | } |
| | 1 | 1917 | | else if (baseTypeName == "double" || baseTypeName == "global::System.Double") |
| | | 1918 | | { |
| | 0 | 1919 | | builder.AppendLine($" var {varName} = section[\"{param.Name}\"] is {{ }} v{index} && double.TryPa |
| | | 1920 | | } |
| | | 1921 | | else |
| | | 1922 | | { |
| | | 1923 | | // Try enum parsing for other types |
| | 1 | 1924 | | builder.AppendLine($" var {varName} = section[\"{param.Name}\"] is {{ }} v{index} && global::Syst |
| | | 1925 | | } |
| | 1 | 1926 | | } |
| | | 1927 | | |
| | | 1928 | | private static void RegisterValidator(StringBuilder builder, DiscoveredOptions opt, string safeAssemblyName, HashSet |
| | | 1929 | | { |
| | 75 | 1930 | | var typeName = opt.TypeName; |
| | 75 | 1931 | | var shortTypeName = GeneratorHelpers.GetShortTypeName(typeName); |
| | | 1932 | | |
| | | 1933 | | // Register DataAnnotations validator if present |
| | 75 | 1934 | | if (opt.HasDataAnnotations) |
| | | 1935 | | { |
| | 17 | 1936 | | var dataAnnotationsValidatorClassName = $"global::{safeAssemblyName}.Generated.{shortTypeName}DataAnnotation |
| | 17 | 1937 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOptions<{ty |
| | | 1938 | | } |
| | | 1939 | | |
| | 75 | 1940 | | if (opt.ValidateOnStart && opt.HasValidatorMethod) |
| | | 1941 | | { |
| | 4 | 1942 | | var validatorClassName = $"global::{safeAssemblyName}.Generated.{shortTypeName}Validator"; |
| | 4 | 1943 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOptions<{ty |
| | | 1944 | | |
| | 4 | 1945 | | if (opt.HasExternalValidator && opt.ValidatorMethod != null && !opt.ValidatorMethod.Value.IsStatic) |
| | | 1946 | | { |
| | 1 | 1947 | | externalValidatorsToRegister.Add(opt.ValidatorTypeName!); |
| | | 1948 | | } |
| | | 1949 | | } |
| | 75 | 1950 | | } |
| | | 1951 | | |
| | | 1952 | | private static void RegisterValidatorForFactory(StringBuilder builder, DiscoveredOptions opt, string safeAssemblyNam |
| | | 1953 | | { |
| | 1 | 1954 | | var typeName = opt.TypeName; |
| | 1 | 1955 | | var shortTypeName = GeneratorHelpers.GetShortTypeName(typeName); |
| | | 1956 | | |
| | | 1957 | | // For factory pattern, we need to add OptionsBuilder validation manually |
| | | 1958 | | // Since we're using AddSingleton<IOptions<T>>, we also need to register for IOptionsSnapshot and IOptionsMonito |
| | 1 | 1959 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IOptionsSnapshot<{typeNa |
| | | 1960 | | |
| | | 1961 | | // Add startup validation |
| | 1 | 1962 | | builder.AppendLine($" services.AddOptions<{typeName}>().ValidateOnStart();"); |
| | | 1963 | | |
| | | 1964 | | // Register DataAnnotations validator if present |
| | 1 | 1965 | | if (opt.HasDataAnnotations) |
| | | 1966 | | { |
| | 0 | 1967 | | var dataAnnotationsValidatorClassName = $"global::{safeAssemblyName}.Generated.{shortTypeName}DataAnnotation |
| | 0 | 1968 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOptions<{ty |
| | | 1969 | | } |
| | | 1970 | | |
| | 1 | 1971 | | if (opt.HasValidatorMethod) |
| | | 1972 | | { |
| | 1 | 1973 | | var validatorClassName = $"global::{safeAssemblyName}.Generated.{shortTypeName}Validator"; |
| | 1 | 1974 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Options.IValidateOptions<{ty |
| | | 1975 | | |
| | 1 | 1976 | | if (opt.HasExternalValidator && opt.ValidatorMethod != null && !opt.ValidatorMethod.Value.IsStatic) |
| | | 1977 | | { |
| | 0 | 1978 | | externalValidatorsToRegister.Add(opt.ValidatorTypeName!); |
| | | 1979 | | } |
| | | 1980 | | } |
| | 1 | 1981 | | } |
| | | 1982 | | |
| | | 1983 | | private static void GeneratePropertyBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string targe |
| | | 1984 | | { |
| | | 1985 | | // Handle complex types first |
| | 120 | 1986 | | if (prop.ComplexTypeKind != ComplexTypeKind.None) |
| | | 1987 | | { |
| | 18 | 1988 | | GenerateComplexTypeBinding(builder, prop, index, targetPath); |
| | 18 | 1989 | | return; |
| | | 1990 | | } |
| | | 1991 | | |
| | 102 | 1992 | | var varName = $"v{index}"; |
| | | 1993 | | |
| | | 1994 | | // Determine how to parse the value based on type |
| | 102 | 1995 | | var typeName = prop.TypeName; |
| | 102 | 1996 | | var baseTypeName = GetBaseTypeName(typeName); |
| | | 1997 | | |
| | 102 | 1998 | | builder.Append($" if (section[\"{prop.Name}\"] is {{ }} {varName}"); |
| | | 1999 | | |
| | | 2000 | | // Check if it's an enum first |
| | 102 | 2001 | | if (prop.IsEnum && prop.EnumTypeName != null) |
| | | 2002 | | { |
| | 12 | 2003 | | builder.AppendLine($" && global::System.Enum.TryParse<{prop.EnumTypeName}>({varName}, true, out var p{index} |
| | | 2004 | | } |
| | 90 | 2005 | | else if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 2006 | | { |
| | | 2007 | | // String: direct assignment |
| | 50 | 2008 | | builder.AppendLine($") {targetPath}.{prop.Name} = {varName};"); |
| | | 2009 | | } |
| | 40 | 2010 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 2011 | | { |
| | 23 | 2012 | | builder.AppendLine($" && int.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};"); |
| | | 2013 | | } |
| | 17 | 2014 | | else if (baseTypeName == "bool" || baseTypeName == "global::System.Boolean") |
| | | 2015 | | { |
| | 5 | 2016 | | builder.AppendLine($" && bool.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};"); |
| | | 2017 | | } |
| | 12 | 2018 | | else if (baseTypeName == "double" || baseTypeName == "global::System.Double") |
| | | 2019 | | { |
| | 5 | 2020 | | builder.AppendLine($" && double.TryParse({varName}, System.Globalization.NumberStyles.Any, System.Globalizat |
| | | 2021 | | } |
| | 7 | 2022 | | else if (baseTypeName == "float" || baseTypeName == "global::System.Single") |
| | | 2023 | | { |
| | 0 | 2024 | | builder.AppendLine($" && float.TryParse({varName}, System.Globalization.NumberStyles.Any, System.Globalizati |
| | | 2025 | | } |
| | 7 | 2026 | | else if (baseTypeName == "decimal" || baseTypeName == "global::System.Decimal") |
| | | 2027 | | { |
| | 0 | 2028 | | builder.AppendLine($" && decimal.TryParse({varName}, System.Globalization.NumberStyles.Any, System.Globaliza |
| | | 2029 | | } |
| | 7 | 2030 | | else if (baseTypeName == "long" || baseTypeName == "global::System.Int64") |
| | | 2031 | | { |
| | 0 | 2032 | | builder.AppendLine($" && long.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};"); |
| | | 2033 | | } |
| | 7 | 2034 | | else if (baseTypeName == "short" || baseTypeName == "global::System.Int16") |
| | | 2035 | | { |
| | 0 | 2036 | | builder.AppendLine($" && short.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};") |
| | | 2037 | | } |
| | 7 | 2038 | | else if (baseTypeName == "byte" || baseTypeName == "global::System.Byte") |
| | | 2039 | | { |
| | 0 | 2040 | | builder.AppendLine($" && byte.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};"); |
| | | 2041 | | } |
| | 7 | 2042 | | else if (baseTypeName == "char" || baseTypeName == "global::System.Char") |
| | | 2043 | | { |
| | 0 | 2044 | | builder.AppendLine($" && {varName}.Length == 1) {targetPath}.{prop.Name} = {varName}[0];"); |
| | | 2045 | | } |
| | 7 | 2046 | | else if (baseTypeName == "global::System.TimeSpan") |
| | | 2047 | | { |
| | 0 | 2048 | | builder.AppendLine($" && global::System.TimeSpan.TryParse({varName}, out var p{index})) {targetPath}.{prop.N |
| | | 2049 | | } |
| | 7 | 2050 | | else if (baseTypeName == "global::System.DateTime") |
| | | 2051 | | { |
| | 0 | 2052 | | builder.AppendLine($" && global::System.DateTime.TryParse({varName}, out var p{index})) {targetPath}.{prop.N |
| | | 2053 | | } |
| | 7 | 2054 | | else if (baseTypeName == "global::System.DateTimeOffset") |
| | | 2055 | | { |
| | 0 | 2056 | | builder.AppendLine($" && global::System.DateTimeOffset.TryParse({varName}, out var p{index})) {targetPath}.{ |
| | | 2057 | | } |
| | 7 | 2058 | | else if (baseTypeName == "global::System.Guid") |
| | | 2059 | | { |
| | 0 | 2060 | | builder.AppendLine($" && global::System.Guid.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} |
| | | 2061 | | } |
| | 7 | 2062 | | else if (baseTypeName == "global::System.Uri") |
| | | 2063 | | { |
| | 0 | 2064 | | builder.AppendLine($" && global::System.Uri.TryCreate({varName}, global::System.UriKind.RelativeOrAbsolute, |
| | | 2065 | | } |
| | | 2066 | | else |
| | | 2067 | | { |
| | | 2068 | | // Unsupported type - skip silently (matching ConfigurationBinder behavior) |
| | 7 | 2069 | | builder.AppendLine($") {{ }} // Skipped: {typeName} (not a supported primitive)"); |
| | | 2070 | | } |
| | 7 | 2071 | | } |
| | | 2072 | | |
| | | 2073 | | private static void GenerateComplexTypeBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string ta |
| | | 2074 | | { |
| | 18 | 2075 | | var sectionVar = $"sec{index}"; |
| | | 2076 | | |
| | 18 | 2077 | | switch (prop.ComplexTypeKind) |
| | | 2078 | | { |
| | | 2079 | | case ComplexTypeKind.NestedObject: |
| | 6 | 2080 | | GenerateNestedObjectBinding(builder, prop, index, targetPath, sectionVar); |
| | 6 | 2081 | | break; |
| | | 2082 | | |
| | | 2083 | | case ComplexTypeKind.Array: |
| | 3 | 2084 | | GenerateArrayBinding(builder, prop, index, targetPath, sectionVar); |
| | 3 | 2085 | | break; |
| | | 2086 | | |
| | | 2087 | | case ComplexTypeKind.List: |
| | 5 | 2088 | | GenerateListBinding(builder, prop, index, targetPath, sectionVar); |
| | 5 | 2089 | | break; |
| | | 2090 | | |
| | | 2091 | | case ComplexTypeKind.Dictionary: |
| | 4 | 2092 | | GenerateDictionaryBinding(builder, prop, index, targetPath, sectionVar); |
| | | 2093 | | break; |
| | | 2094 | | } |
| | 4 | 2095 | | } |
| | | 2096 | | |
| | | 2097 | | private static void GenerateNestedObjectBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string t |
| | | 2098 | | { |
| | 6 | 2099 | | var nestedPath = $"{targetPath}.{prop.Name}"; |
| | | 2100 | | |
| | 6 | 2101 | | builder.AppendLine($" // Bind nested object: {prop.Name}"); |
| | 6 | 2102 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | | 2103 | | |
| | | 2104 | | // Initialize if null (for nullable properties) |
| | 6 | 2105 | | if (prop.IsNullable) |
| | | 2106 | | { |
| | 1 | 2107 | | builder.AppendLine($" {nestedPath} ??= new {GetNonNullableTypeName(prop.TypeName)}();"); |
| | | 2108 | | } |
| | | 2109 | | |
| | | 2110 | | // Generate bindings for nested properties |
| | 6 | 2111 | | if (prop.NestedProperties != null) |
| | | 2112 | | { |
| | 6 | 2113 | | var nestedIndex = index * 100; // Use offset to avoid variable name collisions |
| | 30 | 2114 | | foreach (var nestedProp in prop.NestedProperties) |
| | | 2115 | | { |
| | | 2116 | | // Temporarily swap section context for nested binding |
| | 9 | 2117 | | GenerateNestedPropertyBinding(builder, nestedProp, nestedIndex, nestedPath, sectionVar); |
| | 9 | 2118 | | nestedIndex++; |
| | | 2119 | | } |
| | | 2120 | | } |
| | 6 | 2121 | | } |
| | | 2122 | | |
| | | 2123 | | private static void GenerateNestedPropertyBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string |
| | | 2124 | | { |
| | | 2125 | | // Handle complex types recursively |
| | 10 | 2126 | | if (prop.ComplexTypeKind != ComplexTypeKind.None) |
| | | 2127 | | { |
| | 2 | 2128 | | var innerSectionVar = $"sec{index}"; |
| | 2 | 2129 | | switch (prop.ComplexTypeKind) |
| | | 2130 | | { |
| | | 2131 | | case ComplexTypeKind.NestedObject: |
| | 1 | 2132 | | builder.AppendLine($" // Bind nested object: {prop.Name}"); |
| | 1 | 2133 | | builder.AppendLine($" var {innerSectionVar} = {sectionVarName}.GetSection(\"{prop.Nam |
| | 1 | 2134 | | if (prop.IsNullable) |
| | | 2135 | | { |
| | 0 | 2136 | | builder.AppendLine($" {targetPath}.{prop.Name} ??= new {GetNonNullableTypeName(pr |
| | | 2137 | | } |
| | 1 | 2138 | | if (prop.NestedProperties != null) |
| | | 2139 | | { |
| | 1 | 2140 | | var nestedIndex = index * 100; |
| | 4 | 2141 | | foreach (var nestedProp in prop.NestedProperties) |
| | | 2142 | | { |
| | 1 | 2143 | | GenerateNestedPropertyBinding(builder, nestedProp, nestedIndex, $"{targetPath}.{prop.Name}", |
| | 1 | 2144 | | nestedIndex++; |
| | | 2145 | | } |
| | | 2146 | | } |
| | | 2147 | | break; |
| | | 2148 | | |
| | | 2149 | | case ComplexTypeKind.Array: |
| | | 2150 | | case ComplexTypeKind.List: |
| | | 2151 | | case ComplexTypeKind.Dictionary: |
| | | 2152 | | // For collections inside nested objects, generate appropriate binding |
| | 1 | 2153 | | GenerateCollectionBindingInNested(builder, prop, index, targetPath, sectionVarName); |
| | | 2154 | | break; |
| | | 2155 | | } |
| | 2 | 2156 | | return; |
| | | 2157 | | } |
| | | 2158 | | |
| | | 2159 | | // Generate primitive binding using the nested section |
| | 8 | 2160 | | var varName = $"v{index}"; |
| | 8 | 2161 | | var baseTypeName = GetBaseTypeName(prop.TypeName); |
| | | 2162 | | |
| | 8 | 2163 | | builder.Append($" if ({sectionVarName}[\"{prop.Name}\"] is {{ }} {varName}"); |
| | | 2164 | | |
| | 8 | 2165 | | if (prop.IsEnum && prop.EnumTypeName != null) |
| | | 2166 | | { |
| | 0 | 2167 | | builder.AppendLine($" && global::System.Enum.TryParse<{prop.EnumTypeName}>({varName}, true, out var p{index} |
| | | 2168 | | } |
| | 8 | 2169 | | else if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 2170 | | { |
| | 6 | 2171 | | builder.AppendLine($") {targetPath}.{prop.Name} = {varName};"); |
| | | 2172 | | } |
| | 2 | 2173 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 2174 | | { |
| | 2 | 2175 | | builder.AppendLine($" && int.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};"); |
| | | 2176 | | } |
| | 0 | 2177 | | else if (baseTypeName == "bool" || baseTypeName == "global::System.Boolean") |
| | | 2178 | | { |
| | 0 | 2179 | | builder.AppendLine($" && bool.TryParse({varName}, out var p{index})) {targetPath}.{prop.Name} = p{index};"); |
| | | 2180 | | } |
| | | 2181 | | else |
| | | 2182 | | { |
| | | 2183 | | // For other types, generate appropriate TryParse |
| | 0 | 2184 | | builder.AppendLine($") {{ }} // Skipped: {prop.TypeName}"); |
| | | 2185 | | } |
| | 0 | 2186 | | } |
| | | 2187 | | |
| | | 2188 | | private static void GenerateCollectionBindingInNested(StringBuilder builder, OptionsPropertyInfo prop, int index, st |
| | | 2189 | | { |
| | 1 | 2190 | | var collectionSection = $"colSec{index}"; |
| | 1 | 2191 | | builder.AppendLine($" var {collectionSection} = {sectionVarName}.GetSection(\"{prop.Name}\");"); |
| | | 2192 | | |
| | 1 | 2193 | | switch (prop.ComplexTypeKind) |
| | | 2194 | | { |
| | | 2195 | | case ComplexTypeKind.List: |
| | 1 | 2196 | | GenerateListBindingCore(builder, prop, index, $"{targetPath}.{prop.Name}", collectionSection); |
| | 1 | 2197 | | break; |
| | | 2198 | | case ComplexTypeKind.Array: |
| | 0 | 2199 | | GenerateArrayBindingCore(builder, prop, index, $"{targetPath}.{prop.Name}", collectionSection); |
| | 0 | 2200 | | break; |
| | | 2201 | | case ComplexTypeKind.Dictionary: |
| | 0 | 2202 | | GenerateDictionaryBindingCore(builder, prop, index, $"{targetPath}.{prop.Name}", collectionSection); |
| | | 2203 | | break; |
| | | 2204 | | } |
| | 0 | 2205 | | } |
| | | 2206 | | |
| | | 2207 | | private static void GenerateArrayBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string targetPa |
| | | 2208 | | { |
| | 3 | 2209 | | builder.AppendLine($" // Bind array: {prop.Name}"); |
| | 3 | 2210 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | 3 | 2211 | | GenerateArrayBindingCore(builder, prop, index, $"{targetPath}.{prop.Name}", sectionVar); |
| | 3 | 2212 | | } |
| | | 2213 | | |
| | | 2214 | | private static void GenerateArrayBindingCore(StringBuilder builder, OptionsPropertyInfo prop, int index, string targ |
| | | 2215 | | { |
| | 3 | 2216 | | var itemsVar = $"items{index}"; |
| | 3 | 2217 | | var elementType = prop.ElementTypeName ?? "string"; |
| | 3 | 2218 | | var hasNestedProps = prop.NestedProperties != null && prop.NestedProperties.Count > 0; |
| | | 2219 | | |
| | 3 | 2220 | | builder.AppendLine($" var {itemsVar} = new global::System.Collections.Generic.List<{elementType}> |
| | 3 | 2221 | | builder.AppendLine($" foreach (var child in {sectionVar}.GetChildren())"); |
| | 3 | 2222 | | builder.AppendLine(" {"); |
| | | 2223 | | |
| | 3 | 2224 | | if (hasNestedProps) |
| | | 2225 | | { |
| | | 2226 | | // Complex element type |
| | 1 | 2227 | | var itemVar = $"item{index}"; |
| | 1 | 2228 | | builder.AppendLine($" var {itemVar} = new {elementType}();"); |
| | 1 | 2229 | | var nestedIndex = index * 100; |
| | 6 | 2230 | | foreach (var nestedProp in prop.NestedProperties!) |
| | | 2231 | | { |
| | 2 | 2232 | | GenerateChildPropertyBinding(builder, nestedProp, nestedIndex, itemVar, "child"); |
| | 2 | 2233 | | nestedIndex++; |
| | | 2234 | | } |
| | 1 | 2235 | | builder.AppendLine($" {itemsVar}.Add({itemVar});"); |
| | | 2236 | | } |
| | | 2237 | | else |
| | | 2238 | | { |
| | | 2239 | | // Primitive element type |
| | 2 | 2240 | | GeneratePrimitiveCollectionAdd(builder, elementType, index, itemsVar); |
| | | 2241 | | } |
| | | 2242 | | |
| | 3 | 2243 | | builder.AppendLine(" }"); |
| | 3 | 2244 | | builder.AppendLine($" {targetPath} = {itemsVar}.ToArray();"); |
| | 3 | 2245 | | } |
| | | 2246 | | |
| | | 2247 | | private static void GenerateListBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string targetPat |
| | | 2248 | | { |
| | 5 | 2249 | | builder.AppendLine($" // Bind list: {prop.Name}"); |
| | 5 | 2250 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | 5 | 2251 | | GenerateListBindingCore(builder, prop, index, $"{targetPath}.{prop.Name}", sectionVar); |
| | 5 | 2252 | | } |
| | | 2253 | | |
| | | 2254 | | private static void GenerateListBindingCore(StringBuilder builder, OptionsPropertyInfo prop, int index, string targe |
| | | 2255 | | { |
| | 6 | 2256 | | var elementType = prop.ElementTypeName ?? "string"; |
| | 6 | 2257 | | var hasNestedProps = prop.NestedProperties != null && prop.NestedProperties.Count > 0; |
| | | 2258 | | |
| | 6 | 2259 | | builder.AppendLine($" {targetPath}.Clear();"); |
| | 6 | 2260 | | builder.AppendLine($" foreach (var child in {sectionVar}.GetChildren())"); |
| | 6 | 2261 | | builder.AppendLine(" {"); |
| | | 2262 | | |
| | 6 | 2263 | | if (hasNestedProps) |
| | | 2264 | | { |
| | | 2265 | | // Complex element type |
| | 1 | 2266 | | var itemVar = $"item{index}"; |
| | 1 | 2267 | | builder.AppendLine($" var {itemVar} = new {elementType}();"); |
| | 1 | 2268 | | var nestedIndex = index * 100; |
| | 6 | 2269 | | foreach (var nestedProp in prop.NestedProperties!) |
| | | 2270 | | { |
| | 2 | 2271 | | GenerateChildPropertyBinding(builder, nestedProp, nestedIndex, itemVar, "child"); |
| | 2 | 2272 | | nestedIndex++; |
| | | 2273 | | } |
| | 1 | 2274 | | builder.AppendLine($" {targetPath}.Add({itemVar});"); |
| | | 2275 | | } |
| | | 2276 | | else |
| | | 2277 | | { |
| | | 2278 | | // Primitive element type |
| | 5 | 2279 | | GeneratePrimitiveListAdd(builder, elementType, index, targetPath); |
| | | 2280 | | } |
| | | 2281 | | |
| | 6 | 2282 | | builder.AppendLine(" }"); |
| | 6 | 2283 | | } |
| | | 2284 | | |
| | | 2285 | | private static void GenerateDictionaryBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string tar |
| | | 2286 | | { |
| | 4 | 2287 | | builder.AppendLine($" // Bind dictionary: {prop.Name}"); |
| | 4 | 2288 | | builder.AppendLine($" var {sectionVar} = section.GetSection(\"{prop.Name}\");"); |
| | 4 | 2289 | | GenerateDictionaryBindingCore(builder, prop, index, $"{targetPath}.{prop.Name}", sectionVar); |
| | 4 | 2290 | | } |
| | | 2291 | | |
| | | 2292 | | private static void GenerateDictionaryBindingCore(StringBuilder builder, OptionsPropertyInfo prop, int index, string |
| | | 2293 | | { |
| | 4 | 2294 | | var elementType = prop.ElementTypeName ?? "string"; |
| | 4 | 2295 | | var hasNestedProps = prop.NestedProperties != null && prop.NestedProperties.Count > 0; |
| | | 2296 | | |
| | 4 | 2297 | | builder.AppendLine($" {targetPath}.Clear();"); |
| | 4 | 2298 | | builder.AppendLine($" foreach (var child in {sectionVar}.GetChildren())"); |
| | 4 | 2299 | | builder.AppendLine(" {"); |
| | | 2300 | | |
| | 4 | 2301 | | if (hasNestedProps) |
| | | 2302 | | { |
| | | 2303 | | // Complex value type |
| | 1 | 2304 | | var itemVar = $"item{index}"; |
| | 1 | 2305 | | builder.AppendLine($" var {itemVar} = new {elementType}();"); |
| | 1 | 2306 | | var nestedIndex = index * 100; |
| | 6 | 2307 | | foreach (var nestedProp in prop.NestedProperties!) |
| | | 2308 | | { |
| | 2 | 2309 | | GenerateChildPropertyBinding(builder, nestedProp, nestedIndex, itemVar, "child"); |
| | 2 | 2310 | | nestedIndex++; |
| | | 2311 | | } |
| | 1 | 2312 | | builder.AppendLine($" {targetPath}[child.Key] = {itemVar};"); |
| | | 2313 | | } |
| | | 2314 | | else |
| | | 2315 | | { |
| | | 2316 | | // Primitive value type |
| | 3 | 2317 | | GeneratePrimitiveDictionaryAdd(builder, elementType, index, targetPath); |
| | | 2318 | | } |
| | | 2319 | | |
| | 4 | 2320 | | builder.AppendLine(" }"); |
| | 4 | 2321 | | } |
| | | 2322 | | |
| | | 2323 | | private static void GenerateChildPropertyBinding(StringBuilder builder, OptionsPropertyInfo prop, int index, string |
| | | 2324 | | { |
| | 6 | 2325 | | var varName = $"cv{index}"; |
| | 6 | 2326 | | var baseTypeName = GetBaseTypeName(prop.TypeName); |
| | | 2327 | | |
| | 6 | 2328 | | builder.Append($" if ({sectionVar}[\"{prop.Name}\"] is {{ }} {varName}"); |
| | | 2329 | | |
| | 6 | 2330 | | if (prop.IsEnum && prop.EnumTypeName != null) |
| | | 2331 | | { |
| | 0 | 2332 | | builder.AppendLine($" && global::System.Enum.TryParse<{prop.EnumTypeName}>({varName}, true, out var cp{index |
| | | 2333 | | } |
| | 6 | 2334 | | else if (baseTypeName == "string" || baseTypeName == "global::System.String") |
| | | 2335 | | { |
| | 3 | 2336 | | builder.AppendLine($") {targetVar}.{prop.Name} = {varName};"); |
| | | 2337 | | } |
| | 3 | 2338 | | else if (baseTypeName == "int" || baseTypeName == "global::System.Int32") |
| | | 2339 | | { |
| | 3 | 2340 | | builder.AppendLine($" && int.TryParse({varName}, out var cp{index})) {targetVar}.{prop.Name} = cp{index};"); |
| | | 2341 | | } |
| | 0 | 2342 | | else if (baseTypeName == "bool" || baseTypeName == "global::System.Boolean") |
| | | 2343 | | { |
| | 0 | 2344 | | builder.AppendLine($" && bool.TryParse({varName}, out var cp{index})) {targetVar}.{prop.Name} = cp{index};") |
| | | 2345 | | } |
| | | 2346 | | else |
| | | 2347 | | { |
| | 0 | 2348 | | builder.AppendLine($") {{ }} // Skipped: {prop.TypeName}"); |
| | | 2349 | | } |
| | 0 | 2350 | | } |
| | | 2351 | | |
| | | 2352 | | private static void GeneratePrimitiveCollectionAdd(StringBuilder builder, string elementType, int index, string list |
| | | 2353 | | { |
| | 2 | 2354 | | var baseType = GetBaseTypeName(elementType); |
| | | 2355 | | |
| | 2 | 2356 | | if (baseType == "string" || baseType == "global::System.String") |
| | | 2357 | | { |
| | 1 | 2358 | | builder.AppendLine($" if (child.Value is {{ }} val{index}) {listVar}.Add(val{index});"); |
| | | 2359 | | } |
| | 1 | 2360 | | else if (baseType == "int" || baseType == "global::System.Int32") |
| | | 2361 | | { |
| | 1 | 2362 | | builder.AppendLine($" if (child.Value is {{ }} val{index} && int.TryParse(val{index}, out |
| | | 2363 | | } |
| | | 2364 | | else |
| | | 2365 | | { |
| | 0 | 2366 | | builder.AppendLine($" // Skipped: unsupported element type {elementType}"); |
| | | 2367 | | } |
| | 0 | 2368 | | } |
| | | 2369 | | |
| | | 2370 | | private static void GeneratePrimitiveListAdd(StringBuilder builder, string elementType, int index, string targetPath |
| | | 2371 | | { |
| | 5 | 2372 | | var baseType = GetBaseTypeName(elementType); |
| | | 2373 | | |
| | 5 | 2374 | | if (baseType == "string" || baseType == "global::System.String") |
| | | 2375 | | { |
| | 4 | 2376 | | builder.AppendLine($" if (child.Value is {{ }} val{index}) {targetPath}.Add(val{index});" |
| | | 2377 | | } |
| | 1 | 2378 | | else if (baseType == "int" || baseType == "global::System.Int32") |
| | | 2379 | | { |
| | 1 | 2380 | | builder.AppendLine($" if (child.Value is {{ }} val{index} && int.TryParse(val{index}, out |
| | | 2381 | | } |
| | | 2382 | | else |
| | | 2383 | | { |
| | 0 | 2384 | | builder.AppendLine($" // Skipped: unsupported element type {elementType}"); |
| | | 2385 | | } |
| | 0 | 2386 | | } |
| | | 2387 | | |
| | | 2388 | | private static void GeneratePrimitiveDictionaryAdd(StringBuilder builder, string elementType, int index, string targ |
| | | 2389 | | { |
| | 3 | 2390 | | var baseType = GetBaseTypeName(elementType); |
| | | 2391 | | |
| | 3 | 2392 | | if (baseType == "string" || baseType == "global::System.String") |
| | | 2393 | | { |
| | 1 | 2394 | | builder.AppendLine($" if (child.Value is {{ }} val{index}) {targetPath}[child.Key] = val{ |
| | | 2395 | | } |
| | 2 | 2396 | | else if (baseType == "int" || baseType == "global::System.Int32") |
| | | 2397 | | { |
| | 2 | 2398 | | builder.AppendLine($" if (child.Value is {{ }} val{index} && int.TryParse(val{index}, out |
| | | 2399 | | } |
| | | 2400 | | else |
| | | 2401 | | { |
| | 0 | 2402 | | builder.AppendLine($" // Skipped: unsupported element type {elementType}"); |
| | | 2403 | | } |
| | 0 | 2404 | | } |
| | | 2405 | | |
| | | 2406 | | private static string GetNonNullableTypeName(string typeName) |
| | | 2407 | | { |
| | 2 | 2408 | | if (typeName.EndsWith("?")) |
| | 0 | 2409 | | return typeName.Substring(0, typeName.Length - 1); |
| | 2 | 2410 | | return typeName; |
| | | 2411 | | } |
| | | 2412 | | |
| | | 2413 | | private static string GetBaseTypeName(string typeName) |
| | | 2414 | | { |
| | | 2415 | | // Handle nullable types like "global::System.Nullable<int>" or "int?" |
| | 156 | 2416 | | if (typeName.StartsWith("global::System.Nullable<") && typeName.EndsWith(">")) |
| | | 2417 | | { |
| | 0 | 2418 | | return typeName.Substring("global::System.Nullable<".Length, typeName.Length - "global::System.Nullable<".Le |
| | | 2419 | | } |
| | 156 | 2420 | | if (typeName.EndsWith("?")) |
| | | 2421 | | { |
| | 4 | 2422 | | return typeName.Substring(0, typeName.Length - 1); |
| | | 2423 | | } |
| | 152 | 2424 | | return typeName; |
| | | 2425 | | } |
| | | 2426 | | |
| | | 2427 | | private static void GenerateApplyDecoratorsMethod(StringBuilder builder, IReadOnlyList<DiscoveredDecorator> decorato |
| | | 2428 | | { |
| | 434 | 2429 | | builder.AppendLine(" /// <summary>"); |
| | 434 | 2430 | | builder.AppendLine(" /// Applies all discovered decorators, interceptors, and hosted services to the service |
| | 434 | 2431 | | builder.AppendLine(" /// Decorators are applied in order, with lower Order values applied first (closer to th |
| | 434 | 2432 | | builder.AppendLine(" /// </summary>"); |
| | 434 | 2433 | | builder.AppendLine(" /// <param name=\"services\">The service collection to apply decorators to.</param>"); |
| | 434 | 2434 | | builder.AppendLine(" public static void ApplyDecorators(IServiceCollection services)"); |
| | 434 | 2435 | | builder.AppendLine(" {"); |
| | | 2436 | | |
| | | 2437 | | // Register ServiceCatalog first |
| | 434 | 2438 | | breadcrumbs.WriteInlineComment(builder, " ", "Register service catalog for DI resolution"); |
| | 434 | 2439 | | builder.AppendLine($" services.AddSingleton<global::NexusLabs.Needlr.Catalog.IServiceCatalog, global::{sa |
| | 434 | 2440 | | builder.AppendLine(); |
| | | 2441 | | |
| | | 2442 | | // Register hosted services first (before decorators apply) |
| | 434 | 2443 | | if (hasHostedServices) |
| | | 2444 | | { |
| | 6 | 2445 | | breadcrumbs.WriteInlineComment(builder, " ", "Register hosted services"); |
| | 6 | 2446 | | builder.AppendLine(" RegisterHostedServices(services);"); |
| | 6 | 2447 | | if (decorators.Count > 0 || hasInterceptors) |
| | | 2448 | | { |
| | 0 | 2449 | | builder.AppendLine(); |
| | | 2450 | | } |
| | | 2451 | | } |
| | | 2452 | | |
| | 434 | 2453 | | if (decorators.Count == 0 && !hasInterceptors) |
| | | 2454 | | { |
| | 401 | 2455 | | if (!hasHostedServices) |
| | | 2456 | | { |
| | 395 | 2457 | | breadcrumbs.WriteInlineComment(builder, " ", "No decorators, interceptors, or hosted services dis |
| | | 2458 | | } |
| | | 2459 | | } |
| | | 2460 | | else |
| | | 2461 | | { |
| | 33 | 2462 | | if (decorators.Count > 0) |
| | | 2463 | | { |
| | | 2464 | | // Group decorators by service type and order by Order property |
| | 18 | 2465 | | var decoratorsByService = decorators |
| | 27 | 2466 | | .GroupBy(d => d.ServiceTypeName) |
| | 39 | 2467 | | .OrderBy(g => g.Key); |
| | | 2468 | | |
| | 78 | 2469 | | foreach (var serviceGroup in decoratorsByService) |
| | | 2470 | | { |
| | | 2471 | | // Write verbose breadcrumb for decorator chain |
| | 21 | 2472 | | if (breadcrumbs.Level == BreadcrumbLevel.Verbose) |
| | | 2473 | | { |
| | 0 | 2474 | | var chainItems = serviceGroup.OrderBy(d => d.Order).ToList(); |
| | 0 | 2475 | | var lines = new List<string> |
| | 0 | 2476 | | { |
| | 0 | 2477 | | "Resolution order (outer → inner → target):" |
| | 0 | 2478 | | }; |
| | 0 | 2479 | | for (int i = 0; i < chainItems.Count; i++) |
| | | 2480 | | { |
| | 0 | 2481 | | var dec = chainItems[i]; |
| | 0 | 2482 | | var sourcePath = dec.SourceFilePath != null |
| | 0 | 2483 | | ? BreadcrumbWriter.GetRelativeSourcePath(dec.SourceFilePath, projectDirectory) |
| | 0 | 2484 | | : $"[{dec.AssemblyName}]"; |
| | 0 | 2485 | | lines.Add($" {i + 1}. {dec.DecoratorTypeName.Split('.').Last()} (Order={dec.Order}) ← {sour |
| | | 2486 | | } |
| | 0 | 2487 | | lines.Add($"Triggered by: [DecoratorFor<{serviceGroup.Key.Split('.').Last()}>] attributes"); |
| | | 2488 | | |
| | 0 | 2489 | | breadcrumbs.WriteVerboseBox(builder, " ", |
| | 0 | 2490 | | $"Decorator Chain: {serviceGroup.Key.Split('.').Last()}", |
| | 0 | 2491 | | lines.ToArray()); |
| | | 2492 | | } |
| | | 2493 | | else |
| | | 2494 | | { |
| | 21 | 2495 | | breadcrumbs.WriteInlineComment(builder, " ", $"Decorators for {serviceGroup.Key}"); |
| | | 2496 | | } |
| | | 2497 | | |
| | 123 | 2498 | | foreach (var decorator in serviceGroup.OrderBy(d => d.Order)) |
| | | 2499 | | { |
| | 27 | 2500 | | builder.AppendLine($" services.AddDecorator<{decorator.ServiceTypeName}, {decorator.Decor |
| | | 2501 | | } |
| | | 2502 | | } |
| | | 2503 | | } |
| | | 2504 | | |
| | 33 | 2505 | | if (hasInterceptors) |
| | | 2506 | | { |
| | 15 | 2507 | | builder.AppendLine(); |
| | 15 | 2508 | | breadcrumbs.WriteInlineComment(builder, " ", "Register intercepted services with their proxies"); |
| | 15 | 2509 | | builder.AppendLine($" global::{safeAssemblyName}.Generated.InterceptorRegistrations.RegisterInter |
| | | 2510 | | } |
| | | 2511 | | } |
| | | 2512 | | |
| | 434 | 2513 | | builder.AppendLine(" }"); |
| | 434 | 2514 | | } |
| | | 2515 | | |
| | | 2516 | | private static void GenerateRegisterProvidersMethod(StringBuilder builder, IReadOnlyList<DiscoveredProvider> provide |
| | | 2517 | | { |
| | 17 | 2518 | | builder.AppendLine(" /// <summary>"); |
| | 17 | 2519 | | builder.AppendLine(" /// Registers all generated providers as Singletons."); |
| | 17 | 2520 | | builder.AppendLine(" /// Providers are strongly-typed service locators that expose services via typed propert |
| | 17 | 2521 | | builder.AppendLine(" /// </summary>"); |
| | 17 | 2522 | | builder.AppendLine(" /// <param name=\"services\">The service collection to register to.</param>"); |
| | 17 | 2523 | | builder.AppendLine(" public static void RegisterProviders(IServiceCollection services)"); |
| | 17 | 2524 | | builder.AppendLine(" {"); |
| | | 2525 | | |
| | 70 | 2526 | | foreach (var provider in providers) |
| | | 2527 | | { |
| | 18 | 2528 | | var shortName = provider.SimpleTypeName; |
| | 18 | 2529 | | var sourcePath = provider.SourceFilePath != null |
| | 18 | 2530 | | ? BreadcrumbWriter.GetRelativeSourcePath(provider.SourceFilePath, projectDirectory) |
| | 18 | 2531 | | : $"[{provider.AssemblyName}]"; |
| | | 2532 | | |
| | 18 | 2533 | | breadcrumbs.WriteInlineComment(builder, " ", $"Provider: {shortName} ← {sourcePath}"); |
| | | 2534 | | |
| | 18 | 2535 | | if (provider.IsInterface) |
| | | 2536 | | { |
| | | 2537 | | // Interface mode: register the generated implementation |
| | 12 | 2538 | | var implName = provider.ImplementationTypeName; |
| | 12 | 2539 | | builder.AppendLine($" services.AddSingleton<{provider.TypeName}, global::{safeAssemblyName}.Gener |
| | | 2540 | | } |
| | 6 | 2541 | | else if (provider.IsPartial) |
| | | 2542 | | { |
| | | 2543 | | // Shorthand class mode: register the partial class as its generated interface |
| | 6 | 2544 | | var interfaceName = provider.InterfaceTypeName; |
| | 6 | 2545 | | var providerNamespace = GetNamespaceFromTypeName(provider.TypeName); |
| | 6 | 2546 | | builder.AppendLine($" services.AddSingleton<global::{providerNamespace}.{interfaceName}, {provide |
| | | 2547 | | } |
| | | 2548 | | } |
| | | 2549 | | |
| | 17 | 2550 | | builder.AppendLine(" }"); |
| | 17 | 2551 | | } |
| | | 2552 | | |
| | | 2553 | | private static void GenerateRegisterHostedServicesMethod(StringBuilder builder, IReadOnlyList<DiscoveredHostedServic |
| | | 2554 | | { |
| | 6 | 2555 | | builder.AppendLine(" /// <summary>"); |
| | 6 | 2556 | | builder.AppendLine(" /// Registers all discovered hosted services (BackgroundService and IHostedService imple |
| | 6 | 2557 | | builder.AppendLine(" /// Each service is registered as singleton and also as IHostedService for the host to d |
| | 6 | 2558 | | builder.AppendLine(" /// </summary>"); |
| | 6 | 2559 | | builder.AppendLine(" /// <param name=\"services\">The service collection to register to.</param>"); |
| | 6 | 2560 | | builder.AppendLine(" private static void RegisterHostedServices(IServiceCollection services)"); |
| | 6 | 2561 | | builder.AppendLine(" {"); |
| | | 2562 | | |
| | 26 | 2563 | | foreach (var hostedService in hostedServices) |
| | | 2564 | | { |
| | 7 | 2565 | | var typeName = hostedService.TypeName; |
| | 7 | 2566 | | var shortName = typeName.Split('.').Last(); |
| | 7 | 2567 | | var sourcePath = hostedService.SourceFilePath != null |
| | 7 | 2568 | | ? BreadcrumbWriter.GetRelativeSourcePath(hostedService.SourceFilePath, projectDirectory) |
| | 7 | 2569 | | : $"[{hostedService.AssemblyName}]"; |
| | | 2570 | | |
| | 7 | 2571 | | breadcrumbs.WriteInlineComment(builder, " ", $"Hosted service: {shortName} ← {sourcePath}"); |
| | | 2572 | | |
| | | 2573 | | // Register the concrete type as singleton |
| | 7 | 2574 | | builder.AppendLine($" services.AddSingleton<{typeName}>();"); |
| | | 2575 | | |
| | | 2576 | | // Register as IHostedService that forwards to the concrete type |
| | 7 | 2577 | | builder.AppendLine($" services.AddSingleton<global::Microsoft.Extensions.Hosting.IHostedService>(sp = |
| | | 2578 | | } |
| | | 2579 | | |
| | 6 | 2580 | | builder.AppendLine(" }"); |
| | 6 | 2581 | | } |
| | | 2582 | | |
| | | 2583 | | |
| | | 2584 | | |
| | | 2585 | | private static string GenerateInterceptorProxiesSource(IReadOnlyList<DiscoveredInterceptedService> interceptedServic |
| | | 2586 | | { |
| | 15 | 2587 | | var builder = new StringBuilder(); |
| | 15 | 2588 | | var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName); |
| | | 2589 | | |
| | 15 | 2590 | | breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Interceptor Proxies"); |
| | 15 | 2591 | | builder.AppendLine("#nullable enable"); |
| | 15 | 2592 | | builder.AppendLine(); |
| | 15 | 2593 | | builder.AppendLine("using System;"); |
| | 15 | 2594 | | builder.AppendLine("using System.Reflection;"); |
| | 15 | 2595 | | builder.AppendLine("using System.Threading.Tasks;"); |
| | 15 | 2596 | | builder.AppendLine(); |
| | 15 | 2597 | | builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); |
| | 15 | 2598 | | builder.AppendLine(); |
| | 15 | 2599 | | builder.AppendLine("using NexusLabs.Needlr;"); |
| | 15 | 2600 | | builder.AppendLine(); |
| | 15 | 2601 | | builder.AppendLine($"namespace {safeAssemblyName}.Generated;"); |
| | 15 | 2602 | | builder.AppendLine(); |
| | | 2603 | | |
| | | 2604 | | // Generate each proxy class |
| | 60 | 2605 | | foreach (var service in interceptedServices) |
| | | 2606 | | { |
| | 15 | 2607 | | CodeGen.InterceptorCodeGenerator.GenerateInterceptorProxyClass(builder, service, breadcrumbs, projectDirecto |
| | 15 | 2608 | | builder.AppendLine(); |
| | | 2609 | | } |
| | | 2610 | | |
| | | 2611 | | // Generate the registration helper |
| | 15 | 2612 | | builder.AppendLine("/// <summary>"); |
| | 15 | 2613 | | builder.AppendLine("/// Helper class for registering intercepted services."); |
| | 15 | 2614 | | builder.AppendLine("/// </summary>"); |
| | 15 | 2615 | | builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1 |
| | 15 | 2616 | | builder.AppendLine("public static class InterceptorRegistrations"); |
| | 15 | 2617 | | builder.AppendLine("{"); |
| | 15 | 2618 | | builder.AppendLine(" /// <summary>"); |
| | 15 | 2619 | | builder.AppendLine(" /// Registers all intercepted services and their proxies."); |
| | 15 | 2620 | | builder.AppendLine(" /// </summary>"); |
| | 15 | 2621 | | builder.AppendLine(" /// <param name=\"services\">The service collection to register to.</param>"); |
| | 15 | 2622 | | builder.AppendLine(" public static void RegisterInterceptedServices(IServiceCollection services)"); |
| | 15 | 2623 | | builder.AppendLine(" {"); |
| | | 2624 | | |
| | 60 | 2625 | | foreach (var service in interceptedServices) |
| | | 2626 | | { |
| | 15 | 2627 | | var proxyTypeName = GeneratorHelpers.GetProxyTypeName(service.TypeName); |
| | 15 | 2628 | | var lifetime = service.Lifetime switch |
| | 15 | 2629 | | { |
| | 2 | 2630 | | GeneratorLifetime.Singleton => "Singleton", |
| | 13 | 2631 | | GeneratorLifetime.Scoped => "Scoped", |
| | 0 | 2632 | | GeneratorLifetime.Transient => "Transient", |
| | 0 | 2633 | | _ => "Scoped" |
| | 15 | 2634 | | }; |
| | | 2635 | | |
| | | 2636 | | // Register all interceptor types |
| | 66 | 2637 | | foreach (var interceptorType in service.AllInterceptorTypeNames) |
| | | 2638 | | { |
| | 18 | 2639 | | breadcrumbs.WriteInlineComment(builder, " ", $"Register interceptor: {interceptorType.Split('.'). |
| | 18 | 2640 | | builder.AppendLine($" if (!services.Any(d => d.ServiceType == typeof({interceptorType})))"); |
| | 18 | 2641 | | builder.AppendLine($" services.Add{lifetime}<{interceptorType}>();"); |
| | | 2642 | | } |
| | | 2643 | | |
| | | 2644 | | // Register the actual implementation type |
| | 15 | 2645 | | builder.AppendLine($" // Register actual implementation"); |
| | 15 | 2646 | | builder.AppendLine($" services.Add{lifetime}<{service.TypeName}>();"); |
| | | 2647 | | |
| | | 2648 | | // Register proxy for each interface |
| | 60 | 2649 | | foreach (var iface in service.InterfaceNames) |
| | | 2650 | | { |
| | 15 | 2651 | | builder.AppendLine($" // Register proxy for {iface}"); |
| | 15 | 2652 | | builder.AppendLine($" services.Add{lifetime}<{iface}>(sp => new {proxyTypeName}("); |
| | 15 | 2653 | | builder.AppendLine($" sp.GetRequiredService<{service.TypeName}>(),"); |
| | 15 | 2654 | | builder.AppendLine($" sp));"); |
| | | 2655 | | } |
| | | 2656 | | } |
| | | 2657 | | |
| | 15 | 2658 | | builder.AppendLine(" }"); |
| | 15 | 2659 | | builder.AppendLine(); |
| | 15 | 2660 | | builder.AppendLine(" /// <summary>"); |
| | 15 | 2661 | | builder.AppendLine(" /// Gets the number of intercepted services discovered at compile time."); |
| | 15 | 2662 | | builder.AppendLine(" /// </summary>"); |
| | 15 | 2663 | | builder.AppendLine($" public static int Count => {interceptedServices.Count};"); |
| | 15 | 2664 | | builder.AppendLine("}"); |
| | | 2665 | | |
| | 15 | 2666 | | return builder.ToString(); |
| | | 2667 | | } |
| | | 2668 | | |
| | | 2669 | | private static string GenerateFactoriesSource(IReadOnlyList<DiscoveredFactory> factories, string assemblyName, Bread |
| | | 2670 | | { |
| | 26 | 2671 | | var builder = new StringBuilder(); |
| | 26 | 2672 | | var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName); |
| | | 2673 | | |
| | 26 | 2674 | | breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Generated Factories"); |
| | 26 | 2675 | | builder.AppendLine("#nullable enable"); |
| | 26 | 2676 | | builder.AppendLine(); |
| | 26 | 2677 | | builder.AppendLine("using System;"); |
| | 26 | 2678 | | builder.AppendLine(); |
| | 26 | 2679 | | builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); |
| | 26 | 2680 | | builder.AppendLine(); |
| | 26 | 2681 | | builder.AppendLine($"namespace {safeAssemblyName}.Generated;"); |
| | 26 | 2682 | | builder.AppendLine(); |
| | | 2683 | | |
| | | 2684 | | // Generate factory interfaces and implementations for each type |
| | 104 | 2685 | | foreach (var factory in factories) |
| | | 2686 | | { |
| | 26 | 2687 | | if (factory.GenerateInterface) |
| | | 2688 | | { |
| | 25 | 2689 | | CodeGen.FactoryCodeGenerator.GenerateFactoryInterface(builder, factory, breadcrumbs, projectDirectory); |
| | 25 | 2690 | | builder.AppendLine(); |
| | 25 | 2691 | | CodeGen.FactoryCodeGenerator.GenerateFactoryImplementation(builder, factory, breadcrumbs, projectDirecto |
| | 25 | 2692 | | builder.AppendLine(); |
| | | 2693 | | } |
| | | 2694 | | } |
| | | 2695 | | |
| | | 2696 | | // Generate the registration helper |
| | 26 | 2697 | | builder.AppendLine("/// <summary>"); |
| | 26 | 2698 | | builder.AppendLine("/// Helper class for registering factory types."); |
| | 26 | 2699 | | builder.AppendLine("/// </summary>"); |
| | 26 | 2700 | | builder.AppendLine("[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"NexusLabs.Needlr.Generators\", \"1 |
| | 26 | 2701 | | builder.AppendLine("public static class FactoryRegistrations"); |
| | 26 | 2702 | | builder.AppendLine("{"); |
| | 26 | 2703 | | builder.AppendLine(" /// <summary>"); |
| | 26 | 2704 | | builder.AppendLine(" /// Registers all generated factories."); |
| | 26 | 2705 | | builder.AppendLine(" /// </summary>"); |
| | 26 | 2706 | | builder.AppendLine(" /// <param name=\"services\">The service collection to register to.</param>"); |
| | 26 | 2707 | | builder.AppendLine(" public static void RegisterFactories(IServiceCollection services)"); |
| | 26 | 2708 | | builder.AppendLine(" {"); |
| | | 2709 | | |
| | 104 | 2710 | | foreach (var factory in factories) |
| | | 2711 | | { |
| | 26 | 2712 | | breadcrumbs.WriteInlineComment(builder, " ", $"Factory for {factory.SimpleTypeName}"); |
| | | 2713 | | |
| | | 2714 | | // Register Func<> for each constructor |
| | 26 | 2715 | | if (factory.GenerateFunc) |
| | | 2716 | | { |
| | 106 | 2717 | | foreach (var ctor in factory.Constructors) |
| | | 2718 | | { |
| | 28 | 2719 | | CodeGen.FactoryCodeGenerator.GenerateFuncRegistration(builder, factory, ctor, " "); |
| | | 2720 | | } |
| | | 2721 | | } |
| | | 2722 | | |
| | | 2723 | | // Register interface factory |
| | 26 | 2724 | | if (factory.GenerateInterface) |
| | | 2725 | | { |
| | 25 | 2726 | | var factoryInterfaceName = $"I{factory.SimpleTypeName}Factory"; |
| | 25 | 2727 | | var factoryImplName = $"{factory.SimpleTypeName}Factory"; |
| | 25 | 2728 | | builder.AppendLine($" services.AddSingleton<global::{safeAssemblyName}.Generated.{factoryInterfac |
| | | 2729 | | } |
| | | 2730 | | } |
| | | 2731 | | |
| | 26 | 2732 | | builder.AppendLine(" }"); |
| | 26 | 2733 | | builder.AppendLine(); |
| | 26 | 2734 | | builder.AppendLine(" /// <summary>"); |
| | 26 | 2735 | | builder.AppendLine(" /// Gets the number of factory types generated at compile time."); |
| | 26 | 2736 | | builder.AppendLine(" /// </summary>"); |
| | 26 | 2737 | | builder.AppendLine($" public static int Count => {factories.Count};"); |
| | 26 | 2738 | | builder.AppendLine("}"); |
| | | 2739 | | |
| | 26 | 2740 | | return builder.ToString(); |
| | | 2741 | | } |
| | | 2742 | | |
| | | 2743 | | private static string GenerateProvidersSource(IReadOnlyList<DiscoveredProvider> providers, string assemblyName, Brea |
| | | 2744 | | { |
| | 11 | 2745 | | var builder = new StringBuilder(); |
| | 11 | 2746 | | var safeAssemblyName = GeneratorHelpers.SanitizeIdentifier(assemblyName); |
| | | 2747 | | |
| | 11 | 2748 | | breadcrumbs.WriteFileHeader(builder, assemblyName, "Needlr Generated Providers"); |
| | 11 | 2749 | | builder.AppendLine("#nullable enable"); |
| | 11 | 2750 | | builder.AppendLine(); |
| | 11 | 2751 | | builder.AppendLine("using System;"); |
| | 11 | 2752 | | builder.AppendLine(); |
| | 11 | 2753 | | builder.AppendLine("using Microsoft.Extensions.DependencyInjection;"); |
| | 11 | 2754 | | builder.AppendLine(); |
| | 11 | 2755 | | builder.AppendLine($"namespace {safeAssemblyName}.Generated;"); |
| | 11 | 2756 | | builder.AppendLine(); |
| | | 2757 | | |
| | | 2758 | | // Generate provider implementations (interface-based only) |
| | 46 | 2759 | | foreach (var provider in providers) |
| | | 2760 | | { |
| | 12 | 2761 | | CodeGen.ProviderCodeGenerator.GenerateProviderImplementation(builder, provider, $"{safeAssemblyName}.Generat |
| | 12 | 2762 | | builder.AppendLine(); |
| | | 2763 | | } |
| | | 2764 | | |
| | 11 | 2765 | | return builder.ToString(); |
| | | 2766 | | } |
| | | 2767 | | |
| | | 2768 | | private static string GenerateShorthandProviderSource(DiscoveredProvider provider, string assemblyName, BreadcrumbWr |
| | | 2769 | | { |
| | 6 | 2770 | | var builder = new StringBuilder(); |
| | 6 | 2771 | | var providerNamespace = GetNamespaceFromTypeName(provider.TypeName); |
| | | 2772 | | |
| | 6 | 2773 | | breadcrumbs.WriteFileHeader(builder, assemblyName, $"Needlr Generated Provider: {provider.SimpleTypeName}"); |
| | 6 | 2774 | | builder.AppendLine("#nullable enable"); |
| | 6 | 2775 | | builder.AppendLine(); |
| | 6 | 2776 | | builder.AppendLine("using System;"); |
| | 6 | 2777 | | builder.AppendLine(); |
| | 6 | 2778 | | builder.AppendLine($"namespace {providerNamespace};"); |
| | 6 | 2779 | | builder.AppendLine(); |
| | | 2780 | | |
| | 6 | 2781 | | CodeGen.ProviderCodeGenerator.GenerateProviderInterfaceAndPartialClass(builder, provider, providerNamespace, bre |
| | | 2782 | | |
| | 6 | 2783 | | return builder.ToString(); |
| | | 2784 | | } |
| | | 2785 | | |
| | | 2786 | | private static string GetNamespaceFromTypeName(string fullyQualifiedName) |
| | | 2787 | | { |
| | 12 | 2788 | | var name = fullyQualifiedName; |
| | 12 | 2789 | | if (name.StartsWith("global::")) |
| | | 2790 | | { |
| | 12 | 2791 | | name = name.Substring(8); |
| | | 2792 | | } |
| | | 2793 | | |
| | 12 | 2794 | | var lastDot = name.LastIndexOf('.'); |
| | 12 | 2795 | | return lastDot >= 0 ? name.Substring(0, lastDot) : string.Empty; |
| | | 2796 | | } |
| | | 2797 | | |
| | | 2798 | | |
| | | 2799 | | |
| | | 2800 | | |
| | | 2801 | | |
| | | 2802 | | /// <summary> |
| | | 2803 | | /// Filters out nested options types. |
| | | 2804 | | /// A nested options type is one that is used as a property type in another options type. |
| | | 2805 | | /// These should not be registered separately - they are bound as part of their parent. |
| | | 2806 | | /// </summary> |
| | | 2807 | | private static List<DiscoveredOptions> FilterNestedOptions(List<DiscoveredOptions> options, Compilation compilation) |
| | | 2808 | | { |
| | | 2809 | | // Build a set of all options type names |
| | 63 | 2810 | | var optionsTypeNames = new HashSet<string>(options.Select(o => o.TypeName)); |
| | | 2811 | | |
| | | 2812 | | // Find all options types that are used as properties in other options types |
| | 19 | 2813 | | var nestedTypeNames = new HashSet<string>(); |
| | | 2814 | | |
| | 126 | 2815 | | foreach (var opt in options) |
| | | 2816 | | { |
| | | 2817 | | // Find the type symbol for this options type |
| | 44 | 2818 | | var typeSymbol = FindTypeSymbol(compilation, opt.TypeName); |
| | 44 | 2819 | | if (typeSymbol == null) |
| | | 2820 | | continue; |
| | | 2821 | | |
| | | 2822 | | // Check all properties of this type |
| | 640 | 2823 | | foreach (var member in typeSymbol.GetMembers()) |
| | | 2824 | | { |
| | 276 | 2825 | | if (member is not IPropertySymbol property) |
| | | 2826 | | continue; |
| | | 2827 | | |
| | | 2828 | | // Skip non-class property types (primitives, structs, etc.) |
| | 56 | 2829 | | if (property.Type is not INamedTypeSymbol propertyType) |
| | | 2830 | | continue; |
| | | 2831 | | |
| | 56 | 2832 | | if (propertyType.TypeKind != TypeKind.Class) |
| | | 2833 | | continue; |
| | | 2834 | | |
| | | 2835 | | // Get the fully qualified name of the property type |
| | 42 | 2836 | | var propertyTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(propertyType); |
| | | 2837 | | |
| | | 2838 | | // If this property type is also an [Options] type, mark it as nested |
| | 42 | 2839 | | if (optionsTypeNames.Contains(propertyTypeName)) |
| | | 2840 | | { |
| | 9 | 2841 | | nestedTypeNames.Add(propertyTypeName); |
| | | 2842 | | } |
| | | 2843 | | } |
| | | 2844 | | } |
| | | 2845 | | |
| | | 2846 | | // Return only root options (those not used as properties in other options) |
| | 63 | 2847 | | return options.Where(o => !nestedTypeNames.Contains(o.TypeName)).ToList(); |
| | | 2848 | | } |
| | | 2849 | | |
| | | 2850 | | /// <summary> |
| | | 2851 | | /// Finds a type symbol by its fully qualified name. |
| | | 2852 | | /// </summary> |
| | | 2853 | | private static INamedTypeSymbol? FindTypeSymbol(Compilation compilation, string fullyQualifiedName) |
| | | 2854 | | { |
| | | 2855 | | // Strip global:: prefix if present |
| | 44 | 2856 | | var typeName = fullyQualifiedName.StartsWith("global::") |
| | 44 | 2857 | | ? fullyQualifiedName.Substring(8) |
| | 44 | 2858 | | : fullyQualifiedName; |
| | | 2859 | | |
| | 44 | 2860 | | return compilation.GetTypeByMetadataName(typeName); |
| | | 2861 | | } |
| | | 2862 | | |
| | | 2863 | | /// <summary> |
| | | 2864 | | /// Expands open generic decorators into concrete decorator registrations |
| | | 2865 | | /// for each discovered closed implementation of the open generic interface. |
| | | 2866 | | /// </summary> |
| | | 2867 | | private static void ExpandOpenDecorators( |
| | | 2868 | | IReadOnlyList<DiscoveredType> injectableTypes, |
| | | 2869 | | IReadOnlyList<DiscoveredOpenDecorator> openDecorators, |
| | | 2870 | | List<DiscoveredDecorator> decorators) |
| | | 2871 | | { |
| | | 2872 | | // Group injectable types by the open generic interfaces they implement |
| | 6 | 2873 | | var interfaceImplementations = new Dictionary<INamedTypeSymbol, List<(INamedTypeSymbol closedInterface, Discover |
| | | 2874 | | |
| | 40 | 2875 | | foreach (var discoveredType in injectableTypes) |
| | | 2876 | | { |
| | | 2877 | | // We need to check each interface this type implements to see if it's a closed version of an open generic |
| | 60 | 2878 | | foreach (var openDecorator in openDecorators) |
| | | 2879 | | { |
| | | 2880 | | // Check if this type implements the open generic interface |
| | 46 | 2881 | | foreach (var interfaceName in discoveredType.InterfaceNames) |
| | | 2882 | | { |
| | | 2883 | | // This is string-based matching - we need to match the interface name pattern |
| | | 2884 | | // For example, if open generic is IHandler<> and the interface is IHandler<Order>, we should match |
| | 7 | 2885 | | var openGenericName = TypeDiscoveryHelper.GetFullyQualifiedName(openDecorator.OpenGenericInterface); |
| | | 2886 | | |
| | | 2887 | | // Extract the base name (before the <>) |
| | 7 | 2888 | | var openGenericBaseName = GeneratorHelpers.GetGenericBaseName(openGenericName); |
| | 7 | 2889 | | var interfaceBaseName = GeneratorHelpers.GetGenericBaseName(interfaceName); |
| | | 2890 | | |
| | 7 | 2891 | | if (openGenericBaseName == interfaceBaseName) |
| | | 2892 | | { |
| | | 2893 | | // This interface is a closed version of the open generic |
| | | 2894 | | // Create a closed decorator registration |
| | 7 | 2895 | | var closedDecoratorTypeName = GeneratorHelpers.CreateClosedGenericType( |
| | 7 | 2896 | | TypeDiscoveryHelper.GetFullyQualifiedName(openDecorator.DecoratorType), |
| | 7 | 2897 | | interfaceName, |
| | 7 | 2898 | | openGenericName); |
| | | 2899 | | |
| | 7 | 2900 | | decorators.Add(new DiscoveredDecorator( |
| | 7 | 2901 | | closedDecoratorTypeName, |
| | 7 | 2902 | | interfaceName, |
| | 7 | 2903 | | openDecorator.Order, |
| | 7 | 2904 | | openDecorator.AssemblyName, |
| | 7 | 2905 | | openDecorator.SourceFilePath)); |
| | | 2906 | | } |
| | | 2907 | | } |
| | | 2908 | | } |
| | | 2909 | | } |
| | 6 | 2910 | | } |
| | | 2911 | | |
| | | 2912 | | |
| | | 2913 | | /// <summary> |
| | | 2914 | | /// Discovers all referenced assemblies that have the [GenerateTypeRegistry] attribute. |
| | | 2915 | | /// These assemblies need to be force-loaded to ensure their module initializers run. |
| | | 2916 | | /// </summary> |
| | | 2917 | | private static IReadOnlyList<string> DiscoverReferencedAssembliesWithTypeRegistry(Compilation compilation) |
| | | 2918 | | { |
| | 434 | 2919 | | var result = new List<string>(); |
| | | 2920 | | |
| | 147466 | 2921 | | foreach (var reference in compilation.References) |
| | | 2922 | | { |
| | 73299 | 2923 | | if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol) |
| | | 2924 | | { |
| | | 2925 | | // Skip the current assembly |
| | 73112 | 2926 | | if (SymbolEqualityComparer.Default.Equals(assemblySymbol, compilation.Assembly)) |
| | | 2927 | | continue; |
| | | 2928 | | |
| | 73112 | 2929 | | if (TypeDiscoveryHelper.HasGenerateTypeRegistryAttribute(assemblySymbol)) |
| | | 2930 | | { |
| | 17 | 2931 | | result.Add(assemblySymbol.Name); |
| | | 2932 | | } |
| | | 2933 | | } |
| | | 2934 | | } |
| | | 2935 | | |
| | 434 | 2936 | | return result; |
| | | 2937 | | } |
| | | 2938 | | |
| | | 2939 | | /// <summary> |
| | | 2940 | | /// Discovers types from referenced assemblies with [GenerateTypeRegistry] for diagnostics purposes. |
| | | 2941 | | /// Unlike the main discovery, this includes internal types since we're just showing them in diagnostics. |
| | | 2942 | | /// </summary> |
| | | 2943 | | private static Dictionary<string, List<DiagnosticTypeInfo>> DiscoverReferencedAssemblyTypesForDiagnostics(Compilatio |
| | | 2944 | | { |
| | 95 | 2945 | | var result = new Dictionary<string, List<DiagnosticTypeInfo>>(); |
| | | 2946 | | |
| | 32328 | 2947 | | foreach (var reference in compilation.References) |
| | | 2948 | | { |
| | 16069 | 2949 | | if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol) |
| | | 2950 | | { |
| | | 2951 | | // Skip the current assembly |
| | 16069 | 2952 | | if (SymbolEqualityComparer.Default.Equals(assemblySymbol, compilation.Assembly)) |
| | | 2953 | | continue; |
| | | 2954 | | |
| | 16069 | 2955 | | if (!TypeDiscoveryHelper.HasGenerateTypeRegistryAttribute(assemblySymbol)) |
| | | 2956 | | continue; |
| | | 2957 | | |
| | 14 | 2958 | | var assemblyTypes = new List<DiagnosticTypeInfo>(); |
| | | 2959 | | |
| | | 2960 | | // First pass: collect intercepted service names so we can identify their proxies |
| | 14 | 2961 | | var interceptedServiceNames = new HashSet<string>(); |
| | 120 | 2962 | | foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assemblySymbol.GlobalNamespace)) |
| | | 2963 | | { |
| | 46 | 2964 | | if (InterceptorDiscoveryHelper.HasInterceptAttributes(typeSymbol)) |
| | | 2965 | | { |
| | 2 | 2966 | | interceptedServiceNames.Add(typeSymbol.Name); |
| | | 2967 | | } |
| | | 2968 | | } |
| | | 2969 | | |
| | 120 | 2970 | | foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assemblySymbol.GlobalNamespace)) |
| | | 2971 | | { |
| | | 2972 | | // Check if it's a registerable type (injectable, plugin, factory source, or interceptor) |
| | 46 | 2973 | | var hasFactoryAttr = FactoryDiscoveryHelper.HasGenerateFactoryAttribute(typeSymbol); |
| | 46 | 2974 | | var hasInterceptAttr = InterceptorDiscoveryHelper.HasInterceptAttributes(typeSymbol); |
| | 46 | 2975 | | var isInterceptorProxy = typeSymbol.Name.EndsWith("_InterceptorProxy"); |
| | | 2976 | | |
| | 46 | 2977 | | if (!hasFactoryAttr && !hasInterceptAttr && !isInterceptorProxy && |
| | 46 | 2978 | | !TypeDiscoveryHelper.WouldBeInjectableIgnoringAccessibility(typeSymbol) && |
| | 46 | 2979 | | !TypeDiscoveryHelper.WouldBePluginIgnoringAccessibility(typeSymbol)) |
| | | 2980 | | continue; |
| | | 2981 | | |
| | 18 | 2982 | | var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol); |
| | 18 | 2983 | | var shortName = typeSymbol.Name; |
| | 18 | 2984 | | var lifetime = TypeDiscoveryHelper.DetermineLifetime(typeSymbol) ?? GeneratorLifetime.Singleton; |
| | 18 | 2985 | | var interfaces = TypeDiscoveryHelper.GetRegisterableInterfaces(typeSymbol) |
| | 14 | 2986 | | .Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i)) |
| | 18 | 2987 | | .ToArray(); |
| | 18 | 2988 | | var dependencies = TypeDiscoveryHelper.GetBestConstructorParameters(typeSymbol)? |
| | 18 | 2989 | | .ToArray() ?? Array.Empty<string>(); |
| | 18 | 2990 | | var isDecorator = TypeDiscoveryHelper.HasDecoratorForAttribute(typeSymbol) || |
| | 18 | 2991 | | OpenDecoratorDiscoveryHelper.HasOpenDecoratorForAttribute(typeSymbol); |
| | 18 | 2992 | | var isPlugin = TypeDiscoveryHelper.WouldBePluginIgnoringAccessibility(typeSymbol); |
| | 18 | 2993 | | var keyedValues = TypeDiscoveryHelper.GetKeyedServiceKeys(typeSymbol); |
| | 18 | 2994 | | var keyedValue = keyedValues.Length > 0 ? keyedValues[0] : null; |
| | | 2995 | | |
| | | 2996 | | // Check if this service has an interceptor proxy (its name + "_InterceptorProxy" exists) |
| | 18 | 2997 | | var hasInterceptorProxy = interceptedServiceNames.Contains(shortName); |
| | | 2998 | | |
| | 18 | 2999 | | assemblyTypes.Add(new DiagnosticTypeInfo( |
| | 18 | 3000 | | typeName, |
| | 18 | 3001 | | shortName, |
| | 18 | 3002 | | lifetime, |
| | 18 | 3003 | | interfaces, |
| | 18 | 3004 | | dependencies, |
| | 18 | 3005 | | isDecorator, |
| | 18 | 3006 | | isPlugin, |
| | 18 | 3007 | | hasFactoryAttr, |
| | 18 | 3008 | | keyedValue, |
| | 18 | 3009 | | isInterceptor: hasInterceptAttr, |
| | 18 | 3010 | | hasInterceptorProxy: hasInterceptorProxy)); |
| | | 3011 | | } |
| | | 3012 | | |
| | 14 | 3013 | | if (assemblyTypes.Count > 0) |
| | | 3014 | | { |
| | 14 | 3015 | | result[assemblySymbol.Name] = assemblyTypes; |
| | | 3016 | | } |
| | | 3017 | | } |
| | | 3018 | | } |
| | | 3019 | | |
| | 95 | 3020 | | return result; |
| | | 3021 | | } |
| | | 3022 | | } |