首页 > 解决方案 > 在 fromquery 参数中传递 null 时,Asp.net 核心 Web api 引发错误

问题描述

我在使用空值调用 asp.net 核心 Web API 方法时遇到问题,请检查以下 API 方法

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

我正在调用这个方法,如下所示

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

我收到以下错误

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "出现一个或多个验证错误。", "status": 400, "traceId" :“00-1c1d75974b9c4c489b3cca6b17f005ec-2aaa26d807bc3e42-00”,“错误”:{“userIds”:[“值'null'无效。” ] }

如何使 asp.net 核心 Web API 接受来自查询的空值。

标签: c#asp.net-core.net-coreasp.net-core-webapi

解决方案


当您在评论中写道时,您需要问题中的 URL 才能按原样工作。然后我们必须对 asp.net Core 绑定查询参数的方式进行一些更改,这可以通过实现自定义模型绑定器来完成。

请注意,这是针对特定问题的简单活页夹。有关更通用的解决方案,请查看源代码,例如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;
    }
}

要验证我们到目前为止所做的工作,请更改控制器方法的签名,如下所示:

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

您不想像上面那样重复注释所有控制器方法,所以让我们将此绑定器应用于所有控制器。首先,一个活页夹提供者:

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

不是在 index = 0 处插入新的 binder 提供程序(在许多示例中都会这样做),让我们使用这个扩展方法为新的 binder 提供程序找到一个合适的位置:

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

然后像这样更改 Startup#ConfigureServices:

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

通过上述更改,您现在可以使用原始控制器,并且将应用上述绑定器。

最后,编写上述代码时使用的端点测试:

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

开发/测试期间使用的环境:asp.net Core 5、Kestrel、XUnit、Rider。


推荐阅读