1

I am facing an issue while calling asp.net core web API method with null value please check the below API method

    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [AllowAnonymous]
    public ActionResult<List<int?>> Test([FromQuery] List<int?> userIds = null)
    {
        try
        {
            return Ok(userIds);
        }
        catch (Exception ex)
        {
            return HandleException(ex);
        }
    }

and I am calling this method like below

https://localhost:44349/api/v1/Sessions/Test?userIds=null&userIds=1&userIds=2

I am getting the below error

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-1c1d75974b9c4c489b3cca6b17f005ec-2aaa26d807bc3e42-00", "errors": { "userIds": [ "The value 'null' is not valid." ] }

how to make asp.net core web API to accept null valeus in from query.

3
  • I'm afraid because it's a QueryString, it's trying to populate a List<int> with "null", not actual NULL, which is why you're getting the 400. Is there a reason this endpoint should be taking in null, rather than just working if ?userIds is not a part of the querystring? Commented Jul 3, 2021 at 13:49
  • Use List<String> as parameter in your API and convert to List<int?> Commented Jul 4, 2021 at 11:03
  • Making it List<String> will still fail with error - "Index and length must refer to a location within the string. (Parameter 'length')" Commented Jan 24, 2024 at 15:07

2 Answers 2

2

The reason this is happening is because it's trying to populate a string value of "null" into an int? type.

If you want userIds to be null, simply don't add them to the query-string.

For an example, when I change the input type to be a List<string>, and pass the values as you have above, notice the strings aren't null. They are "null":

enter image description here

Correction

I'm not sure if leaving out the query params will make the list null. It may default to an empty list. In this event, you can check the count of userIds.

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

Comments

1

As you write in a comment, you'll need the URL from your question to work as-is. Then we'll have to do some changes in how asp.net Core binds query parameters, and that may be accomplished by implementing a custom model binder.

Please note that this is a simple binder targeting a specific problem. For more generic solutions, have a look at the source for e.g. Microsoft.AspNetCore.Mvc.ModelBinding.Binders.CollectionModelBinder

public class CustomUserIdsBinder : IModelBinder
{
    private const string NullValue = "null";

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

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        var result = new List<int?>();
        foreach (var currentValue in valueProviderResult)
        {
            // remove this code block if you want to filter out null-values
            if (string.IsNullOrEmpty(currentValue)
                || NullValue.Equals(currentValue, StringComparison.OrdinalIgnoreCase))
            {
                result.Add(null);
                continue;
            }

            if (int.TryParse(currentValue, out var currentIntValue))
            {
                result.Add(currentIntValue);
            }
        }

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

To verify what we've done so far, change the signature for your controller method like this:

public ActionResult<List<int?>> Test(
    [FromQuery][ModelBinder(BinderType = typeof(CustomUserIdsBinder))]
    List<int?> userIds = null)

You don't want to repeat annotating all controller methods like above, so let's apply this binder to all controllers. First, a binder provider:

public class CustomUserIdsBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        return context.Metadata.ModelType == typeof(List<int?>) 
            ? new BinderTypeModelBinder(typeof(CustomUserIdsBinder)) 
            : null;
    }
}

Instead of inserting the new binder provider at index = 0 (which is done in many examples), let's find a proper position for the new binder provider using this extension method:

public static class BinderProviderExtensions
{
    public static void UseCustomUserIdsBinderProvider(this MvcOptions options)
    {
        var collectionBinderProvider = options.ModelBinderProviders
            .FirstOrDefault(x => x.GetType() == typeof(CollectionModelBinderProvider));

        if (collectionBinderProvider == null)
        {
            return;
        }

        // indexToPutNewBinderProvider = 15 in my test-app
        var indexToPutNewBinderProvider = options.ModelBinderProviders.IndexOf(collectionBinderProvider);
        options.ModelBinderProviders.Insert(indexToPutNewBinderProvider, new CustomUserIdsBinderProvider());
    }
}

Then change Startup#ConfigureServices like this:

services.AddControllers(options => options.UseCustomUserIdsBinderProvider());

With the above changes, you can now use your original controller, and the above binder will be applied.

Finally, end-point tests used when writing the above code:

public class ControllerWithCustomBindingTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private const string TestUrl = "/api/v1/Sessions/Test?userIds=null&userIds=1&userIds=2";
    private readonly WebApplicationFactory<Startup> _webApplicationFactory;

    public ControllerWithCustomBindingTests(WebApplicationFactory<Startup> factory) => _webApplicationFactory = factory;

    [Theory]
    [InlineData(TestUrl)]
    public async Task SessionTest_UrlWithNull_ReceiveOk(string url) => 
        Assert.Equal(HttpStatusCode.OK, (await _webApplicationFactory.CreateClient().GetAsync(url)).StatusCode);

    [Theory]
    [InlineData(TestUrl)]
    public async Task SessionTest_UrlWithNull_ReceiveListOfThreeItems(string url)
    {
        var items = await
            (await _webApplicationFactory.CreateClient().GetAsync(url))
            .Content.ReadFromJsonAsync<IEnumerable<int?>>();

        Assert.Equal(3, items?.Count());
    }
}

Environment used during devel/testing: asp.net Core 5, Kestrel, XUnit, Rider.

3 Comments

This works but in the asp.net framework we used to pass the data like below localhost:44349/api/v1/Sessions/… we are using many like this call in our client-side app so we need a workaround in api side to take like url mentions is there any configuration present in asp.net core web api to accept null as query param
@DineshGanesan: Updated my answer with a custom binder. BR
All of that to deal with nulls. Wow. Something seems to be broken in the plumbing here.

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.