使用 JSON Merge Patch with Net Core Web Api 和 System.Text.Json 区分 NULL 和不存在

Distinguish between NULL and not present using JSON Merge Patch with NetCore WebApi and System.Text.Json

我想通过 JSON 合并补丁 支持部分更新。域模型基于始终有效的概念,没有 public 设置器。因此我不能只将更改应用到 class 类型。我需要将更改转换为特定命令。

由于 classes 具有可为 null 的属性,我需要能够区分设置为 null 和未提供的属性。

我知道 JSON 补丁。我可以使用 patch.JsonPatchDocument.Operations 浏览更改列表。 JSON 补丁很冗长,对客户来说更难。 JSON 补丁需要使用 Newtonsoft.Json(Microsoft 声明了一个选项,可以将 Startup.ConfigureServices 更改为仅对 JSON 补丁(https://docs.microsoft.com/en-us/aspnet/core/web-api/jsonpatch?view=aspnetcore-6.0)使用 Newtonsoft.Json

Newtonsoft 支持 IsSpecified-可用作 JSON DTO classes 中合并补丁的解决方案的属性(). This would solve the problem, but again requires Newtonsoft. System.Text.Json does not support this feature. There is an open issue for 2 years (https://github.com/dotnet/runtime/issues/40395 ),但没什么好期待的。

有一个 post 描述了使用 custom JsonConverter for Web API (https://github.com/dotnet/runtime/issues/40395) 的解决方案。此解决方案仍可用于 NetCore 吗?

我想知道在填充 DTO 对象后是否可以选择 访问原始 json 或控制器方法内的 json 对象.然后我可以手动检查是否设置了 属性。 Web Api 关闭了流,所以我不能再访问正文了。似乎有办法改变这种行为 (https://gunnarpeipman.com/aspnet-core-request-body/#comments)。它看起来很复杂,感觉就像一把太大的枪。我也不明白 NetCore 6 做了哪些更改。

这么简单的一道题竟然要绕那么多圈,真让我吃惊。有没有一种简单的方法可以使用 System.Text.Json 和 NetCore 6 来实现我的目标?还有其他选择吗?使用 Newtonsoft 会有任何其他不良副作用吗?

通过 jhmckimm 的有用评论,我找到了 DBC 使用 Text.Json 和 Optional 展示了一个绝妙的解决方案。这应该在 Microsoft 文档中!

在启动中我添加了:

services.AddControllers()
  .AddJsonOptions(o => o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)
  .AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new OptionalConverter()));

由于我们使用 <Nullable>enable</Nullable><WarningsAsErrors>nullable</WarningsAsErrors>,因此我调整了可空值的代码。

public readonly struct Optional<T>
    {
        public Optional(T? value)
        {
            this.HasValue = true;
            this.Value = value;
        }

        public bool HasValue { get; }
        public T? Value { get; }
        public static implicit operator Optional<T>(T value) => new Optional<T>(value);
        public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
    }

public class OptionalConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType) { return false; }
            if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false; }
            return true;
        }

        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
        {
            Type valueType = typeToConvert.GetGenericArguments()[0];

            return (JsonConverter)Activator.CreateInstance(
                type: typeof(OptionalConverterInner<>).MakeGenericType(new Type[] { valueType }),
                bindingAttr: BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null
            )!;
        }

        private class OptionalConverterInner<T> : JsonConverter<Optional<T>>
        {
            public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                T? value = JsonSerializer.Deserialize<T>(ref reader, options);
                return new Optional<T>(value);
            }

            public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) =>
                JsonSerializer.Serialize(writer, value.Value, options);
        }
    }

我的测试 DTO 如下所示:

public class PatchGroupDTO
    {
        public Optional<Guid?> SalesGroupId { get; init; }

        public Optional<Guid?> AccountId { get; init; }

        public Optional<string?> Name { get; init; }

        public Optional<DateTime?> Start { get; init; }

        public Optional<DateTime?> End { get; init; }
    }

我现在可以访问这些字段并检查 .HasValue 是否设置了值。它也适用于写作,并允许我们根据许可对字段进行条带化。