Home > Enterprise >  How do you centralise ASP.NET Core Web API attribute validation on multiple DTOs with similar proper
How do you centralise ASP.NET Core Web API attribute validation on multiple DTOs with similar proper

Time:01-20

Is there a way to centralise the model validation of the same property name across multiple DTOs?

For example, if I have the following classes to be used as the request body in a Web API action.

public class RegisterRequest
{
    [Required]
    [EmailAddress]
    public string EmailAddress { get; set; } = null!;

    [Required]
    [MinLength(8)]
    [RegularExpression(UserSettings.PasswordRegex)]
    public string Password { get; set; } = null!;

    [Required]
    [MaxLength(100)]
    public string DisplayName { get; set; } = null!;
}
public class UserProfileRequest
{
    [Required]
    public int UserId { get; set; }
    [Required]
    [MaxLength(100)]
    public string DisplayName { get; set; } = null!;
    [Range(3, 3)]
    public string? CCN3 { get; set; }
}

Can I centralise the attribute validation on DisplayName, duplicating the attributes goes against single responsibility principle. I believe I could achieve the centralised validation using an IFilterFactory and dropping the usage of attributes.

CodePudding user response:

I opted to use a custom ActionFilterAttribute to achieve centralisation of the validation. The example below is for validating the country code (CCN3).

CountryCodeValidationAttribute.cs - custom attribute to be applied to properties (contains no logic)

[AttributeUsage(AttributeTargets.Property)]
public class CountryCodeValidationAttribute : Attribute
{

}

CountryCodeValidationActionFilter.cs - custom action filter that supports dependency injection and looks for the custom attribute on the properties. In my case I'm returning the standard invalid model bad request response.

public class CountryCodeValidationActionFilter : ActionFilterAttribute
{
    private readonly ICountryService countryService;
    private readonly IOptions<ApiBehaviorOptions> apiBehaviorOptions;

    public CountryCodeValidationActionFilter(
        ICountryService countryService,
        IOptions<ApiBehaviorOptions> apiBehaviorOptions)
    {
        this.countryService = countryService;
        this.apiBehaviorOptions = apiBehaviorOptions;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var actionArguments = context.ActionArguments;

        foreach (var actionArgument in actionArguments)
        {
            if (actionArgument.Value == null) continue;

            var propertiesWithAttributes = actionArgument.Value
                .GetType()
                .GetProperties()
                .Where(x => x.GetCustomAttributes(true).Any(y => y.GetType() == typeof(CountryCodeValidationAttribute)))
                .ToList();

            foreach (var property in propertiesWithAttributes)
            {
                var value = property.GetValue(actionArgument.Value)?.ToString();

                if (value != null && await countryService.GetCountryAsync(value) != null) await next();
                else
                {
                    context.ModelState.AddModelError(property.Name, "Must be a valid country code");
                    context.Result = apiBehaviorOptions.Value.InvalidModelStateResponseFactory(context);
                }
            }
        }

        await base.OnActionExecutionAsync(context, next);
    }
}

Program.cs - register the custom action filter.

builder.Services.AddMvc(options =>
{
    options.Filters.Add(typeof(CountryCodeValidationActionFilter));
});

UserProfile.cs - apply the [CountryCodeValidation] attribute to the CountryCode property.

public class UserProfile
{
    [Required]
    [MaxLength(100)]
    public string DisplayName { get; set; } = null!;
    [CountryCodeValidation]
    public string? CountryCode { get; set; }
}

I can take this same approach and apply it to the DisplayName property to create a centralised validation for it

  •  Tags:  
  • Related