【问题标题】:How to tell the difference between null/default and a property being omitted when binding a model in ASP.NET Core在 ASP.NET Core 中绑定模型时如何区分 null/default 和被省略的属性
【发布时间】:2022-07-23 11:15:27
【问题描述】:

我的场景是这样的:

我正在 REST API 中实现更新操作,该 API 处理典型的 CRUD 类型操作并使用 ASP.NET Core 开发。 API 中的操作通常采用 json 负载。

我希望能够区分何时从 json 有效负载中省略某个属性,以及何时将 json 有效负载中的 a 属性设置为 null(或该特定类型的默认值)。

例如,在一个特定的操作中,有一个可选的到期日期,它可以是 DateTime 值或 null。 因此,一旦模型被绑定 - 并且到期日期为空 - 如何判断它是否为空,因为客户端想要将其更新为空(设置为无到期日期),或者因为客户端从有效负载中省略了它(并且所以会表明他们根本不想更新它)?

我尝试过的:

我在代码分析命名空间中实现了一个类似于Optional 的结构,因为它具有对一个值的引用,并跟踪该值是否已设置/是否有意义。

我尝试为我的结构实现自定义 JSON 反序列化器和 TypeConverter,但两种方法似乎都不起作用。令人沮丧的是,如果属性的值为 null,在模型绑定期间似乎没有调用 TypeConverter 的自定义反序列化器或 ConvertFrom 方法,这会导致我的可选项的默认构造函数用于未省略的 null 值,所以我无法区分空值和遗漏。

我拥有的可选值是模型上的属性(我想在多个模型中使用它们),所以从我收集到的信息来看,使用自定义模型绑定器是不合适的(并且并没有真正让我得到任何我不认为更接近)。

举个具体的例子,我的结构体的精简版如下;

[TypeConverter(typeof(OptionalConverter))]
[JsonConverter(typeof(OptionalJsonConverter))]
public readonly struct Optional<T>
{
    private readonly T _value;

    public T Value
    {
        get
        {
            return _value;
        }
    }

    public bool HasValue { get; }

    public Optional(T value)
    {
        HasValue = true;
        _value = value;
    }

    public static implicit operator Optional<T>(T value)
    {
        return new Optional<T>(value);
    }

    public static explicit operator T(Optional<T> value)
    {
        return value.Value;
    }
}

相关的类型转换器方法如下:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    return true;
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    return Activator.CreateInstance(typeof(Optional<>).MakeGenericType(context.PropertyDescriptor.PropertyType), new object[] { value });
}

我的相关 JSON 转换器方法是(我使用的是 newtonsoft(请注意,如果我手动反序列化字符串,它会按照我想要的方式工作)):

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    if (reader.TokenType != JsonToken.Undefined)
    {
        var genericTypes = objectType.GetGenericArguments();
        var value = serializer.Deserialize(reader, genericTypes[0]);

        return Activator.CreateInstance(
            typeof(Optional<>).MakeGenericType(genericTypes[0]),
                new object[] { value });
    }

   return existingValue;
}

我有如下测试路线;

[HttpPut]
[Route("foo")]
public IActionResult Foo(SomeDto someDto)
{
    return Ok();
}

还有一个测试模型DTO;

public class SomeDto
{
    public Optional<string> Foo { get; set; }

    public Optional<string> Bar { get; set; }

    public Optional<string> Baz { get; set; }
}

鉴于{ "foo": "foo", "bar": null } 的 /foo 的 PUT,我希望将 someDto 的值绑定为:

{
  Foo: { Value: "foo", HasValue: true },
  Bar: { Value: null, HasValue: true }, <-- was passed as null.
  Baz: { Value: null, HasValue: false } <-- omitted.
}

但是我得到了

{
  Foo: { Value: "foo", HasValue: true },
  Bar: { Value: null, HasValue: false }, <-- was passed as null.
  Baz: { Value: null, HasValue: false } <-- omitted.
}

这似乎是因为一旦值为 null,ASP.NET 绑定器就会使用结构的默认构造函数,因此不会让您有机会使用 JSON 或类型转换器提供不同的值。 我对解决这个绑定问题可能缺少的东西感到茫然,但也许我缺少了一些东西。如果没有人确认这种方法不可行,至少会有所帮助。

注意:

我意识到还有其他方法可以实现类似的结果,例如有一个单独的路由来更新实体上的每个字段,或者使用 jsonpatch。但是这些对客户端如何使用 API 有影响,所以我宁愿只在无法解决的情况下走这条路。

【问题讨论】:

标签: asp.net asp.net-core binding deserialization model-binding


【解决方案1】:

我设法回答了我的问题,因为事实证明这是我自己的愚蠢错误。

我有默认的序列化设置来忽略空值,这就是为什么空值没有被传递给 JsonConverter 或 TypeConverter。

即我有

services.AddControllers(options =>
{
    ...
})
.AddNewtonsoftJson(options =>
{
    ...
    options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    ...
});

所以有两种解决方案;

  1. 请勿设置上述设置(或将其设置为包含)。
  2. 创建一个可以覆盖设置的自定义合同解析器(示例如下)。
public class OptionalJsonContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>))
        {
            // We always want this to be include unless otherwise specified so the value always gets deserialized
            // even when null.
            property.NullValueHandling ??= NullValueHandling.Include;
            
            // You can also add code to set ShouldSerialize to false if the 
            // Optional.HasValue is false here.
        }

        return property;
    }
}

然后将您的启动更新为:

services.AddControllers(options =>
{
    ...
})
.AddNewtonsoftJson(options =>
{
    ...
    options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    options.SerializerSettings.ContractResolver = new OptionalJsonContractResolver();
    ...
});

我仍然需要解决这个问题,看看是否还有其他令人烦恼的副作用,但这至少回答了我最初的问题。

【讨论】:

    猜你喜欢
    • 2021-10-28
    • 2019-11-21
    • 2022-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-20
    • 2019-06-28
    • 2018-01-02
    相关资源
    最近更新 更多