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

时间:2016-02-21 04:52:01

标签: c# asp.net-web-api json.net

我正在使用一个通用方法,它允许我修改返回给客户端的对象的JSON,特别是删除返回对象中的某些属性。与建议here类似。

这些修改是非确定性的,因为它们是根据与用户相关的规则按请求确定的。所以这不适合缓存的方法。

我已经回顾了几种方法。最明显的选择是JsonConverter,但是存在问题,如列出hereherehere

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

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

我也尝试在WriteJson中使用不同的序列化程序(即不是提供给方法的序列化程序)但是这不支持递归(因为序列化程序不使用我的JsonConverter)所以任何嵌套我的JsonConverter不处理这些项目。从默认的序列化程序的转换器集合中删除我的JsonConverter也存在同样的问题。

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

理想情况下,JToken.FromObject会有某种方式选择性地不在对象本身上调用JsonConverter,但在序列化期间仍然将它应用于任何子对象。只有当传递给CanConvert的对象与传递给CanWrite的最后一个对象的类型不同时,我才通过修改CanConvertWriteJson设置为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. 使用自定义ContractResolver (这会遭受相同的缓存 问题,即使我使用现在过时的构造函数 DefaultContractResolver将“shareCache”设置为false)
  5. 任何人都可以建议: -

    • 根据请求制作JsonConverters的方法
    • 假设无法按请求制作,使用JsonConverter解决线程问题的方法
    • JsonConverter的替代方案,它允许我在JSON对象返回到不依赖大量反射开销的客户端之前全局检查和修改它们
    • 别的什么?

    提前感谢您花时间阅读本文。

2 个答案:

答案 0 :(得分:2)

为多线程,多类型方案修复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

原型fiddle

答案 1 :(得分:1)

以典型的方式,提出这个问题的过程让我对问题采取了新的看法。

我找到了一种可能的解决方法:创建自定义MediaTypeFormatter。

herehere的帮助下,一个潜在的解决方案: -

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实例,结合原始帖子中的其他修复,似乎解决了这个问题。

这可能不是最好的方法,所以我会留下这个更好的建议,或直到我意识到为什么这不起作用。