如何区分 Asp.Net 核心模型绑定中 JSON 中的空数据和不存在数据?

How to differentiate between null and non existing data in JSON in Asp.Net Core model binding?

我想在 Asp.Net 核心的一个动作中区分这两个 json 输入:

{
  "field1": null,
  "field2": null
}

{
  "field1": null,
}

我在 C# 中有一个像这样的普通 class:

public class MyData
{
   public string Field1 { get; set;}
   public string Field2 { get; set;}
}

我想 运行 可以接受 null 作为值的对象的部分更新,但是当该字段不在输入中时,这意味着我根本不想更新该字段(将其设置为 null 的其他原因)。

低调的会是这样:

public class MyData
{
  public string Field1 { get; set; }
  public string Field2 { get; set; }

  // this can be extension method also.
  public bool HasProperty(string propertyName)
  {
    return GetType().GetProperty(propertyName) != null;
  }
}

主要内容:

  string json = "{  \"field1\": null,  \"field2\": null }";
  MyData jsonObject = null;

  jsonObject = JsonConvert.DeserializeObject<MyData>(json);

  MyData source= null; // this will contain values  

  if (jsonObject.HasProperty("Field1"))
    source.Field1 = jsonObject.Field1;

参考:Check if a property exist in a class

简介: Asp.net core获取你的request body然后反序列化为一个MyData类型的对象,然后通过传入对象调用你controller中的方法作为参数。从对象 myData 中,您无法知道 field2 是否为空或未通过。 属性 的两种方式都将具有空值。您试图查找的信息在反序列化时丢失了。

解决方法: 这个需要阅读请求体,检查请求体是否传递了该字段。在 asp.net 核心中,一旦请求主体被读取(由 asp.net 核心框架创建 MyData 对象),读取请求主体有点复杂。我们需要倒回请求流,然后读取它。它的代码如下。

[HttpPost]
public void Post([FromBody] MyData myData)
{
    HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
    System.IO.StreamReader sr = new System.IO.StreamReader(HttpContext.Request.Body);
    var requestBody = sr.ReadToEnd();
    //Now check the requestBody if the field was passed using JSON parsing or string manipulation
    Console.WriteLine(requestBody);
}

警告:虽然这会起作用。您尝试做的事情会降低可读性并使其他开发人员难以理解。区分字段值是否为空或不存在于请求正文中不是一种常见做法。

这就是我最终所做的,因为所有其他选项似乎都过于复杂(例如 jsonpatch、模型绑定)或者不会提供我想要的灵活性。

这个解决方案意味着要为每个 属性 编写一些样板文件,但不要太多:

public class UpdateRequest : PatchRequest
{
    [MaxLength(80)]
    [NotNullOrWhiteSpaceIfSet]
    public string Name
    {
       get => _name;
       set { _name = value; SetHasProperty(nameof(Name)); }
    }  
}

public abstract class PatchRequest
{
    private readonly HashSet<string> _properties = new HashSet<string>();

    public bool HasProperty(string propertyName) => _properties.Contains(propertyName);

    protected void SetHasProperty(string propertyName) => _properties.Add(propertyName);
}

然后可以这样读取值:

if (request.HasProperty(nameof(request.Name)) { /* do something with request.Name */ }

这是使用自定义属性对其进行验证的方式:

var patchRequest = (PatchRequest) validationContext.ObjectInstance;
if (patchRequest.HasProperty(validationContext.MemberName) {/* do validation*/}

再多加 2 美分,我们采用与 类似的方式,只是我们不是从 setter 调用 SetHasProperty,而是覆盖 DefaultContractResolver ]:

public class PatchRequestContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);

        prop.SetIsSpecified += (o, o1) =>
        {
            if (o is PatchRequest patchRequest)
            {
                patchRequest.SetHasProperty(prop.PropertyName);
            }
        };

        return prop;
    }
}

然后在启动中注册这个解析器:

services
    .AddControllers()
    .AddNewtonsoftJson(settings =>
        settings.SerializerSettings.ContractResolver = new PatchRequestContractResolver());```

请注意,我们仍在使用 JSON.Net 而不是 System.Text.Json(.Net 3+ 的默认设置)进行反序列化。截至目前 there's no way 使用 System.Text.Json

做类似于 DefaultContractResolver 的事情