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

5

我曾经只是将简单的JSON对象发送到ASP.NET MVC控制器,并且绑定引擎将其解析为方法的简单参数:

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

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

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

firstnamelastname 将会自动从Json对象中拉取并映射到相应的简单参数中。

我将一个后端组件迁移到了.NET Core 5.0,方法签名保持不变,然而,当我提交相同的简单JSON对象时,我的参数是null。我甚至尝试对每个参数都使用[FromBody],但它们仍然是null。直到我创建了一个包含参数名的额外类,模型绑定才能生效:

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

然后我必须更新我的方法签名,看起来像这样:

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

现在,请求中有firstnamelastname属性,这些属性是从post请求中填写的。

是否有一个模型绑定器设置,可以让我回到将一个简单的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框架版本中,控制器方法会自动解析这些值。现在,在Core上,需要传递一个模型。我尝试了application/json、multipart/form-data和application/x-www-form-urlencoded作为Content-type,它们最终都得到了空值。

最小的.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
@Campbell 是的,我做到了。我下面发布的答案是我用来将 .Net Framework 4.8 代码移植到 .Net Core 的方法,而不需要重写 UI 调用。不确定为什么会被踩,但如果在 .NET Core 应用程序的启动期间添加此提供程序,则此代码可以正常工作。 - Josh
2个回答

3

我研究了 .Net 核心源代码的细节,找到了表单值提供程序的示例,然后为这个项目设计了一个解决方案。如果从头开始,他们不必解决这个问题,但我们正在将基于 Angular 构建的现有 UI 移动到 .Net core 上,并且不想将所有服务器端调用重写为控制器方法参数的模型。

因此,简要回顾一下,在以前的 .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}

在研究价值提供者时,我创建了自己的价值提供者,并在启动类配置期间将其推到列表顶部。

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 保持一致而不改变方法签名。


非常感谢。我处于同样的情况,它非常有效。如果有人想要忽略运行错误的可能性,他们可以像这样初始化字典,例如Action方法有string userName,而客户端正在传递username。因此,Dictionary<string,object> asDict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); 这可以帮助解决不必要的字典元素未找到错误。 - Jawand Singh
这可能是一个愚蠢但相关的后续问题..我在解决“porting”问题时遇到了麻烦,而且不清楚如何为特定控制器填充PrefixContainer,这是否是替换旧的“RoutePrefix”属性的正确容器呢? - Pogrindis

0

这取决于内容类型。您必须使用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。在这种情况下,请将以下代码添加到启动文件中。

using Microsoft.AspNetCore.Mvc;

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

尝试将请求头的内容更改为application/x-www-form-urlencoded以及multipart/form-data,但参数仍以null值进入。 - Josh
@Josh,请问你能演示一下如何调用该操作吗? - Serge
发布了更新。 - Josh
一切在我的端口都正常工作。您在Fiddler中的请求应该像这样: POST https://localhost:5001/Blah/Posted HTTP/1.1 Host: localhost:5001 Content-Type: application/x-www-form-urlencoded Content-Length: 37 firstname=firstname&lastname=lastname - Serge
@Josh,你必须使用键值对,而不是JSON。 - Serge
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接