.NET API中如何区分DTO属性显式null与未传入?
区分DTO属性显式传入null与未传入的解决方案
在ASP.NET Core中,默认的JSON序列化会把未传入的可空属性和显式传入null的属性都映射为null,要区分这两种场景,有以下几种实用方案:
方法1:利用JsonExtensionData追踪传入的属性键
在DTO中添加标记了[JsonExtensionData]的字典,它会捕获请求体中所有的属性键(包括已定义的属性),通过检查字典中是否存在目标属性名,就能判断是显式传了null还是根本没传。
using System.Text.Json.Serialization; public class ExampleDto { public int? PropertyA { get; init; } public string PropertyB { get; init; } // 捕获所有请求体中的属性键(注意JSON键是驼峰格式) [JsonExtensionData] private IDictionary<string, JsonElement> _extensionData; // 对外提供判断方法 public bool IsPropertyASupplied() { return _extensionData?.ContainsKey("propertyA") ?? false; } }
接口中使用示例:
public IActionResult Update([FromBody] ExampleDto dto) { if (dto.IsPropertyASupplied()) { // 显式传入了PropertyA(值可能为null),执行设为null的逻辑 // 例如:dbEntity.PropertyA = dto.PropertyA; } else { // 未传入PropertyA,不处理该属性 } // 其他业务逻辑... return Ok(); }
方法2:自定义Optional<T>包装类型
创建泛型包装类明确区分「未设置」和「设置为null」的状态,配合自定义JSON转换器实现序列化/反序列化逻辑。
首先定义包装类和转换器:
using System.Text.Json; using System.Text.Json.Serialization; public class Optional<T> { public bool HasValue { get; } public T Value { get; } private Optional(T value, bool hasValue) { Value = value; HasValue = hasValue; } public static Optional<T> Unset() => new(default, false); public static Optional<T> Set(T value) => new(value, true); // 隐式转换,简化赋值 public static implicit operator Optional<T>(T value) => Set(value); } public class OptionalJsonConverter<T> : JsonConverter<Optional<T>> { public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // 只要JSON中存在该属性,就标记为已设置(不管值是不是null) var value = JsonSerializer.Deserialize<T>(ref reader, options); return Optional<T>.Set(value); } public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) { if (value.HasValue) { JsonSerializer.Serialize(writer, value.Value, options); } else { writer.WriteNullValue(); } } }
修改DTO并使用:
public class ExampleDto { [JsonConverter(typeof(OptionalJsonConverter<int?>))] public Optional<int?> PropertyA { get; init; } = Optional<int?>.Unset(); public string PropertyB { get; init; } }
接口逻辑:
public IActionResult Update([FromBody] ExampleDto dto) { if (dto.PropertyA.HasValue) { // 显式传入了PropertyA,执行对应逻辑 // dbEntity.PropertyA = dto.PropertyA.Value; } else { // 未传入PropertyA,跳过处理 } return Ok(); }
这种方法语义清晰,适合需要频繁区分该场景的项目。
方法3:直接解析JsonDocument(手动映射)
如果不想修改现有DTO结构,可以直接接收JsonDocument,手动解析并判断属性是否存在,再映射到DTO对象。
public IActionResult Update([FromBody] JsonDocument requestDoc) { var root = requestDoc.RootElement; var dto = new ExampleDto(); // 处理PropertyB if (root.TryGetProperty("propertyB", out var propB)) { dto.PropertyB = propB.GetString(); } // 判断PropertyA是否被传入 bool isPropertyASupplied = root.TryGetProperty("propertyA", out var propA); if (isPropertyASupplied) { dto.PropertyA = propA.ValueKind == JsonValueKind.Null ? null : propA.GetInt32(); // 执行设为null的逻辑 } else { // 未传入,不处理 } // 其他业务逻辑... return Ok(); }
这种方法无需修改DTO,但需要手动处理属性映射,适合简单场景。
内容的提问来源于stack exchange,提问作者Ruben Hart
相关产品推荐
相关产品推荐

