Как заставить JSON.NET игнорировать объектные отношения?

Я работаю над проектом Entity Framework. Я хочу сериализовать кучу экземпляров класса сущностей. Я связал их вместе в контейнерный класс:

public class Pseudocontext
{
    public List<Widget> widgets;
    public List<Thing> things;

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

В частности, у моих классов сущностей есть виртуальные члены, которые позволяют мне писать код C#, который перемещается по всем моим отношениям между сущностями, не беспокоясь о фактических значениях ключей, соединениях и т. Д., И я хочу, чтобы JSON.NET игнорировал связанные части моей сущности классы.

На первый взгляд, кажется, есть опция конфигурации JSON.NET, которая делает именно то, о чем я говорю:

JsonSerializer serializer = new JsonSerializer();
serializer.PreserveReferencesHandling = PreserveReferencesHandling.None;

К сожалению, JSON.NET, похоже, игнорирует второе утверждение выше.

Я на самом деле нашел веб-страницу ( http://json.codeplex.com/workitem/24608), где кто-то еще довел эту проблему до сведения самого Джеймса Ньютона-Кинга, и его ответ (полностью) был "Написать распознаватель таможенных договоров."

Насколько я нахожу этот ответ неадекватным, я пытался следовать его указаниям. Я бы очень хотел иметь возможность написать "распознаватель контрактов", который игнорирует все, кроме примитивных типов, строк, объектов DateTime и моего собственного класса Pseudocontext вместе со списками, которые он содержит напрямую. Если у кого-то есть пример чего-то, что хотя бы напоминает это, это может быть все, что мне нужно. Это то, что я придумал самостоятельно:

public class WhatDecadeIsItAgain : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        JsonContract contract = base.CreateContract(objectType);
        if (objectType.IsPrimitive || objectType == typeof(DateTime) || objectType == typeof(string)
            || objectType == typeof(Pseudocontext) || objectType.Name.Contains("List"))
        {
            contract.Converter = base.CreateContract(objectType).Converter;
        }
        else
        {
            contract.Converter = myDefaultConverter;
        }
        return contract;
    }
    private static GeeThisSureTakesALotOfClassesConverter myDefaultConverter = new GeeThisSureTakesALotOfClassesConverter();
}

public class GeeThisSureTakesALotOfClassesConverter : Newtonsoft.Json.Converters.CustomCreationConverter<object>
{
    public override object Create(Type objectType)
    {
        return null;
    }
}

Когда я пытаюсь использовать вышеупомянутое (устанавливая serializer.ContractResolver в экземпляр WhatDecadeIsItAgain до сериализации), я получаю ошибки OutOfMemory во время сериализации, которые указывают, что JSON.NET сталкивается с ссылочными циклами, которые никогда не завершаются (несмотря на мои попытки сделать JSON.NET просто игнорирует ссылки на объекты).

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

Я понятия не имею, насколько верны эти предположения, и это нелегко сказать. Дизайн JSON.NET в значительной степени основан на наследовании реализации, переопределении методов и т. Д.; Я не очень люблю ООП, и я нахожу такой дизайн довольно неясным. Если бы я мог реализовать интерфейс "Пользовательский обработчик контрактов", Visual Studio 2012 мог бы очень быстро заглушить требуемые методы, и я думаю, что у меня не возникнет проблем с заполнением заглушек реальной логикой.

У меня не было бы проблем с написанием, например, метода, который возвращает "true", если я хочу сериализовать объект предоставленного типа, и "false" в противном случае. Возможно, я что-то упустил, но я не нашел такого метода для переопределения, и при этом я не смог найти гипотетический интерфейс (ICustomContractResolver?), Который бы сказал мне, что я на самом деле должен делать в последнем фрагменте кода вставлен выше.

Кроме того, я понимаю, что есть атрибуты JSON.NET ([JsonIgnore]?), Предназначенные для таких ситуаций. Я не могу действительно использовать этот подход, так как я использую "модель в первую очередь". Если я не решу разорвать всю архитектуру моего проекта, мои классы сущностей будут сгенерированы автоматически, и они не будут содержать атрибуты JsonIgnore, и я не чувствую себя комфортно при редактировании автоматических классов, содержащих эти атрибуты.

Кстати, какое-то время у меня были настроены вещи для сериализации ссылок на объекты, и я просто игнорировал все лишние данные "$ref" и "$id", которые JSON.NET возвращал в своих выходных данных сериализации. По крайней мере, на данный момент я отказался от этого подхода, потому что (довольно неожиданно) сериализация заняла слишком много времени (~45 минут, чтобы получить ~5 МБ JSON).

Я не смог связать это внезапное изменение производительности с чем-то конкретным, что я сделал. Во всяком случае, объем данных в моей базе данных теперь ниже, чем это было, когда сериализация фактически завершалась в разумные сроки. Но я был бы более чем доволен возвращением к статус-кво анте (в котором мне просто нужно было игнорировать "$ref", "$id" и т. Д.), Если бы этого можно было достичь.

На данный момент я также открыт для перспективы использования некоторой другой библиотеки JSON или другой стратегии в целом. Мне кажется, что я мог бы просто использовать StringBuilder, System.Reflection и т. Д. И получить свое собственное, самодельное решение... но разве JSON.NET не может справиться с такими вещами довольно легко??

4 ответа

Решение

Во-первых, чтобы решить ваши проблемы с помощью ссылочных циклов PreserveReferencesHandling настройка контролирует, испускает ли Json.Net $id а также $ref отслеживать межобъектные ссылки. Если у вас есть этот набор None и ваш граф объектов содержит циклы, то вам также нужно будет установить ReferenceLoopHandling в Ignore предотвратить ошибки.

Теперь, чтобы заставить Json.Net полностью игнорировать все ссылки на объекты и сериализовать только примитивные свойства (кроме Pseudocontext конечно), вам нужен пользовательский Контракт Resolver, как вы предложили. Но не волнуйтесь, это не так сложно, как вы думаете. У резольвера есть возможность ввести ShouldSerialize метод для каждого свойства, чтобы контролировать, должно ли это свойство быть включено в вывод. Итак, все, что вам нужно сделать, это извлечь ваш преобразователь из стандартного, а затем переопределить CreateProperty метод такой, что он устанавливает ShouldSerialize соответственно. (Вам не нужен кастом JsonConverter здесь, хотя можно решить эту проблему с помощью этого подхода, хотя и с немного большим количеством кода.)

Вот код для распознавателя:

class CustomResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty prop = base.CreateProperty(member, memberSerialization);

        if (prop.DeclaringType != typeof(PseudoContext) && 
            prop.PropertyType.IsClass && 
            prop.PropertyType != typeof(string))
        {
            prop.ShouldSerialize = obj => false;
        }

        return prop;
    }
}

Вот полная демонстрация, показывающая распознаватель в действии.

class Program
{
    static void Main(string[] args)
    {
        // Set up some dummy data complete with reference loops
        Thing t1 = new Thing { Id = 1, Name = "Flim" };
        Thing t2 = new Thing { Id = 2, Name = "Flam" };

        Widget w1 = new Widget
        {
            Id = 5,
            Name = "Hammer",
            IsActive = true,
            Price = 13.99M,
            Created = new DateTime(2013, 12, 29, 8, 16, 3),
            Color = Color.Red,
        };
        w1.RelatedThings = new List<Thing> { t2 };
        t2.RelatedWidgets = new List<Widget> { w1 };

        Widget w2 = new Widget
        {
            Id = 6,
            Name = "Drill",
            IsActive = true,
            Price = 45.89M,
            Created = new DateTime(2014, 1, 22, 2, 29, 35),
            Color = Color.Blue,
        };
        w2.RelatedThings = new List<Thing> { t1 };
        t1.RelatedWidgets = new List<Widget> { w2 };

        // Here is the container class we wish to serialize
        PseudoContext pc = new PseudoContext
        {
            Things = new List<Thing> { t1, t2 },
            Widgets = new List<Widget> { w1, w2 }
        };

        // Serializer settings
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ContractResolver = new CustomResolver();
        settings.PreserveReferencesHandling = PreserveReferencesHandling.None;
        settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        settings.Formatting = Formatting.Indented;

        // Do the serialization and output to the console
        string json = JsonConvert.SerializeObject(pc, settings);
        Console.WriteLine(json);
    }

    class PseudoContext
    {
        public List<Thing> Things { get; set; }
        public List<Widget> Widgets { get; set; }
    }

    class Thing
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public List<Widget> RelatedWidgets { get; set; }
    }

    class Widget
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsActive { get; set; }
        public decimal Price { get; set; }
        public DateTime Created { get; set; }
        public Color Color { get; set; }
        public List<Thing> RelatedThings { get; set; }
    }

    enum Color { Red, White, Blue }
}

Выход:

{
  "Things": [
    {
      "Id": 1,
      "Name": "Flim"
    },
    {
      "Id": 2,
      "Name": "Flam"
    }
  ],
  "Widgets": [
    {
      "Id": 5,
      "Name": "Hammer",
      "IsActive": true,
      "Price": 13.99,
      "Created": "2013-12-29T08:16:03",
      "Color": 0
    },
    {
      "Id": 6,
      "Name": "Drill",
      "IsActive": true,
      "Price": 45.89,
      "Created": "2014-01-22T02:29:35",
      "Color": 2
    }
  ]
}

Надеюсь, что это находится в поле того, что вы искали.

Кроме того, если вы ищете способ сделать это для всех ваших классов моделей с разными именами типов элементов (например, у вас есть некоторые модели, созданные Entity Framework), этот ответ может помочь, и вы можете игнорировать свойства навигации в сериализации JSON: Это.

Более простым способом является изменение шаблона модели T4 (.tt) для добавления атрибутов JSONIgnore к свойствам навигации, которые просто оставят примитивные типы как сериализуемые.

Есть еще одно простое решение. Добавление этой строки в конструктор DbContext:

       public MyContextEntities() : base("name=MyContextEntities")
    {
        Configuration.ProxyCreationEnabled = false;
    }

JsonConvert не будет сериализовать все, что находится в пространстве имен System.Data.Entity.DynamicProxies.*. На практике вы получите json, в котором будут заполнены только те зависимости, которые вы добавили в Include().

Другие вопросы по тегам