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

时间:2017-12-08 16:06:09

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

我想创建和端点符合" JSON Merge Patch" https://tools.ietf.org/html/rfc7396

请不要将它与" JavaScript Object Notation(JSON)Patch"混淆。 https://tools.ietf.org/html/rfc6902

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

  • 删除属性值,此处删除了电子邮件值:

    {
        surname: "Kowalski"
        email: null
    }
    
  • 属性未包括在内,因为客户端根本不想更新它,此处不包含电子邮件,因为它不应该更新:

    {
        surname: "Kowalski"
    }
    

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

您是否有建议如何实施?

4 个答案:

答案 0 :(得分:4)

此处您需要3种不同的电子邮件状态:

  1. 更新的已填充值(例如test@mail.com
  2. 如果要删除电子邮件,则
  3. null
  4. 如果不接触电子邮件,则缺少值。
  5. 所以问题实际上是如何在模型的string属性中表达这3个状态。您不能仅使用原始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<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属性,则OptionalValueHasValue设置为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
        }
    }
    

答案 1 :(得分:2)

当使用不支持JavaScript和TypeScript那样的undefinednull之间的区别的语言时,这是一个特殊的问题。您可能还会考虑其他选项:

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

上面的每个选项都有其自身的缺点,因此在决定走哪条路之前要仔细考虑。

答案 2 :(得分:1)

您可以使用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 :(得分:1)

我以相同的问题来到这个话题。我的解决方案与“ 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<Optional<string>>(() => 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
        }
    }