1 句代码,搞定 ASP.NET Core 绑定多个源到同一个类

问题

有群友希望将路由中的信息绑定到一个Dto对象中:.

public class DDDDDto
{
    [FromRoute(Name ="collectionId")]
    public Guid collectionId { get; set; }
    [BindProperty(Name = "relativeUrl")]
    public string relativeUrl { get; set; }
}

这样就不用在 Action 中定义一堆参数了:

[HttpGet("collections/{collectionId}/patn/{relativeUrl}/children", Name = "QueryChildrenOfItem")]
public async Task<DDDDDto> QueryChildren(
    [FromRoute(Name = "collectionId")] Guid collectionId,
    [FromRoute(Name = "relativeUrl")] string relativeUrl)

想法很好,对吧!

但是实际运行,却发现达不到想要的效果,没有绑定到数据:

1 句代码,搞定 ASP.NET Core 绑定多个源到同一个类

这说明 ASP.NET Core 默认是无法将其他不同的源绑定到单个类的。

那能不能换种方式,解决这个问题呢?

源码探究

首先,我们查看源码,想看看FromRouteAttribute是如何工作的。

仅在InferParameterBindingInfoConvention类中找到一处调用:

var message = Resources.FormatApiController_MultipleBodyParametersFound(
    action.DisplayName,
    nameof(FromQueryAttribute),
    nameof(FromRouteAttribute),
    nameof(FromBodyAttribute));

message += Environment.NewLine + parameters;
throw new InvalidOperationException(message);

结果,这段代码还是用来生成异常信息的!?

不过,这段代码前面的部分引起了我们的注意:

1 句代码,搞定 ASP.NET Core 绑定多个源到同一个类

这明显是在设置绑定源信息:

bindingSource = InferBindingSourceForParameter(parameter);
 
parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
parameter.BindingInfo.BindingSource = bindingSource;

InferBindingSourceForParameter的实现代码如下:

internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
{
    if (IsComplexTypeParameter(parameter))
    {
        if (_serviceProviderIsService?.IsService(parameter.ParameterType) is true)
        {
            return BindingSource.Services;
        }

        return BindingSource.Body;
    }

    if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
    {
        return BindingSource.Path;
    }

    return BindingSource.Query;
}

单个类肯定是IsComplexTypeParameter, 这将让方法返回BindingSource.Body

这也正好解释了: 正常情况下,如果使用单个类作为 Action 参数,默认从 Body 源绑定的原因。

那么,能否改变 ASP.NET Core 这一默认的绑定行为吗?

柳暗花明

继续查看InferParameterBindingInfoConvention的使用,我们发现它的调用,居然在一个条件分支内:

if (!options.SuppressInferBindingSourcesForParameters)
{
    var serviceProviderIsService = serviceProvider.GetService<IServiceProviderIsService>();
    var convention = options.DisableImplicitFromServicesParameters || serviceProviderIsService is null ?
        new InferParameterBindingInfoConvention(modelMetadataProvider) :
        new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService);
    ActionModelConventions.Add(convention);
}

那么,如果让SuppressInferBindingSourcesForParameters设为true,会有什么效果呢?

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressInferBindingSourcesForParameters = true;
});

下面,就是见证奇迹的时刻:

1 句代码,搞定 ASP.NET Core 绑定多个源到同一个类

我们也尝试了从其他源,比如 Query,传递数据,都可以正常绑定。

1 句代码,我们搞定了 ASP.NET Core 将多个来源绑定到同一个类的功能。

结论

后来,我们在官方文档(https://docs.microsoft.com/zh-cn/aspnet/core/web-api/?view=aspnetcore-6.0)找到了解释:

1 句代码,搞定 ASP.NET Core 绑定多个源到同一个类

当没有在参数上显式指定[FromXXX]时,ASP.NET Core 会进行绑定源推理,比如会推断单个类的绑定源为 Body。

设置 SuppressInferBindingSourcesForParameter 为 true,则会禁用绑定源推理。ASP.NET Core 运行时会尝试按顺序从所有源中拉取数据进行绑定。