Как заставить 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().