Общий метод изменения JSON перед возвратом клиенту

Я получаю общий метод, который позволяет мне модифицировать JSON объекта, возвращаемого клиенту, в частности удаление определенных свойств в возвращаемых объектах. Подобно тому, что предлагается здесь.

Изменения не являются детерминированными, поскольку они определяются по запросу, на основе правил, связанных с пользователем. Таким образом, это не подходит для метода, который кэшируется.

Я рассмотрел несколько методов. Наиболее очевидным выбором будет JsonConverter, однако есть проблемы с этим, перечисленные здесь, здесь и здесь.

Основная проблема с этим подходом заключается в том, что вызов JToken.FromObject в WriteJson для получения JSON для определенного значения, рекурсивно вызывает тот же JsonConverter, что приводит к циклу.

Я попробовал вариант решения, перечисленного здесь, который предоставляет метод временного отключения CanWrite для предотвращения проблемы с циклом. Однако, похоже, что это не работает для нескольких одновременных запросов. Один экземпляр JsonConverter делится между несколькими потоками, которые изменяются и считывают состояние свойства CanWrite в разное время, вызывая непоследовательные результаты.

Я также пытался использовать другой сериализатор в WriteJson (то есть, отличный от того, который был предоставлен методу), но это не поддерживает рекурсию (поскольку этот сериализатор не использует мой JsonConverter), поэтому любые вложенные элементы не обрабатываются моими JsonConverter. Устранение моего JsonConverter из коллекции преобразователей сериализатора по умолчанию имеет ту же проблему.

В принципе, если я хочу иметь возможность рекурсивно обрабатывать объект модели, я собираюсь получить проблему с реферированием ссылок.

В идеале, JToken.FromObject будет иметь какой-то способ выборочно НЕ вызывать JsonConverter на самом объекте, но все равно применяет его к любым дочерним объектам во время сериализации. Я получил половину пути, чтобы исправить это, изменив CanConvert чтобы установить CanWrite в true, только если объект, переданный CanConvert был другим типом для последнего объекта, переданного в WriteJson.

Однако для этого для работы мне понадобится 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 (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 /questions/2339500/jsonnet-throws-stackoverflowexception-when-using-jsonconvert/6150388#6150388
            //
            // 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)

Кто-нибудь может предложить: -

  • Способ сделать JsonConverters по запросу
  • Предполагая, что это невозможно сделать по запросу, можно устранить проблему с потоком с помощью JsonConverter
  • Альтернатива JsonConverter, которая позволяет мне глобально проверять и изменять объекты JSON до того, как они будут возвращены клиенту, который не полагается на много отраженных накладных расходов
  • Что-то другое?

Заранее благодарим за то, что нашли время, чтобы прочитать это.

+2
источник поделиться
2 ответа

Одной из возможностей исправления TestConverter для многопоточных 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.

Прототип скрипта.

+1
источник

В типичной форме процесс постановки вопроса заставлял меня взять новую проблему.

Я нашел одно возможное решение: создание настраиваемого MediaTypeFormatter.

С помощью здесь и здесь можно найти потенциальное решение: -

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 для каждого запроса, который в сочетании с другими исправлениями в исходном сообщении, похоже, решает проблему.

Вероятно, это не самый лучший способ сделать это, поэтому я оставлю это открытым для некоторых лучших предложений или пока не пойму, почему это не сработает.

+1
источник

Связанные вопросы


Похожие вопросы

Посмотрите другие вопросы по меткам или Задайте вопрос