【问题标题】:Model Binding with Nested JSON Objects与嵌套 JSON 对象的模型绑定
【发布时间】:2018-07-20 07:33:40
【问题描述】:

我正在编写一个端点来接受来自第 3 方的 webhook 上的 POST 请求,并且他们发送的数据是 JSON 编码的正文。所以,我无法控制发送给我的数据,我需要处理它。我的问题是他们在他们的 JSON 中做了很多嵌套,因为我只使用了他们发送给我的几个键,所以我不想创建一堆不必要的嵌套模型来获取我想要的数据。这是一个示例负载:

{
    id: "123456",
    user: {
        "name": {
            "first": "John",
            "Last": "Doe"
        }
    },
    "payment": {
        "type": "cash"
    }
}

我想把它放在一个看起来像这样的模型中:

public class SalesRecord
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public string PaymentType {get; set;}
}

端点示例(还没有太多):

[HttpPost("create", Name = "CreateSalesRecord")]
public ActionResult Create([FromBody] SalesRecord record)
{
    return Ok(record);
}

我过去的工作是在 Phalcon PHP 框架中,我通常只是直接访问 POST 正文并自己在模型中设置值。我当然看到了模型绑定的优点,但我还不明白如何正确解决这种情况。

【问题讨论】:

  • 您必须创建一个自定义模型绑定器来解析和绑定您的模型。
  • 为您的端点显示所需的代码
  • 我还没有真正编写端点,此时我只是试图正确解析数据。查看更新
  • 我现在正在审查它。我建议您花一些时间查看我在上面发布的第二个链接。他们将引导您完成如何创建自定义活页夹。

标签: c# asp.net-core


【解决方案1】:

对于这样的场景,需要一个自定义模型绑定器。该框架允许这种灵活性。

使用此处提供的演练

Custom model binder sample

并使其适应这个问题。

以下示例使用SalesRecord 模型上的ModelBinder 属性:

[ModelBinder(BinderType = typeof(SalesRecordBinder))]
[JsonConverter(typeof(JsonPathConverter))]
public class SalesRecord {
    [JsonProperty("user.name.first")]
    public string FirstName {get; set;}
    [JsonProperty("user.name.last")]
    public string LastName {get; set;}
    [JsonProperty("payment.type")]
    public string PaymentType {get; set;}
}

在前面的代码中,ModelBinder 属性指定了应该用于绑定SalesRecord 操作参数的IModelBinder 的类型。

SalesRecordBinder 用于绑定 SalesRecord 参数,尝试使用自定义 JSON 转换器解析发布的内容以简化反序列化。

class JsonPathConverter : JsonConverter {
    public override object ReadJson(JsonReader reader, Type objectType, 
                                    object existingValue, JsonSerializer serializer) {
        JObject jo = JObject.Load(reader);
        object targetObj = Activator.CreateInstance(objectType);

        foreach (PropertyInfo prop in objectType.GetProperties()
                                                .Where(p => p.CanRead && p.CanWrite)) {
            JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                                            .OfType<JsonPropertyAttribute>()
                                            .FirstOrDefault();

            string jsonPath = (att != null ? att.PropertyName : prop.Name);
            JToken token = jo.SelectToken(jsonPath);

            if (token != null && token.Type != JTokenType.Null) {
                object value = token.ToObject(prop.PropertyType, serializer);
                prop.SetValue(targetObj, value, null);
            }
        }
        return targetObj;
    }

    public override bool CanConvert(Type objectType) {
        // CanConvert is not called when [JsonConverter] attribute is used
        return false;
    }

    public override bool CanWrite {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, 
                                    JsonSerializer serializer) {
        throw new NotImplementedException();
    }
}

Source: Can I specify a path in an attribute to map a property in my class to a child property in my JSON?

public class SalesRecordBinder : IModelBinder {

    public Task BindModelAsync(ModelBindingContext bindingContext) {
        if (bindingContext == null){
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider
            .GetValue(bindingContext.ModelName);

        if (valueProviderResult == ValueProviderResult.None){
            return Task.CompletedTask;
        }

        var json = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(json)) {
            return Task.CompletedTask;
        }

        //Try to parse the provided value into the desired model
        var model = JsonConvert.DeserializeObject<SalesRecord>(json);

        //Model will be null if unable to desrialize.
        if (model == null) {
            bindingContext.ModelState
                .TryAddModelError(
                    bindingContext.ModelName,
                    "Invalid data"
                );
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, model);

        //could consider checking model state if so desired.

        //set result state of binding the model
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

从那里开始,现在应该是在动作中使用模型的简单问题

[HttpPost("create", Name = "CreateSalesRecord")]
public IActionResult Create([FromBody] SalesRecord record) {
    if(ModelState.IsValid) {
        //...
        return Ok();
    }

    return BadRequest(ModelState);
}

免责声明:这尚未经过测试。可能还有一些问题需要解决,因为它基于上面提供的链接来源。

【讨论】:

    【解决方案2】:

    注意:这假定 JSON 输入将始终有效。如果这不是真的,您将不得不添加一些检查。

    如果您不想让这变得过于复杂,您可以使用 DLR 的帮助。 NewtonSoft.Json 序列化程序允许您反序列化为 dynamic 对象:

    [HttpPost]
    public IActionResult CreateSalesRecord([FromBody]dynamic salesRecord)
    {
        return Ok(new SalesRecord
        {
            FirstName = salesRecord.user.name.first,
            LastName = salesRecord.user.name.Last,
            PaymentType = salesRecord.payment.type
        });
    }
    

    【讨论】:

    • 不是动态路线的忠实粉丝,因为动态敲击效果。您是否查看了自定义模型绑定器?
    • @Nkosi 我也不是粉丝,当然也不会在生产代码中采用这种方式。我宁愿拥有与输入匹配的模型,而不是自定义活页夹
    • 您仍然可以拥有模型,您只需绑定然后使用自定义绑定器。该框架允许这种灵活性。如果你想看看,我在 OP 的 cmets 中添加了一些链接。
    • @Nkosi 谢谢,我会记住这一点,这是一种有趣的方法
    • @Nkosi 您能否详细说明您所指的“动态连锁效应”?
    【解决方案3】:
    [HttpPost]
        public IActionResult Json(string json)
        {
            JObject j = JObject.Parse(json);
            MyModel m = j.ToObject<MyModel>();
            return View();
        }
    

    如果你的 Json 是字符串格式,你可以试试这个。我相信它的工作你的数据模型必须是 Json 的精确表示。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-27
      • 1970-01-01
      • 2019-08-27
      • 2013-08-24
      • 2022-01-05
      • 1970-01-01
      相关资源
      最近更新 更多