2

I have a WebAPI controller which looks like this:

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public ActionResult<string> Get([FromQuery]DataFilter dataFilter)
    {
        return string.Join(Environment.NewLine, dataFilter?.Filter?.Select(f => f.ToString()) ?? Enumerable.Empty<string>());
    }
}

Nothing fancy, just a controller which recevies some data from query string and outputs it as a response. The class it receives looks like this:

public class DataFilter
{
    public IEnumerable<FilterType> Filter { get; set; }
}

public enum FilterType
{
    One,
    Two,
    Three,
}

These are just sample classes to illustrate the problem, which is a validation error when trying to call this method like this:

/api/values?filter=

And the response:

{
  "errors": {
    "Filter": [
      "The value '' is invalid."
    ]
  },
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "80000164-0002-fa00-b63f-84710c7967bb"
}

If i set my FilterType to be nullable, it works, but the array simply contains null values in this case. And if used like this:

/api/values?filter=&filter=&filter=

It will simply contain 3 null values. And i wanted it to be simply empty or null, since there are no real values passed.

The ASP .Net Core github account contains some similiar issues, but it is repored that they were fixed in 2.2, which is i'm using. But perhaps they are different or i missunderstand something.

EDIT_0: Just to show what i meant about nullable.

If i change my class to this:

public IEnumerable<FilterType?> Filter { get; set; } //notice that nullable is added to an Enum, not the list

Then when called like this:

/api/values?filter=&filter=&filter=

I get 3 elements in my "Filter" property. All nulls. Not exactly what i expect. Good as a workaround, but not a solution at all.

4
  • 1
    can you simply not specify a query string param: i.e: hit /api/values? Commented Apr 26, 2019 at 13:44
  • That would be the easiest solution, but unfortunately no. I have to make sure the API works in this case. Commented Apr 26, 2019 at 13:56
  • So what the problem is? If you make parameter nullable everything works fine as you mentioned which is correct behavior. I just cannot understand what is the question? Commented Apr 26, 2019 at 14:33
  • Added some explanation to the question regarding your comment. Commented Apr 26, 2019 at 14:52

3 Answers 3

1

You can create custom model binder whose task is removing validation errors generated by default CollectionModelBinder. This should be sufficient in your case because default model binder works as it needed for you, doesn't add invalid values to collection.

public class EmptyCollectionModelBinder : CollectionModelBinder<FilterType>
{
    public EmptyCollectionModelBinder(IModelBinder elementBinder) : base(elementBinder)
    {
    }

    public override async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        await base.BindModelAsync(bindingContext);
        //removing validation only for this collection
        bindingContext.ModelState.ClearValidationState(bindingContext.ModelName);
    }
}

Create and register model binder provider

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

        if (context.Metadata.ModelType == typeof(IEnumerable<FilterType>))
        {
            var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(typeof(FilterType)));

            return new EmptyCollectionModelBinder(elementBinder);
        }

        return null;
    }
}

Startup.cs

services
    .AddMvc(options =>
    {
        options.ModelBinderProviders.Insert(0, new EmptyCollectionModelBinderProvider());
    })
Sign up to request clarification or add additional context in comments.

3 Comments

What will happen when the user provides a value, will the collection still be populated
This issue is about any type of collections, not just enums. The same validation error occurs even for integers. Also, i don't like the idea of removing validation states, even for just one specific type.
this shouldn't be an issue for reference types
0

You have a few options. You can create a custom model binder to handle your filter type Or:

You can create your IEnumerable With Nullables:

public IEnumerable<FilterType?> Filter { get; set; }

And Filter out the Nulls in the Calling Code:

return string.Join(Environment.NewLine, dataFilter?.Filter?.Where(f => f != null)
   .Select(f => f.ToString()) ?? Enumerable.Empty<string>());

2 Comments

Thanks, but this seems like a workaround for a workaround, while using a workaround.
Yeah, then you can create a custom model binder
0

I've soled this case using custom TypeConverter and moved to JSON format for passing arrays (e.g. filter=["one","two"])

Here is how i defined it:

public class JsonArrayTypeConverter<T> : TypeConverter
{
    private static readonly TypeConverter _Converter = TypeDescriptor.GetConverter(typeof(T));

    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
        sourceType == typeof(string) || TypeDescriptor.GetConverter(sourceType).CanConvertFrom(context, sourceType);

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        try
        {
            return JsonConvert.DeserializeObject<IEnumerable<T>>((string)value);
        }
        catch (Exception)
        {
            var dst = _Converter.ConvertFrom(context, culture, value); //in case this is not an array or something is broken, pass this element to a another converter and still return it as a list
            return new T[] { (T)dst };
        }
    }
}

And global registration:

TypeDescriptor.AddAttributes(typeof(IEnumerable<FilterType>), new TypeConverterAttribute(typeof(JsonArrayTypeConverter<FilterType>)));

Now i don't get null items in my filter list and also have support for JSON lists with multiple type support (enums, strings, integers, etc.).

The only downside is that this won't work with passing elements as before (e.g. filter=one&filter=two&filter=three). And not nice looking query string in browser address bar.

Comments

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.