在返回到客户端之前修改 JSON 的通用方法

本文关键字:JSON 方法 修改 返回 客户端 | 更新日期: 2023-09-27 18:35:36

我正在寻找一种通用方法,该方法允许我修改返回给客户端的对象的JSON,特别是删除返回对象中的某些属性。类似于此处的建议。

这些修改是不确定的,因为它们是根据与用户关联的规则根据每个请求确定的。因此,这不适合缓存的方法。

我已经回顾了几种方法。最明显的选择是 JsonConverter,但是这存在问题,如此处、此处和此处所列。

这种方法的主要问题是,在WriteJson中调用JToken.FromObject以获取特定值的 JSON,递归调用相同的 JsonConverter,从而导致循环。

我已经尝试了此处列出的解决方案的变体,它提供了一种暂时禁用CanWrite以防止循环问题的方法。但是,它似乎不适用于多个并发请求。JsonConverter 的单个实例在不同时间更改和读取 CanWrite 属性状态的多个线程之间共享,从而导致结果不一致。

我还尝试在 WriteJson 中使用不同的序列化程序(即提供给该方法的序列化程序除外),但这不支持递归(因为该序列化程序不使用我的 JsonConverter),因此任何嵌套项目都不会由我的 JsonConverter 处理。从默认序列化程序的转换器集合中删除我的 JsonConverter 也有同样的问题。

基本上,如果我希望能够递归处理我的模型对象,我将遇到自引用循环问题。

理想情况下,JToken.FromObject有某种方法可以选择性地不调用对象本身的 JsonConverter,但在序列化期间仍将其应用于任何子对象。只有当传递给CanConvert的对象与传递给WriteJson的最后一个对象不同时,我才通过修改CanConvertCanWrite设置为 true 来解决此问题。

但是,要使其正常工作,我需要一个每个请求范围的 JsonConverter(出于与上述线程原因相同的原因),但我看不到如何获得它。

这是我拥有的示例:-

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Test
{
    public class TestConverter : JsonConverter
    {
        bool CannotWrite { get; set; }
        public override bool CanWrite { get { return !CannotWrite; } }
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            JToken token;
            //----------------------------------------
            // this works; but because it's (i think) creating a new
            // serializer inside the FromObject method
            // which means any nested objects won't get processed
            //token = JToken.FromObject(value);
            //----------------------------------------
            // this creates loop because calling FromObject will cause this
            // same JsonConverter to get called on the same object again
            //token = JToken.FromObject(value, serializer);
            //----------------------------------------
            // this gets around the loop issue, but the JsonConverter will
            // not apply to any nested objects
            //serializer.Converters.Remove(this);
            //token = JToken.FromObject(value, serializer);
            //----------------------------------------
            // see https://stackoverflow.com/a/29720068/1196867
            //
            // this works as it allows us to use the same serializer, but
            // temporarily sets CanWrite to false so the invocation of
            // FromObject doesn't cause a loop
            //
            // this also means we can't process nested objects, however
            // see below in CanConvert for a potential workaround.
            using (new PushValue<bool>(true, () => CannotWrite, (cantWrite) => CannotWrite = cantWrite))
            {
                token = JToken.FromObject(value, serializer);
            }
            // store the type of this value so we can check it in CanConvert when called for any nested objects
            this.currentType = value.GetType();
            //----------------------------------------
            // in practice this would be obtained dynamically
            string[] omit = new string[] { "Name" };
            JObject jObject = token as JObject;
            foreach (JProperty property in jObject.Properties().Where(p => omit.Contains(p.Name, StringComparer.OrdinalIgnoreCase)).ToList())
            {
                property.Remove();
            }
            token.WriteTo(writer);
        }
        private Type currentType;
        public override bool CanConvert(Type objectType)
        {
            if (typeof(Inua.WebApi.Authentication.IUser).IsAssignableFrom(objectType))
            {
                // if objectType is different to the type which is currently being processed,
                // then set CanWrite to true, so this JsonConverter will apply to any nested
                // objects that we want to process
                if (this.currentType != null && this.currentType != objectType)
                {
                    this.CannotWrite = false;
                }
                return true;
            }
            return false;
        }
        public override bool CanRead { get { return false; } }
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
}

我考虑过的选项:-

  1. 使用自定义 JsonConverter,但手动构建 JSON 而不是利用JToken.FromObject(增加了很多复杂性)
  2. 使用 ActionFilterAttribute 并从序列化之前的模型(我需要对每个请求修改模型对象)
  3. 在我的模型中使用ShouldSerialzeX()执行查找的方法(不容易维护)
  4. 使用自定义合约解析程序(这遭受相同的缓存问题,即使我在将"shareCache"设置为false的默认合同解析器)

谁能建议:-

  • 一种使 JsonConverters 按请求的方法
  • 假设它不能按请求进行,这是一种使用 JsonConverter 解决线程问题的方法
  • JsonConverter 的替代方案,允许我在将 JSON 对象返回到客户端之前对其进行全局检查和修改,这不依赖于大量的反射开销
  • 别的?

提前感谢您抽出宝贵时间阅读本文。

在返回到客户端之前修改 JSON 的通用方法

修复多线程、多类型方案TestConverter的一种可能性是创建要序列化的类型[ThreadStatic]堆栈。 然后,在 CanConvert 中,如果候选类型与堆栈顶部的类型属于同一类型,则返回 false

请注意,这仅在转换器包含在 JsonSerializerSettings.Converters 中时才有效。 如果转换器直接应用于类或属性,例如,

    [JsonConverter(typeof(TestConverter<Inua.WebApi.Authentication.IUser>))]

然后无限递归仍然会发生,因为直接应用的转换器不需要CanConvert

因此:

public class TestConverter<TBaseType> : JsonConverter
{
    [ThreadStatic]
    static Stack<Type> typeStack;
    static Stack<Type> TypeStack { get { return typeStack = (typeStack ?? new Stack<Type>()); } }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken token;
        using (TypeStack.PushUsing(value.GetType()))
        {
            token = JToken.FromObject(value, serializer);
        }
        // in practice this would be obtained dynamically
        string[] omit = new string[] { "Name" };
        JObject jObject = token as JObject;
        foreach (JProperty property in jObject.Properties().Where(p => omit.Contains(p.Name, StringComparer.OrdinalIgnoreCase)).ToList())
        {
            property.Remove();
        }
        token.WriteTo(writer);
    }
    public override bool CanConvert(Type objectType)
    {
        if (typeof(TBaseType).IsAssignableFrom(objectType))
        {
            return TypeStack.PeekOrDefault() != objectType;
        }
        return false;
    }
    public override bool CanRead { get { return false; } }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
public static class StackExtensions
{
    public struct PushValue<T> : IDisposable
    {
        readonly Stack<T> stack;
        public PushValue(T value, Stack<T> stack)
        {
            this.stack = stack;
            stack.Push(value);
        }
        #region IDisposable Members
        // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
        public void Dispose()
        {
            if (stack != null)
                stack.Pop();
        }
        #endregion
    }
    public static T PeekOrDefault<T>(this Stack<T> stack)
    {
        if (stack == null)
            throw new ArgumentNullException();
        if (stack.Count == 0)
            return default(T);
        return stack.Peek();
    }
    public static PushValue<T> PushUsing<T>(this Stack<T> stack, T value)
    {
        if (stack == null)
            throw new ArgumentNullException();
        return new PushValue<T>(value, stack);
    }
}

在您的情况下TBaseType将是Inua.WebApi.Authentication.IUser.

原型小提琴。

以典型的方式,提出问题的过程使我对问题有了新的认识。

我找到了一个可能的解决方法:创建自定义 MediaType格式化程序。

在这里和这里的帮助下,一个潜在的解决方案:-

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Formatting;
using System.Text;
using System.Threading.Tasks;
namespace Test
{
    public class TestFormatter : MediaTypeFormatter
    {
        public TestFormatter()
        {
            SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"));
        }
        public override bool CanReadType(Type type)
        {
            return false;
        }
        public override bool CanWriteType(Type type)
        {
            return true;
        }
        public override Task WriteToStreamAsync(Type type, object value, System.IO.Stream writeStream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext)
        {
            JsonSerializer serializer = new JsonSerializer();
            serializer.ContractResolver = new CamelCasePropertyNamesContractResolver();
            serializer.Converters.Add(new TestConverter());
            return Task.Factory.StartNew(() =>
            {
                using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(writeStream, Encoding.ASCII)) { CloseOutput = false })
                {
                    serializer.Serialize(jsonTextWriter, value);
                    jsonTextWriter.Flush();
                }
            });
        }
    }
}

然后配置应用程序以使用它:-

// insert at 0 so it runs before System.Net.Http.Formatting.JsonMediaTypeFormatter
config.Formatters.Insert(0, new TestFormatter());

这会为每个请求创建一个我的 JsonConverter 的新实例,该实例与原始帖子中的其他修复程序相结合,似乎可以解决问题。

这可能不是最好的方法,所以我会保留这个开放以获得一些更好的建议,或者直到我意识到为什么这行不通。