在 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
但是,我在区分请求中的两种情况时遇到了一个小问题:
删除 属性 值,这里的电子邮件值被删除:
{
surname: "Kowalski"
email: null
}
属性 不包括因为客户根本不想更新它,这里不包括电子邮件,因为它不应该被更新:
{
surname: "Kowalski"
}
出现问题是因为在这两种情况下,模型绑定后电子邮件的值为 null。
您对如何实施有什么建议吗?
此处的电子邮件值需要 3 个不同的状态:
- 更新的填充值(例如
test@mail.com
)
null
应删除电子邮件的值
- 如果不应触摸电子邮件,则缺少值。
所以问题实际上是如何在模型的 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
}
}
当使用不支持区分 undefined
和 null
的语言时,这是一个特殊的问题,就像 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
库。
利用 the existent Optional
struct 定义模型(无需创建新的 OptionalValue
class)
{
public string Surname { get; set; }
[JsonConverter(typeof(OptionalConverter<string>))]
public Optional<string> Email { get; set; } = default;
}
告诉 Swagger(如果适用)将格式设置为字符串 input/type 以获得更好的客户体验:
c.MapType<Optional<string>>(() => new OpenApiSchema { Type = "string" });
添加自定义 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");
}
也就是。现在你有 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
}
}
我想创建符合“JSON合并补丁”的端点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" }
出现问题是因为在这两种情况下,模型绑定后电子邮件的值为 null。
您对如何实施有什么建议吗?
此处的电子邮件值需要 3 个不同的状态:
- 更新的填充值(例如
test@mail.com
) null
应删除电子邮件的值- 如果不应触摸电子邮件,则缺少值。
所以问题实际上是如何在模型的 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
}
}
当使用不支持区分 undefined
和 null
的语言时,这是一个特殊的问题,就像 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
库。
利用 the existent
Optional
struct 定义模型(无需创建新的OptionalValue
class){ public string Surname { get; set; } [JsonConverter(typeof(OptionalConverter<string>))] public Optional<string> Email { get; set; } = default; }
告诉 Swagger(如果适用)将格式设置为字符串 input/type 以获得更好的客户体验:
c.MapType<Optional<string>>(() => new OpenApiSchema { Type = "string" });
添加自定义 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"); }
也就是。现在你有 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 } }