简体   繁体   English

使用嵌套JSON对象进行模型绑定

[英]Model Binding with Nested JSON Objects

I'm writing an endpoint to accept a POST request on a webhook from a 3rd party and the data they are sending is a JSON encoded body. 我正在编写一个端点来接受来自第三方的webhook上的POST请求,并且他们发送的数据是JSON编码的主体。 So, I have no control over the data being sent to me and I need to handle it. 所以,我无法控制发送给我的数据,我需要处理它。 My problem is that they do a lot of nesting in their JSON and since I'm only using a few of the keys they are sending me I don't want to create a bunch of unnecessary nested models just to get the data I want. 我的问题是他们在他们的JSON中做了很多嵌套,因为我只使用他们发送给我的一些键,我不想创建一堆不必要的嵌套模型来获取我想要的数据。 Here is an example payload: 这是一个示例有效负载:

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

and I want to put that in a model that looks like: 我想把它放在一个看起来像这样的模型中:

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

Example of the endpoint (not much there yet): 端点示例(目前还不多):

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

My past work has been in the Phalcon PHP Framework where I would generally just access the POST Body directly and set the values in the model myself. 我过去的工作一直在Phalcon PHP框架中,我通常只是直接访问POST Body并自己设置模型中的值。 I certainly see the merits of model binding but I don't understand how to properly work around this situation yet. 我当然看到模型绑定的优点,但我不明白如何正确解决这种情况。

For a scenario like this one would need a custom model binder. 对于这样的场景,需要一个自定义模型绑定器。 The framework allows for such flexibility. 该框架允许这种灵活性。

Using the walkthrough provided here 使用此处提供的演练

Custom model binder sample 定制模型粘合剂样品

and adapting it to this question. 并使其适应这个问题。

The following sample uses the ModelBinder attribute on the SalesRecord model: 下面的示例使用ModelBinder的属性SalesRecord模型:

[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;}
}

In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind SalesRecord action parameters. 在上面的代码中, ModelBinder属性指定了应该用于绑定SalesRecord操作参数的IModelBinder的类型。

The SalesRecordBinder is used to bind an SalesRecord parameter by trying to parse the posted content using a custom JSON converter to simplify the deseiralization. SalesRecordBinder用于通过尝试使用自定义JSON转换器解析发布的内容来绑定SalesRecord参数,以简化去酶化。

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? 来源:我可以在属性中指定一个路径,将我的类中的属性映射到我的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;
    }
}

From there it should be now a simple matter of using the model in the action 从那里开始,现在应该是在动作中使用模型的简单问题

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

    return BadRequest(ModelState);
}

Disclaimer: This has not been tested as yet. 免责声明:尚未经过测试。 There may be issues still to be fixed as it is based on the linked sources provided above. 可能还有一些问题需要解决,因为它基于上面提供的链接源。

Note : this assumes the JSON input will always be valid. 注意 :这假定JSON输入始终有效。 You will have to add some checking if this is not true. 如果不是这样,你将不得不添加一些检查。

If you don't want to make this too complex, you can use the help of the DLR. 如果您不想使其过于复杂,可以使用DLR的帮助。 The NewtonSoft.Json serializer allows you to de-serialize into dynamic objects: 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
    });
}
[HttpPost]
    public IActionResult Json(string json)
    {
        JObject j = JObject.Parse(json);
        MyModel m = j.ToObject<MyModel>();
        return View();
    }

You could try this if Your Json is in string format. 如果你的Json是字符串格式,你可以尝试这个。 I believe for it to work Your data model will have to be exact representation of Json. 我相信它的工作你的数据模型必须是Json的精确表示。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM