A C# source generator that automatically extracts values from data annotation
attributes and exposes them as strongly-typed constants. Access your
StringLength, Range, and Required attribute values at constants in your
classes.
When working with data annotations, you often need to reference validation constraints. A good way to solve it is to create constants. But it takes time to do and makes your data models harder to read. And it's harder when your models are auto generated like when you are scaffolding with Entity Framework.
This source generator creates the constants automatically for you. If you have this model:
public partial class Product
{
[Required]
[StringLength(100)]
public string? Name { get; set; }
[Required]
[Range(0.01, 999999.99)]
public decimal Price { get; set; }
}Constants will be generated that you can access like this:
// Name length constraints
int maxNameLength = Product.Annotations.Name.MaximumLength; // 100
bool nameRequired = Product.Annotations.Name.IsRequired; // true
// Price constraints
double minPrice = Product.Annotations.Price.Minimum; // 0.01
double maxPrice = Product.Annotations.Price.Maximum; // 999999.99There are two ways configure use DataAnnotationValuesExtractor depending on your needs:
Apply [DataAnnotationValues] directly to each class you want to generate
constants for:
[DataAnnotationValues(StringLength = true, Range = true, Required = true)]
public partial class Product
{
[Required]
[StringLength(100)]
public string? Name { get; set; }
[Required]
[Range(0.01, 999999.99)]
public decimal Price { get; set; }
public string? Sku { get; set; }
}Create a dummy class and use the DataAnnotationValuesToGenerate attribute for
each class you want to generate constants for. You can use the
DataAnnotationValuesConfiguration attribute to configure what to be generated.
using Pekspro.DataAnnotationValuesExtractor;
[DataAnnotationValuesConfiguration(StringLength = true, Range = true, Required = true)]
[DataAnnotationValuesToGenerate(typeof(Customer))]
[DataAnnotationValuesToGenerate(typeof(Order))]
[DataAnnotationValuesToGenerate(typeof(Product))]
partial class ValidationConstants
{
}Your model classes remain unchanged.
This approach is especially useful when working with auto-generated models, such as those created by Entity Framework scaffolding. If you do, and you have all your models in a folder, you can use this PowerShell script to generate the attributes for all models in that folder:
Get-ChildItem -Filter '*.cs' |
Where-Object { -not ($_.BaseName -match '(?i)context') } |
ForEach-Object { "[DataAnnotationValuesToGenerate(typeof($($_.BaseName)))]" } |
Set-ClipboardNo matter which approach your are using, you can access generated constants like this:
// Name length constraints
int maxNameLength = Product.Annotations.Name.MaximumLength; // 100
int minNameLength = Product.Annotations.Name.MinimumLength; // 0
bool nameRequired = Product.Annotations.Name.IsRequired; // true
// Price constraints
double minPrice = Product.Annotations.Price.Minimum; // 0.01
double maxPrice = Product.Annotations.Price.Maximum; // 999999.99
bool priceRequired = Product.Annotations.Price.IsRequired;
// Sky constraints
bool skuRequired = Product.Annotations.Sku.IsRequired; // falseAdd the package to your project:
dotnet add package Pekspro.DataAnnotationValuesExtractorFor optimal setup, configure the package reference in your .csproj to exclude
it from your output assembly:
<ItemGroup>
<PackageReference Include="Pekspro.DataAnnotationValuesExtractor" Version="0.0.1"
PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>Why these attributes?
PrivateAssets="all"- Projects referencing yours won't inherit this package, they don't need it.ExcludeAssets="runtime"- The attributes DLL won't be copied to your build output, this is also not needed.
Both [DataAnnotationValues] and [DataAnnotationValuesConfiguration] support
the following properties to control which constants are generated:
| Property | Default | Generated Constants | Description |
|---|---|---|---|
StringLength |
true |
MaximumLength, MinimumLength |
Extract values from [StringLength] attribute. |
Range |
true |
Minimum, Maximum, MinimumIsExclusive, MaximumIsExclusive |
Extract values from [Range] attribute. |
Required |
false |
IsRequired |
Detect presence of [Required] attribute. |
Do you miss some annotations? Create an issue and let me know.
There are two ways to inspect the generated source code:
Right-click on your class name and select Go to Definition (F12). Visual Studio will show you the generated partial class.
Add this to your .csproj file to save generated files to disk:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\$(Configuration)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>Generated files will be saved to obj\[Configuration]\GeneratedFiles\
directory.
Given this input:
[DataAnnotationValues(StringLength = true, Range = true, Required = true)]
public partial class Player
{
[Required]
[StringLength(50)]
public string? Name { get; set; }
[StringLength(100, MinimumLength = 6)]
public string? Email { get; set; }
[Range(1, 100)]
public int Score { get; set; }
}The generator produces:
public partial class Player
{
/// <summary>
/// Data annotation values.
/// </summary>
public static class Annotations
{
/// <summary>
/// Data annotation values for Name.
/// </summary>
public static class Name
{
/// <summary>
/// Maximum length for Name.
/// </summary>
public const int MaximumLength = 50;
/// <summary>
/// Minimum length for Name.
/// </summary>
public const int MinimumLength = 0;
/// <summary>
/// Indicates whether Name is required.
/// </summary>
public const bool IsRequired = true;
}
/// <summary>
/// Data annotation values for Email.
/// </summary>
public static class Email
{
/// <summary>
/// Maximum length for Email.
/// </summary>
public const int MaximumLength = 100;
/// <summary>
/// Minimum length for Email.
/// </summary>
public const int MinimumLength = 6;
/// <summary>
/// Indicates whether Email is required.
/// </summary>
public const bool IsRequired = false;
}
/// <summary>
/// Data annotation values for Score.
/// </summary>
public static class Score
{
/// <summary>
/// Minimum value for Score.
/// </summary>
public const int Minimum = 1;
/// <summary>
/// Maximum value for Score.
/// </summary>
public const int Maximum = 100;
/// <summary>
/// Indicates whether the minimum value for Score is exclusive.
/// </summary>
public const bool MinimumIsExclusive = false;
/// <summary>
/// Indicates whether the maximum value for Score is exclusive.
/// </summary>
public const bool MaximumIsExclusive = false;
/// <summary>
/// Indicates whether Score is required.
/// </summary>
public const bool IsRequired = false;
}
}
}- .NET SDK 7.0 or later - Required for the source generator to run
- Target Framework - Can target .NET Core 3.1, .NET Standard 2.0, or any later framework
- Partial Classes - Generated code uses partial classes, so it must be in the same assembly as your models
Note: You need .NET 7 SDK installed, but your project can target earlier frameworks.
Contributions are welcome! Please feel free to submit issues or pull requests on GitHub.
This project is heavily inspired by the NetEscapades.EnumExtractors project.
This project is licensed under the MIT License. See the LICENSE file for details.