简体   繁体   中英

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

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 .

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; }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM