DTO <—> Entity Types conversion

Sometimes, we have to work with DTO very similar than our entities.

I wrote this class to make the conversion.

/// <summary>
/// Extension methods to convert an EntityType to a DTO and reversely
/// </summary>
public static class DTOConversion
{
    /// <summary>
    /// Gets an EntityType from a DTO
    /// </summary>
    /// <typeparam name="DTO">DTO type</typeparam>
    /// <typeparam name="EntityType">Entity type</typeparam>
    /// <param name="context">ObjectContext used to get metadata</param>
    /// <param name="dto">The DTO object</param>
    /// <returns></returns>
    public static EntityType GetEntitytype<DTO, EntityType>(this ObjectContext context, DTO dto)
        where DTO : class
        where EntityType : class, new()
    {
        return Converter<DTO, EntityType>.Convert(dto, context, false);
    }

    /// <summary>
    /// Gets a DTO from an EntityType
    /// </summary>
    /// <typeparam name="EntityType">Entity type</typeparam>
    /// <typeparam name="DTO">DTO type</typeparam>
    /// <param name="context">ObjectContext used to get metadata</param>
    /// <param name="entityType">The entity</param>
    /// <returns></returns>
    public static DTO GetDTO<EntityType, DTO>(this ObjectContext context, EntityType entityType)
        where EntityType : class
        where DTO : class, new()
    {
        return Converter<EntityType, DTO>.Convert(entityType, context, true);
    }
}

/// <summary>
/// Converter class used to convert a DTO to an EntityType an reversely
/// </summary>
/// <typeparam name="TIn">The initial DTO type / entity type</typeparam>
/// <typeparam name="TOut">The converted type (entity type / DTO type)</typeparam>
public static class Converter<TIn, TOut>
    where TIn : class
    where TOut : class, new()
{
    private static WeakReference<Func<TIn, ObjectContext, Dictionary<object, object>, TOut>> _cache;

    /// <summary>
    /// Converts a DTO / EntityType to an EntityType / DTO
    /// </summary>
    /// <param name="source">The initial object (DTO / Entity)</param>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Bolean used to know the conversion way</param>
    /// <param name="instances">A cache used to solve cyclic object graph</param>
    /// <returns>The converted object (Entity / DTO)</returns>
    public static TOut Convert(TIn source, ObjectContext context, bool entityTypeToDTO, Dictionary<object, object> instances = null)
    {
        if (source == null)
            return null;
        if (instances == null)
            instances = new Dictionary<object, object>();
        object value;
        if (instances.TryGetValue(source, out value)) // used to solve cyclic object graph
            return (TOut)value;
        Func<TIn, ObjectContext, Dictionary<object, object>, TOut> generateFunc = GetConverterFunc(context, entityTypeToDTO);
        return generateFunc(source, context, instances);
    }

    /// <summary>
    /// Gets a delegate to make the convesion
    /// </summary>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns></returns>
    private static Func<TIn, ObjectContext, Dictionary<object, object>, TOut> GetConverterFunc(ObjectContext context, bool entityTypeToDTO)
    {
        Func<TIn, ObjectContext, Dictionary<object, object>, TOut> generateFunc = null;
        if (_cache != null && _cache.IsAlive)
            try
            {
                generateFunc = _cache.Target;
            }
            catch (InvalidOperationException)
            {
            }
        if (generateFunc == null)
        {
            generateFunc = GenerateFunc(context, entityTypeToDTO);
            _cache = new WeakReference<Func<TIn, ObjectContext, Dictionary<object, object>, TOut>>(generateFunc);
        }
        return generateFunc;
    }

    /// <summary>
    /// Generates a Func from an Expression tree
    /// </summary>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns></returns>
    private static Func<TIn, ObjectContext, Dictionary<object, object>, TOut> GenerateFunc(ObjectContext context, bool entityTypeToDTO)
    {
        var sourceParameter = Expression.Parameter(typeof(TIn));
        var contextParameter = Expression.Parameter(typeof(ObjectContext));
        var instancesParameter = Expression.Parameter(typeof(Dictionary<object, object>));
        var result = Expression.Parameter(typeof(TOut)); // used as a variable and not as a parameter
        var expression = Expression.Lambda<Func<TIn, ObjectContext, Dictionary<object, object>, TOut>>(
            Expression.Block(
                new ParameterExpression[] { result },
                Expression.Assign(result, Expression.New(typeof(TOut))), // new TOut
                // Add result in instances dictionary (to solve cyclic object graph)
                // ReflectionExtension.GetMethod((Dictionary<object, object> d) => d.Add(null, null)) is equivalent to typeof(Dictionary<object, object>).GetMethod("Add")
                Expression.Call(instancesParameter, ReflectionExtension.GetMethod((Dictionary<object, object> d) => d.Add(null, null)), sourceParameter, result),
                Expression.Block(
                // We set every property that we can
                    from property in GetProperties(context, entityTypeToDTO)
                    select GetSetPropertyExpression ( result, property, sourceParameter, contextParameter, instancesParameter, context, entityTypeToDTO )
                ),
                result
            ),
            sourceParameter,
            contextParameter,
            instancesParameter
        );
        return expression.Compile();
    }

    /// <summary>
    /// Gets Expression to set TOut property
    /// </summary>
    /// <param name="result">The converted object (Entity / DTO)</param>
    /// <param name="property">TIn and TOut properties information</param>
    /// <param name="sourceParameter">The initial object (DTO / entity) expression parameter</param>
    /// <param name="contextParameter">The ObjectContext expression parameter</param>
    /// <param name="instancesParameter">A cache used to solve cyclic object graph expression parameter</param>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns></returns>
    private static Expression GetSetPropertyExpression ( ParameterExpression result, EntityTypePropertyInfo property, ParameterExpression sourceParameter,
        ParameterExpression contextParameter, ParameterExpression instancesParameter, ObjectContext context, bool entityTypeToDTO )
    {
        Expression propertGetValueExpression; // TOut property value expression
        MemberExpression getTInPropertyValueExpression = Expression.MakeMemberAccess(sourceParameter, property.TInProperty);
        if (property.IsScalarProperty)
            propertGetValueExpression = getTInPropertyValueExpression;
        else if (property.NavigationProperty != null)
            propertGetValueExpression =
                GetNavigationPropertyExpression ( getTInPropertyValueExpression, property, sourceParameter, contextParameter, instancesParameter, context, entityTypeToDTO );
        else if (property.ComplexProperty != null)
            propertGetValueExpression =
                GetInstanceProperty(getTInPropertyValueExpression, property, sourceParameter, contextParameter, instancesParameter, context, entityTypeToDTO);
        else
            throw new NotImplementedException();

        // Assign the TOut property
        return Expression.Assign(
            Expression.MakeMemberAccess(result, property.TOutProperty),
            propertGetValueExpression);
    }

    /// <summary>
    /// Gets Expression to get TOut property value converted from the TIn navigation property
    /// </summary>
    /// <param name="getTInPropertyValueExpression">The Expression used to get TIn property</param>
    /// <param name="property">TIn and TOut properties information</param>
    /// <param name="sourceParameter">The initial object (DTO / entity) expression parameter</param>
    /// <param name="contextParameter">The ObjectContext expression parameter</param>
    /// <param name="instancesParameter">A cache used to solve cyclic object graph expression parameter</param>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns></returns>
    private static Expression GetNavigationPropertyExpression ( MemberExpression getTInPropertyValueExpression, EntityTypePropertyInfo property, ParameterExpression sourceParameter,
        ParameterExpression contextParameter, ParameterExpression instancesParameter, ObjectContext context, bool entityTypeToDTO )
    {
        switch (property.NavigationProperty.ToEndMember.RelationshipMultiplicity)
        {
            case RelationshipMultiplicity.One:
            case RelationshipMultiplicity.ZeroOrOne:
                return GetInstanceProperty(getTInPropertyValueExpression, property, sourceParameter, contextParameter, instancesParameter, context, entityTypeToDTO);
            case RelationshipMultiplicity.Many:
                return GetCollectionProperty(getTInPropertyValueExpression, property, sourceParameter, contextParameter, instancesParameter, context, entityTypeToDTO);
        }
        throw new NotImplementedException();
    }

    /// <summary>
    /// Gets Expression to get TOut property value converted from the TIn instance property
    /// </summary>
    /// <param name="getTInPropertyValueExpression">The Expression used to get TIn property</param>
    /// <param name="property">TIn and TOut properties information</param>
    /// <param name="sourceParameter">The initial object (DTO / entity) expression parameter</param>
    /// <param name="contextParameter">The ObjectContext expression parameter</param>
    /// <param name="instancesParameter">A cache used to solve cyclic object graph expression parameter</param>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns>The expression used to get TOut property value</returns>
    private static Expression GetInstanceProperty(MemberExpression getTInPropertyValueExpression, EntityTypePropertyInfo property, ParameterExpression sourceParameter,
        ParameterExpression contextParameter, ParameterExpression instancesParameter, ObjectContext context, bool entityTypeToDTO)
    {
        // Convert the child object
        return Expression.Call(
            null, // Static method
            // Get Convert MethodInfo for related types
            typeof(Converter<,>).MakeGenericType(property.TInProperty.PropertyType, property.TOutProperty.PropertyType).
                GetMethod(
                    ReflectionExtension.GetMethod(() => Convert(null, null, true, null)).Name,
                    BindingFlags.Static | BindingFlags.Public,
                    null,
                    new Type[] { property.TInProperty.PropertyType, typeof(ObjectContext), typeof(bool), typeof(Dictionary<object, object>) },
                    null
                ),
            getTInPropertyValueExpression,
            contextParameter,
            Expression.Constant(entityTypeToDTO),
            instancesParameter);
    }

    /// <summary>
    /// Gets Expression to get TOut property value converted from the TIn collection property
    /// </summary>
    /// <param name="getTInPropertyValueExpression">The Expression used to get TIn property</param>
    /// <param name="property">TIn and TOut properties information</param>
    /// <param name="sourceParameter">The initial object (DTO / entity) expression parameter</param>
    /// <param name="contextParameter">The ObjectContext expression parameter</param>
    /// <param name="instancesParameter">A cache used to solve cyclic object graph expression parameter</param>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns>The expression used to get TOut property value</returns>
    private static Expression GetCollectionProperty(MemberExpression getTInPropertyValueExpression, EntityTypePropertyInfo property, ParameterExpression sourceParameter,
        ParameterExpression contextParameter, ParameterExpression instancesParameter, ObjectContext context, bool entityTypeToDTO)
    {
        // Get initial collection element type
        Type tInElementType = GetCollectionElementType(property.TInProperty.PropertyType);
        // Get result collection element type
        Type tOutElementType = GetCollectionElementType(property.TOutProperty.PropertyType);
        ParameterExpression initialChildParameter = Expression.Parameter(tInElementType);
        // Convert collection and chidren objects
        return Expression.Call(
            null, // Static method
            // Get CreateAndInitializeCollection MethodInfo for related types
            typeof(Converter<,>).MakeGenericType(tInElementType, tOutElementType).
                GetMethod(
                    ReflectionExtension.GetMethod(() => CreateAndInitializeCollection<List<TOut>>(new TIn[0], null)).Name,
                    BindingFlags.Static | BindingFlags.NonPublic,
                    null,
                    new Type[] { typeof(IEnumerable<>).MakeGenericType(tInElementType), typeof(Func<,>).MakeGenericType(tInElementType, tOutElementType) },
                    null
                ).MakeGenericMethod(property.TOutProperty.PropertyType),
            // collectionSource
            getTInPropertyValueExpression,
            // selector
            Expression.Lambda(
                Expression.Call(
                    null, // Static method
            // Get Convert MethodInfo for related types
                    typeof(Converter<,>).MakeGenericType(tInElementType, tOutElementType).
                        GetMethod(
                            ReflectionExtension.GetMethod(() => Convert(null, null, true, null)).Name,
                            BindingFlags.Static | BindingFlags.Public,
                            null,
                            new Type[] { tInElementType, typeof(ObjectContext), typeof(bool), typeof(Dictionary<object, object>) },
                            null
                        ),
                    initialChildParameter,
                    contextParameter,
                    Expression.Constant(entityTypeToDTO),
                    instancesParameter
                ),
                initialChildParameter
            )
        );
    }

    /// <summary>
    /// Gets collection elements type
    /// </summary>
    /// <param name="collectionType">The type of the collection</param>
    /// <returns>Collection elements type</returns>
    /// <exception cref="System.NotImplementedException">Thrown if the collectionType does not implement IEnumerable&lt;T&gt;"/></exception>
    private static Type GetCollectionElementType(Type collectionType)
    {
        Type iEnumerableOfTType = collectionType.GetInterfaces().FirstOrDefault(
                interfaceType => interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
            );
        if (iEnumerableOfTType == null)
            throw new NotImplementedException();
        return iEnumerableOfTType.GetGenericArguments()[0];
    }

    /// <summary>
    /// Returns a T2Collection of T2Item initialized with a collection of T1Item converted
    /// </summary>
    /// <typeparam name="T1Item">Type of initial collection elements</typeparam>
    /// <typeparam name="T2Collection">Type of result collection</typeparam>
    /// <typeparam name="T2Item">Type of result collection elements</typeparam>
    /// <param name="collectionSource">The initial collection</param>
    /// <param name="selector">The delagate used to convert initial collection elements into result collection elements</param>
    /// <returns></returns>
    private static TCollection CreateAndInitializeCollection<TCollection>(IEnumerable<TIn> collectionSource, Func<TIn, TOut> selector)
        where TCollection : class, ICollection<TOut>, new()
    {
        if (collectionSource == null)
            return null;
        TCollection value = new TCollection();
        foreach (TOut item in collectionSource.Select(selector))
            value.Add(item);
        return value;
    }

    /// <summary>
    /// Gets metadata properties
    /// </summary>
    /// <param name="context">The ObjectContext used to get model metadata</param>
    /// <param name="entityTypeToDTO">Boolean used to know the conversion way</param>
    /// <returns></returns>
    private static IEnumerable<EntityTypePropertyInfo> GetProperties(ObjectContext context, bool entityTypeToDTO)
    {
        return from tInPoperty in typeof(TIn).GetProperties()
                where tInPoperty.CanRead
                // Gets TOut writable property with the same name than propertyTIn
                let tOutProperty = typeof(TOut).GetProperties().FirstOrDefault(propertyInfo => propertyInfo.Name == tInPoperty.Name && propertyInfo.CanWrite)
                where tOutProperty != null
                // Gets metadata EntityType
                let entityType = context.GetEntityType(entityTypeToDTO ? typeof(TIn) : typeof(TOut))
                // Gets the navigation property with propertyTIn name (can be null)
                let navigationProperty = entityType.NavigationProperties.FirstOrDefault(p => p.Name == tInPoperty.Name)
                // Gets the complex property with propertyIn name (can be null)
                let complexProperty = entityType.Properties.FirstOrDefault(p => p.TypeUsage.EdmType is ComplexType && p.Name == tInPoperty.Name)
                select new EntityTypePropertyInfo
                {
                    TInProperty = tInPoperty,
                    TOutProperty = tOutProperty,
                    EntityType = entityType,
                    NavigationProperty = navigationProperty,
                    ComplexProperty = complexProperty
                };
    }
}

/// <summary>
/// PropertyInfo with more information
/// </summary>
public class EntityTypePropertyInfo
{
    /// <summary>
    /// Gets or Sets the property of TIn type
    /// </summary>
    public PropertyInfo TInProperty { get; set; }

    /// <summary>
    /// Gets or sets the property of TOut type
    /// </summary>
    public PropertyInfo TOutProperty { get; set; }

    /// <summary>
    /// Gets or sets the EntityType associated to TIn or TOut
    /// </summary>
    public EntityType EntityType { get; set; }

    /// <summary>
    /// Gets or sets the navigation property associated to the entity type property
    /// </summary>
    /// <remarks>Can be null</remarks>
    public NavigationProperty NavigationProperty { get; set; }

    /// <summary>
    /// Gets or sets the complex property associated to the entity type property
    /// </summary>
    public EdmProperty ComplexProperty { get; set; }

    /// <summary>
    /// Gets true if the property is a scalar type, false else
    /// </summary>
    public bool IsScalarProperty
    {
        get { return ComplexProperty == null && NavigationProperty == null; }
    }
}

/// <summary>
/// WeakReference with typed target
/// </summary>
/// <typeparam name="T">The type of the target</typeparam>
public class WeakReference<T> : WeakReference
    where T : class
{
    /// <summary>
    /// Initializes a new instance of the WeakReference<T> class, referencing the specified object
    /// </summary>
    /// <param name="target">The object to track or null</param>
    public WeakReference(T target)
        : base(target)
    {
    }

    /// <summary>
    /// Gets or sets the object (the target) referenced by the current System.WeakReference object
    /// </summary>
    /// <returns>
    /// null if the object referenced by the current System.WeakReference object
    /// has been garbage collected; otherwise, a reference to the object referenced by the
    /// current System.WeakReference object
    /// </returns>
    /// <exception cref="System.InvalidOperationException">
    /// The reference to the target object is invalid. This exception can be thrown while setting this
    /// property if the value is a null reference or if the object has been finalized during the set
    /// operation
    /// </exception>
    public new T Target
    {
        get { return base.Target as T; }
        set { base.Target = value; }
    }
}

/// <summary>
/// ObjectContext extension methods
/// </summary>
public static class ObjectContextExtension
{
    /// <summary>
    /// Gets the EntityType metadata from a System.Type
    /// </summary>
    /// <param name="context">The ObjectContext used to get metadata EntityType</param>
    /// <param name="type">The System.Type</param>
    /// <returns>null if type is unknown by the model, the metadata EntityType else</returns>
    public static EntityType GetEntityType(this ObjectContext context, Type type)
    {
        return context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().FirstOrDefault(et => et.Name == type.Name);
    }
}

/// <summary>
/// Reflection methods using Expression to avoid to use member's name
/// </summary>
public static class ReflectionExtension
{
    /// <summary>
    /// Returns MethodInfo from an expression
    /// </summary>
    /// <typeparam name="T">The method type</typeparam>
    /// <param name="exp">The expression</param>
    /// <returns>The MethodInfo</returns>
    public static MethodInfo GetMethod<T>(Expression<Action<T>> exp)
    {
        return GetMethod((LambdaExpression)exp);
    }
    /// <summary>
    /// Returns MethodInfo from an expression
    /// </summary>
    /// <param name="exp">The expression</param>
    /// <returns>The MethodInfo</returns>
    public static MethodInfo GetMethod(Expression<Action> exp)
    {
        return GetMethod((LambdaExpression)exp);
    }
    /// <summary>
    /// Returns MethodInfo from an expression
    /// </summary>
    /// <param name="exp">The expression</param>
    /// <returns>The MethodInfo</returns>
    private static MethodInfo GetMethod(LambdaExpression exp)
    {
        MethodCallExpression methodCallExpression = exp.Body as MethodCallExpression;
        if (methodCallExpression == null)
            throw new ArgumentException();
        return methodCallExpression.Method;
    }
}


 



To use it, DTO properties name have to be the same than EntityTypes properties name.



Hope that helps

This entry was posted in 7671, 7674. Bookmark the permalink.

5 Responses to DTO <—> Entity Types conversion

  1. James says:

    Intriguing…Can you post an example?

    p.s. your captcha images don’t load in Google Chrome.

  2. Herbert Leonard ;-) says:

    Hi Matthieu,

    Gratulations for this very useful and ambitious class. After few conversion tasks (enum, nullable, etc.), i could have an EDM representation of my DTO class, then reverse it from EDM to DTO).

    The DTO was built from a very complex XSD (40 files), so it’s a good Proof of Concept for your class.

    Let me please suggest you the following code in order to make your class support Array types for DTO to EDM conversions (useful eg. for interoperable Webservices) :

    —————————————

    private static TCollection CreateAndInitializeCollection(IEnumerable collectionSource, Func selector)
    where TCollection : class, ICollection//, new() // Commented, : Array do not support the new() constraint
    {
    if (collectionSource == null) return null;

    IEnumerable OutCollection = collectionSource.Select(selector);

    if(!typeof(TCollection).IsArray)
    {TCollection tcol = (TCollection)Activator.CreateInstance(typeof(TCollection));
    foreach (TOut item in OutCollection)tcol.Add(item);
    return tcol;
    }
    else
    {
    List acol = (List)Activator.CreateInstance(typeof(List));
    foreach (TOut item in OutCollection) acol.Add(item);
    return (TCollection)(object)(acol.ToArray());
    }

    }

    —————————————
    Thank you again et bonne journée,
    David

  3. Alex says:

    Very nice! This has saved me lots of time – thanks!

  4. roei says:

    Hi

    Thank you for this great post.

    I’m trying to implememt this but i keep getting this excption:

    “Non-empty collection required
    Parameter name: expressions”

    Maybe you understand what i’m i doing worng?

    Thanks again

    roei

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>