【问题标题】:Distinguish between NULL and not present using JSON Merge Patch with NetCore WebApi and System.Text.Json使用带有 Net Core Web Api 和 System.Text.Json 的 JSON Merge Patch 区分 NULL 和不存在
【发布时间】:2022-02-11 19:12:40
【问题描述】:

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

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

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

Newtonsoft 支持 IsSpecified-可用作 DTO 类 (How to make Json.NET set IsSpecified properties for properties with complex values?) 中 JSON 合并补丁解决方案的属性。这将解决问题,但再次需要 Newtonsoft。 System.Text.Json 不支持此功能。有一个未解决的问题 2 年 (https://github.com/dotnet/runtime/issues/40395),但没什么可期待的。

有一篇文章描述了一个带有 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 会有其他不良副作用吗?

【问题讨论】:

  • Optionals 是你的朋友。
  • 可选选项是什么意思?我不明白。
  • 似乎没有标准实现,但它们是一种通用类型,可让您区分空值和无值。
  • 您将在实现中看到的两个常见属性是HasValue 和Value。有一些包可以为你实现这个。
  • Here 是 Roslyn 使用的实现。

标签: json asp.net-core-webapi system.text.json json-patch


【解决方案1】:

通过 jhmckimm 的有用 cmets 我发现 Custom JSON serializer for optional property with System.Text.Json。 DBC 展示了一个使用 Text.Json 和 Optional 的绝妙解决方案。这应该在 Microsoft 文档中!

在 Startup 中我添加了:

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 是否设置了值。它也适用于写入,并允许我们根据权限对字段进行条带化。

【讨论】:

  • 我们正在寻求做类似的事情。这里要考虑的一些事情是这是否适用于 Automapper(DTO 和 DAO 之间的映射)以及从 DTO 自动生成 swagger 定义。
  • 我没用过AutoMapper,但我想你应该可以配置AutoMapper,因为Optional只是一个类。我手动编写了我的 ToDTO() 方法。我仍然有自动生成 Swagger 定义的问题。我的帖子stackoverflow.com/questions/71265953/… 还没有得到任何答案。我还在 github 上为 swashbuckle 写了一个问题:github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2359。那里也没有解决办法。如果您找到解决方案,请在此处发布。
猜你喜欢
  • 2019-04-18
  • 2017-05-07
  • 2021-04-13
  • 2018-06-29
  • 2020-11-16
  • 2018-12-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多