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;
    }
}

Вот что я использую:

  1. Имейте интерфейс маркера и наследуйте его на своих объектах, например, в моем случае пустой IEntity.

Мы будем использовать интерфейс маркера для обнаружения типов сущностей NHibernate в преобразователе контрактов.

         public class CustomerEntity : IEntity {    ...   }
  1. Создайте настраиваемый преобразователь контрактов для 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. Если этот проект заинтересует других, я потрачу больше времени, чтобы сделать его лучше.

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