As you have probably noticed from my previous posts, I have recently been working on a WCF service as a single repository for data that is common across a number of internal applications. The service uses NHibernate to translate between the domain object graph and the MS Sql Server database. To reduce the load on this high volume system, I have configured a number of properties on each object to be 'lazy-loaded'. You will be aware that NHibernate wraps such properties with its own proxies to fetch the data when the property is accessed.
Additionally, I do not want to be sending my domain objects across the application boundary, so I have created a library of matching DTOs and am using the Automapper (http://automapper.codeplex.com/) object-object mapper to perform the domain-DTO translation. Automapper has some very helpful options:
These two options allow a single NHibernate configuration for the domain, but permit fine-grained control of how much data is loaded from the database and 'sent over the wire' to consuming applications.
So I have written two custom resolvers for Automapper which I have found useful. I have provided them for your reference.
To resolve lazy loaded ILists of domain objects:
You will note that I am catching the LazyInitializationException and returning the default. This allows me to perform object-object mapping when I am disconnected from an NHibernate session and just copy the values of properties already loaded. This is more an edge case situation. By being connected to the session I can use the Automapper mapping profile to control how much information is retrieved from the database.
The mapper can be configured to use this custom resolver like this:
To resolve a single lazy loaded domain object:
As with the previous example, I am catching the LazyInitializationException to permit resolution of the object while detached from the Session.
The mapper can be configured to use this custom resolver like this:
Using Automapper with Nhibernate this way is providing a lot of control and flexibility. The custom resolvers I have shown above means that this can be done with a minimum of additional effort.
Thanks jbogard and friends.
Additionally, I do not want to be sending my domain objects across the application boundary, so I have created a library of matching DTOs and am using the Automapper (http://automapper.codeplex.com/) object-object mapper to perform the domain-DTO translation. Automapper has some very helpful options:
- Mapping profiles allow me to specify differing depth of object graph resolution.
- Custom resolvers allow me to iterate across the properties of my domain object, permitting me to invoke and therefore resolve and load all the lazy-loaded properties that have been configured for that mapping profile.
These two options allow a single NHibernate configuration for the domain, but permit fine-grained control of how much data is loaded from the database and 'sent over the wire' to consuming applications.
So I have written two custom resolvers for Automapper which I have found useful. I have provided them for your reference.
To resolve lazy loaded ILists of domain objects:
/// <summary>
/// Custom resolver for lists set to lazy loading in nhibernate
/// </summary>
/// <typeparam name="TSource">The type of the domain object the lazy loaded list is on.</typeparam>
/// <typeparam name="TDestination">The type of list to resolve to eg. IList of Person. Must be an IList.</typeparam>
/// <typeparam name="TItem">The type of the items in the list.</typeparam>
public class ListLazyInitResolver<TSource, TDestination, TItem> : ValueResolver<TSource, TDestination> where TDestination : IList<TItem>
{
private ResolveListItemDelegate _resolveListItem;
string _propertyName;
/// <summary>
/// Resolves a lazy loaded list on the supplied domain object
/// </summary>
/// <param name="source">A domain object with a list set to lazy load in nhibernae</param>
/// <returns>The resolved list, or null if the object is disconnected from a session</returns>
protected override TDestination ResolveCore(TSource source)
{
try
{
dynamic items = source.GetType().GetProperty(_propertyName).GetValue(source, null);
int itemCount = items.Count;
if (itemCount > -1)//force access to collection for NHiberante lazy load resolution
{
IList<TItem> resultList = new List<TItem>(itemCount);
Parallel.For(0, itemCount, delegate(int i)
{
TItem item = _resolveListItem(items[i]);//needs to be on two lines like this to avoid RuntimeBinderException
resultList.Add(item);
});
return (TDestination)resultList;//this cast is required
}
}
catch (NHibernate.LazyInitializationException)
{ }
return default(TDestination);
}
/// <summary>
/// A delegate with the mapping instruction to invoke when resolving individual items in the list
/// </summary>
public delegate dynamic ResolveListItemDelegate(dynamic sourceItem);
/// <summary>
/// Initializes a new instance of the <see cref="ListLazyInitResolver<TSource, TDestination, TItem>"/> class.
/// </summary>
/// <param name="resolveListItem">The delegate with the mapping instruction to invoke when resolving items in the list</param>
/// <param name="propertyName">Name of the lazy loaded list property on the source domain object.</param>
public ListLazyInitResolver(ResolveListItemDelegate resolveListItem, string propertyName)
{
if (resolveListItem == null) throw new ArgumentNullException("resolveListItem");
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("propertyName may not be null, empty, or whitespace");
_resolveListItem = resolveListItem;
_propertyName = propertyName;
}
}
/// Custom resolver for lists set to lazy loading in nhibernate
/// </summary>
/// <typeparam name="TSource">The type of the domain object the lazy loaded list is on.</typeparam>
/// <typeparam name="TDestination">The type of list to resolve to eg. IList of Person. Must be an IList.</typeparam>
/// <typeparam name="TItem">The type of the items in the list.</typeparam>
public class ListLazyInitResolver<TSource, TDestination, TItem> : ValueResolver<TSource, TDestination> where TDestination : IList<TItem>
{
private ResolveListItemDelegate _resolveListItem;
string _propertyName;
/// <summary>
/// Resolves a lazy loaded list on the supplied domain object
/// </summary>
/// <param name="source">A domain object with a list set to lazy load in nhibernae</param>
/// <returns>The resolved list, or null if the object is disconnected from a session</returns>
protected override TDestination ResolveCore(TSource source)
{
try
{
dynamic items = source.GetType().GetProperty(_propertyName).GetValue(source, null);
int itemCount = items.Count;
if (itemCount > -1)//force access to collection for NHiberante lazy load resolution
{
IList<TItem> resultList = new List<TItem>(itemCount);
Parallel.For(0, itemCount, delegate(int i)
{
TItem item = _resolveListItem(items[i]);//needs to be on two lines like this to avoid RuntimeBinderException
resultList.Add(item);
});
return (TDestination)resultList;//this cast is required
}
}
catch (NHibernate.LazyInitializationException)
{ }
return default(TDestination);
}
/// <summary>
/// A delegate with the mapping instruction to invoke when resolving individual items in the list
/// </summary>
public delegate dynamic ResolveListItemDelegate(dynamic sourceItem);
/// <summary>
/// Initializes a new instance of the <see cref="ListLazyInitResolver<TSource, TDestination, TItem>"/> class.
/// </summary>
/// <param name="resolveListItem">The delegate with the mapping instruction to invoke when resolving items in the list</param>
/// <param name="propertyName">Name of the lazy loaded list property on the source domain object.</param>
public ListLazyInitResolver(ResolveListItemDelegate resolveListItem, string propertyName)
{
if (resolveListItem == null) throw new ArgumentNullException("resolveListItem");
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("propertyName may not be null, empty, or whitespace");
_resolveListItem = resolveListItem;
_propertyName = propertyName;
}
}
You will note that I am catching the LazyInitializationException and returning the default. This allows me to perform object-object mapping when I am disconnected from an NHibernate session and just copy the values of properties already loaded. This is more an edge case situation. By being connected to the session I can use the Automapper mapping profile to control how much information is retrieved from the database.
The mapper can be configured to use this custom resolver like this:
Mapper.CreateMap()
.ForMember(dest => dest.ChangeHistory,
opt => opt.ResolveUsing, PhoneHistoryDto>>().ConstructedBy(() => new ListLazyInitResolver, PhoneHistoryDto>(delegate(dynamic historyRecord)
{ return Mapper.Map(historyRecord); },
"ChangeHistory")));
.ForMember(dest => dest.ChangeHistory,
opt => opt.ResolveUsing
{ return Mapper.Map
"ChangeHistory")));
To resolve a single lazy loaded domain object:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AutoMapper;
namespace RICS.Brazil.Mapping.ObjectMap
{
/// <summary>
/// Custom resolver for objects set to lazy loading in nhibernate
/// </summary>
/// <typeparam name="TSource">The type of domain object the lazy loaded property is on.</typeparam>
/// <typeparam name="TDestination">The type the domsin object resolves to</typeparam>
public class SoloLazyInitResolver<TSource, TDestination> : ValueResolver<TSource, TDestination>
{
private ResolveSoloItemDelegate _resolveSoloItem;
string _propertyName;
/// <summary>
/// Resolves the object property to the specified type
/// </summary>
/// <param name="source">The domain object the property is on</param>
/// <returns>A resovled object or null if the source object is disconnected from the nhibernate session</returns>
protected override TDestination ResolveCore(TSource source)
{
try
{
dynamic item = source.GetType().GetProperty(_propertyName).GetValue(source, null);
if (item != null)
{
TDestination result = _resolveSoloItem(item);
return result;
}
}
catch (NHibernate.LazyInitializationException)//should never be raised
{ }
return default(TDestination);
}
/// <summary>
/// A delegate with the mapping instruction to invoke when resolving the lazy loaded property
/// </summary>
public delegate dynamic ResolveSoloItemDelegate(dynamic sourceItem);
/// <summary>
/// Initializes a new instance of the <see cref="SoloLazyInitResolver<TSource, TDestination>"/> class.
/// </summary>
/// <param name="resolveSoloItem">The delegate with the mapping instruction to invoke when resolving the peoperty on the object</param>
/// <param name="propertyName">Name of the lazy loaded property on the source domain object.</param>
public SoloLazyInitResolver(ResolveSoloItemDelegate resolveSoloItem, string propertyName)
{
if (resolveSoloItem == null) throw new ArgumentNullException("resolveSoloItem");
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("propertyName may not be null, empty, or whitespace");
_resolveSoloItem = resolveSoloItem;
_propertyName = propertyName;
}
}
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AutoMapper;
namespace RICS.Brazil.Mapping.ObjectMap
{
/// <summary>
/// Custom resolver for objects set to lazy loading in nhibernate
/// </summary>
/// <typeparam name="TSource">The type of domain object the lazy loaded property is on.</typeparam>
/// <typeparam name="TDestination">The type the domsin object resolves to</typeparam>
public class SoloLazyInitResolver<TSource, TDestination> : ValueResolver<TSource, TDestination>
{
private ResolveSoloItemDelegate _resolveSoloItem;
string _propertyName;
/// <summary>
/// Resolves the object property to the specified type
/// </summary>
/// <param name="source">The domain object the property is on</param>
/// <returns>A resovled object or null if the source object is disconnected from the nhibernate session</returns>
protected override TDestination ResolveCore(TSource source)
{
try
{
dynamic item = source.GetType().GetProperty(_propertyName).GetValue(source, null);
if (item != null)
{
TDestination result = _resolveSoloItem(item);
return result;
}
}
catch (NHibernate.LazyInitializationException)//should never be raised
{ }
return default(TDestination);
}
/// <summary>
/// A delegate with the mapping instruction to invoke when resolving the lazy loaded property
/// </summary>
public delegate dynamic ResolveSoloItemDelegate(dynamic sourceItem);
/// <summary>
/// Initializes a new instance of the <see cref="SoloLazyInitResolver<TSource, TDestination>"/> class.
/// </summary>
/// <param name="resolveSoloItem">The delegate with the mapping instruction to invoke when resolving the peoperty on the object</param>
/// <param name="propertyName">Name of the lazy loaded property on the source domain object.</param>
public SoloLazyInitResolver(ResolveSoloItemDelegate resolveSoloItem, string propertyName)
{
if (resolveSoloItem == null) throw new ArgumentNullException("resolveSoloItem");
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("propertyName may not be null, empty, or whitespace");
_resolveSoloItem = resolveSoloItem;
_propertyName = propertyName;
}
}
}
As with the previous example, I am catching the LazyInitializationException to permit resolution of the object while detached from the Session.
The mapper can be configured to use this custom resolver like this:
Mapper.CreateMap()
.ForMember(dest => dest.LastModifiedBy, opt => opt.ResolveUsing>().ConstructedBy(() => new SoloLazyInitResolver(delegate(dynamic lastModifiedBy)
{
return Mapper.Map(lastModifiedBy);
}, "LastModifiedBy")));
.ForMember(dest => dest.LastModifiedBy, opt => opt.ResolveUsing
{
return Mapper.Map
}, "LastModifiedBy")));
Using Automapper with Nhibernate this way is providing a lot of control and flexibility. The custom resolvers I have shown above means that this can be done with a minimum of additional effort.
Thanks jbogard and friends.
We have being trying to achieve what you mentioned in this comment..
ReplyDelete'Mapping profiles allow me to specify differing depth of object graph resolution.'
Ideally we would be defining multiple mappings (with differing depths) for the same source/destination pairs. Is this what you managed to do with Profiles?
Hi Ali, yes I did manage to do this with profiles. However, one of the limitations of AutoMapper is its reliance on static methods. I overcame by grabbing the source code, and performing some minor refactoring to reduce that dependency. In my consuming application I then created a static collection of AutoMapper instances, using different instances depending on the scenario involved. You might like to try the same approach. Good luck!
ReplyDelete