在 ASP.NET Core 3 Web API 中进行模型验证之前从 json 数组属性中删除空项

问题描述 投票:0回答:1

我正在尝试从用户发布的有效负载中删除 json 数组属性的空项。我的模型属性在任何需要验证的地方都有很好的注释,比如 Required Attribute 等等。

我的 API 方法将 json 接受到模型绑定中,如下所示:

public async Task<IActionResult> Post([FromBody]List<MyModelEntity> myModelEntity) 
{ ... }

还有一些没有 List 的 API,例如:

    public async Task<IActionResult> Post([FromBody]MyModelEntity myModelEntity) 
{ ... }

GET 操作不需要删除空项。我正在使用下面的自定义

JsonConverter
在反序列化期间删除空项目(只是在模型验证之前尝试这样做):-

public class NullListFilterConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsGenericType &&
            (objectType.GetGenericTypeDefinition() == typeof(List<>) || objectType.GetGenericTypeDefinition() == typeof(IList<>)))
        {
            Type itemType = objectType.GetGenericArguments()[0];
            return itemType.IsClass;
        }
        return false;
    }

    //with Reflection ==> Performance degradation concern
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        var list = existingValue as IList
            ?? (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(objectType.GetGenericArguments()));

        serializer.Populate(reader, list);

        // Filter out null items in the list
        var nonNullItems = list.Cast<object>().Where(x => x != null).ToList();
        list.Clear();
        foreach (var item in nonNullItems)
        {
            list.Add(item);
        }

        // assign null to list count 0
        return list.Cast<object>().Count() == 0 ? null : list;
    }

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
    }
}

此代码通过执行 -

工作得很好
  1. 如果有的话,从 Json 数组属性中删除所有空项
  2. 如果在 Json 有效负载中提供额外的不匹配属性,模型实体照常为 null。

这个实现的唯一问题是一些性能问题,因为它使用反射。

为了避免反射,我尝试了以下方法:-

//without Reflection
public object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var jToken = JToken.Load(reader);
    var nullTokens = jToken.SelectTokens("$..[?(@ == null)]");
    nullTokens?.ToList().ForEach(token =>
    {
        if (token.Parent is JArray parentArray)
        {
            parentArray.Remove(token);
        }
        else if (token.Parent is JProperty parentProperty)
        {
            parentProperty.Value = JValue.CreateNull();
        }
    });

    return jToken.ToObject(objectType);
}

但是不知何故这段代码无法处理 json 有效负载中提供的额外属性,并且不会使整个模型实体为空。它失去了处理 web api 的不匹配模型接受的默认行为。

处理这个问题的方法是什么?还有其他方法可以从 json 数组属性中删除空项吗?

c# json.net asp.net-core-webapi asp.net-core-3.1 model-binding
1个回答
1
投票

因为您似乎只对在反序列化过程中从

List<T>
列表(或声明为
IList<T>
的属性,Json.NET 将反序列化为
List<T>
)过滤空值感兴趣,所以您可以利用
List<T>
还实现了非泛型
IList
接口以避免昂贵的反射调用:

public class NullListFilterConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) => CanConvert(objectType, out var _);

    public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        if (!CanConvert(objectType, out var itemType))
            throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
        if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
            return null;

        // Here we take advantage of the fact that List<T> also implements the non-generic IList:
        var list = existingValue as IList ?? (IList)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator!();

        while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
        {
            if (serializer.Deserialize(reader, itemType) is var value && value != null)
                list.Add(value);
        }
        // assign null to list count 0
        return list.Count == 0 ? null : list;
    }

    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => 
        throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");

    static bool CanConvert(Type type, out Type? itemType)
    {
        if (type.IsGenericType  && type.GetGenericTypeDefinition() is var genType
            // Here we assume that types declared as IList<T> are deserialized as List<T> by Json.NET
            && (genType == typeof(List<>) || genType == typeof(IList<>))) 
        {
            itemType = type.GetGenericArguments()[0];
            return itemType.IsClass; // TODO: decide whether to filter nullable value types.
        }
        itemType = null;
        return false;
    }   
}

public static partial class JsonExtensions
{
    public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) => 
        reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
    
    public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
        reader.ReadAndAssert().MoveToContentAndAssert();

    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

备注:

  • JsonContract.DefaultCreator()
    将是一个缓存委托,调用
    List<T>
    构造函数,该构造函数由 Json.NET 在合约创建期间使用代码生成技术创建。由于在调用
    ReadJson()
    时已经生成并保存了创建者委托,因此使用它会比调用
    Activator.CreateInstance()
    更快。

  • 注释不是 JSON 标准的正式部分,但受 Json.NET 支持。如果您的应用程序需要评论支持,您必须在您的

    JsonConverter.Read()
    中手动跳过它们。
    JsonExtensions
    类中的方法会自动处理此问题,并在遇到截断文件时抛出异常。

  • 我没有填充列表并在之后过滤掉空项,而是使用非泛型

    JsonSerializer.Deserialize(JsonReader, Type)
    重载单独反序列化每个项目,然后在将它们添加到列表之前跳过空值。

  • 您需要一种不同的方法来避免从泛型集合中过滤空值时避免昂贵的反射调用,例如

    HashSet<T>
    不实现非泛型可修改集合接口,例如
    IList
    .

  • 如果您对性能问题很敏感,我建议您避免将 JSON 预加载到转换器内的临时

    JArray
    中。正如 Matt Watson 的 11 种提高 JSON 性能和使用的方法 所指出的,加载到
    JToken
    层次结构比直接反序列化慢 20%。

演示小提琴这里.

© www.soinside.com 2019 - 2024. All rights reserved.