NHibernate i serializacja drzewa obiektów przy użyciu biblioteki Json.NET

07.03.2013

Jeżeli korzystasz z NHibernate i Json.NET i chcesz bezpośrednio serializować obiekty biznesowe pobrane z bazy danych przy użyciu NHibernate do formatu JSON to bardzo szybko napotkasz na problem w postaci wyjątku:

Error getting value from 'ReadOnly' on 'NHibernate.Proxy.DefaultLazyInitializer'.

Problem ten powstaje w momencie gdy próbujesz serializować encję pobraną przez NHibernate, która posiada niezainicjowane wiązania z innymi encjami (obiekty proxy do obsługi lazy loading). Klasa serializująca przemierza drzewo obiektów w poszukiwaniu publicznych właściwości obiektów. Po natrafieniu na proxy, nie umie go przetworzyć na JSON i rzucany jest wyjątek.

Można próbować ominąć ten problem ładując wszystkie powiązane obiekty, mapując obiekty encji na inne obiekty (DTO) lub odpowiednio sterując procesem serializacji w celu wyeliminowania prób serializacji niezainicjowanych właściwości. Wczytywanie wszystkich powiązanych obiektów nie wchodzi w grę chociażby ze względów wydajnościowych. W takim przypadku klasa serializująca będzie próbowała przemierzyć cały graf obiektów. Drugie rozwiązanie - mapowanie na DTO, zadziała. Odrzuciłem jednak to rozwiązanie ze względu na przymus pisania nudnych definicji mapowania dla biblioteki mapującej np. Value Injector czy AutoMapper. Dla mnie, najlepszym rozwiązaniem okazało się sterowanie procesem serializacji.

Json.NET ma duże możliwości jeżeli chodzi o kontrolę i dostosowywanie procesu serializacji obiektów do własnych wymagań. Aby rozwiązać problem z proxy trzeba przeciążyć odbiekt DefaultContractResolver i dodać do niego odpowiednie zachowania. Nowy contract resolver powinien mieć możliwość definiowania zestawu właściwości, które będą serializowane. Musi to dotyczyć wszystkich powiązanych obiektów (na każdym poziomie!).

Przykładowo mamy 4 obiekty powiązane w następujący sposób: A –> B –> C –> (wiele) D. Własna implementacja DefaultContractResolver musi dawać możliwość ignorowania/uwzględniania  właściwości w każdym z powiązanych obiektów (A,B,C i D). Daje to możliwość przecinania wiązań pomiędzy obiektami a co za tym idzie serializację wybranych fragmentów drzewa obiektów. Np. tylko A -> B, samo B -> C (wiele) D czy A –> B -> C bez konieczności rzutowania na DTO. Dodanie ignorowania po typie (nazwie właściwości) umożliwi szybkie omijanie kolekcji ISet<> czy obiektów biznesowych dziedziczących z tego samego typu (np. EntityBase), które nie wczytane przez NHibernate zamieniane są na obiekty proxy. Kod contract resolvera:

public class NHibernateContractResolver : DefaultContractResolver
{
    private readonly IResolverConfiguration _configuration;
    private HashSet<int> _pathSet = new HashSet<int>();

    public NHibernateContractResolver(IResolverConfiguration configuration)
    {
        _configuration = configuration;
    }

    protected override List<MemberInfo> GetSerializableMembers(Type objectType)
    {
        var members = base.GetSerializableMembers(objectType);
        return members.Where(x => _configuration.IsIncluded((PropertyInfo) x)).ToList();
    }

    protected override IValueProvider CreateMemberValueProvider(MemberInfo member)
    {
        var propertyInfo = member.DeclaringType.GetProperty(member.Name);
        if (!propertyInfo.PropertyType.Namespace.StartsWith("System")
            ||
            (propertyInfo.PropertyType.IsGenericType &&
             propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof (Iesi.Collections.Generic.ISet<>)))
        {
            return new ProxyProvider(member);
        }
        return base.CreateMemberValueProvider(member);
    }
}

public class ProxyProvider : IValueProvider
{
    private readonly MemberInfo _memberInfo;

    public ProxyProvider(MemberInfo memberInfo)
    {
        _memberInfo = memberInfo;
    }

    public void SetValue(object target, object value)
    {
        throw new NotSupportedException();
    }

    public object GetValue(object target)
    {
        if (!NHibernateUtil.IsInitialized(target.GetType().GetProperty(_memberInfo.Name).GetValue(target, null)))
        {
            return null;
        }

        try
        {
            return ReflectionUtils.GetMemberValue(_memberInfo, target);
        }
        catch (Exception ex)
        {
            throw new JsonSerializationException(
                string.Format("Error getting value from '{0}' on '{1}'.", CultureInfo.InvariantCulture, _memberInfo.Name,
                              target.GetType()), ex);
        }
    }
}

public abstract class ResolverConfigurationBase : IResolverConfiguration
{
    protected readonly ReflectionSetDefinition ReflectionSet = new ReflectionSetDefinition();

    public abstract bool IsIncluded(PropertyInfo propertyInfo);

    public IContractResolver Build()
    {
        return new NHibernateContractResolver(this);
    }
}

public class IncludeResolver : ResolverConfigurationBase, IIncludeConfiguration
{
    public IIncludeConfiguration Include(Type type)
    {
        ReflectionSet.AddGlobalType(type);
        return this;
    }

    public IIncludeConfiguration Include(string propertyName)
    {
        ReflectionSet.AddPropertyName(propertyName);
        return this;
    }

    public IIncludeConfiguration Include<T>(Expression<Func<T, object>> exp)
    {
        string propertyName = (exp.Body as MemberExpression).Member.Name;

        ReflectionSet.AddPropertyNameInType(typeof (T), propertyName);
        return this;
    }

    public IIncludeConfiguration Include<T>()
    {
        ReflectionSet.AddGlobalType(typeof (T));
        return this;
    }

    public IIncludeConfiguration IncludeTypeInType<T>(Type type)
    {
        ReflectionSet.AddInType(typeof (T), type);
        return this;
    }

    public IIncludeConfiguration Include<T>(params Expression<Func<T, object>>[] expArr)
    {
        foreach (var exp in expArr)
        {
            Include(exp);
        }

        return this;
    }

    public IIncludeConfiguration IncludeStartsWith(string propertyName)
    {
        ReflectionSet.AddPropertyNameStartsWith(propertyName);
        return this;
    }

    public IIncludeConfiguration IncludeEndsWith(string propertyName)
    {
        ReflectionSet.AddPropertyNameEndsWith(propertyName);
        return this;
    }

    public static IIncludeConfiguration Create()
    {
        return new IncludeResolver();
    }

    public override bool IsIncluded(PropertyInfo propertyInfo)
    {
        return ReflectionSet.Contains(propertyInfo);
    }
}

public class IgnoreResolver : ResolverConfigurationBase, IIgnoreConfiguration
{
    public IIgnoreConfiguration Ignore(Type type)
    {
        ReflectionSet.AddGlobalType(type);
        return this;
    }

    public IIgnoreConfiguration Ignore(string propertyName)
    {
        ReflectionSet.AddPropertyName(propertyName);
        return this;
    }

    public IIgnoreConfiguration Ignore<T>(Expression<Func<T, object>> exp)
    {
        string propertyName = (exp.Body as MemberExpression).Member.Name;

        ReflectionSet.AddPropertyNameInType(typeof (T), propertyName);
        return this;
    }

    public IIgnoreConfiguration Ignore<T>()
    {
        ReflectionSet.AddGlobalType(typeof (T));
        return this;
    }

    public IIgnoreConfiguration IgnoreTypeInType<T>(Type type)
    {
        ReflectionSet.AddInType(typeof (T), type);
        return this;
    }

    public IIgnoreConfiguration Ignore<T>(params Expression<Func<T, object>>[] expArr)
    {
        foreach (var exp in expArr)
        {
            Ignore(exp);
        }

        return this;
    }

    public IIgnoreConfiguration IgnoreStartsWith(string propertyName)
    {
        ReflectionSet.AddPropertyNameStartsWith(propertyName);
        return this;
    }

    public IIgnoreConfiguration IgnoreEndsWith(string propertyName)
    {
        ReflectionSet.AddPropertyNameEndsWith(propertyName);
        return this;
    }

    public override bool IsIncluded(PropertyInfo propertyInfo)
    {
        return !ReflectionSet.Contains(propertyInfo);
    }

    public static IIgnoreConfiguration Create()
    {
        return new IgnoreResolver();
    }
}

public class ReflectionSetDefinition
{
    private readonly List<string> _nameEndsWithSet = new List<string>();
    private readonly Dictionary<Type, HashSet<string>> _nameInTypeDict = new Dictionary<Type, HashSet<string>>();
    private readonly HashSet<string> _nameSet = new HashSet<string>();
    private readonly List<string> _nameStartsWithSet = new List<string>();
    private readonly Dictionary<Type, HashSet<Type>> _typeInTypeDict = new Dictionary<Type, HashSet<Type>>();
    private readonly HashSet<Type> _typeSet = new HashSet<Type>();

    public bool Contains(PropertyInfo propertyInfo)
    {
        var propertyType = propertyInfo.PropertyType;
        var declaringType = propertyInfo.DeclaringType;
        var propertyName = propertyInfo.Name;
        var genericType = propertyType.IsGenericType;

        if (_nameSet.Contains(propertyName))
        {
            return true;
        }

        if (_nameStartsWithSet.Any(propertyName.StartsWith))
        {
            return true;
        }

        if (_nameEndsWithSet.Any(propertyName.EndsWith))
        {
            return true;
        }

        if (_typeInTypeDict.ContainsKey(declaringType) &&
            (_typeInTypeDict[declaringType].Contains(propertyType) ||
             _typeInTypeDict[declaringType].AnyAssignableFrom(propertyType)))
        {
            return true;
        }

        if (_nameInTypeDict.ContainsKey(declaringType) &&
            _nameInTypeDict[declaringType].Contains(propertyName))
        {
            return true;
        }

        if (_typeSet.Contains(propertyType) ||
            _typeSet.AnyAssignableFrom(propertyType))
        {
            return true;
        }

        if (genericType)
        {
            var genericTypeDefinition = propertyType.GetGenericTypeDefinition();
            if (_typeSet.Contains(propertyType) ||
                _typeSet.Contains(genericTypeDefinition) ||
                _typeSet.AnyAssignableFrom(genericTypeDefinition))
            {
                return true;
            }

            if (_typeInTypeDict.ContainsKey(declaringType) &&
                (_typeInTypeDict[declaringType].Contains(genericTypeDefinition) ||
                 _typeInTypeDict[declaringType].AnyAssignableFrom(genericTypeDefinition)))
            {
                return true;
            }
        }

        return false;
    }

    public void AddPropertyName(string propertyName)
    {
        _nameSet.Add(propertyName);
    }

    public void AddGlobalType(Type type)
    {
        _typeSet.Add(type);
    }

    public void AddInType(Type containingType, Type type)
    {
        if (!_typeInTypeDict.ContainsKey(containingType))
        {
            _typeInTypeDict.Add(containingType, new HashSet<Type>());
        }

        var set = _typeInTypeDict[containingType];
        set.Add(type);
    }

    public void AddPropertyNameInType(Type containingType, string propertyName)
    {
        if (!_nameInTypeDict.ContainsKey(containingType))
        {
            _nameInTypeDict.Add(containingType, new HashSet<string>());
        }

        var set = _nameInTypeDict[containingType];
        set.Add(propertyName);
    }

    public void AddPropertyNameStartsWith(string propertyName)
    {
        _nameStartsWithSet.Add(propertyName);
    }

    public void AddPropertyNameEndsWith(string propertyName)
    {
        _nameEndsWithSet.Add(propertyName);
    }
}

public static class TypeHashSetExtensions
{
    public static bool AnyAssignableFrom(this HashSet<Type> typeSet, Type type)
    {
        return typeSet.Any(x => x.IsAssignableFrom(type));
    }
}

public static class ObjectExtensions
{
    public static string ToJson(this object o, IContractResolver contractResolver)
    {
        var serializer = new JsonSerializer { ContractResolver = contractResolver };
        var builder = new StringBuilder();
        using (var stringWriter = new StringWriter(builder))
        {
            using (var writer = new JsonTextWriter(stringWriter))
            {
                serializer.Serialize(writer, o);
                return builder.ToString();
            }
        }
    }
}

Interfejs

Interfejs tej mikro biblioteki jest typu fluent, czyli “wszystko w jednej linni”. Na początku wybieramy tryb resolvera. Możemy albo wykluczać właściwości – IgnoreResolver, lub włączać je do procesu serializacji – InlucdeResolver:

IgnoreResolver.Create()..
// lub
IncludeResolver.Create()..

Po kropce mamy dostępny interfejs (przykład dla IgnoreResolver, interfejs dla IncludeResolver jest analogiczny):

public interface IIgnoreConfiguration
{
    IContractResolver Build();

    IIgnoreConfiguration Ignore<T>();

    IIgnoreConfiguration Ignore(Type type);

    IIgnoreConfiguration Ignore(string propertyName);

    IIgnoreConfiguration IgnoreStartsWith(string propertyName);

    IIgnoreConfiguration IgnoreEndsWith(string propertyName);

    IIgnoreConfiguration Ignore<T>(Expression<Func<T, object>> exp);

    IIgnoreConfiguration Ignore<T>(params Expression<Func<T, object>>[] expArr);

    IIgnoreConfiguration IgnoreTypeInType<T>(Type type);
}

Kolejno:

  • Build() – buduje docelowy resolver dla Json.NET. Metoda wywoływana na samym końcu.
  • Ignore<T>() – dodaje dany typ T na listę typów ignorowanych w każdym z serializowanych obiektów. Typ jest ignorowany w każdym z serializowanych obiektów.
  • Ignore(Type type) – to samo co wyżej. Umożliwia dodawania do listy ignorowanych typy generyczne (definicje typów!) w postaci IList<> czy ISet<>.
  • Ignore(string propertyName) – dodaje nazwę właściwości do listy ignorowanych. Właściwość o danej nazwie jest ignorowana w każdym z serializowanych obiektów.
  • IgnoreStartsWith(string propertyName) – analogicznie do powyższego dla właściwości rozpoczynającej się od określonego ciągu znaków.
  • IgnoreEndsWith(string propertyName) – analogicznie do powyższego dla właściwości kończącej się określonym ciągiem znaków.
  • Ignore<T>(Expression<Func<T, object>> exp) – ignoruje określoną właściwość w określonym typie. Przykład: Ignore<A>(x => x.Id) ignoruje właściwość Id tylko w typie A.
  • Ignore<T>(params Expression<Func<T, object>>[] expArr) – analogicznie do powyższego, z tą różnicą, że jako parametr przyjmuje listę właściwości np. Ignore<A>(x => x.Id, x => x.AText)
  • IgnoreTypeInType<T>(Type type) - ignoruje określony typ właściwości w określony typie. Przykład: Ignore<B>(typeof(string)) ignoruj wszystkie właściwości typu string podczas serializacji typu B.

Poniżej kilka przykładów wykorzystania nowego resolver dla przygotowanych wcześniej danych.

Przykłady

Załóżmy, że mamy obiekty A, B, C powiązane w następujący sposób: A (wiele) B -> C:

public class A
{
    public A()
    {
        Items = new HashedSet<B>();
    }

    public virtual Guid Id { get; set; }

    public virtual string AText { get; set; }

    public virtual ISet<B> Items { get; set; }

    public virtual void AddItem(B b)
    {
        b.A = this;
        Items.Add(b);
    }
}

public class B
{
    public virtual Guid Id { get; set; }

    public virtual string BText { get; set; }

    public virtual A A { get; set; }

    public virtual C C { get; set; }
}

public class C
{
    public virtual Guid Id { get; set; }

    public virtual string CText { get; set; }
}

Przygotowanie danych testowych:

var a = new A
{
    AText = "objA", Id = Guid.NewGuid(),
};

a.AddItem(new B() { BText = "objB1", Id = Guid.Empty, C = new C() {Id = Guid.NewGuid(), CText = "objC1"}});
a.AddItem(new B() { BText = "objB2", Id = Guid.Empty, C = new C() {Id = Guid.NewGuid(), CText = "objC2"}});

Ignoruj Id we wszystkich obiektach

var resolver = IgnoreResolver.Create().Ignore("Id").Build();
var json = a.ToJson(resolver); 

Ignoruj wszystkie właściwości w A poza Id

var resolver = IgnoreResolver.Create(true).Include("Id").Build();

Ignoruj we wszystkich obiektach wszystkie właściwości typu string

var resolver = IgnoreResolver.Create().IgnoreType<string>().Build();

Ignoruj wszystkie właściwości typu string w obiekcie B

var resolver = IgnoreResolver.Create().IgnoreType<B>(typeof(string)).Build();

Uwzględnij tylko Id

var resolver = IncludeResolver.Create().Include("Id").Build();

Uwzględnij tylko właściwości typu string

var resolver = IgnoreResolver.Create().Ignore<string>().Build();

Ignoruj we wszystkich obiektach, wszystkie właściwości inne niż typu string lub wiązanie

var resolver = IncludeResolver.Create()
    .Include<string>()
    .Include(typeof(Iesi.Collections.Generic.ISet<>))
    .Include<BaseObject>()
    .Build();