在 ASP.NET 核心中实施 "JSON Merge Patch" - 区分 null 和未定义 属性 的最佳方法

Implementing "JSON Merge Patch" in ASP.NET Core - best way do differentiate between null and not defined property

我想创建符合“JSON合并补丁”的端点https://www.rfc-editor.org/rfc/rfc7396

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

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

出现问题是因为在这两种情况下,模型绑定后电子邮件的值为 null。

您对如何实施有什么建议吗?

此处的电子邮件值需要 3 个不同的状态:

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

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

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<T>:

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<string>()。如果输入 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
    }
}

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

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

上面的每个选项都有其自身的缺点,因此在决定采用哪种方式之前请仔细考虑。

您可以使用 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

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

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

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

    c.MapType<Optional<string>>(() => new OpenApiSchema { Type = "string" });

  3. 添加自定义 JSON 转换器基于 System.text.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
        }
    }