You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[OptionsValidator] Source Generator for [Range] Ignores ErrorMessage and DisplayName #123137
#pragma warning disable ASP0029[ValidatableType]
#pragma warning restore ASP0029publicclassWeatherForecast{publicint?Id{get;set;}publicrequiredDateTimeDate{get;set;}[Range(-20,55,ErrorMessage="Temperature must be between -20 and 55.")]publicrequiredintTemperatureC{get;set;}[Required(ErrorMessage="Summary is required.")]publicstring?Summary{get;set;}publicintTemperatureF=>32+(int)(TemperatureC/0.5556);[ValidateObjectMembers]publicSubWeatherForecastSubWeather{get;set;}=new();}
#pragma warning disable ASP0029[ValidatableType]
#pragma warning restore ASP0029publicclassSubWeatherForecast{[Required(ErrorMessage="Name is required.")]publicstring?Name{get;set;}}[OptionsValidator]publicpartialclassWeatherForecastValidator:IValidateOptions<WeatherForecast>{}[OptionsValidator]publicpartialclassSubWeatherForecastValidator:IValidateOptions<SubWeatherForecast>{}
When this is triggered through Minimal API I get the correct error message of Temperature must be between -20 and 55.
The error message reverts to default "The field WeatherForecast.TemperatureC must be between -20 and 55."
When I do the same thing with [Required(ErrorMessage = "Summary is required.")] it works correctly.
In addition when validating with Minimal API default the key on the sub class is correctly SubWeather.Name but when processing with validator.Validate the key is cut off and it is only "Name"
Reproduction Steps
Use the Dot Net 10 template, use minimal api end point, include builder.Services.Validation(). On the end point .DisableValidation(), then
#:sdkMicrosoft.NET.Sdk.Web
#:property PublishAot=false
using Microsoft.AspNetCore.Mvc;usingMicrosoft.Extensions.Options;usingMicrosoft.Extensions.Validation;usingSystem.ComponentModel.DataAnnotations;varbuilder=WebApplication.CreateBuilder();builder.Services.AddValidation();varapp=builder.Build();app.MapPost("",([FromForm]WeatherForecastweatherForecast)=>{return$"Forecast is '{weatherForecast.Summary}' with temp {weatherForecast.TemperatureC}C.";}).DisableAntiforgery();app.MapPost("validator",([FromForm]WeatherForecastweatherForecast)=>{varvalidator=newWeatherForecastValidator();varresult=validator.Validate(null,weatherForecast);if(result.Succeeded){return$"Forecast is '{weatherForecast.Summary}' with temp {weatherForecast.TemperatureC}C.";}else{returnresult.FailureMessage;}}).DisableAntiforgery().DisableValidation();app.Run();
#pragma warning disable ASP0029[ValidatableType]
#pragma warning restore ASP0029publicclassWeatherForecast{publicint?Id{get;set;}publicrequiredDateTimeDate{get;set;}[Range(-20,55,ErrorMessage="Temperature must be between -20 and 55.")]publicrequiredintTemperatureC{get;set;}[Required(ErrorMessage="Summary is required.")]publicstring?Summary{get;set;}publicintTemperatureF=>32+(int)(TemperatureC/0.5556);[ValidateObjectMembers]publicSubWeatherForecastSubWeather{get;set;}=new();}
#pragma warning disable ASP0029[ValidatableType]
#pragma warning restore ASP0029publicclassSubWeatherForecast{[Required(ErrorMessage="Name is required.")]publicstring?Name{get;set;}}[OptionsValidator]publicpartialclassWeatherForecastValidator:IValidateOptions<WeatherForecast>{}
Tests
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool" http://localhost:5000
Forecast is 'Cool' with temp 10C.
curl -d "Date=2000-01-01&TemperatureC=100&Summary=Cool" http://localhost:5000
{"title":"One or more validation errors occurred.","errors":{"TemperatureC":["Temperature must be between -20 and 55."]}}
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool&SubWeather.Name=" http://localhost:5000
{"title":"One or more validation errors occurred.","errors":{"SubWeather.Name":["Name is required."]}}
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool" http://localhost:5000/validator
Forecast is 'Cool' with temp 10C.
curl -d "Date=2000-01-01&TemperatureC=100&Summary=Cool&SubWeather.Name=Test" http://localhost:5000/validator
TemperatureC: The field WeatherForecast.TemperatureC must be between -20 and 55.
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool&SubWeather.Name=" http://localhost:5000/validator
Name: Name is required.
For some reason, the source generator synthesizes its own version of the Range attribute, rather than relying on the one from System.ComponentModel.Annotations. The synthesized attribute ignores any custom error message, and always returns a default message:
namespace __OptionValidationGeneratedAttributes
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "10.0.14.15411")]
[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)]
file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute
{
...
public override string FormatErrorMessage(string name) =>
string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum);
...
private string GetValidationErrorMessage()
{
return (MinimumIsExclusive, MaximumIsExclusive) switch
{
(false, false) => "The field {0} must be between {1} and {2}.",
(true, false) => "The field {0} must be between {1} exclusive and {2}.",
(false, true) => "The field {0} must be between {1} and {2} exclusive.",
(true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.",
};
}
...
}
}
Searching this repo for __SourceGen__ suggests the problem also affects:
LengthAttribute
MinLengthAttribute
MaxLengthAttribute
CompareAttribute
It may affect other attributes as well, but those are the ones that come up in the test projects in this repo.
The documentation explains why the attributes are replaced - although it makes no mention of the CompareAttribute.
I'm guessing the custom error message is ignored because it could potentially end up using reflection - specifically, if you use the ErrorMessageResourceType, the SetResourceAccessorByPropertyLookup method has to use reflection to locate the property or method used to return the error message.
But it's slightly annoying that the synthesized attributes ignore hard-coded, non-localized error messages. Particularly when other validation attributes respect the custom error messages, and could even end up triggering the reflection code path.
The generator replaces [Range] with a synthesized __SourceGen__RangeAttribute, and its FormatErrorMessage always uses the built-in message. So ErrorMessage and ErrorMessageResourceName/ErrorMessageResourceType are ignored. The fix is to use the built-in message only when none of them is set, as RangeAttribute does.
MaxLength, MinLength, Length and Compare already use ErrorMessageString, so only Range is affected.
The nested member name (SubWeather.Name reported as Name) is a separate change, and I'm leaving it out.
Activity
dotnet-policy-service commentedon Jan 13, 2026
Tagging subscribers to this area: @dotnet/area-system-componentmodel-dataannotations
See info in area-owners.md if you want to be subscribed.
mguinness commentedon Jan 27, 2026
Repro
Tests
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool" http://localhost:5000Forecast is 'Cool' with temp 10C.
curl -d "Date=2000-01-01&TemperatureC=100&Summary=Cool" http://localhost:5000{"title":"One or more validation errors occurred.","errors":{"TemperatureC":["Temperature must be between -20 and 55."]}}
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool&SubWeather.Name=" http://localhost:5000{"title":"One or more validation errors occurred.","errors":{"SubWeather.Name":["Name is required."]}}
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool" http://localhost:5000/validatorForecast is 'Cool' with temp 10C.
curl -d "Date=2000-01-01&TemperatureC=100&Summary=Cool&SubWeather.Name=Test" http://localhost:5000/validatorTemperatureC: The field WeatherForecast.TemperatureC must be between -20 and 55.
curl -d "Date=2000-01-01&TemperatureC=10&Summary=Cool&SubWeather.Name=" http://localhost:5000/validatorName: Name is required.
dotnet-policy-service commentedon Mar 26, 2026
Tagging subscribers to this area: @dotnet/area-extensions-options
See info in area-owners.md if you want to be subscribed.
RichardD2 commentedon Mar 26, 2026
For some reason, the source generator synthesizes its own version of the
Rangeattribute, rather than relying on the one fromSystem.ComponentModel.Annotations. The synthesized attribute ignores any custom error message, and always returns a default message:Searching this repo for
__SourceGen__suggests the problem also affects:LengthAttributeMinLengthAttributeMaxLengthAttributeCompareAttributeIt may affect other attributes as well, but those are the ones that come up in the test projects in this repo.
RichardD2 commentedon Mar 26, 2026
The documentation explains why the attributes are replaced - although it makes no mention of the
CompareAttribute.I'm guessing the custom error message is ignored because it could potentially end up using reflection - specifically, if you use the
ErrorMessageResourceType, theSetResourceAccessorByPropertyLookupmethod has to use reflection to locate the property or method used to return the error message.But it's slightly annoying that the synthesized attributes ignore hard-coded, non-localized error messages. Particularly when other validation attributes respect the custom error messages, and could even end up triggering the reflection code path.
Laurianti commentedon Oct 16, 2026
I'd like to take the
ErrorMessagepart of this.The generator replaces
[Range]with a synthesized__SourceGen__RangeAttribute, and itsFormatErrorMessagealways uses the built-in message. SoErrorMessageandErrorMessageResourceName/ErrorMessageResourceTypeare ignored. The fix is to use the built-in message only when none of them is set, asRangeAttributedoes.MaxLength,MinLength,LengthandComparealready useErrorMessageString, so onlyRangeis affected.The nested member name (
SubWeather.Namereported asName) is a separate change, and I'm leaving it out.