To ensure the long-term sustainability of this project, users of this package who generate revenue must pay an Open Source Maintenance Fee. While the source code is freely available under the terms of the License, this package and other aspects of the project require adherence to the Maintenance Fee.
To pay the Maintenance Fee, become a Sponsor at the proper OSMF tier. A single fee covers all of Devlooped packages.
System.ComponentModel.DataAnnotations finds attributes and property values by reflection. Native AOT and trimming keep a member only when static analysis can see a use, so Validator warns on a trimmed or AOT publish and can miss the properties it was meant to check. The framework has no AOT-safe replacement for object validation. The generators that ship with .NET cover options classes, Minimal APIs, and Blazor. A library or console app that validates its own objects sits outside them, and Validator has no API for method arguments or constructors.
This package is that layer. The attributes stay on the members. A source generator turns each rule into a direct call with the attribute arguments inlined, so the published app keeps the check, the bounds, the display name, and any custom validator method.
Devlooped.DataAnnotations.NativeValidation is AOT-safe validation for System.ComponentModel.DataAnnotations attributes. Reference the package. The APIs live in the Devlooped.DataAnnotations namespace. A source generator reads the attributes during compilation, emits the checks, and registers them from a module initializer. Generated code does not instantiate validation attributes and does not reflect over members.
NativeValidator follows Validator:
TryValidateObject/ValidateObjectandTryValidateProperty/ValidatePropertytake aValidationContext.Try*returns false and appends eachValidationResult.Validate*throwsValidationExceptionfor the first failure. A null results collection stops at the first failure.TryValidateObjectwithout the bool checks required properties only.TryValidate/Validate(instance, method, arguments)run the rules registered for a method or constructor. Pass theMethodBaseand the arguments in parameter order. A member with no registered rules succeeds.Required,StringLength, and the other per-check methods are theIsValidlayer. A bad value returnsValidationResult, including a value of the wrong type. Malformed bounds throwInvalidOperationException.
Property failures skip type-level rules and IValidatableObject. Object validation is not recursive, matching Validator. Walk nested objects in the caller.
Pass a display name into ValidationContext. The overloads that omit it are the ones marked RequiresUnreferencedCode. Built-in checks use the display name the generator copied from [Display(Name = ...)] or [DisplayName].
using Devlooped.DataAnnotations;
public class Account
{
[Required]
[EmailAddress]
[Display(Name = "Email address")]
public string? Email { get; set; }
public void Rename([Required, StringLength(8, MinimumLength = 2)] string? name, [Range(1, 10)] int attempt) { }
}
var results = new List<ValidationResult>();
var context = new ValidationContext(account, "Account", null, null);
var valid = NativeValidator.TryValidateObject(account, context, results, validateAllProperties: true);
NativeValidator.ValidateProperty(
" ",
new ValidationContext(account, "Email address", null, null) { MemberName = nameof(Account.Email) });
var rename = typeof(Account).GetMethod(nameof(Account.Rename))!;
NativeValidator.TryValidate(account, rename, results, "ada", 2);
NativeValidator.Validate(account, rename, "a", 1);Type.GetMethod has its own trim annotation. Object and property validation do not need it. Argument validation matches the MethodBase you already hold.
The same checks are available when the attribute arguments are already in hand:
NativeValidator.Required(value, "Email", "Email address", allowEmptyStrings: false, errorMessage: null);[MinLength], [MaxLength], [Length], [Range(typeof(T), ...)], and [Compare] still reference constructors the BCL marks RequiresUnreferencedCode, so the trim analyzer warns at the declaration. The generator reads those arguments at compile time and never runs the constructors. Suppress IL2026 on the model when every check goes through NativeValidator. The sample order does this. src/Sample publishes with PublishAot, and its build runs the trim and AOT analyzers over the generated registrations.
NativeValidation.Generator ships in the package at analyzers/dotnet/cs. It emits Devlooped.Generated.NativeValidationRegistrations when the compilation can see Devlooped.DataAnnotations.NativeValidation. A module initializer calls one register method per closed type.
Each annotated member is stored under its declaring type, metadata name (set_Email, Rename, .ctor), and NativeValidationSignature (comma-separated parameter types). Object validation uses a second table. RegisterProperties stores the property name, type, and a getter lambda, so the value is read by a direct property access. Type-level attributes go through RegisterType. Interface implementations and overrides contribute attributes declared on the interface or base member. [MetadataType] buddy classes are included. After the generated rules pass, IValidatableObject.Validate is an interface call.
The enforced attributes are Required, StringLength, MinLength, MaxLength, Length, Range, RegularExpression, Compare, EmailAddress, Phone, CreditCard, Url, EnumDataType, AllowedValues, DeniedValues, Base64String, FileExtensions, and CustomValidation.
Each one becomes a call into NativeValidator with the arguments inlined:
- Length checks take a
Countlambda when the value type has a publicCountproperty.stringandICollectionuse their own length. Range(typeof(T), minimum, maximum)parses withTryParseon the closed operand type, includingDateOnlyand enums.Comparereads the other property with a direct cast to its declaring type.RegularExpressioncompiles one staticRegexfor the pattern and timeout.CustomValidationcalls the public static method directly and builds a trim-safeValidationContextwhen that method takes one.[Display(Name = ...)]and[DisplayName]are copied into the call.ErrorMessageis copied as a string.ErrorMessageResourceNamereads that public static string property.
[DataType] is a UI hint and is not a check. EmailAddress, Phone, CreditCard, and Url are.
Any other ValidationAttribute subclass warns as NVA001 and is not enforced. Virtual IsValid is not itself trim-annotated. The generator inlines the built-in checks whose implementations reflect, and it does not construct an arbitrary attribute to call IsValid. NVA002 reports a supported attribute the generator had to skip: a Range with no closed operand type, a CustomValidation method that is not a single public static ValidationResult method, a resource property it cannot read, an out parameter, a ref struct, a pointer, and the same class of problem.
Static members are skipped. Instance properties are registered through the setter, including non-virtual members. Methods and constructors are registered the same way.
The options generator reports SYSLIB1201 for open generic member types. ASP.NET's generator includes a type it cannot see from a signature only when that type carries [ValidatableType]. This generator registers a type when an annotated member's declaring type does not depend on a type parameter. Members declared on open Box<T> are skipped, because that declaration has no closed type to emit. Naming Box<int> in ordinary source, including new Box<int>(), does not register it either. A closed type that appears only at run time (MakeGenericType, or a factory the walk cannot see) is the same gap.
Validated on a generic factory registers every closed type argument and the closed return type at each call site in the compilation. Create<int>() on [Validated] static Box<T> Create<T>() registers Box<int>. A generic method annotated with any attribute derived from ValidationAttribute is recognized the same way, as if it also carried [Validated]. A wrapper that only writes Create<T>() does not close T. Mark the wrapper [Validated] as well, or annotate that generic method with a ValidationAttribute subclass. The generator substitutes call-site arguments through these methods, up to eight levels, and registers the constructed return type plus its base types and interfaces.
public class Box<T>
{
[Required]
public T? Value { get; set; }
}
public static class Boxes
{
[Validated]
public static Box<T> Create<T>() => new();
[Validated]
public static object Make<T>() => Create<T>();
}
// Make<int>() registers Box<int>.[assembly: Validate<T>] registers one closed type when no factory call in the compilation closes it. Repeat the attribute for each closed type. Use it when the closed type is only built from a run-time Type.
[assembly: Validate<Box<string>>]What follows is the state of the art in .NET as of October 9, 2026.
Validator discovers attributes and properties by reflection. Native AOT and trimming keep a member only when static analysis can see a use, so the BCL marks those entry points RequiresUnreferencedCode. Calling them in a trimmed or AOT publish reports IL2026.
TryValidateProperty is annotated "The Type of validationContext.ObjectType cannot be statically discovered." TryValidateObject carries the same annotation as the ValidationContext constructors that omit a display name:
Constructing a ValidationContext without a display name is not trim-safe because it uses reflection to discover the type of the instance being validated in order to resolve the DisplayNameAttribute when a display name is not provided.
The constructor that takes the display name is trim-safe. .NET 10 added that overload so a caller can name the display string without the warning. Attribute discovery is a separate path. ValidationAttributeStore loads properties with TypeDescriptor.GetProperties and attributes with TypeDescriptor.GetAttributes. TypeDescriptor.GetProperties is annotated because a PropertyDescriptor's PropertyType cannot be statically discovered, and after trimming that call can return no properties (dotnet/runtime#101202). TryValidateObject on the .NET 10 API is still RequiresUnreferencedCode. The BCL has no AOT-safe replacement for it. A shared generator on System.ComponentModel.Annotations was discussed in 2024 and was not a committed design.
Several attribute constructors are trim-unsafe on their own, so the warning appears on the model even when application code never calls Validator:
MinLengthAttribute,MaxLengthAttribute, andLengthAttributereflect for aCountproperty on types that do not implementICollection. Trimming can remove that property.RangeAttribute(Type, string, string)looks up aTypeConverter. Its annotation calls out generic converters such asNullableConverter, which needDynamicallyAccessedMembers.CompareAttributeis annotated "The property referenced by 'otherProperty' may be trimmed."IsValidcallsGetRuntimePropertyandGetValue.
CustomValidationAttribute keeps the validator type's public methods through DynamicallyAccessedMembers, then IsValid finds the method with GetMethods and calls it through MethodInfo.Invoke.
The platform generators cover the hosts that own them. The options validation generator emits IValidateOptions<T> for a partial class marked [OptionsValidator]. The generated Validate method still calls Validator.TryValidateValue and new ValidationContext, and it replaces Range, MinLength, MaxLength, and Length with generated attribute subclasses because those types rely on reflection. It suppresses IL2026 on the context with the justification that the object never takes the reflection path. ASP.NET Core validation generates metadata for Minimal APIs and Blazor in the assembly that calls AddValidation. It does not cover MVC or Razor Pages. Without that metadata, Blazor's DataAnnotationsValidator falls back to Validator, and that fallback checks top-level properties only. The attributes those generators still consume are the open gap: not every ValidationAttribute is Native AOT compatible. Library trimming guidance treats this class of API the way it treats reflection-based serializers: keep the declarative surface, and generate the code that runs.
A Native AOT library or console app that validates its own objects, method arguments, or constructors sits outside those generators. Validator has no method or constructor API. This package is that layer. The attributes stay on the members. Each check becomes a direct call with the attribute arguments inlined, so the trimmer sees the check, the bounds, the display name, and the custom validator method.