【问题标题】:Implementing "JSON Merge Patch" in ASP.NET Core - best way do differentiate between null and not defined property在 ASP.NET Core 中实现“JSON 合并补丁” - 区分空属性和未定义属性的最佳方法
【发布时间】:2018-05-22 22:01:03
【问题描述】:

我想创建和端点符合“JSON Merge Patch”https://www.rfc-editor.org/rfc/rfc7396

请不要将其与“JavaScript Object Notation (JSON) Patch”混淆 https://www.rfc-editor.org/rfc/rfc6902

但是,我在区分请求中的两种情况时遇到了一点问题:

  • 删除属性值,这里的电子邮件值正在被删除:

      {
          surname: "Kowalski"
          email: null
      }
    
  • 属性不包括在内,因为客户根本不想更新它,这里的电子邮件不包括在内,因为它不应该被更新:

      {
          surname: "Kowalski"
      }
    

出现问题是因为在模型绑定后的这两种情况下 email 的值都会为 null。

您有什么建议可以如何实现吗?

【问题讨论】:

    标签: c# rest asp.net-core api-design


    【解决方案1】:

    您需要 3 个不同的状态来获取电子邮件值:

    1. 更新的填充值(例如test@mail.com
    2. null 值是否应删除电子邮件
    3. 如果不应触摸电子邮件,则缺少值。

    所以问题实际上是如何在模型的string 属性中表达这三种状态。您不能仅使用原始 string 属性来执行此操作,因为正如您正确描述的那样,null 值和缺失值会发生冲突。 解决方案是使用一些标志来指示请求中是否提供了该值。您可以将此标志作为模型中的另一个属性,也可以在string 上创建一个简单的包装器,非常类似于Nullable<T> 类。 我建议创建简单的通用OptionalValue<T> 类:

    public class OptionalValue<T>
    {
        private T value;
        public T Value
        {
            get => value;
    
            set
            {
                HasValue = true;
                this.value = value;
            }
        }
    
        public bool HasValue { get; set; }
    }
    

    然后你需要自定义的JsonConverter,它可以将通常的json值反序列化为OptionalValue&lt;T&gt;

    class OptionalValueConverter<T> : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(OptionalValue<T>);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return new OptionalValue<T>
            {
                Value = (T) reader.Value,
            };
        }
    
        public override bool CanWrite => false;
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    您的模型将如下所示:

    public class SomeModel
    {
        public string Surname { get; set; }
    
        [JsonConverter(typeof(OptionalValueConverter<string>))]
        public OptionalValue<string> Email { get; set; } = new OptionalValue<string>();
    }
    

    请注意,您为电子邮件分配了空的OptionalValue&lt;string&gt;()。如果输入 json 不包含 email 值而不是 Email 属性将保留它 OptionalValue 并将 HasValue 设置为 false。 如果输入json包含一些email,甚至null,那么OptionalValueConverter将创建OptionalValue的实例,HasValue设置为true

    现在在控制器操作中,您可以确定 email 的 3 种状态中的任何一种:

    [HttpPatch]
    public void Patch([FromBody]SomeModel data)
    {
        if (data.Email.HasValue)
        {
            //  Email presents in Json
            if (data.Email.Value == null)
            {
                //  Email should be removed
            }
            else
            {
                //  Email should be updated
            }
        }
        else
        {
            //  Email does not present in Json and should not be affected
        }
    }
    

    【讨论】:

    • 它似乎工作,但随后我生成的 Swagger 还包含模型中的“HasValue”属性。可以隐藏吗?
    • 完全是一个单独的问题。请使用 Swagger 标签将其发布为新问题。
    • 好的,谢谢。你能回答另一个问题吗?假设我实施了您的解决方案。如果有人随后在 C# 项目中调用此 API 并想要进行部分更新。他应该怎么做?他将使用一个“SomeModel”类,其中包含他将序列化为 JSON 的所有属性。因此,他还将有特定的代码来区分被忽略的属性和他想要设置为 null 的属性。这是正确的吗 ?您认为这是部分更新的最佳解决方案吗?我不知道我是否应该改用 JSON PATCH...(或者 r0bnet 的解决方案,带有我想在 headers 中设置为 null 的 props)
    • 如果您在客户端使用 POCO 模型,您需要以某种方式指示是否应该将缺失值序列化为 null(如果应该删除值)或者根本不应该序列化(如果应该不可触摸)。考虑在构建请求时使用匿名对象,并仅填充应触及(更新或删除)的属性。
    • 谢谢。我想我最终会使用一个查询参数来指定我要更新的字段,而忽略其他的......
    【解决方案2】:

    您可以使用 JsonMergePatch 库吗? https://github.com/Morcatko/Morcatko.AspNetCore.JsonMergePatch

    用法很简单:

    [HttpPatch]
    [Consumes(JsonMergePatchDocument.ContentType)]
    public void Patch([FromBody] JsonMergePatchDocument<Model> patch)
    {
       ...
       patch.ApplyTo(backendModel);
       ...
    }
    

    它似乎支持将某些属性设置为 null,而其他属性保持不变。在内部,JsonMergePatchDocument 创建一个 JsonPatch 文档,请求中的每个项目都有一个 OperationType.Replace。 https://github.com/Morcatko/Morcatko.AspNetCore.JsonMergePatch/blob/master/src/Morcatko.AspNetCore.JsonMergePatch/Formatters/JsonMergePatchInputFormatter.cs

    【讨论】:

      【解决方案3】:

      当使用不支持区分 undefinednull 的语言(如 JavaScript 和 TypeScript)时,这是一个特殊的问题。您还可以考虑其他选项:

      • 使用 PUT(并非总是可行)
      • 对于字符串,请使用"" 将其删除,因为空字符串通常不是有效值(也不总是可行)
      • 添加一个额外的自定义标头以指示您是否真的要删除该值,并将默认值设置为 false(例如,X-MYAPP-SET-EMAIL=true 将删除电子邮件,如果它为空)。不利的一面是,这可能会破坏您的要求并给客户开发人员带来痛苦

      上面的每个选项都有自己的缺点,所以在决定要走哪条路之前请仔细考虑。

      【讨论】:

        【解决方案4】:

        我带着同样的问题来到这个帖子。我的解决方案与“CodeFuller”类似,但更完整,因为它使用更少的代码来覆盖 API 文档,并且更好。它还使用System.text.json instead of the Newtonsoft 库。

        1. 利用the existent Optional struct 定义您的模型(无需创建新的OptionalValue 类)

          {
              public string Surname { get; set; }
          
              [JsonConverter(typeof(OptionalConverter<string>))]
              public Optional<string> Email { get; set; } = default;
          }
          
          
        2. 告诉 Swagger(如果适用)格式化为字符串输入/类型以获得更好的客户体验:

          c.MapType&lt;Optional&lt;string&gt;&gt;(() =&gt; new OpenApiSchema { Type = "string" });

        3. 添加基于System.text.json的自定义JSON转换器:

          public class OptionalConverter<T> : JsonConverter<Optional<T>>
              {
                  // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to
                  public override bool CanConvert(Type typeToConvert) =>
                      typeToConvert == typeof(Optional<T>);
          
                  public override Optional<T> Read(
                      ref Utf8JsonReader reader,
                      Type typeToConvert,
                      JsonSerializerOptions options) =>
                      new Optional<T>(JsonSerializer.Deserialize<T>(ref reader, options));
          
                  public override void Write(
                      Utf8JsonWriter writer,
                      Optional<T> value,
                      JsonSerializerOptions options) =>
                      throw new NotImplementedException("OptionalValue is not suppose to be written");
              }
          
          
        4. 就是这样。现在你有 3 个状态:

          [HttpPatch]
          [Consumes("application/merge-patch+json")]
          public void Patch([FromBody]SomeModel data)
          {
              if (data.Email.HasValue)
              {
                  //  Email presents in Json
                  if (data.Email.Value == null)
                  {
                      //  Email should be removed
                  }
                  else
                  {
                      //  Email should be updated
                  }
              }
              else
              {
                  //  Email does not present in Json and should not be affected
              }
          }
          
          

        【讨论】:

          猜你喜欢
          • 2018-02-22
          • 1970-01-01
          • 1970-01-01
          • 2020-09-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-08-18
          • 1970-01-01
          相关资源
          最近更新 更多