【问题标题】:Binding .NET Core Json to simple parameters将 .NET Core Json 绑定到简单参数
【发布时间】:2021-12-29 18:42:07
【问题描述】:

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

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

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

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

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

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

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

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

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

现在,该请求具有从发布请求中填写的属性firstnamelastname

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

如何调用 web 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 作为 Content-type,它们都以 null 结尾价值观。

最小的 .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;
    }        
}

【问题讨论】:

  • 你在这方面有什么进展吗?我有一个类似的问题,我正在尝试移植 .NET Framework 代码,其中我有许多控制器方法,例如 public method Blah(string firstname, string lastname) {} 并且我试图以 {"firstname": "John", "lastname": "Smith"} 格式传递 JSON,除非我添加,否则该方法不会绑定它们中间模型
  • @Campbell 是的,我做到了。我在下面发布的答案是我用来将 .Net Framework 4.8 代码移植到 .Net Core 而无需重写 UI 调用的答案。不知道为什么它被否决了,但是如果您在 .NET 核心应用程序启动期间添加此提供程序,则此代码有效。

标签: c# json .net-core binding


【解决方案1】:

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

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

但如果您使用 application/x-www-form-urlencoded 或 multipart/form-data form enctype 或 ajax content-type 那么这将起作用

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

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

using Microsoft.AspNetCore.Mvc;

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

【讨论】:

  • 尝试将请求头的内容更改为 application/x-www-form-urlencoded 并尝试了 multipart/form-data 参数仍然为空值。
  • @Josh 你能告诉请你怎么称呼这个动作吗?
  • 发布了更新。
  • @Josh 我正在使用 Postman 进行测试,一切正常。但是如果你使用 ApiController 属性尝试添加一些代码来启动。请参阅我的更新答案。在您的情况下,最好使用 'Content-Type: application/x-www-form-urlencoded' 因为 form-data 应该是最后的选择。
  • 我启动了一个空白的 web api 项目并且能够重现同样的问题。见上文。
【解决方案2】:

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

因此,回顾一下,在以前的 .Net 版本中,您可以将 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}

查看值提供者,我创建了自己的值提供者,并在配置 Startup 类期间将其推到列表顶部。

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

JsonBodyValueProviderFactory 是我写的一个工厂类。它检查请求,如果内容类型是 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 中。然后我使用顶级属性并将它们添加到字典中以便于查找。

获取值并响应参数名称的实际类是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 保持一致,而无需更改方法签名。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-08
    • 1970-01-01
    • 1970-01-01
    • 2020-11-05
    • 2017-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多