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

4 Answers 4

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/

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, you saved my day! I was hoping I didn't have to write so much 'glue code' though..
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.

1 Comment

Great idea, I was doing this before for other resources because if this problem - stackoverflow.com/a/58155986/2800222 so just reused it for custom validation attributes. Thanks for inspiration.
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.

Comments

-1

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

Check this link

1 Comment

ErrorMessageString should already be the localized value. See the documentation msdn.microsoft.com/it-it/library/….

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.