This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a Roslyn C# Source Generators project that provides compile-time code generation for .NET applications. The solution contains four main source generators:
- DependencyRegistrationGenerator - Automatically generates dependency injection service registrations
- OptionsBindingGenerator - Automatically generates configuration options binding code
- MappingGenerator - Automatically generates type-safe object-to-object mapping code
- AnnotationConstantsGenerator - Automatically generates compile-time constants from DataAnnotation attributes
All generators eliminate boilerplate code and improve developer productivity while maintaining Native AOT compatibility.
src/
Atc.SourceGenerators/ # Main generator implementations
DependencyRegistrationGenerator.cs
OptionsBindingGenerator.cs
ObjectMappingGenerator.cs
AnnotationConstantsGenerator.cs
RuleIdentifierConstants.cs # Diagnostic ID constants
RuleCategoryConstants.cs # Diagnostic category constants
Atc.SourceGenerators.Annotations/ # Shared attribute definitions (published as separate package)
RegistrationAttribute.cs
OptionsBindingAttribute.cs
MapToAttribute.cs
Lifetime.cs
OptionsLifetime.cs
test/
Atc.SourceGenerators.Tests/ # Unit tests using Roslyn testing infrastructure
DependencyRegistrationGeneratorTests.cs
OptionsBindingGeneratorTests.cs
ObjectMappingGeneratorTests.cs
AnnotationConstantsGeneratorTests.cs
sample/
Atc.SourceGenerators.DependencyRegistration/ # DI registration sample
Atc.SourceGenerators.DependencyRegistration.Domain/ # Multi-project DI sample
Atc.SourceGenerators.OptionsBinding/ # Options binding sample
Atc.SourceGenerators.OptionsBinding.Domain/ # Multi-project options sample
Atc.SourceGenerators.Mapping/ # Object mapping API sample
Atc.SourceGenerators.Mapping.Domain/ # Domain models with mappings (includes BaseEntity/AuditableEntity/Book for inheritance demo)
Atc.SourceGenerators.Mapping.DataAccess/ # Database entities with mappings
Atc.SourceGenerators.AnnotationConstants/ # DataAnnotation constants sample
PetStore.Api/ # Complete 3-layer ASP.NET Core API with OpenAPI/Scalar
PetStore.Api.Contract/ # API contracts (DTOs)
PetStore.Domain/ # Domain layer using all generators
PetStore.DataAccess/ # Data access layer with repositories
dotnet builddotnet testdotnet test --filter "FullyQualifiedName~Generator_Should_Auto_Detect_Multiple_Interfaces"# DependencyRegistration sample
dotnet run --project sample/Atc.SourceGenerators.DependencyRegistration
# OptionsBinding sample
dotnet run --project sample/Atc.SourceGenerators.OptionsBinding
# Mapping sample (minimal API with 3-layer architecture)
dotnet run --project sample/Atc.SourceGenerators.Mapping
# PetStore API (complete 3-layer app with all generators + OpenAPI/Scalar)
dotnet run --project sample/PetStore.Api
# Open browser to https://localhost:42616/scalar/v1 for API documentationdotnet cleanThis project uses Roslyn 5.0.0 (.NET 10) source generators and requires .NET 10 SDK for building.
Consumer Projects:
- Projects that reference
Atc.SourceGeneratorsmust be built with .NET 10 SDK (or later) - Consumer projects can target ANY .NET version (.NET 9, .NET 8, .NET Framework, etc.)
- This is a build-time requirement only - runtime target framework is independent
Why .NET 10 SDK is required:
- Roslyn 5.0.0 APIs ship with .NET 10 SDK
- Source generators execute during compilation, requiring the SDK's Roslyn version
- Target framework and SDK version are independent concepts in .NET
Example Scenario:
<!-- Consumer project can target .NET 9 -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Requires .NET 10 SDK to build due to Roslyn 5.0.0 -->
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
</ItemGroup>
</Project># Must use .NET 10 SDK to build
dotnet build # Executes source generators using Roslyn 5.0.0Both generators follow the Incremental Generator pattern (IIncrementalGenerator):
- PostInitialization - Generate attribute definitions as fallback (for projects that don't reference Atc.SourceGenerators.Annotations)
- Syntax Provider - Filter candidate classes with attributes (predicate + transform)
- Compilation Combination - Combine filtered classes with compilation context
- Source Output - Validate, analyze, and generate extension methods
Key Features:
- Auto-detects all implemented interfaces (excluding System.* and Microsoft.* namespaces)
- Abstract base class support - Register services against abstract base classes (e.g.,
AuthenticationStateProvider,DelegatingHandler) - Generic interface registration - Full support for open generic types like
IRepository<T>andIHandler<TRequest, TResponse> - Keyed service registration - Multiple implementations of the same interface with different keys (.NET 8+)
- Factory method registration - Custom initialization logic via static factory methods
- Instance registration - Register pre-created singleton instances via static fields, properties, or methods
- TryAdd registration - Conditional registration for default implementations (library pattern)
- Decorator pattern support - Wrap services with cross-cutting concerns (logging, caching, validation) using
Decorator = true - Conditional registration - Register services based on configuration values (feature flags, environment-specific services)
- Assembly scanning filters - Exclude types by namespace, pattern (wildcards), or interface implementation
- Runtime filtering - Exclude services when calling registration methods via optional parameters (different apps, different service subsets)
- Supports explicit
Asparameter to override auto-detection - Generates
AddDependencyRegistrationsFrom{SmartSuffix}()extension methods with 4 overloads - Smart naming - uses short suffix if unique, full name if conflicts exist
- Transitive dependency registration - automatically registers services from referenced assemblies
- Hosted service detection - automatically uses
AddHostedService<T>()forBackgroundServiceorIHostedServiceimplementations - Default lifetime: Singleton (can specify Scoped or Transient)
Generated Code Pattern:
// Input: [Registration] public class UserService : IUserService { }
// Output: services.AddSingleton<IUserService, UserService>();
// Abstract Base Class Input: [Registration(Lifetime.Scoped, As = typeof(AuthenticationStateProvider))]
// public class ServerAuthenticationStateProvider : AuthenticationStateProvider { }
// Abstract Base Class Output: services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
// Generic Input: [Registration(Lifetime.Scoped)] public class Repository<T> : IRepository<T> where T : class { }
// Generic Output: services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// Keyed Input: [Registration(Lifetime.Scoped, As = typeof(IPaymentProcessor), Key = "Stripe")]
// Keyed Output: services.AddKeyedScoped<IPaymentProcessor, StripePaymentProcessor>("Stripe");
// Factory Input: [Registration(Lifetime.Scoped, As = typeof(IEmailSender), Factory = nameof(Create))]
// public static IEmailSender Create(IServiceProvider sp) => new EmailSender();
// Factory Output: services.AddScoped<IEmailSender>(sp => EmailSender.Create(sp));
// Instance Input: [Registration(As = typeof(IAppConfiguration), Instance = nameof(DefaultInstance))]
// public static readonly AppConfiguration DefaultInstance = new();
// Instance Output: services.AddSingleton<IAppConfiguration>(AppConfiguration.DefaultInstance);
// TryAdd Input: [Registration(As = typeof(ILogger), TryAdd = true)]
// TryAdd Output: services.TryAddSingleton<ILogger, DefaultLogger>();
// Hosted Service Input: [Registration] public class MaintenanceService : BackgroundService { }
// Hosted Service Output: services.AddHostedService<MaintenanceService>();
// Decorator Input: [Registration(Lifetime.Scoped, As = typeof(IOrderService), Decorator = true)]
// public class LoggingOrderServiceDecorator : IOrderService { }
// Decorator Output: services.Decorate<IOrderService>((provider, inner) =>
// ActivatorUtilities.CreateInstance<LoggingOrderServiceDecorator>(provider, inner));
// Conditional Input: [Registration(As = typeof(ICache), Condition = "Features:UseRedisCache")]
// public class RedisCache : ICache { }
// Conditional Output: if (configuration.GetValue<bool>("Features:UseRedisCache"))
// {
// services.AddSingleton<ICache, RedisCache>();
// }
// Negated Conditional: [Registration(As = typeof(ICache), Condition = "!Features:UseRedisCache")]
// public class MemoryCache : ICache { }
// Negated Output: if (!configuration.GetValue<bool>("Features:UseRedisCache"))
// {
// services.AddSingleton<ICache, MemoryCache>();
// }Smart Naming:
// If "Domain" suffix is unique in the compilation context:
PetStore.Domain → AddDependencyRegistrationsFromDomain()
// If multiple assemblies have "Domain" suffix:
PetStore.Domain + AnotherApp.Domain → AddDependencyRegistrationsFromPetStoreDomain()Conditional Registration Configuration:
When an assembly contains services with Condition parameter, an IConfiguration parameter is added to all generated extension method signatures:
// Without conditional services:
services.AddDependencyRegistrationsFromDomain();
// With conditional services (IConfiguration required):
services.AddDependencyRegistrationsFromDomain(configuration);
services.AddDependencyRegistrationsFromDomain(configuration, includeReferencedAssemblies: true);Transitive Registration (4 Overloads):
// Overload 1: Default (no transitive registration)
services.AddDependencyRegistrationsFromDomain();
// Overload 2: Auto-detect ALL referenced assemblies recursively
services.AddDependencyRegistrationsFromDomain(includeReferencedAssemblies: true);
// Overload 3: Register specific referenced assembly (short or full name)
services.AddDependencyRegistrationsFromDomain("DataAccess");
services.AddDependencyRegistrationsFromDomain("MyApp.DataAccess");
// Overload 4: Register multiple specific assemblies
services.AddDependencyRegistrationsFromDomain("DataAccess", "Infrastructure");
// Note: Configuration is only passed to the calling assembly, not transitively to referenced assemblies
// Each assembly with conditional services should be called directly with configuration if neededHow Transitive Registration Works:
- Auto-detect mode: Scans ALL referenced assemblies for
[Registration]attributes, recursively - Manual mode: Only includes assemblies with matching prefix (e.g., "MyApp.*")
- Prefix filtering: When using assembly names, only same-prefix assemblies are registered
- Silent skip: Non-existent assemblies or assemblies without registrations are silently skipped
Assembly Scanning Filters:
Assembly-level filters allow excluding types from automatic registration during assembly scanning. Apply multiple [RegistrationFilter] attributes to exclude specific namespaces, naming patterns, or interface implementations.
// AssemblyInfo.cs - Exclude by namespace
[assembly: RegistrationFilter(
ExcludeNamespaces = new[] { "MyApp.Internal", "MyApp.Tests" })]
// Exclude by pattern (wildcards: * = any characters, ? = single character)
[assembly: RegistrationFilter(
ExcludePatterns = new[] { "*Mock*", "*Test*", "*Fake*" })]
// Exclude types implementing specific interfaces
[assembly: RegistrationFilter(
ExcludeImplementing = new[] { typeof(ITestUtility), typeof(IInternalService) })]
// Multiple filters can be combined
[assembly: RegistrationFilter(ExcludeNamespaces = new[] { "MyApp.Legacy" })]
[assembly: RegistrationFilter(ExcludePatterns = new[] { "*Deprecated*" })]How Assembly Scanning Filters Work:
- Namespace filtering: Exact match or sub-namespace match (e.g., "MyApp.Internal" excludes "MyApp.Internal.Deep.Nested")
- Pattern matching: Case-insensitive wildcard matching on both short type name and full type name
- Interface filtering: Uses
SymbolEqualityComparerfor proper generic type comparison - Multiple filters: All filter attributes are combined (union of all exclusions)
- Applied globally: Filters apply to both current assembly and referenced assemblies during transitive registration
Runtime Filtering: Runtime filters allow excluding services when calling the registration methods, rather than at compile time. All generated methods support three optional filter parameters:
// Exclude specific types
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(EmailService), typeof(SmsService) });
// Exclude by namespace (including sub-namespaces)
services.AddDependencyRegistrationsFromDomain(
excludedNamespaces: new[] { "MyApp.Domain.Internal" });
// Exclude by pattern (wildcards: * and ?)
services.AddDependencyRegistrationsFromDomain(
excludedPatterns: new[] { "*Mock*", "*Test*" });
// Combine all three
services.AddDependencyRegistrationsFromDomain(
excludedNamespaces: new[] { "MyApp.Internal" },
excludedPatterns: new[] { "*Test*" },
excludedTypes: new[] { typeof(LegacyService) });
// Works with transitive registration too
services.AddDependencyRegistrationsFromDomain(
includeReferencedAssemblies: true,
excludedTypes: new[] { typeof(EmailService) });How Runtime Filtering Works:
- Applied at registration: Filters are evaluated when services are being added to the container
- Application-specific: Different applications can exclude different services from the same library
- Propagated: Filters are automatically passed to referenced assembly calls
- Generic type support: Properly handles generic types using
typeof(Repository<>)syntax - Complement to compile-time: Use compile-time filters for global exclusions, runtime for application-specific
Runtime vs Compile-Time Filtering:
- Compile-time (assembly-level): Fixed at build time, applies to ALL registrations from that assembly
- Runtime (method parameters): Flexible per application, allows different apps to exclude different services
Diagnostics:
ATCDIR001- Service 'As' type must be an interface or abstract class (Error)ATCDIR002- Class does not implement specified interface or inherit from abstract class (Error)ATCDIR003- Duplicate registration with different lifetimes (Warning)ATCDIR004- Hosted services must use Singleton lifetime (Error)ATCDIR005- Factory method not found (Error)ATCDIR006- Factory method has invalid signature (Error)ATCDIR007- Instance member not found (Error)ATCDIR008- Instance member must be static (Error)ATCDIR009- Instance and Factory are mutually exclusive (Error)ATCDIR010- Instance registration requires Singleton lifetime (Error)
Key Features:
- Section name resolution priority:
- Explicit constructor parameter
public const string SectionNamepublic const string NameTitlepublic const string Name- Auto-inferred from class name
- Supports validation:
ValidateDataAnnotations,ValidateOnStart,ErrorOnMissingKeys(fail-fast for missing sections), Custom validators (IValidateOptions<T>) - Configuration change callbacks: Auto-generated IHostedService for OnChange notifications with Monitor lifetime - perfect for feature flags and runtime configuration updates
- Post-configuration support:
PostConfigurecallbacks for normalizing/transforming values after binding (e.g., path normalization, URL lowercase) - ConfigureAll support: Set common default values for all named options instances before individual binding with
ConfigureAllcallbacks (e.g., baseline retry/timeout settings) - Named options support: Multiple configurations of the same options type with different names (e.g., Primary/Secondary email servers)
- Child sections: Simplified syntax for creating multiple named instances from subsections using
ChildSectionsproperty (e.g.,Email→ Primary/Secondary/Fallback) - Direct type registration:
AlsoRegisterDirectTypeparameter allows registering options classes for bothIOptions<T>AND direct type injection (for migration scenarios and third-party library compatibility) - Nested subsection binding: Automatic binding of complex properties to configuration subsections (e.g.,
StorageOptions.Database.Retry→"Storage:Database:Retry") - supported out-of-the-box by Microsoft's.Bind()method - Supports lifetime selection: Singleton (IOptions), Scoped (IOptionsSnapshot), Monitor (IOptionsMonitor)
- Requires classes to be declared
partial - Smart naming - uses short suffix if unique, full name if conflicts exist
- Transitive registration: Generates 4 overloads for each assembly to support automatic or selective registration of referenced assemblies
- Early access to options: Avoid BuildServiceProvider anti-pattern with GetOrAdd methods for accessing options during service registration
Early Access to Options (Avoids BuildServiceProvider Anti-Pattern):
Three APIs available for accessing options during service registration:
| Method | Reads Cache | Writes Cache | Use Case |
|---|---|---|---|
Get[Type]... |
✅ Yes | ❌ No | Efficient retrieval (uses cached if available, no side effects) |
GetOrAdd[Type]... |
✅ Yes | ✅ Yes | Early access with caching for idempotency |
GetOptions<T>() |
✅ Yes | ❌ No | Smart dispatcher (calls Get[Type]... internally) |
// Problem: Need options values during service registration but don't want BuildServiceProvider()
// Solution: Three APIs available for early access
// API 1: Get methods - Efficient retrieval (reads cache, doesn't populate)
var dbOptions1 = services.GetDatabaseOptionsFromDomain(configuration);
var dbOptions2 = services.GetDatabaseOptionsFromDomain(configuration);
// If GetOrAdd was never called: dbOptions1 != dbOptions2 (different instances)
// If GetOrAdd was called first: dbOptions1 == dbOptions2 (returns cached instance)
// API 2: GetOrAdd methods - With caching (idempotent, populates cache)
var dbCached1 = services.GetOrAddDatabaseOptionsFromDomain(configuration);
var dbCached2 = services.GetOrAddDatabaseOptionsFromDomain(configuration);
// dbCached1 == dbCached2 (same instance, cached for reuse)
// API 3: Generic smart dispatcher (calls Get internally - reads cache, doesn't populate)
var dbOptions3 = services.GetOptions<DatabaseOptions>(configuration);
// Internally calls GetDatabaseOptionsFromDomain() - benefits from cache if available
// Works in multi-assembly projects - no CS0121 ambiguity!
// Example: Call GetOrAdd first, then Get benefits from cache
var dbFromAdd = services.GetOrAddDatabaseOptionsFromDomain(configuration); // Populates cache
var dbFromGet = services.GetDatabaseOptionsFromDomain(configuration); // Uses cache
// dbFromAdd == dbFromGet (true - Get found it in cache)
// Use options to make conditional registration decisions
if (dbFromAdd.EnableFeatureX)
{
services.AddScoped<IFeatureX, FeatureXService>();
}
// Normal AddOptionsFrom* methods register with service collection
services.AddOptionsFromDomain(configuration);
// Options available via IOptions<T>, IOptionsSnapshot<T>, IOptionsMonitor<T>How the Smart Dispatcher Works:
- Library assemblies (no OptionsBinding references): Don't generate
GetOptions<T>()- use assembly-specific methods - Consuming assemblies (with OptionsBinding references): Generate smart dispatcher that routes based on type:
public static T GetOptions<T>(...) { var type = typeof(T); // Current assembly options if (type == typeof(DatabaseOptions)) return (T)(object)services.GetDatabaseOptionsFromOptionsBinding(configuration); // Referenced assembly options if (type == typeof(CacheOptions)) return (T)(object)services.GetCacheOptionsFromDomain(configuration); throw new InvalidOperationException($"Type '{type.FullName}' is not registered..."); }
- Result: No CS0121 ambiguity, convenient generic API, compile-time type safety, no caching side effects!
Generated Code Pattern:
// Input: [OptionsBinding("Database")] public partial class DatabaseOptions { }
// Output:
services.AddOptions<DatabaseOptions>()
.Bind(configuration.GetSection("Database"))
.ValidateDataAnnotations()
.ValidateOnStart();
// Input with custom validator:
[OptionsBinding("Database", ValidateDataAnnotations = true, Validator = typeof(DatabaseOptionsValidator))]
public partial class DatabaseOptions { }
// Output with custom validator:
services.AddOptions<DatabaseOptions>()
.Bind(configuration.GetSection("Database"))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IValidateOptions<DatabaseOptions>, DatabaseOptionsValidator>();
// Input with ErrorOnMissingKeys (fail-fast for missing configuration):
[OptionsBinding("Database", ErrorOnMissingKeys = true, ValidateOnStart = true)]
public partial class DatabaseOptions { }
// Output with ErrorOnMissingKeys:
services.AddOptions<DatabaseOptions>()
.Bind(configuration.GetSection("Database"))
.Validate(options =>
{
var section = configuration.GetSection("Database");
if (!section.Exists())
{
throw new global::System.InvalidOperationException(
"Configuration section 'Database' is missing. " +
"Ensure the section exists in your appsettings.json or other configuration sources.");
}
return true;
})
.ValidateOnStart();
// Input with named options (multiple configurations):
[OptionsBinding("Email:Primary", Name = "Primary")]
[OptionsBinding("Email:Secondary", Name = "Secondary")]
[OptionsBinding("Email:Fallback", Name = "Fallback")]
public partial class EmailOptions { }
// Output with named options:
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
// Usage: Access via IOptionsSnapshot<T>.Get(name)
var emailSnapshot = serviceProvider.GetRequiredService<IOptionsSnapshot<EmailOptions>>();
var primaryEmail = emailSnapshot.Get("Primary");
var secondaryEmail = emailSnapshot.Get("Secondary");
// Input with OnChange callback (requires Monitor lifetime):
[OptionsBinding("Features", Lifetime = OptionsLifetime.Monitor, OnChange = nameof(OnFeaturesChanged))]
public partial class FeaturesOptions
{
public bool EnableNewUI { get; set; }
public bool EnableBetaFeatures { get; set; }
internal static void OnFeaturesChanged(FeaturesOptions options, string? name)
{
Console.WriteLine($"[OnChange] EnableNewUI: {options.EnableNewUI}");
Console.WriteLine($"[OnChange] EnableBetaFeatures: {options.EnableBetaFeatures}");
}
}
// Output with OnChange callback (auto-generated IHostedService):
// Generates internal IHostedService class:
internal sealed class FeaturesOptionsMonitorService : IHostedService, IDisposable
{
private readonly IOptionsMonitor<FeaturesOptions> _monitor;
private IDisposable? _changeToken;
public FeaturesOptionsMonitorService(IOptionsMonitor<FeaturesOptions> monitor) => _monitor = monitor;
public Task StartAsync(CancellationToken cancellationToken)
{
_changeToken = _monitor.OnChange(FeaturesOptions.OnFeaturesChanged);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public void Dispose() => _changeToken?.Dispose();
}
// Generates registration code:
services.AddHostedService<FeaturesOptionsMonitorService>();
services.AddSingleton<IOptionsChangeTokenSource<FeaturesOptions>>(
new ConfigurationChangeTokenSource<FeaturesOptions>(
configuration.GetSection("Features")));
services.Configure<FeaturesOptions>(configuration.GetSection("Features"));
// Input with PostConfigure (path normalization):
[OptionsBinding("Storage", PostConfigure = nameof(NormalizePaths))]
public partial class StorageOptions
{
public string BasePath { get; set; } = string.Empty;
public string CachePath { get; set; } = string.Empty;
private static void NormalizePaths(StorageOptions options)
{
options.BasePath = EnsureTrailingSlash(options.BasePath);
options.CachePath = EnsureTrailingSlash(options.CachePath);
}
private static string EnsureTrailingSlash(string path)
=> string.IsNullOrWhiteSpace(path) || path.EndsWith(Path.DirectorySeparatorChar)
? path
: path + Path.DirectorySeparatorChar;
}
// Output with PostConfigure:
services.AddOptions<StorageOptions>()
.Bind(configuration.GetSection("Storage"))
.PostConfigure(options => StorageOptions.NormalizePaths(options));
// Input with ConfigureAll (set defaults for all named instances):
[OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
[OptionsBinding("Email:Secondary", Name = "Secondary")]
[OptionsBinding("Email:Fallback", Name = "Fallback")]
public partial class EmailOptions
{
public string SmtpServer { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public int MaxRetries { get; set; }
public int TimeoutSeconds { get; set; } = 30;
internal static void SetDefaults(EmailOptions options)
{
options.MaxRetries = 3;
options.TimeoutSeconds = 30;
options.Port = 587;
}
}
// Output with ConfigureAll (runs BEFORE individual configurations):
services.ConfigureAll<EmailOptions>(options => EmailOptions.SetDefaults(options));
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
// Input with ChildSections (simplified syntax for multiple named instances):
[OptionsBinding("Email", ChildSections = new[] { "Primary", "Secondary", "Fallback" }, ConfigureAll = nameof(SetDefaults))]
public partial class EmailOptions
{
public string SmtpServer { get; set; } = string.Empty;
public int Port { get; set; } = 587;
public int MaxRetries { get; set; }
internal static void SetDefaults(EmailOptions options)
{
options.MaxRetries = 3;
options.Port = 587;
}
}
// Output with ChildSections (generates identical code to multiple attributes):
services.ConfigureAll<EmailOptions>(options => EmailOptions.SetDefaults(options));
services.Configure<EmailOptions>("Primary", configuration.GetSection("Email:Primary"));
services.Configure<EmailOptions>("Secondary", configuration.GetSection("Email:Secondary"));
services.Configure<EmailOptions>("Fallback", configuration.GetSection("Email:Fallback"));
// ChildSections is equivalent to writing multiple [OptionsBinding] attributes:
// [OptionsBinding("Email:Primary", Name = "Primary", ConfigureAll = nameof(SetDefaults))]
// [OptionsBinding("Email:Secondary", Name = "Secondary")]
// [OptionsBinding("Email:Fallback", Name = "Fallback")]
// Input with AlsoRegisterDirectType (for legacy code or third-party library compatibility):
[OptionsBinding("LegacyApi", ValidateDataAnnotations = true, AlsoRegisterDirectType = true)]
public partial class LegacyApiOptions
{
[Required, Url]
public string ApiEndpoint { get; set; } = string.Empty;
[Required, MinLength(32)]
public string ApiKey { get; set; } = string.Empty;
}
// Output with AlsoRegisterDirectType (generates both registrations):
// Standard IOptions<T> registration
services.AddOptions<LegacyApiOptions>()
.Bind(configuration.GetSection("LegacyApi"))
.ValidateDataAnnotations()
.ValidateOnStart();
// Also register direct type (for legacy code or third-party libraries)
services.AddSingleton(sp => sp.GetRequiredService<global::Microsoft.Extensions.Options.IOptions<global::MyApp.LegacyApiOptions>>().Value);
// Usage - Both injection patterns now work:
// Pattern 1: Standard IOptions<T> (recommended for new code)
public class ApiService(IOptions<LegacyApiOptions> options) { }
// Pattern 2: Direct type (for legacy code or third-party libraries that expect unwrapped types)
public class LegacyLibraryClient(LegacyApiOptions options) { }Smart Naming:
// If "Domain" suffix is unique in the compilation context:
PetStore.Domain → AddOptionsFromDomain()
// If multiple assemblies have "Domain" suffix:
PetStore.Domain + AnotherApp.Domain → AddOptionsFromPetStoreDomain()Transitive Registration:
// Overload 1: Base registration
services.AddOptionsFromDomain(configuration);
// Overload 2: Auto-detect all referenced assemblies
services.AddOptionsFromDomain(configuration, includeReferencedAssemblies: true);
// Overload 3: Register specific assembly
services.AddOptionsFromDomain(configuration, "DataAccess");
// Overload 4: Register multiple assemblies
services.AddOptionsFromDomain(configuration, "DataAccess", "Infrastructure");Diagnostics:
ATCOPT001- Options class must be partial (Error)ATCOPT002- Section name cannot be null or empty (Error)ATCOPT003- Const section name cannot be null or empty (Error)ATCOPT004- OnChange requires Monitor lifetime (Error)ATCOPT005- OnChange not supported with named options (Error)ATCOPT006- OnChange callback method not found (Error)ATCOPT007- OnChange callback has invalid signature (Error)ATCOPT008- PostConfigure not supported with named options (Error)ATCOPT009- PostConfigure callback method not found (Error)ATCOPT010- PostConfigure callback has invalid signature (Error)ATCOPT011- ConfigureAll requires multiple named options (Error)ATCOPT012- ConfigureAll callback method not found (Error)ATCOPT013- ConfigureAll callback has invalid signature (Error)ATCOPT014- ChildSections cannot be used with Name property (Error)ATCOPT015- ChildSections requires at least 2 items (Error)ATCOPT016- ChildSections array contains null or empty value (Error)
Key Features:
- Automatic property-to-property mapping by name (case-insensitive)
- Property exclusion - Use
[MapIgnore]attribute to exclude sensitive or internal properties from mapping (works on both source and target properties) - Custom property names - Use
[MapProperty("TargetName")]attribute to map properties with different names between source and target types - Constructor mapping - Automatically detects and uses constructors when mapping to records or classes with primary constructors:
- Prefers constructor calls over object initializers when available
- Supports records with positional parameters (C# 9+)
- Supports classes with primary constructors (C# 12+)
- Mixed initialization - Uses constructor for required parameters and object initializer for remaining properties
- Case-insensitive parameter matching - Matches property names to constructor parameter names regardless of casing
- Smart enum conversion:
- Uses EnumMapping extension methods when enums have
[MapTo]attributes (safe, with special case handling) - Falls back to simple casts for enums without
[MapTo]attributes
- Uses EnumMapping extension methods when enums have
- Collection mapping support - Automatically maps collections with LINQ
.Select():- Supports
List<T>,IList<T>,IEnumerable<T>,ICollection<T>,IReadOnlyList<T>,IReadOnlyCollection<T>,T[] - Generates appropriate
.ToList(),.ToArray(), or collection constructor calls - Automatically chains element mappings (e.g.,
source.Items?.Select(x => x.MapToItemDto()).ToList()!)
- Supports
- Base class property inheritance - Automatically includes properties from base classes:
- Traverses entire inheritance hierarchy (Entity → AuditableEntity → ConcreteEntity)
- Handles property overrides correctly (no duplicates)
- Respects
[MapIgnore]on base class properties - Works with all mapping features (PropertyNameStrategy, Bidirectional, etc.)
- Perfect for entity base classes with audit fields (Id, CreatedAt, UpdatedAt, etc.)
- Nested object mapping (automatically chains mappings)
- Null safety (null checks for nullable properties)
- Multi-layer support (Entity → Domain → DTO chains)
- Bidirectional mapping support - Generate both forward and reverse mappings with
Bidirectional = true - Record support - Works with classes, records, and structs
- Requires types to be declared
partial
Generated Code Pattern (Object Initializer):
// Input:
[MapTo(typeof(UserDto))]
public partial class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public UserStatus Status { get; set; }
public Address? Address { get; set; }
}
// Output (extension method in Atc.Mapping namespace):
public static UserDto MapToUserDto(this User source)
{
if (source is null)
{
return default!;
}
return new UserDto
{
Id = source.Id,
Name = source.Name,
Status = source.Status.MapToUserStatusDto(), // ✨ Safe enum mapping (if UserStatus has [MapTo])
// OR: Status = (UserStatusDto)source.Status, // ⚠️ Fallback cast (if no [MapTo])
Address = source.Address?.MapToAddress()! // Automatic nested mapping
};
}Generated Code Pattern (Constructor Mapping):
// Input - Record with constructor:
public record OrderDto(Guid Id, string CustomerName, decimal Total, DateTimeOffset OrderDate);
[MapTo(typeof(OrderDto))]
public partial record Order(Guid Id, string CustomerName, decimal Total, DateTimeOffset OrderDate);
// Output - Constructor call:
public static OrderDto MapToOrderDto(this Order source)
{
if (source is null)
{
return default!;
}
return new OrderDto(
source.Id,
source.CustomerName,
source.Total,
source.OrderDate);
}Generated Code Pattern (Mixed Constructor + Initializer):
// Input - Record with constructor and extra properties:
public record ProductDto(Guid Id, string Name, decimal Price)
{
public string Description { get; set; } = string.Empty;
public bool InStock { get; set; }
}
[MapTo(typeof(ProductDto))]
public partial class Product
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Description { get; set; } = string.Empty;
public bool InStock { get; set; }
}
// Output - Mixed constructor + initializer:
public static ProductDto MapToProductDto(this Product source)
{
if (source is null)
{
return default!;
}
return new ProductDto(
source.Id,
source.Name,
source.Price)
{
Description = source.Description,
InStock = source.InStock
};
}Generated Code Pattern (Base Class Property Inheritance):
// Input - Base entity with common properties:
public abstract partial class BaseEntity
{
public Guid Id { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
public abstract partial class AuditableEntity : BaseEntity
{
public DateTimeOffset? UpdatedAt { get; set; }
public string? UpdatedBy { get; set; }
}
[MapTo(typeof(BookDto))]
public partial class Book : AuditableEntity
{
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class BookDto
{
public Guid Id { get; set; } // From BaseEntity
public DateTimeOffset CreatedAt { get; set; } // From BaseEntity
public DateTimeOffset? UpdatedAt { get; set; } // From AuditableEntity
public string? UpdatedBy { get; set; } // From AuditableEntity
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public decimal Price { get; set; }
}
// Output - All properties from entire hierarchy included:
public static BookDto MapToBookDto(this Book source)
{
if (source is null)
{
return default!;
}
return new BookDto
{
Id = source.Id, // ✨ From BaseEntity (2 levels up)
CreatedAt = source.CreatedAt, // ✨ From BaseEntity
UpdatedAt = source.UpdatedAt, // ✨ From AuditableEntity (1 level up)
UpdatedBy = source.UpdatedBy, // ✨ From AuditableEntity
Title = source.Title,
Author = source.Author,
Price = source.Price
};
}Mapping Rules:
- Base Class Property Collection: Generator traverses the entire inheritance hierarchy:
- Walks up from most derived class to
System.Object - Collects properties from each level (respecting accessibility and
[MapIgnore]) - Handles property overrides correctly (keeps most derived version, no duplicates)
- Works with unlimited inheritance depth
- Walks up from most derived class to
- Constructor Detection: Generator automatically detects suitable constructors:
- Finds public constructors where ALL parameters match source properties (case-insensitive)
- Prefers constructors with more parameters
- Uses constructor call syntax when a suitable constructor is found
- Falls back to object initializer syntax when no matching constructor exists
- Property Matching: Properties are matched by name (case-insensitive):
Idmatchesid,ID,Id(supports different casing conventions)- Enables mapping between PascalCase properties and camelCase constructor parameters
- Direct Mapping: Properties with same name and type are mapped directly
- Smart Enum Conversion:
- If source enum has
[MapTo(typeof(TargetEnum))], uses.MapToTargetEnum()extension method (safe) - If target enum has
[MapTo(typeof(SourceEnum), Bidirectional = true)], uses reverse mapping method (safe) - Otherwise, falls back to
(TargetEnum)source.Enumcast (less safe)
- If source enum has
- Collection Mapping: If both source and target properties are collections:
- Extracts element types and generates
.Select(x => x.MapToXxx())code - Uses
.ToList()for most collection types (List, IEnumerable, ICollection, IList, IReadOnlyList) - Uses
.ToArray()for array types - Uses collection constructors for
Collection<T>andReadOnlyCollection<T>
- Extracts element types and generates
- Nested Objects: If a property type has a
MapToXxx()method, it's used automatically - Null Safety: Nullable properties use
?.and!for proper null handling
3-Layer Architecture Support:
UserEntity (DataAccess) [MapTo(typeof(User))]
↓ .MapToUser()
User (Domain) [MapTo(typeof(UserDto))]
↓ .MapToUserDto()
UserDto (API)
Diagnostics:
ATCMAP001- Mapping class must be partial (Error)ATCMAP002- Target type must be a class or struct (Error)ATCMAP003- MapProperty target property not found (Error)
Key Features:
- Intelligent name-based enum value matching (case-insensitive)
- Automatic special case detection (None → Unknown, Active → Enabled, etc.)
- Bidirectional mapping support with
Bidirectional = true - Zero runtime cost - pure switch expressions
- Type-safe with compile-time diagnostics
- No "partial" requirement (enums can't be partial)
Generated Code Pattern:
// Input:
[MapTo(typeof(PetStatusDto), Bidirectional = true)]
public enum PetStatusEntity
{
None, // Special case: maps to PetStatusDto.Unknown
Pending,
Available,
Adopted,
}
public enum PetStatusDto
{
Unknown, // Special case: maps from PetStatusEntity.None
Available,
Pending,
Adopted,
}
// Output (extension method in Atc.Mapping namespace):
public static PetStatusDto MapToPetStatusDto(this PetStatusEntity source)
{
return source switch
{
PetStatusEntity.None => PetStatusDto.Unknown, // Special case mapping
PetStatusEntity.Pending => PetStatusDto.Pending,
PetStatusEntity.Available => PetStatusDto.Available,
PetStatusEntity.Adopted => PetStatusDto.Adopted,
_ => throw new global::System.ArgumentOutOfRangeException(
nameof(source), source, "Unmapped enum value"),
};
}
// Reverse mapping (Bidirectional = true):
public static PetStatusEntity MapToPetStatusEntity(this PetStatusDto source)
{
return source switch
{
PetStatusDto.Unknown => PetStatusEntity.None, // Special case mapping
PetStatusDto.Available => PetStatusEntity.Available,
PetStatusDto.Pending => PetStatusEntity.Pending,
PetStatusDto.Adopted => PetStatusEntity.Adopted,
_ => throw new global::System.ArgumentOutOfRangeException(
nameof(source), source, "Unmapped enum value"),
};
}Mapping Rules:
- Exact Match: Enum values with same name (case-sensitive) map directly
- Case-Insensitive Match: Falls back to case-insensitive comparison
- Special Cases: Automatically detects common patterns:
None↔Unknown,Default,NotSetActive↔Enabled,On,RunningInactive↔Disabled,Off,StoppedDeleted↔Removed,ArchivedPending↔InProgress,ProcessingCompleted↔Done,Finished
- Unmapped Values: Generate warnings at compile time, throw at runtime
Special Case Detection:
The generator uses EnumMappingUtility which contains a dictionary of common enum naming patterns. This eliminates the need for manual configuration when dealing with standard patterns like database "None" values mapping to API "Unknown" values.
3-Layer Architecture Support:
PetStatusEntity (DataAccess) [MapTo(typeof(Domain.PetStatus))]
↓ .MapToPetStatus()
PetStatus (Domain) [MapTo(typeof(Api.PetStatus))]
↓ .MapToPetStatus()
PetStatus (API)
Diagnostics:
ATCENUM001- Target type must be an enum (Error)ATCENUM002- Source enum value has no matching target value (Warning)
Key Features:
- Automatic scanning of classes/records with DataAnnotation attributes
- No opt-in attribute required - scans all types automatically
- Full support for 17 Microsoft DataAnnotation attributes
- Atc attributes support - IPAddress, Uri, String, KeyString, IsoCurrencySymbol, IgnoreDisplay, EnumGuid, CasingStyleDescription (when Atc package is referenced)
- Zero reflection at runtime - all metadata accessible at compile time
- Native AOT compatible
- Configurable via .editorconfig (
atc_annotation_constants.include_unannotated_properties)
Supported Microsoft DataAnnotation Attributes:
- Display: DisplayName, Description, ShortName, GroupName, Prompt, Order
- Validation: Required (with AllowEmptyStrings, ErrorMessage), StringLength, MinLength, MaxLength, Range, RegularExpression
- Data Types: EmailAddress, Phone, Url, CreditCard, DataType
- Metadata: Key, Compare, Editable, ScaffoldColumn, Timestamp
Supported Atc Attributes (from Atc package):
IPAddressAttribute- IsIPAddress, IPAddressRequiredIsoCurrencySymbolAttribute- IsIsoCurrencySymbol, IsoCurrencySymbolRequired, AllowedIsoCurrencySymbolsStringAttribute- IsAtcString, AtcStringRequired, AtcStringMinLength, AtcStringMaxLength, AtcStringRegularExpression, AtcStringInvalidCharacters, AtcStringInvalidPrefixStringsKeyStringAttribute- IsKeyString (plus all StringAttribute constants)UriAttribute- IsAtcUri, AtcUriRequired, AtcUriAllowHttp, AtcUriAllowHttps, AtcUriAllowFtp, AtcUriAllowFtps, AtcUriAllowFile, AtcUriAllowOpcTcpIgnoreDisplayAttribute- IsIgnoreDisplayEnumGuidAttribute- EnumGuidCasingStyleDescriptionAttribute- CasingStyleDefault, CasingStylePrefix
Generated Code Pattern:
// Input:
using System.ComponentModel.DataAnnotations;
public class Product
{
[Display(Name = "Product Name", Description = "The display name of the product")]
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[Display(Name = "Price")]
[Required]
[Range(typeof(decimal), "0.01", "999999.99")]
public decimal Price { get; set; }
}
// Output (static partial class in same namespace):
namespace MyNamespace;
public static partial class AnnotationConstants
{
public static partial class Product
{
public static partial class Name
{
public const string DisplayName = "Product Name";
public const string Description = "The display name of the product";
public const bool IsRequired = true;
public const int MaximumLength = 100;
}
public static partial class Price
{
public const string DisplayName = "Price";
public const bool IsRequired = true;
public const string Minimum = "0.01";
public const string Maximum = "999999.99";
public static readonly System.Type OperandType = typeof(decimal);
}
}
}
// Usage:
string displayName = AnnotationConstants.Product.Name.DisplayName; // "Product Name"
bool isRequired = AnnotationConstants.Product.Name.IsRequired; // true
int maxLength = AnnotationConstants.Product.Name.MaximumLength; // 100
string priceMin = AnnotationConstants.Product.Price.Minimum; // "0.01"
Type priceType = AnnotationConstants.Product.Price.OperandType; // typeof(decimal)Configuration via .editorconfig:
# Include properties without annotations (default: false)
atc_annotation_constants.include_unannotated_properties = trueUse Cases:
- Blazor dynamic forms without reflection
- Client-side validation metadata
- API documentation generation
- TypeScript type generation
- UI label/placeholder generation
The PetStore.Api sample demonstrates all four generators working together in a realistic 3-layer ASP.NET Core application with OpenAPI/Scalar documentation.
┌─────────────────────────────────────────────────────────────┐
│ PetStore.Api (ASP.NET Core 10.0 Minimal API) │
│ - GenerateDocumentationFile=true (for OpenAPI) │
│ - Endpoints: POST /pets, GET /pets/{id} │
│ - OpenAPI/Scalar integration │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PetStore.Domain │
│ - [Registration] PetService, ValidationService │
│ - [Registration] PetMaintenanceService (BackgroundService) │
│ - [OptionsBinding] PetStoreOptions, PetMaintenanceOptions │
│ - [MapTo] Pet → PetDto, Pet → PetEntity │
│ - GenerateDocumentationFile=false │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PetStore.DataAccess │
│ - [Registration] PetRepository │
│ - [MapTo] PetEntity → Pet │
│ - GenerateDocumentationFile=false │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PetStore.Api.Contract │
│ - DTOs: CreatePetRequest, PetDto │
│ - GenerateDocumentationFile=false │
└─────────────────────────────────────────────────────────────┘
The generators create these registration methods per assembly (with smart naming):
// From PetStore.Domain (suffix "Domain" is unique → AddDependencyRegistrationsFromDomain)
services.AddDependencyRegistrationsFromDomain();
services.AddOptionsFromDomain(configuration);
// From PetStore.DataAccess (suffix "DataAccess" is unique → AddDependencyRegistrationsFromDataAccess)
services.AddDependencyRegistrationsFromDataAccess();
// Mapping extensions available via 'using Atc.Mapping'
var dto = pet.MapToPetResponse();
var entity = pet.MapToPetEntity();
var domain = entity.MapToPet();POST /pets { "name": "Buddy", "species": "Dog" }
↓
API receives CreatePetRequest
↓
IPetService.CreatePetAsync() [injected via DI generator]
↓
Validation with PetStoreOptions [bound via Options generator]
↓
CreatePetRequest.MapToPet() [Mapping generator]
↓
IPetRepository.SaveAsync() [injected via DI generator]
↓
Pet.MapToPetEntity() [Mapping generator]
↓
Save to storage
↓
PetEntity.MapToPet().MapToPetDto() [Mapping generator chain]
↓
Return PetDto to client
- Zero boilerplate DI registration: All services auto-registered, including hosted services
- Background service support:
PetMaintenanceServiceautomatically registered withAddHostedService<T>() - Type-safe configuration: Options validated and bound automatically
- Automatic mapping chains: Entity ↔ Domain ↔ DTO conversions
- OpenAPI integration: Full API documentation with Scalar UI
- Multi-project architecture: Shows how generators work across project boundaries
The project follows the pattern from atc-analyzer for managing diagnostic identifiers and categories:
RuleIdentifierConstants.cs:
- Defines all diagnostic IDs as constants organized by category
ATCDIR001-099- Dependency Injection diagnosticsATCOPT001-099- Options Binding diagnosticsATCMAP001-099- Object Mapping diagnostics
RuleCategoryConstants.cs:
- Defines diagnostic categories:
DependencyInjection,OptionsBinding,ObjectMapping - Used in DiagnosticDescriptor creation for proper categorization
AnalyzerReleases.Unshipped.md:
- Track all changes to diagnostic IDs, categories, or severities
- Update when modifying existing diagnostics
- Target Framework: Generators must target
netstandard2.0for Roslyn compatibility - Attribute Generation: All generators ALWAYS emit fallback attribute definitions in
PostInitializationto ensure attributes are available early in compilation. This is required even if projects referenceAtc.SourceGenerators.Annotations. CS0436 warnings are expected and should be suppressed via<NoWarn>$(NoWarn);CS0436</NoWarn> - Generated Code Attributes: Always include best-practice attributes on generated types:
[GeneratedCode]- Marks code as auto-generated[EditorBrowsable(Never)]- Hides from IntelliSense[CompilerGenerated]- Marks as compiler-generated[DebuggerNonUserCode]- Improves debugging (classes/methods only)[ExcludeFromCodeCoverage]- Excludes from coverage (classes/methods only)
- Testing: Use the test helper pattern in
DependencyRegistrationGeneratorTests.cs- create syntax trees, run generator, validate diagnostics and output - Incremental Generation: Leverage Roslyn's incremental generation pipeline for performance
- Diagnostics: Always use constants from
RuleIdentifierConstantsandRuleCategoryConstants- never hardcode diagnostic IDs or categories
private static (ImmutableArray<Diagnostic> Diagnostics, string Output) GetGeneratedOutput(string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create("TestAssembly", [syntaxTree], references, options);
var generator = new DependencyRegistrationGenerator();
var driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);
// Extract and validate generated code
}Both generators use StringBuilderExtensions.AppendLineLf() for consistent LF-only line endings in generated code (see src/Atc.SourceGenerators/StringBuilderExtensions.cs).
Both generators generate assembly-specific extension methods with smart naming:
- DI:
AddDependencyRegistrationsFrom{SmartSuffix}() - Options:
AddOptionsFrom{SmartSuffix}()
Smart Naming Rules:
- If the assembly suffix (last segment) is unique in the compilation context, use just the suffix
- If multiple assemblies share the same suffix, use the full sanitized assembly name
- Assembly names are sanitized to create valid C# identifiers (dots, dashes, spaces removed)
Examples:
PetStore.Domainwith no other "Domain" →AddDependencyRegistrationsFromDomain()PetStore.Domain+AnotherApp.Domain→AddDependencyRegistrationsFromPetStoreDomain()andAddDependencyRegistrationsFromAnotherAppDomain()
When using these source generators with OpenAPI/Swagger documentation (e.g., Microsoft.AspNetCore.OpenApi, Scalar.AspNetCore), follow these XML documentation settings:
Library Projects (Domain, DataAccess, Contracts):
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS0436;SA0001;CS1591;IDE0005</NoWarn>
</PropertyGroup>API Projects (Web API, Minimal API):
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS0436;SA0001;CS1591;IDE0005</NoWarn>
</PropertyGroup>Annotations Assembly:
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<NoWarn>$(NoWarn);EnableGenerateDocumentationFile</NoWarn>
</PropertyGroup>- Fallback Attributes: Each generator emits attribute definitions in
PostInitializationfor projects that don't referenceAtc.SourceGenerators.Annotations - Duplicate Prevention: If multiple projects generate XML docs for the same types (via fallback attributes), OpenAPI will throw
ArgumentException: An item with the same key has already been added - Standard Practice: Library projects typically don't need XML documentation files; only the final API project needs them for OpenAPI/Swagger
- CS0436: "The type 'X' in 'Y' conflicts with the imported type 'X' in 'Z'" - Expected when fallback attributes are generated
- SA0001: StyleCop warning - typically disabled for generated code
- CS1591: "Missing XML comment for publicly visible type" - Common to suppress in projects without XML docs
- IDE0005: "Remove unnecessary usings" - May conflict with generated code
- EnableGenerateDocumentationFile: Roslyn analyzer suggestion to enable XML docs - can be ignored for library/annotation projects
All generators emit best-practice attributes on generated types:
[global::System.CodeDom.Compiler.GeneratedCode("Atc.SourceGenerators.{GeneratorName}", "1.0.0")]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
[global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.Diagnostics.DebuggerNonUserCode] // Classes/methods only
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // Classes/methods onlyThese attributes:
- Hide generated code from IntelliSense
- Exclude from code coverage
- Mark as compiler-generated
- Improve debugging experience
- Clean and rebuild:
dotnet clean && dotnet build - Check that the class has the correct attribute and accessibility (public)
- For options: verify class is declared
partial - Restart IDE/OmniSharp if using VS Code
This is expected and by design. All generators emit fallback attribute definitions in PostInitialization to ensure attributes are available early in the compilation pipeline. This happens even if projects reference Atc.SourceGenerators.Annotations.
Always suppress this warning:
<NoWarn>$(NoWarn);CS0436</NoWarn>If you encounter System.ArgumentException: An item with the same key has already been added when using OpenAPI/Swagger:
-
Disable XML documentation for all library projects (Domain, DataAccess, Contracts):
<GenerateDocumentationFile>false</GenerateDocumentationFile>
-
Enable XML documentation only for the final API project:
<GenerateDocumentationFile>true</GenerateDocumentationFile>
-
Ensure the Annotations project has the warning suppressed:
<NoWarn>$(NoWarn);EnableGenerateDocumentationFile</NoWarn>
See the XML Documentation Configuration section above for detailed explanation.
Run tests with verbose output to see generated source:
dotnet test --logger "console;verbosity=detailed"The Atc.SourceGenerators.csproj uses a special analyzer package structure:
- DLL placed in
analyzers/dotnet/cs/path (not standard lib folder) DevelopmentDependency=true- not included in consuming project outputs- PDBs are embedded in the DLL (
DebugType=embedded) IncludeBuildOutput=false- analyzer packages don't follow standard structure