1

What I was doing with ASP.NET MVC 5

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MaxLengthAttribute), typeof(MyMaxLengthAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(MyRequiredAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MinLengthAttribute), typeof(MyMinLengthAttribute));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAddressAttribute), typeof(MyEmailAddressAttributeAdapter));

Now I'm migrating it to ASP.NET core 6

We can't use DataAnnotationsModelValidatorProvider anymore so I'm trying to use IValidationAttributeAdapterProvider, which doesn't work properly for me.

My codes

My IValidationAttributeAdapterProvider is below.

public class MyValidationAttributeAdapterProvider : ValidationAttributeAdapterProvider, IValidationAttributeAdapterProvider
{
    IAttributeAdapter? IValidationAttributeAdapterProvider.GetAttributeAdapter(
        ValidationAttribute attribute,
        IStringLocalizer? stringLocalizer)
    {
        return attribute switch
        {
            EmailAddressAttribute => new MyEmailAddressAttributeAdapter((EmailAddressAttribute)attribute, stringLocalizer),
            MaxLengthAttribute => new MyMaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer),
            MinLengthAttribute => new MyMinLengthAttribute((MinLengthAttribute)attribute, stringLocalizer),
            RequiredAttribute => new MyRequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer),
            _ => base.GetAttributeAdapter(attribute, stringLocalizer),
        };
    }
}

My model class is below.

public class LogInRequestDTO
{
    [Required]
    [EmailAddress]
    [MaxLength(FieldLengths.Max.User.Mail)]
    [Display(Name = "mail")]
    public string? Mail { get; set; }

    [Required]
    [MinLengthAttribute(FieldLengths.Min.User.Password)]
    [DataType(DataType.Password)]
    [Display(Name = "password")]
    public string? Password { get; set; }
}

And in my Program.cs, I do like below.

builder.Services.AddControllersWithViews()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Resources));
    });

builder.Services.AddSingleton<IValidationAttributeAdapterProvider, MyValidationAttributeAdapterProvider>();

What happed to me

I expect GetAttributeAdapter is called for each attribute like EmailAddressAttribute, MaxLengthAttribute, etc. But it's called only once with EmailAddressAttribute. So, all other validation results are not customized by my adaptors.

If I remove [EmailAddress] from the model class, GetAttributeAdapter is never called.

Am I missing something?

Added on 2022/05/24

What I want to do

  • I want to customize all the validation error message.
  • I don't want to customize for one by one at the place I use [EmailAddress] for example.
  • I need the server side validation only. I don't need the client side validation.

Reproducible project

I created the minimum sample project which can reproduce the problem. https://github.com/KuniyoshiKamimura/IValidationAttributeAdapterProviderSample

  1. Open the solution with Visual Studio 2022(17.2.1).
  2. Set the breakpoint on MyValidationAttributeAdapterProvider.
  3. Run the project.
  4. Input something to the textbox on the browser and submit it.
  5. The breakpoint hits only once with EmailAddressAttribute attribute.
  6. The browser shows the customized message for email and default message for all other validations.
  • Set a breakpoint in` MyValidationAttributeAdapterProvider` to see if you enter each `AttributeAdapter `to check whether other `AttributeAdapter`all right or not, and I want to ask if you are here for front-end validation or back-end validation. If it is front-end validation, can you provide the relevant js code? – Qing Guo May 23 '22 at 09:58
  • @QingGuo Thank you for your comment. I added some information to the original post. – Kuniyoshi Kamimura May 24 '22 at 12:11
  • Ok , I will see your link demo. – Qing Guo May 24 '22 at 12:15
  • "The breakpoint hits only once with EmailAddressAttribute attribute" What do you mean? Other Attribute and `AttributeAdapter` didn't hit at first? I try the demo with my custom Attribute and it donot appear your problem. Could you share your Attribute? – Qing Guo May 24 '22 at 13:29
  • @QingGuo Thank you for checking my project. I just want to change the messages for `RequiredAttribute` and so on. They are set on `SampleDTO`. I don't want to use custom attribute so I don't have one to share to you. I updated the project to clarify the problem. Please check the changes on the latest commit. Did you see the error messages which I can't see? – Kuniyoshi Kamimura May 24 '22 at 23:21

2 Answers2

1

Below is a work demo, you can refer to it.

In all AttributeAdapter, change your code like below.

public class MyEmailAddressAttributeAdapter : AttributeAdapterBase<EmailAddressAttribute>
    {
        // This is called as expected.
        public MyEmailAddressAttributeAdapter(EmailAddressAttribute attribute, IStringLocalizer? stringLocalizer)
           : base(attribute, stringLocalizer)
        {
            //attribute.ErrorMessageResourceType = typeof(Resources);
            //attribute.ErrorMessageResourceName = "ValidationMessageForEmailAddress";
            //attribute.ErrorMessage = null;
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-must-be-true", GetErrorMessage(context));
        }

        // This is called as expected.
        // And I can see the message "Input the valid mail address.".
        public override string GetErrorMessage(ModelValidationContextBase validationContext)
        {
            return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
        }
    }

In homecontroller:

public IActionResult Index()
    {
        return View();
    }
    [HttpPost]
    public IActionResult Index([FromForm][Bind("Test")] SampleDTO dto)
    {
        return View();
    }

Index view:

@model IV2.Models.SampleDTO

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<h4>SampleDTO</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Index">
           <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Test" class="control-label"></label>
                <input asp-for="Test" class="form-control" />
                <span asp-validation-for="Test" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

Result1:

enter image description here

Result2:

enter image description here

Qing Guo
  • 6,041
  • 1
  • 2
  • 10
  • Thank you so much. I confirmed it worked. But as I said before, I don't need the client side validation. So the key point is only to add `@model SampleDTO` and `asp-for="Test"` into ``, right? I pushed the commit that server-side validation messages works as expected and has no client-side validation. – Kuniyoshi Kamimura May 25 '22 at 13:10
  • BTW, how does the `asp-for` fix the problem? I need to know the mechanism because I'm not actually using cshtml(server-side rendering) but React(client-side rendering). (And that's why I don't need client-side validation of ASP.NET core.) So I can't use `asp-for` in my real project. In my old days I was using ASP.NET MVC 5, there wasn't `asp-for` and the validation messages had been correctly changed. Why does ASP.NET core needs `asp-for` and what does it do? How can I enable this mechanism without `asp-for`? – Kuniyoshi Kamimura May 25 '22 at 13:11
  • Also, I'm just wondering why was `MyEmailAddressAttributeAdapter` working without `asp-for` and others were not. I found that `MyValidationAttributeAdapterProvider` was called when the action which gets the parameter is called if I don't set `asp-for`, while it's called just after `View()` for form cshtml (not the result cshtml) is called if I set `asp-for`. But I'm not sure if it's related to it. – Kuniyoshi Kamimura May 25 '22 at 13:36
0

I found the solution.

What I have to use is not ValidationAttributeAdapterProvider but IValidationMetadataProvider.

This article describes the usage in detail. Note that some attributes including EmailAddressAttribute have to be treated in special way as describe here because they have default non-null ErrorMessage.

I confirmed for EmailAddressAttribute and some other attributes.

Also, there's the related article here.