将 .NET Core Json 绑定到简单参数

Binding .NET Core Json to simple parameters

我过去只是 post 简单的 json 对象到 ASP.NET MVC 控制器,绑定引擎会将主体解析为方法的简单参数:

{
    "firstname" : "foo",
    "lastname" : "bar"
}

我可以有一个像这样的 MVC 控制器:

public method Blah(string firstname, string lastname) {}

并且 firstnamelastname 将自动从 Json 对象中拉出并映射到简单参数。

我已经将一个后端部分移动到具有相同方法签名的 .NET Core 5.0,但是,当我 post 相同的简单 JSON 对象时,我的参数为空。我什至尝试对每个参数执行 [FromBody] 但它们仍然为空。直到我创建了一个包含参数名称的额外 class 模型绑定才起作​​用:

public class BlahRequest
{
    public string firstname { get; set;}
    public string lastname { get; set; }
}

然后我必须将我的方法签名更新为如下所示:

public method Blah([FromBody]BlahRequest request) { }

现在,请求的属性 firstnamelastname 从 post 请求中填写。

是否有模型绑定器设置,我可以在其中返回到从简单的 Json 对象绑定到方法的参数?或者我是否必须更新我所有的方法签名以包含具有属性的 class?

如何调用网络 api 方法

原始应用程序是用 Angular 编写的,但我可以通过简单的 Fiddler 请求重新创建它:

POST https://localhost:5001/Blah/Posted HTTP/1.1
Host: localhost:5001
Connection: keep-alive
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded

{"firstname":"foo","lastname":"bar"}

在以前版本的 .Net 框架中,控制器的方法会自动解析这些值。现在,在核心上,它需要传入一个模型。我尝试了 application/json、multipart/form-data 和 application/x-www-form-urlencoded 作为内容类型,但它们都以空值结束。

最小的 .Net Core 项目

public class Startup {
    public Startup(IConfiguration configuration) {
        Configuration = configuration;
    }
    public IConfiguration Configuration { get; }
    public void ConfigureServices(IServiceCollection services){
        services.AddControllers();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}

[Route("[controller]/[action]")]
public class BlahController : ControllerBase {
    public object Posted(string firstname, string lastname) {
        Console.WriteLine(firstname);
        Console.WriteLine(lastname);
        return true;
    }        
}

这取决于内容类型。您必须使用 application/json 内容类型。这就是为什么你必须创建一个视图模型并添加 [FromBody] 属性

public method Blah([FromBody]BlahRequest request) { }

但如果您使用 application/x-www-form-urlencoded 或 multipart/form-data 形式的 enctype 或 ajax 内容类型,那么这将有效

public method Blah(string firstname, string lastname) {}

如果你还有问题,说明你使用的是ApiController。在这种情况下,将此代码添加到 startup

using Microsoft.AspNetCore.Mvc;

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

深入了解 .Net 核心源代码,我找到了表单值提供程序如何工作的示例,然后为该项目逆向设计了一个解决方案。我认为如果有人重新开始,他们就不必解决这个问题,但我们正在将现有的 UI 构建在 Angular 上移动到 .Net 核心上的这个新后端并且不想使用控制器方法的参数将所有服务器端调用重写为模型。

因此,回顾一下,在以前的 .Net 版本中,您可以 post 一个 Json 对象到方法签名中具有多个参数的 Mvc 控制器。

public object GetSalesOrders(string dashboardName, int fillableState = 0){}

调用它的简单方法如下:

POST https://localhost:5001/api/ShippingDashboard/GetSalesOrders HTTP/1.1
Content-Type: application/json

{"dashboardName":"/shippingdashboard/bigshipping","fillableState":1}

查看值提供程序,我创建了自己的值提供程序,并在启动配置期间将其推到列表顶部 class。

services
.AddControllers( options =>{
  options.ValueProviderFactories.Insert(0, new JsonBodyValueProviderFactory());
 })

JsonBodyValueProviderFactory 是我写的工厂class。它检查请求,如果内容类型是 application/json,它将向内容添加一个提供者:

public class JsonBodyValueProviderFactory : IValueProviderFactory{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context) {
    if (context == null) {
        throw new ArgumentNullException(nameof(context));
    }

    var request = context.ActionContext.HttpContext.Request;
    if (request.HasJsonContentType()) {
        // Allocating a Task only when the body is json data
        return AddValueProviderAsync(context);
    }

    return Task.CompletedTask;
}

private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) {
    var request = context.ActionContext.HttpContext.Request;
    var body = "";
    Dictionary<string,object> asDict = new Dictionary<string, object>();
    try {
        using (StreamReader stream = new StreamReader(request.Body)){
            body = await  stream.ReadToEndAsync();
        }
        var obj = JObject.Parse(body);
        foreach(var item in obj.Children()){
            asDict.Add(item.Path, item.Values().First());
        }
        
    } catch (InvalidDataException ex) {
        Console.WriteLine(ex.ToString());
    } catch (IOException ex) {
        Console.WriteLine(ex.ToString());
    }

    var valueProvider = new JsonBodyValueProvider(BindingSource.Form, asDict);
    context.ValueProviders.Add(valueProvider);
}

}

因为我并不总是知道数据的形状,所以我利用了 Json.Net 的 JObject 并将 Json 主体吸入到 JObject 中。然后我使用顶级属性并将它们添加到字典中以便于查找。

取值并响应参数名称的实际class是JsonBodyValueProvider:

public class JsonBodyValueProvider : BindingSourceValueProvider, IEnumerableValueProvider {
private readonly Dictionary<string, object> values;
private PrefixContainer? _prefixContainer;

public JsonBodyValueProvider( BindingSource bindingSource, Dictionary<string,object> values) : base(bindingSource) {
    if (bindingSource == null) {
        throw new ArgumentNullException(nameof(bindingSource));
    }
    if (values == null) {
        throw new ArgumentNullException(nameof(values));
    }
    this.values = values;
}
protected PrefixContainer PrefixContainer {
    get {
        if (_prefixContainer == null) {
            _prefixContainer = new PrefixContainer(values.Keys);
        }
        return _prefixContainer;
    }
}
public override bool ContainsPrefix(string prefix) {
    return PrefixContainer.ContainsPrefix(prefix);
}
public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix) {
    if (prefix == null) {
        throw new ArgumentNullException(nameof(prefix));
    }

    return PrefixContainer.GetKeysFromPrefix(prefix);
}
public override ValueProviderResult GetValue(string key){
    if (key == null) {
        throw new ArgumentNullException(nameof(key));
    }

    if (key.Length == 0) {
        return ValueProviderResult.None;
    }

    var _values = values[key];
    if (!values.ContainsKey(key)) {
        return ValueProviderResult.None;
    } else {
        return new ValueProviderResult(_values.ToString());
    }
}

}

这几乎是 .Net Core 中 FormValueProvider 的副本,我只是调整它以使用输入值字典。

现在我的控制器可以与以前版本的 .Net 保持一致,而无需更改方法签名。