3
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

4
  • 2
    The model builder can't call the custom validation, because it can't create the GroupBy instance because the value is incorrect. You can set the input parameter type to string and manually check/parse the input value. Commented Feb 23, 2021 at 8:37
  • @Vernou thanks. I will do the same. after that i have any doubt i will mention you. Commented Feb 23, 2021 at 8:41
  • Changing the type to 'string' will also cause the swagger definition to see it as a string, how to fix this behavior? Commented Aug 4, 2023 at 3:49
  • 1
    @WilkovanderVeen Generally, tool that generate the swagger definition allow to modify it. With Swashbuckle, you can use filter. If you need help, you can post a new question. Commented Aug 6, 2023 at 19:40

2 Answers 2

5

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.

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

Comments

2

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

1 Comment

No, Actually i need to pass Enums directly in the query params. So I Cannt do this.

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.