ASP.NET Core custom validation attribute localization

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/


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.