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).

abatishchev
  • 98,240
  • 88
  • 296
  • 433
rickyalbert
  • 2,552
  • 4
  • 21
  • 31

4 Answers4

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/

Ramin
  • 390
  • 3
  • 6
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.

  • Great idea, I was doing this before for other resources because if this problem - https://stackoverflow.com/a/58155986/2800222 so just reused it for custom validation attributes. Thanks for inspiration. – Vočko Sep 28 '20 at 01:25
0

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.

Rzassar
  • 2,117
  • 1
  • 33
  • 55
-1

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

Check this link

  • ErrorMessageString should already be the localized value. See the documentation https://msdn.microsoft.com/it-it/library/system.componentmodel.dataannotations.validationattribute.errormessagestring(v=vs.110).aspx. – rickyalbert Mar 14 '17 at 11:24