JSON.NET и nHibernate Ленивая загрузка коллекций
Кто-нибудь использует JSON.NET с nHibernate? Я замечаю, что получаю ошибки, когда пытаюсь загрузить класс с дочерними коллекциями.
9 ответов
Я столкнулся с той же проблемой, поэтому я попытался использовать код @Liedman, но GetSerializableMembers()
никогда не вызывали для прокси-ссылки. Я нашел другой метод для переопределения:
public class NHibernateContractResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
if (typeof(NHibernate.Proxy.INHibernateProxy).IsAssignableFrom(objectType))
return base.CreateContract(objectType.BaseType);
else
return base.CreateContract(objectType);
}
}
У нас была именно эта проблема, которая была решена вдохновением от ответа Ремесленника здесь.
Проблема возникает из-за путаницы в JSON.NET о том, как сериализовать прокси-классы NHibernate. Решение: сериализуйте экземпляры прокси как их базовый класс.
Упрощенная версия кода Handcraftsman выглядит следующим образом:
public class NHibernateContractResolver : DefaultContractResolver {
protected override List<MemberInfo> GetSerializableMembers(Type objectType) {
if (typeof(INHibernateProxy).IsAssignableFrom(objectType)) {
return base.GetSerializableMembers(objectType.BaseType);
} else {
return base.GetSerializableMembers(objectType);
}
}
}
ИМХО, этот код имеет то преимущество, что все еще полагается на поведение JSON.NET по умолчанию в отношении пользовательских атрибутов и т. Д. (А код намного короче!).
Используется так
var serializer = new JsonSerializer{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new NHibernateContractResolver()
};
StringWriter stringWriter = new StringWriter();
JsonWriter jsonWriter = new Newtonsoft.Json.JsonTextWriter(stringWriter);
serializer.Serialize(jsonWriter, objectToSerialize);
string serializedObject = stringWriter.ToString();
Примечание. Этот код был написан для NHibernate 2.1 и использовался для него. Как отмечают некоторые комментаторы, с более поздними версиями NHibernate он не работает "из коробки", вам придется внести некоторые коррективы. Я постараюсь обновить код, если мне когда-нибудь придется делать это с более поздними версиями NHibernate.
Я использую NHibernate с Json.NET и заметил, что я получаю необъяснимые свойства "__interceptors" в моих сериализованных объектах. Поиск в Google нашел это отличное решение от Ли Хенсона, которое я адаптировал для работы с Json.NET 3.5 Release 5 следующим образом.
public class NHibernateContractResolver : DefaultContractResolver
{
private static readonly MemberInfo[] NHibernateProxyInterfaceMembers = typeof(INHibernateProxy).GetMembers();
protected override List<MemberInfo> GetSerializableMembers(Type objectType)
{
var members = base.GetSerializableMembers(objectType);
members.RemoveAll(memberInfo =>
(IsMemberPartOfNHibernateProxyInterface(memberInfo)) ||
(IsMemberDynamicProxyMixin(memberInfo)) ||
(IsMemberMarkedWithIgnoreAttribute(memberInfo, objectType)) ||
(IsMemberInheritedFromProxySuperclass(memberInfo, objectType)));
var actualMemberInfos = new List<MemberInfo>();
foreach (var memberInfo in members)
{
var infos = memberInfo.DeclaringType.BaseType.GetMember(memberInfo.Name);
actualMemberInfos.Add(infos.Length == 0 ? memberInfo : infos[0]);
}
return actualMemberInfos;
}
private static bool IsMemberDynamicProxyMixin(MemberInfo memberInfo)
{
return memberInfo.Name == "__interceptors";
}
private static bool IsMemberInheritedFromProxySuperclass(MemberInfo memberInfo, Type objectType)
{
return memberInfo.DeclaringType.Assembly == typeof(INHibernateProxy).Assembly;
}
private static bool IsMemberMarkedWithIgnoreAttribute(MemberInfo memberInfo, Type objectType)
{
var infos = typeof(INHibernateProxy).IsAssignableFrom(objectType)
? objectType.BaseType.GetMember(memberInfo.Name)
: objectType.GetMember(memberInfo.Name);
return infos[0].GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length > 0;
}
private static bool IsMemberPartOfNHibernateProxyInterface(MemberInfo memberInfo)
{
return Array.Exists(NHibernateProxyInterfaceMembers, mi => memberInfo.Name == mi.Name);
}
}
Чтобы использовать его, просто поместите экземпляр в свойство ContractResolver вашего JsonSerializer. Проблема циклической зависимости, отмеченная jishi, может быть решена путем установки свойства ReferenceLoopHandling в ReferenceLoopHandling.Ignore . Вот метод расширения, который можно использовать для сериализации объектов с использованием Json.Net
public static void SerializeToJsonFile<T>(this T itemToSerialize, string filePath)
{
using (StreamWriter streamWriter = new StreamWriter(filePath))
{
using (JsonWriter jsonWriter = new JsonTextWriter(streamWriter))
{
jsonWriter.Formatting = Formatting.Indented;
JsonSerializer serializer = new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new NHibernateContractResolver(),
};
serializer.Serialize(jsonWriter, itemToSerialize);
}
}
}
Возможно, вы захотите загрузить большую часть объекта, чтобы его можно было сериализовать:
ICriteria ic = _session.CreateCriteria(typeof(Person));
ic.Add(Restrictions.Eq("Id", id));
if (fetchEager)
{
ic.SetFetchMode("Person", FetchMode.Eager);
}
Хороший способ сделать это - добавить bool в конструктор (bool isFetchEager) вашего метода провайдера данных.
Вы получаете круговую ошибку зависимости? Как вы игнорируете объекты из сериализации?
Поскольку отложенная загрузка генерирует прокси-объекты, любые атрибуты, которые есть у ваших учеников, будут потеряны. Я столкнулся с той же проблемой с JSON-сериализатором Newtonsoft, поскольку прокси-объект больше не имел атрибутов [JsonIgnore].
Я бы сказал, что это проблема дизайна на мой взгляд. Поскольку NH устанавливает соединения с базой данных под всеми и имеет посредники в середине, для прозрачности вашего приложения нецелесообразно напрямую сериализовать их (и, как вы можете видеть, Json.NET их совсем не любит).
Вы не должны сериализовать сами сущности, но вы должны преобразовать их в объекты "просмотра" или объекты POCO или DTO (как вы хотите их называть), а затем сериализовать их.
Разница заключается в том, что, хотя сущность NH может иметь прокси, ленивые атрибуты и т. Д. Объекты представления являются простыми объектами с только примитивами, которые по умолчанию сериализуемы.
Как управлять ФК? Мое личное правило:
Уровень сущности: класс Person и связанный с ним класс Gender
Уровень представления: представление "Человек" со свойствами GenderId и GenderName.
Это означает, что вам нужно расширить свои свойства в примитивы при преобразовании для просмотра объектов. Таким образом, ваши объекты json проще и удобнее в обращении.
Когда вам нужно отправить изменения в БД, в моем случае я использую AutoMapper и создаю класс ValueResolver, который может преобразовать ваш новый Guid в объект Gender.
ОБНОВЛЕНИЕ: Проверьте http://blog.andrewawhitaker.com/blog/2014/06/19/queryover-series-part-4-transforming/ для способа получить представление напрямую (AliasToBean) от NH. Это было бы повышение в сторону DB.
Проблема может возникнуть, когда NHibernate оборачивает свойства вложенной коллекции в тип PersistentGenericBag<>.
Переопределения GetSerializableMembers и CreateContract не могут обнаружить, что эти свойства вложенной коллекции являются «проксированными». Один из способов решить эту проблему - переопределить метод CreateProperty. Хитрость заключается в том, чтобы получить значение свойства с помощью отражения и проверить, имеет ли тип PersistentGenericBag. Этот метод также имеет возможность фильтровать любые свойства, вызывающие исключения.
public class NHibernateContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
property.ShouldSerialize = instance =>
{
try
{
PropertyInfo prop = (PropertyInfo)member;
if (prop.CanRead)
{
var value = prop.GetValue(instance, null);
if (value != null && typeof(NHibernate.Collection.Generic.PersistentGenericBag<>).IsSubclassOfRawGeneric(value.GetType()))
return false;
return true;
}
}
catch
{ }
return false;
};
return property;
}
}
Расширение IsSubclassOfRawGeneric, используемое выше:
public static class TypeExtensions
{
public static bool IsSubclassOfRawGeneric(this Type generic, Type? toCheck)
{
while (toCheck != null && toCheck != typeof(object))
{
var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
if (generic == cur)
{
return true;
}
toCheck = toCheck?.BaseType;
}
return false;
}
}
Вот что я использую:
- Имейте интерфейс маркера и наследуйте его на своих объектах, например, в моем случае пустой
IEntity
.
Мы будем использовать интерфейс маркера для обнаружения типов сущностей NHibernate в преобразователе контрактов.
public class CustomerEntity : IEntity { ... }
Создайте настраиваемый преобразователь контрактов для JSON.NET
public class NHibernateProxyJsonValueProvider : IValueProvider { private readonly IValueProvider _valueProvider; public NHibernateProxyJsonValueProvider(IValueProvider valueProvider) { _valueProvider = valueProvider; } public void SetValue(object target, object value) { _valueProvider.SetValue(target, value); } private static (bool isProxy, bool isInitialized) GetProxy(object proxy) { // this is pretty much what NHibernateUtil.IsInitialized() does. switch (proxy) { case INHibernateProxy hibernateProxy: return (true, !hibernateProxy.HibernateLazyInitializer.IsUninitialized); case ILazyInitializedCollection initializedCollection: return (true, initializedCollection.WasInitialized); case IPersistentCollection persistentCollection: return (true, persistentCollection.WasInitialized); default: return (false, false); } } public object GetValue(object target) { object value = _valueProvider.GetValue(target); (bool isProxy, bool isInitialized) = GetProxy(value); if (isProxy) { if (isInitialized) { return value; } if (value is IEnumerable) { return Enumerable.Empty<object>(); } return null; } return value; } } public class NHibernateContractResolver : CamelCasePropertyNamesContractResolver { protected override JsonContract CreateContract(Type objectType) { if (objectType.IsAssignableTo(typeof(IEntity))) { return base.CreateObjectContract(objectType); } return base.CreateContract(objectType); } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty property = base.CreateProperty(member, memberSerialization); property.ValueProvider = new NHibernateProxyJsonValueProvider(property.ValueProvider); return property; } }
- Нормальные неинициализированные лениво загруженные свойства приведут к
null
в выводе json. - Коллекция неинициализированных отложенных свойств приведет к
[]
пустой массив в json.
Итак, чтобы свойство отложенной загрузки появилось в выходных данных json, вам необходимо с готовностью загрузить его в запросе или в коде перед сериализацией.
Применение:
JsonConvert.SerializeObject(entityToSerialize, new JsonSerializerSettings() {
ContractResolver = new NHibernateContractResolver()
});
Или глобально в классе запуска ASP.NET Core
services.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new NHibernateContractResolver();
});
С использованием:
- NET 5.0
- NHibernate 5.3.8
- JSON.NET последняя версия через ASP.NET Core
Если вы сериализуете объекты, содержащие прокси-классы NHibernate, вы можете в конечном итоге загрузить всю базу данных, потому что после доступа к свойству NHibernate инициирует запрос к базе данных. Я только что реализовал единицу работы для NHibernate: которая исправляет две из самых неприятных проблем NHibernate: прокси-классы и декартово произведение при использовании выборки.
Как бы вы это использовали?
var customer = await _dbContext.Customers.Get(customerId) //returns a wrapper to configure the query
.Include(c => c.Addresses.Single().Country, //include Addresses and Country
c => c.PhoneNumbers.Single().PhoneNumberType) //include all PhoneNumbers with PhoneNumberType
.Unproxy() //instructs the framework to strip all the proxy classes when the Value is returned
.Deferred() //instructs the framework to delay execution (future)
.ValueAsync(token); //this is where all deferred queries get executed
Приведенный выше код в основном настраивает запрос: вернуть клиента по идентификатору с несколькими дочерними объектами, которые должны выполняться с другими запросами (фьючерсами), а возвращаемый результат должен быть удален от прокси NHibernate. Запрос выполняется, когда
ValueAsync
называется.NHUnit,NHUnit определяет, следует ли ему присоединяться к основному запросу, создавать новые будущие запросы или использовать пакетную выборку.
На есть Githubпростой пример проекта, показывающий, как использовать пакет NHUnit. Если этот проект заинтересует других, я потрачу больше времени, чтобы сделать его лучше.