Shashwat Prakash
Shashwat Prakash

Reputation: 484

Custom Validator for Enum in ASP.NET Core

public enum GroupBy
{
    status = 0,
    dueDate = 1,
    requester = 2,
    assignee = 3
}

I am using this enum in params of web api like this:-

public async Task<IActionResult> Dashboard(GroupBy groupBy)

My problem is when i am passing correct enum it will give output. But if I pass any invalid enum it will throw error which is built-in error of ASP.NET Core. I tried to implement but while calling this api it won't go inside my custom validator. When I am passing valid enum it will go inside my validator.

So, I want to implement custom validation for it. Somebody please help

Upvotes: 3

Views: 3592

Answers (2)

Thomas D.
Thomas D.

Reputation: 1061

I am not entirely sure if I understand your problem correctly but I think what you are looking for is model validation. Here is a more generic approach than the already provided answer:

  1. Custom validation attribute:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class DefinedEnumValueAttribute : ValidationAttribute
{
    private readonly Type enumType;

    public DefinedEnumValueAttribute(Type enumType)
    {
        if (!enumType.IsEnum)
        {
            throw new ArgumentException($"The given type is not an enum.");
        }

        this.enumType = enumType;
    }

    public override bool IsValid(object value)
    {
        if (value is IEnumerable enumerable)
        {
            return enumerable.Cast<object>().All(val => Enum.IsDefined(enumType, val));
        }
        else
        {
            return Enum.IsDefined(enumType, value);
        }
    }
}
  1. Change your endpoint to something like the following:
public class Settings 
{
    [DefinedEnumValue(typeof(GroupBy))]
    public GroupBy GroupBy { get; set; }
}

public async Task<IActionResult> Dashboard(Settings settings)
{
    if(!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    
    // do your thing

    return Ok();
}

Please note that the attribute can be used for any enum and also for arrays and other enumerables:

public class Settings 
{
    [DefinedEnumValue(typeof(GroupBy))]
    public GroupBy[] Groupings { get; set; }
}

Upvotes: 2

vernou
vernou

Reputation: 7590

When the value is incorrect, the model binder can't create the GroupBy instance and can't call custom validation on this instance.

A solution is to change the input parameter type to string and do manually the check and parse step :

public async Task<IActionResult> Dashboard(string groupBy)
{
    if(!Enum.TryParse(groupBy, out GroupBy by))
    {
        ModelState.AddModelError(nameof(groupBy), $"The value is invalid. Valid value : {Enum.GetValues(typeof(GroupBy))}");
        return BadRequest(ModelState);
    }
    return Ok(by);
}

Other solution is to override the model binder behavior. For this, you need create a custom binder :

public class GroupByBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // Try to fetch the value of the argument by name
        var modelName = "groupBy";
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        // Custom validation
        if (!Enum.TryParse(value, out GroupBy groupBy))
        {
            bindingContext.ModelState.AddModelError(modelName, $"The value is invalid. Valid value : {Enum.GetValues(typeof(GroupBy))}");
            return Task.CompletedTask;
        }

        bindingContext.Result = ModelBindingResult.Success(groupBy);
        return Task.CompletedTask;
    }
}

And now you can :

public async Task<IActionResult> Dashboard2([ModelBinder(typeof(GroupByBinder))] GroupBy groupBy)
{
    if(!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    return Ok(groupBy);
}

You can override this behavior to all GroupBy input parameter :

public class GroupByBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(GroupBy))
        {
            return new BinderTypeModelBinder(typeof(GroupByBinder));
        }

        return null;
    }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers(options =>
        {
            options.ModelBinderProviders.Insert(0, new GroupByBinderProvider());
        });
    }
}

Warning : Model Builder isn't used when the data come from JSON or XML content. More detail on the official documentation.

Upvotes: 5

Related Questions