Skip to content

[OptionsValidator] Source Generator for [Range] Ignores ErrorMessage and DisplayName #123137

Open

Description

@nw5428

Description

I have the below

    #pragma warning disable ASP0029 
    [ValidatableType]
    #pragma warning restore ASP0029
    public class WeatherForecast
    {
        public int? Id { get; set; }
        public required DateTime Date { get; set; }


        [Range(-20, 55, ErrorMessage = "Temperature must be between -20 and 55.")]
        public required int TemperatureC { get; set; }

        [Required(ErrorMessage = "Summary is required.")]
        public string? Summary { get; set; }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

        [ValidateObjectMembers]
        public SubWeatherForecast SubWeather { get; set; } = new();
    }

    #pragma warning disable ASP0029
    [ValidatableType]
    #pragma warning restore ASP0029
    public class SubWeatherForecast
    {
        [Required(ErrorMessage = "Name is required.")]
       public string? Name { get; set; }   
    }  

    [OptionsValidator]
    public partial class WeatherForecastValidator : IValidateOptions<WeatherForecast>
    {
    }

    [OptionsValidator]
    public partial class SubWeatherForecastValidator : IValidateOptions<SubWeatherForecast>
    {
    }

When this is triggered through Minimal API I get the correct error message of Temperature must be between -20 and 55.

However, when I execute through:

            var validator = new WeatherForecastValidator();

            var result = validator.Validate(null, weatherForecast);

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

            var validator = new WeatherForecastValidator();
            var result = validator.Validate(null, weatherForecast);

Expected behavior

validator.Validate(null, weatherForecast); is processed in the same way that Minimal API is processed.

Actual behavior

The ErrorMessage in [Range] is not respected with validator.Validate and sub classes cut off the name of the key.

Regression?

No response

Known Workarounds

Don't use validator.Validate.

Configuration

No response

Other information

No response

Activity

  1. dotnet-policy-service commented on Jan 13, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-componentmodel-dataannotations
    See info in area-owners.md if you want to be subscribed.

  2. mguinness commented on Jan 27, 2026

    @mguinness

    Repro

    #:sdk Microsoft.NET.Sdk.Web
    #:property PublishAot=false
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Options;
    using Microsoft.Extensions.Validation;
    using System.ComponentModel.DataAnnotations;
    
    var builder = WebApplication.CreateBuilder();
    
    builder.Services.AddValidation();
    
    var app = builder.Build();
    
    app.MapPost("", ([FromForm] WeatherForecast weatherForecast) =>
    {
        return $"Forecast is '{weatherForecast.Summary}' with temp {weatherForecast.TemperatureC}C.";
    }).DisableAntiforgery();
    
    app.MapPost("validator", ([FromForm] WeatherForecast weatherForecast) =>
    {
        var validator = new WeatherForecastValidator();
        var result = validator.Validate(null, weatherForecast);
    
        if (result.Succeeded)
        {
            return $"Forecast is '{weatherForecast.Summary}' with temp {weatherForecast.TemperatureC}C.";
        }
        else
        {
            return result.FailureMessage;
        }
    }).DisableAntiforgery().DisableValidation();
    
    app.Run();
    
    #pragma warning disable ASP0029
    [ValidatableType]
    #pragma warning restore ASP0029
    public class WeatherForecast
    {
        public int? Id { get; set; }
        public required DateTime Date { get; set; }
    
        [Range(-20, 55, ErrorMessage = "Temperature must be between -20 and 55.")]
        public required int TemperatureC { get; set; }
    
        [Required(ErrorMessage = "Summary is required.")]
        public string? Summary { get; set; }
    
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    
        [ValidateObjectMembers]
        public SubWeatherForecast SubWeather { get; set; } = new();
    }
    
    #pragma warning disable ASP0029
    [ValidatableType]
    #pragma warning restore ASP0029
    public class SubWeatherForecast
    {
        [Required(ErrorMessage = "Name is required.")]
        public string? Name { get; set; }
    }
    
    [OptionsValidator]
    public partial class WeatherForecastValidator : 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.

  3. added this to the Future milestone on Mar 26, 2026
  4. dotnet-policy-service commented on Mar 26, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-extensions-options
    See info in area-owners.md if you want to be subscribed.

  5. RichardD2 commented on Mar 26, 2026

    @RichardD2

    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.

  6. added
    untriagedNew issue has not been triaged by the area owner
    on Mar 26, 2026
  7. RichardD2 commented on Mar 26, 2026

    @RichardD2

    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.

  8. Laurianti commented on Oct 16, 2026

    @Laurianti

    I'd like to take the ErrorMessage part of this.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions