ASP.NET Core custom validation attribute localization
Asked Answered
B

4

21

I'm trying to implement localization in a custom validation attribute in asp.net core 1.0. This is my simplified viewmodel:

public class EditPasswordViewModel
{
    [Required(ErrorMessage = "OldPasswordRequired")]
    [DataType(DataType.Password)]
    [CheckOldPassword(ErrorMessage = "OldPasswordWrong")]
    public string OldPassword { get; set; }
}

The localization of "OldPasswordRequired" is working fine. However the localization of my custom attribute is not working and returns always "OldPasswordWrong" message. This is the code:

public class CheckOldPasswordAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object classInstance, ValidationContext validationContext)
    {                   
        if (oldPasswordSaved == oldPasswordTyped) //simplified
        {
            return ValidationResult.Success;
        }
        else
        {
            string errorMessage = FormatErrorMessage(ErrorMessageString);
            return new ValidationResult(errorMessage);
        }
    }

}

ErrorMessageString is always "OldPasswordWrong" and FormatErrorMessage returns always "OldPasswordWrong". What am I doing wrong? I'm using the new asp.net core data annotations localizations, so I'm not using ErrorMessageResourceName and ErrorMessageResourceType attributes (I don't have any ViewModel.Designer.cs).

Barnard answered 14/3, 2017 at 11:6 Comment(0)
M
28

Implement an adapter for localization:

public class RequiredIfAttributeAdapter : AttributeAdapterBase<RequiredIfAttribute>
{
    public RequiredIfAttributeAdapter(RequiredIfAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) {}

    public override void AddValidation(ClientModelValidationContext context) {}

    public override string GetErrorMessage(ModelValidationContextBase validationContext)
    {
        return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
    }
}

Implement a provider for the adapter(s):

public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
    private readonly IValidationAttributeAdapterProvider _baseProvider = new ValidationAttributeAdapterProvider();

    public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
    {
        if (attribute is RequiredIfAttribute)
            return new RequiredIfAttributeAdapter(attribute as RequiredIfAttribute, stringLocalizer);
        else
            return _baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
    }
}

Register the provider in Startup.cs:

services.AddSingleton<IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();

Credits to this blog: https://blogs.msdn.microsoft.com/mvpawardprogram/2017/01/03/asp-net-core-mvc/

Mcgowen answered 24/3, 2017 at 14:24 Comment(1)
Thanks, you saved my day! I was hoping I didn't have to write so much 'glue code' though..Barnard
S
6

The answer from Ramin is the correct answer. But I decided to take another path, so I don't have to write adapters and adapter providers for many cases.

The idea is to wrap your specific string localizer in a service interface, and get it from the validation attribute itself.

public class CPFAttribute: ValidationAttribute
{
    public CPFAttribute()
    {
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        string cpf;

        try
        {
            cpf = (string)value;
        }
        catch (Exception)
        {
            return new ValidationResult(GetErrorMessage(validationContext));
        }

        if (string.IsNullOrEmpty(cpf) || cpf.Length != 11 || !StringUtil.IsDigitsOnly(cpf))
        {
            return new ValidationResult(GetErrorMessage(validationContext));
        }

        return ValidationResult.Success;
    }

    private string GetErrorMessage(ValidationContext validationContext)
    {
        if (string.IsNullOrEmpty(ErrorMessage))
        {
            return "Invalid CPF";
        }

        ErrorMessageTranslationService errorTranslation = validationContext.GetService(typeof(ErrorMessageTranslationService)) as ErrorMessageTranslationService;
        return errorTranslation.GetLocalizedError(ErrorMessage);
    }
}

Then the service can be created as:

public class ErrorMessageTranslationService
{
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;
    public ErrorMessageTranslationService(IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _sharedLocalizer = sharedLocalizer;
    }

    public string GetLocalizedError(string errorKey)
    {
        return _sharedLocalizer[errorKey];
    }
}

The service can be registered as a singleton, in the Startup class.

services.AddSingleton<ErrorMessageTranslationService>();

If these validation attributes need to be factored to another assembly, just create an interface for this translation service that can be referenced by all validation attributes you create.

Skitter answered 29/9, 2019 at 14:6 Comment(1)
Great idea, I was doing this before for other resources because if this problem - https://mcmap.net/q/601625/-asp-net-core-custom-validation-attribute-localization so just reused it for custom validation attributes. Thanks for inspiration.Trinitrobenzene
P
1

As a little deviation to the Marcos's answer, in case you are using Asp.Net Core along with the AddDataAnnotationsLocalization() method inside the program.cs or startup.cs:
You can get the localizer as follows:

sealed public class MyCustomValidationAttribute : ValidationAttribute
{
    private static IStringLocalizer localizer;

    //...
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        //...
        return new ValidationResult(GetErrorMessage(validationContext));
        //...
    }

    private string GetErrorMessage(ValidationContext validationContext)
    {
        //...
        return GetLocalizer(validationContext)[ErrorMessage];
    }

    private IStringLocalizer GetLocalizer(ValidationContext validationContext)
    {
        if (localizer is null)
        {
            var factory = validationContext.GetRequiredService<IStringLocalizerFactory>();
            var annotationOptions = validationContext.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
            localizer = annotationOptions.Value.DataAnnotationLocalizerProvider(validationContext.ObjectType, factory);
        }

        return localizer;
    }

    //...
}

Drop the ErrorMessageTranslationService mentioned in the Marcos's answer class and use GetLocalizer() instead to get the LocalizedString that is used to localize other normal annotations throughout your project.

Polestar answered 3/3, 2023 at 21:18 Comment(0)
P
-1

You need to specify the culture. What does formaterrormesage will do? Will it handle the culture?

Check this link

Peggiepeggir answered 14/3, 2017 at 11:12 Comment(1)
ErrorMessageString should already be the localized value. See the documentation msdn.microsoft.com/it-it/library/….Barnard

© 2022 - 2024 — McMap. All rights reserved.