New Data Annotations in .NET 8
.NET 8 introduces several new data annotation attributes for model validation that provide more control over property constraints.
New Attributes Overview
Length Attribute
Specifies both minimum and maximum length for strings:
public class Product
{
[Length(2, 30)]
public string Name { get; set; }
[Length(2, 255)]
public string Description { get; set; }
}
Usage: Replaces the need for both [MinLength] and [MaxLength] on the same property.
Range Attribute with Exclusive Bounds
The Range attribute now supports exclusive minimum and maximum values:
public class Product
{
[Range(1, 1000, MinimumIsExclusive = true, MaximumIsExclusive = false)]
public decimal Price { get; set; }
}
Parameters:
MinimumIsExclusive = true: Value must be > minimum (not >=)MaximumIsExclusive = false: Value can be <= maximum
This means Price must be > 1 and <= 1000.
AllowedValues Attribute
Restricts a property to a specific set of allowed values:
public class Product
{
[AllowedValues("S", "M", "L", "XL", "XXL")]
public string Size { get; set; }
}
Use Case: Validates against an enum-like set of valid options without using an actual enum.
DeniedValues Attribute
Opposite of AllowedValues - specifies values that are NOT allowed:
public class Product
{
[DeniedValues("Electronics", "Computers")]
public string Category { get; set; }
}
Use Case: Blacklisting certain values while allowing everything else.
Base64String Attribute
Validates that a string is valid Base64 format:
public class Product
{
[Base64String]
public string Image { get; set; }
}
Use Case: Ensuring uploaded image data or encoded content is properly formatted.
Complete Example
public class Product
{
public int Id { get; set; }
[Length(2, 30)]
[Required]
public string Name { get; set; }
[Length(2, 255)]
public string Description { get; set; }
[Range(1, 1000, MinimumIsExclusive = true, MaximumIsExclusive = false)]
public decimal Price { get; set; }
[AllowedValues("S", "M", "L", "XL", "XXL")]
public string Size { get; set; }
[DeniedValues("Discontinued", "Recalled")]
public string Status { get; set; }
[Base64String]
public string ImageData { get; set; }
}
Using with ASP.NET Core
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
// ModelState automatically validates data annotations
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Product is valid, proceed with creation
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
Custom Validation Messages
public class Product
{
[Length(2, 30, ErrorMessage = "Name must be between 2 and 30 characters")]
public string Name { get; set; }
[AllowedValues("S", "M", "L", "XL", "XXL",
ErrorMessage = "Size must be S, M, L, XL, or XXL")]
public string Size { get; set; }
}
Combining with FluentValidation
For complex scenarios, combine data annotations with FluentValidation:
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.Length(2, 30)
.Must(NotContainSpecialCharacters)
.WithMessage("Name cannot contain special characters");
RuleFor(x => x.Price)
.GreaterThan(0)
.LessThanOrEqualTo(1000);
}
private bool NotContainSpecialCharacters(string name)
{
return name.All(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c));
}
}
Best Practices
- Use data annotations for simple validation - Quick, declarative, works with model binding
- Use FluentValidation for complex rules - Better for cross-property validation, conditional logic
- Always validate on both client and server - Never trust client-side validation alone
- Provide meaningful error messages - Help users understand what went wrong
Sources
Arhitectura/data annotations.jpeg