13

I have a Web API method:

public List<Task> GetTasks([FromUri] TaskFilter filter)
{

}

The method has parameter with list of nullable identifiers:

public class TaskFilter
{
  public IList<int?> Assignees { get; set; }
}

When I call it:

GET /tasks?assignees=null

Server returns an error:

{
  "message":"The request is invalid.",
  "modelState": {
    "assignees": [ "The value 'null' is not valid for Nullable`1." ]
  }
}

It works only if I pass empty string:

GET /tasks?assignees=

But standard query string converters (from JQuery, Angular, etc) do not work with nulls in such way.

How to make ASP.NET to interpret 'null' as null?

Upd: The query string can contain several identifiers, e.g.:

GET /tasks?assignees=1&assignees=2&assignees=null

Upd2: JQuery converts nulls in array to empty strings, and ASP.NET interprets them as null. So the question is about calling WebAPI from Angular 1.6 ($HttpParamSerializerProvider)

Upd3: I know about workarounds, but I do not ask for them. I want a solution for specific problem:

  • It is a GET method
  • Method accepts a list from Uri
  • A list can contain null values
  • It should be List<int?> because API docs are generated automatically, and I do not want to see text array as parameter type
  • By default ASP.NET expects empty strings for null values (JQuery.param works in that way)
  • But some client libraries (e.g. Angular) does not convert null array items to empty strings
5
  • in your parameter list , if you can change the parameter to the string and check null as string and subsequently parsing them to their respective data type , can this way will work out for you? Commented May 11, 2017 at 5:56
  • a bit beyond me so sry if this doesn't make sense. would $.map() help? i also searched on what you said - "JQuery converts nulls in array to empty strings" and got some possibly good (??) results. Commented May 11, 2017 at 6:44
  • @Webruster, I use automatic API doc generation, and I want see number (not string) list here. @wazz, I can do everything on my front-end, but the problem that the API is public and used in different other clients. So I want to make ASP.NET interpret both 'null' and '' as null. Commented May 11, 2017 at 8:23
  • @Artem at the end in DB it will be stored as number only but from the Client to Controller it will be in string and in Controller it will be paresed to the respective DataType which will not be a conflict with your automatic api doc generation Commented May 11, 2017 at 8:31
  • @Webruster, I say about API docs, DB does not relate to my question at all. Commented May 11, 2017 at 8:40

2 Answers 2

5
+25

You can create a custom model bind for this specific type, inherithing from DefaultModelBinder, for sample:

using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
public class TaskFilterBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext)
    {
        var request = controllerContext.HttpContext.Request;

        var assignees = request.QueryString["assignees"];

        if (assignees == "null") // check if assignees is null (string) then return NULL
            return null;
        return assignees;
    }

}

Finally we need to inform the controller as to the binding we want it to use. This we can specify using attributes

[ModelBinder(typeof(TaskFilterBinder))]

as below:

public List<Task> GetTasks([FromUri(ModelBinder=typeof(TaskFilterBinder))] TaskFilter filter)
{
// Do your stuff.
}

For more reference check this link on Custom Model Binders. Hope, this solves your problem . Thanks

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

1 Comment

The example is for MVC controller, at the beginning of question I wrote "I have a Web API method". But thanks, I will try to adopt this solution to Web API.
5

Finally, I found a solution using custom value provider:

using System;
using System.Collections.Generic;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ValueProviders;
using System.Web.Http.ValueProviders.Providers;
using System.Globalization;
using System.Net.Http;
using System.Web.Http.ModelBinding;

public sealed class NullableValueProviderAttribute : ModelBinderAttribute
{
    private readonly string[] _nullableColumns;

    public NullableValueProviderAttribute(params string[] nullableColumns)
    {
        _nullableColumns = nullableColumns;
    }

    public override IEnumerable<ValueProviderFactory> GetValueProviderFactories(HttpConfiguration configuration)
    {
        return new ValueProviderFactory[] { new NullableValueProviderFactory(_nullableColumns) };
    }
}

public class NullableValueProviderFactory : ValueProviderFactory, IUriValueProviderFactory
{
    private readonly string[] _nullableColumns;

    public NullableValueProviderFactory(string[] nullableColumns)
    {
        _nullableColumns = nullableColumns;
    }

    public override IValueProvider GetValueProvider(HttpActionContext actionContext)
    {
        return new NullableQueryStringValueProvider(actionContext, CultureInfo.InvariantCulture, _nullableColumns);
    }
}

public class NullableQueryStringValueProvider : NameValuePairsValueProvider
{
    private static readonly string[] _nullValues = new string[] { "null", "undefined" };

    private static IEnumerable<KeyValuePair<string, string>> GetQueryNameValuePairs(HttpRequestMessage request, string[] nullableColumns)
    {
        foreach (var pair in request.GetQueryNameValuePairs())
        {
            var isNull = Array.IndexOf(nullableColumns, pair.Key) >= 0 && Array.IndexOf(_nullValues, pair.Value) >= 0;
            yield return isNull ? new KeyValuePair<string, string>(pair.Key, "") : pair;
        };
    }

    public NullableQueryStringValueProvider(HttpActionContext actionContext, CultureInfo culture, string[] nullableColumns) :
        base(GetQueryNameValuePairs(actionContext.ControllerContext.Request, nullableColumns), culture)
    { }
}

And specify it in Web API action:

public List<Task> GetTasks([NullableValueProvider("assignees")] TaskFilter filter)
{
}

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.