Automatically extract DataAnnotation metadata from class/record properties and generate compile-time accessible constants without reflection.
Key Benefits:
- 🎯 Zero reflection - Access annotation values at compile time
- ⚡ Native AOT ready - Works with trimming and ahead-of-time compilation
- 🛡️ Type-safe constants - Strongly-typed int, string, and bool values
- 🔍 Discoverable - Full IntelliSense support via nested class structure
- 🚀 Zero configuration - Works out of the box, no opt-in attribute needed
Quick Example:
// Input: Class with DataAnnotation attributes
public class Product
{
[Display(Name = "Product Name")]
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[Display(Name = "Price")]
[Range(typeof(decimal), "0.01", "999999.99")]
public decimal Price { get; set; }
}
// Generated: Compile-time constants
string label = AnnotationConstants.Product.Name.DisplayName; // "Product Name"
int maxLength = AnnotationConstants.Product.Name.MaximumLength; // 100
bool required = AnnotationConstants.Product.Name.IsRequired; // true
string minPrice = AnnotationConstants.Product.Price.Minimum; // "0.01"
string maxPrice = AnnotationConstants.Product.Price.Maximum; // "999999.99"- 📋 Feature Roadmap - See all implemented and planned features
- 🎯 Sample Projects - Working code examples
Note: This generator supports both Microsoft DataAnnotations and Atc attributes. When the Atc package is referenced, additional validation attributes (IPAddress, Uri, String, KeyString, IsoCurrencySymbol) are also extracted.
- 📝 Annotation Constants Source Generator
// Using reflection to get annotation values - slow and not AOT-compatible 😫
var displayAttr = typeof(Product)
.GetProperty(nameof(Product.Name))
?.GetCustomAttribute<DisplayAttribute>();
string? displayName = displayAttr?.Name; // Runtime reflection call
var stringLengthAttr = typeof(Product)
.GetProperty(nameof(Product.Name))
?.GetCustomAttribute<StringLengthAttribute>();
int? maxLength = stringLengthAttr?.MaximumLength; // Another reflection call
// Problems:
// - Runtime overhead for every access
// - Not compatible with Native AOT
// - No compile-time validation
// - Verbose and repetitive code// Using generated constants - zero reflection, AOT-compatible ✨
string displayName = AnnotationConstants.Product.Name.DisplayName; // Compile-time constant
int maxLength = AnnotationConstants.Product.Name.MaximumLength; // Compile-time constant
// Benefits:
// - Zero runtime overhead (compile-time constants)
// - Native AOT compatible
// - Full IntelliSense support
// - Type-safe (int for lengths, string for names)dotnet add package Atc.SourceGeneratorsOr in your .csproj:
<ItemGroup>
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
</ItemGroup>No special setup needed! Just use standard DataAnnotation attributes on your classes:
using System.ComponentModel.DataAnnotations;
namespace MyApp.Models;
public class Customer
{
[Display(Name = "Customer Name", Description = "Full legal name")]
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = string.Empty;
[Display(Name = "Email Address")]
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Display(Name = "Age")]
[Range(18, 120)]
public int Age { get; set; }
}The generator automatically creates a nested static class structure:
using MyApp.Models;
// Access constants without reflection
Console.WriteLine($"Field: {AnnotationConstants.Customer.Name.DisplayName}"); // "Customer Name"
Console.WriteLine($"Max: {AnnotationConstants.Customer.Name.MaximumLength}"); // 100
Console.WriteLine($"Min: {AnnotationConstants.Customer.Name.MinimumLength}"); // 2
Console.WriteLine($"Required: {AnnotationConstants.Customer.Name.IsRequired}"); // true
Console.WriteLine($"Email Required: {AnnotationConstants.Customer.Email.IsRequired}"); // true
Console.WriteLine($"Is Email: {AnnotationConstants.Customer.Email.IsEmailAddress}"); // true
Console.WriteLine($"Age Min: {AnnotationConstants.Customer.Age.Minimum}"); // "18"
Console.WriteLine($"Age Max: {AnnotationConstants.Customer.Age.Maximum}"); // "120"- 🔍 Automatic Scanning - No opt-in attribute needed; scans all classes with DataAnnotation attributes
- 📋 Display Attribute - DisplayName, Description, ShortName, GroupName, Prompt, Order
- ✅ Validation Attributes - Required, StringLength, Range, MinLength, MaxLength, RegularExpression
- 📧 Data Type Attributes - EmailAddress, Phone, Url, CreditCard, DataType
- 🔑 Metadata Attributes - Key, Editable, ScaffoldColumn, Timestamp, Compare
- ⚙️ Configurable - Customize behavior via .editorconfig
- 📦 Multi-Assembly - Works across project references
- ⚡ Zero Runtime Cost - All constants generated at compile time
- 🚀 Native AOT Ready - No reflection, fully trimming-safe
| Attribute Property | Generated Constant | Type |
|---|---|---|
Display.Name |
DisplayName |
string |
Display.Description |
Description |
string |
Display.ShortName |
ShortName |
string |
Display.GroupName |
GroupName |
string |
Display.Prompt |
Prompt |
string |
Display.Order |
Order |
int |
| Attribute | Generated Constants | Types |
|---|---|---|
Required |
IsRequired, AllowEmptyStrings, RequiredErrorMessage |
bool, bool, string |
StringLength |
MinimumLength, MaximumLength, StringLengthErrorMessage |
int, int, string |
Range |
Minimum, Maximum, OperandType, RangeErrorMessage |
string, string, Type, string |
MinLength |
MinimumLength, MinLengthErrorMessage |
int, string |
MaxLength |
MaximumLength, MaxLengthErrorMessage |
int, string |
RegularExpression |
Pattern, RegularExpressionErrorMessage |
string, string |
| Attribute | Generated Constant | Type |
|---|---|---|
EmailAddress |
IsEmailAddress |
bool |
Phone |
IsPhone |
bool |
Url |
IsUrl |
bool |
CreditCard |
IsCreditCard |
bool |
DataType |
DataType |
int (enum value) |
| Attribute | Generated Constant | Type |
|---|---|---|
Key |
IsKey |
bool |
Editable |
IsEditable |
bool |
ScaffoldColumn |
IsScaffoldColumn |
bool |
Timestamp |
IsTimestamp |
bool |
Compare |
CompareProperty |
string |
When your project references the Atc package, the generator also extracts constants from Atc-specific validation attributes:
| Attribute | Generated Constants | Types |
|---|---|---|
IPAddressAttribute |
IsIPAddress, IPAddressRequired |
bool, bool |
IsoCurrencySymbolAttribute |
IsIsoCurrencySymbol, IsoCurrencySymbolRequired, AllowedIsoCurrencySymbols |
bool, bool, string[] |
StringAttribute |
IsAtcString, AtcStringRequired, AtcStringMinLength, AtcStringMaxLength, AtcStringRegularExpression, AtcStringInvalidCharacters, AtcStringInvalidPrefixStrings |
bool, bool, uint, uint, string, char[], string[] |
KeyStringAttribute |
IsKeyString (plus all StringAttribute constants) |
bool |
UriAttribute |
IsAtcUri, AtcUriRequired, AtcUriAllowHttp, AtcUriAllowHttps, AtcUriAllowFtp, AtcUriAllowFtps, AtcUriAllowFile, AtcUriAllowOpcTcp |
all bool |
IgnoreDisplayAttribute |
IsIgnoreDisplay |
bool |
EnumGuidAttribute |
EnumGuid |
string |
CasingStyleDescriptionAttribute |
CasingStyleDefault, CasingStylePrefix |
string, string |
Example with Atc attributes:
// Add Atc package reference
// <PackageReference Include="Atc" Version="2.*" />
public class NetworkConfig
{
[Display(Name = "Server IP")]
[IPAddress(Required = true)]
public string ServerAddress { get; set; } = string.Empty;
[Display(Name = "API Endpoint")]
[UriAttribute(Required = true, AllowHttp = false, AllowHttps = true)]
public string ApiEndpoint { get; set; } = string.Empty;
[Display(Name = "Currency")]
[IsoCurrencySymbol(IsoCurrencySymbols = new[] { "USD", "EUR", "GBP" })]
public string Currency { get; set; } = "USD";
}
// Access Atc constants
bool isIPAddress = AnnotationConstants.NetworkConfig.ServerAddress.IsIPAddress; // true
bool httpsOnly = AnnotationConstants.NetworkConfig.ApiEndpoint.AtcUriAllowHttps; // true
string[] currencies = AnnotationConstants.NetworkConfig.Currency.AllowedIsoCurrencySymbols; // ["USD", "EUR", "GBP"]@* Use generated constants in Blazor forms *@
<EditForm Model="customer">
<div class="form-group">
<label>@AnnotationConstants.Customer.Name.DisplayName</label>
<InputText @bind-Value="customer.Name"
maxlength="@AnnotationConstants.Customer.Name.MaximumLength" />
<small>@AnnotationConstants.Customer.Name.Description</small>
</div>
</EditForm>// Generate JavaScript validation rules from constants
var validationRules = new
{
name = new
{
required = AnnotationConstants.Customer.Name.IsRequired,
maxLength = AnnotationConstants.Customer.Name.MaximumLength,
minLength = AnnotationConstants.Customer.Name.MinimumLength
},
email = new
{
required = AnnotationConstants.Customer.Email.IsRequired,
isEmail = AnnotationConstants.Customer.Email.IsEmailAddress
}
};
// Serialize and send to JavaScript
var json = JsonSerializer.Serialize(validationRules);// Generate OpenAPI/Swagger documentation from constants
app.MapPost("/customers", (Customer customer) => { })
.WithDescription($"""
Creates a new customer.
Name: {AnnotationConstants.Customer.Name.Description}
- Required: {AnnotationConstants.Customer.Name.IsRequired}
- Max Length: {AnnotationConstants.Customer.Name.MaximumLength}
Email: {AnnotationConstants.Customer.Email.DisplayName}
- Required: {AnnotationConstants.Customer.Email.IsRequired}
""");The generator can be configured via .editorconfig or MSBuild properties.
By default, only properties with at least one DataAnnotation attribute are included. To include all public properties:
Option 1: .editorconfig
[*.cs]
atc_annotation_constants.include_unannotated_properties = trueOption 2: MSBuild property
<PropertyGroup>
<AtcAnnotationConstantsIncludeUnannotatedProperties>true</AtcAnnotationConstantsIncludeUnannotatedProperties>
</PropertyGroup>The generator scans all classes and records in your project that have properties with DataAnnotation attributes.
// This class will be scanned (has [Display] on Name)
public class Product
{
[Display(Name = "Product Name")]
public string Name { get; set; } = string.Empty;
public string Sku { get; set; } = string.Empty; // Ignored by default
}
// This class will NOT be scanned (no DataAnnotation attributes)
public class InternalEntity
{
public int Id { get; set; }
public string Value { get; set; } = string.Empty;
}For each property with DataAnnotation attributes, the generator extracts:
- Display metadata (name, description, etc.)
- Validation rules (required, length constraints, etc.)
- Data type hints (email, phone, etc.)
- Entity metadata (key, editable, etc.)
The generator creates a nested static partial class hierarchy:
// Generated in: AnnotationConstants.MyNamespace.Product.g.cs
namespace MyNamespace;
[global::System.CodeDom.Compiler.GeneratedCode("Atc.SourceGenerators.AnnotationConstants", "1.0.0")]
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
[global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public static partial class AnnotationConstants
{
public static partial class Product
{
public static partial class Name
{
public const string DisplayName = "Product Name";
public const int MaximumLength = 100;
public const bool IsRequired = true;
}
public static partial class Price
{
public const string DisplayName = "Price";
public const string Minimum = "0.01";
public const string Maximum = "999999.99";
public static readonly System.Type OperandType = typeof(decimal);
}
}
}- Blazor/MAUI Forms - Generate form labels and validation rules without reflection
- API Documentation - Extract constraint metadata for OpenAPI/Swagger
- Client-Side Validation - Send validation rules to JavaScript/TypeScript
- Code Generation - Use constants in T4 templates or other generators
- Testing - Verify validation rules match expected values
- Native AOT Applications - Access metadata without breaking trimming
See Also: