3
\$\begingroup\$

I have a T4 Templates project that generates methods to "convert" one Type (class, struct, enum) from a source Assembly its corresponding Type in another. The two types have the identical names and property names, just different namespaces and different types. This may be a re-invented a wheel. Nonetheless, I didn't find any solutions out there immediately, and it felt like a good programming exercise anyway. I'm also not familiar with dynamic languages and though it would be a worthwhile application of the dynamic (System.Dynamic.DynamicObject) keyword.

This following is the .tt file which writes out all the "convert" methods, but most of the work is done in a static class with numerous helper extension methods.

<#@ template language="C#" inherits="CommonT4.BaseTemplate" #>
<#@ Import Namespace="CommonT4" #>
<#@ Import Namespace="System.Collections.Generic" #>
<#@ Import Namespace="System.Text" #>
<#@ Import Namespace="System.Linq" #>
<#@ Import Namespace="System.IO" #>
<#@ Import Namespace="System.Reflection" #>
namespace <#= Namespace #>

{
    public static class ConvertExtension
    {
<#
    //will this work for ValueType (enum, struct)?
    foreach (var typeTuple in this.GetConvertTypeMappings(this.SrcAssembly, this.DestAssembly))
    {
        Type destType = typeTuple.Item2;
        Type srcType = typeTuple.Item1;

#>
        public static <#=destType.ToGenericTypeFullNameString()#> Convert(this <#=srcType.ToGenericTypeFullNameString() #> src0)
        {
            dynamic dest0;
            dynamic <#=HelperExtensionT4.DynamicVariableNameDeclarationsString(1, MAX_LVL) #>

    <#
            HelperExtensionT4.WriteConvertType(this, destType, srcType);
    #>
            return dest0;   
        }   
<#
    }
#>  
    }
}
<#+
    public string Namespace;
    public static int MAX_LVL = 16;
#>

Notice the first line in every generated convert method will declare a series of dynamic variables, most of which will be reused. The reason I used dynamic keywords was so I could reuse the same variable names, even if the types assigned to those variables change later. This would make this T4 template function "WriteConvertType" easier to finish writing, in my opinion. To clarify, the dynamic keyword is not used anywhere within this helper method - the dynamic keyword will show up in the generated code with all the conversion methods. At the end of the question I'll post a snippet of what the generated code looks like, since it is lengthy. This is the helper class where a recursive extension method "WriteConvertType" is defined.

public static void WriteConvertType(BaseTemplate template, Type destType, Type srcType, Stack<Type> stack = null)
        {   
            if (stack == null)
                stack = new Stack<Type>();

            if (srcType.IsNullableType())
            {
                template.WriteLine("dest{0} = new {1}(default({2}));", stack.Count, destType.ToGenericTypeFullNameString(), destType.GetGenericArguments()[0].ToGenericTypeFullNameString());           

                return;
            }
            else
                template.WriteLine("dest{0} = new {1}();", stack.Count, destType.ToGenericTypeFullNameString());            

            var commonProperties = from tuple in template.GetCommonConvertProperties(destType, srcType)
                             select new { desttype = tuple.Item1, srctype = tuple.Item2, Name = tuple.Item3 };
            foreach (var common in commonProperties)
            {
                Type srcElementType;
                Type mappedType;
                if (common.desttype == common.srctype || template.IsTypeMapped(common.desttype, out mappedType))//base case: would-be return;
                    template.WriteLine("dest{0}.{1} = src{2}.{3};", stack.Count, common.Name, stack.Count, common.Name);
                else if (common.srctype.isGenericEnumerableType(out srcElementType))
                {
                    //get matching generic argument/element Type from dest. Assembly or mscorlib:
                    Type destElementType = (from dtype in template.DestAssembly.GetTypes().Union(mscorlib.GetTypes())
                                            where dtype.Name == srcElementType.Name
                                            select dtype).First();
                    template.WriteLine("if (src{0}.{1} != null) {{", stack.Count, common.Name);
                    template.PushIndent("    ");
                    template.WriteLine("dest{0}.{1} = new {2}();"
                        , stack.Count
                        , common.Name
                        , common.desttype.ToGenericTypeFullNameString());                   
                    //caller checks stack:
                    if (!stack.Contains(common.desttype))
                    {
                        template.WriteLine("for (int i{0} = 0; i{0} < src{0}.{1}.Count; i{0}++)"
                           , stack.Count
                           , common.Name);
                        template.WriteLine("{");
                        template.PushIndent("    ");
                        template.WriteLine("src{0} = src{1}.{2}[i{1}];", stack.Count + 1, stack.Count, common.Name);
                        stack.Push(destElementType);
                        WriteConvertType(template, destElementType, srcElementType, stack);//, lvl + 1);
                        stack.Pop();
                        template.WriteLine("dest{0}.{1}.Add(dest{2});", stack.Count, common.Name, stack.Count + 1);
                        template.PopIndent();
                        template.WriteLine("}");//end for
                    }                                           
                    template.PopIndent();
                    template.WriteLine("}");//end if
                }
                else
                {
                    if (!stack.Contains(common.desttype))
                    {
                        template.WriteLine("src{0} = src{1}.{2};", stack.Count + 1, stack.Count, common.Name);//if property type not equal (and not IEnumerable)
                        template.WriteLine("if (src{0} is {1}) {{", stack.Count + 1, common.srctype.ToGenericTypeFullNameString());//if src != null
                        template.PushIndent("    ");//CurrentIndent += 1;
                        stack.Push(common.desttype);
                        WriteConvertType(template, common.desttype, common.srctype, stack);// lvl + 1);
                        stack.Pop();
                        template.WriteLine("dest{0}.{1} = dest{2};", stack.Count, common.Name, stack.Count + 1);//finally, set the property upon return
                        template.PopIndent();
                        template.WriteLine("}");
                    }                   
                }
            }
            return;
        }

Would this be considered excessive use or not an ideal situation for use of the C# dynamic keyword? Does this incur a noticeable performance penalty? Are there any guidelines or patterns/practices for the DLR?

Also, is "Convert" a good title for the method? The purpose of this method does seem to be a convert operation, although it's not really a cast. The purpose will be eventually to marshal the source type that's not serializable by the WCF DataContractSerializer to a destination type that will serialize.

Thanks for reading. I put the source code for this here: http://wcfsilverlighthelper.codeplex.com/

Finally, here is an example of one of the generated methods.

public static Northwind.SL.Model.Product Convert(this Northwind.NET.Model.Product src0)
    {
        dynamic dest0;
        dynamic src1, dest1, src2, dest2, src3, dest3, src4, dest4, src5, dest5, src6, dest6, src7, dest7, src8, dest8, src9, dest9, src10, dest10, src11, dest11, src12, dest12, src13, dest13, src14, dest14, src15, dest15, src16, dest16;

        dest0 = new Northwind.SL.Model.Product();
        dest0.ID = src0.ID;
        src1 = src0.PermissionType;
        if (src1 is System.Nullable<Northwind.NET.Security.PermissionTypeEnum>)
        {
            dest1 = new System.Nullable<Northwind.SL.Security.PermissionTypeEnum>(default(Northwind.SL.Security.PermissionTypeEnum));
            dest0.PermissionType = dest1;
        }
        src1 = src0.SecureName;
        if (src1 is Northwind.NET.Security.SecureString)
        {
            dest1 = new Northwind.SL.Security.SecureString();
            src2 = src1.SecurityHandle;
            if (src2 is Northwind.NET.Security.SecurityHandle)
            {
                dest2 = new Northwind.SL.Security.SecurityHandle();
                dest2.Domain = src2.Domain;
                dest1.SecurityHandle = dest2;
            }
            dest1.Value = src1.Value;
            dest0.SecureName = dest1;
        }
        dest0.Name = src0.Name;
        src1 = src0.SecurityHandle;
        if (src1 is Northwind.NET.Security.SecurityHandle)
        {
            dest1 = new Northwind.SL.Security.SecurityHandle();
            dest1.Domain = src1.Domain;
            dest0.SecurityHandle = dest1;
        }
        dest0.SupplierID = src0.SupplierID;
        dest0.CategoryID = src0.CategoryID;
        dest0.QuantityPerUnit = src0.QuantityPerUnit;
        dest0.UnitPrice = src0.UnitPrice;
        dest0.UnitsInStock = src0.UnitsInStock;
        dest0.UnitsOnOrder = src0.UnitsOnOrder;
        dest0.ReorderLevel = src0.ReorderLevel;
        dest0.Discontinued = src0.Discontinued;
        dest0.RowTimeStamps = src0.RowTimeStamps;
        src1 = src0.Supplier;
        if (src1 is Northwind.NET.Model.Supplier)
        {
            dest1 = new Northwind.SL.Model.Supplier();
            dest1.ID = src1.ID;
            dest1.Name = src1.Name;
            src2 = src1.SecureName;
            if (src2 is Northwind.NET.Security.SecureString)
            {
                dest2 = new Northwind.SL.Security.SecureString();
                src3 = src2.SecurityHandle;
                if (src3 is Northwind.NET.Security.SecurityHandle)
                {
                    dest3 = new Northwind.SL.Security.SecurityHandle();
                    dest3.Domain = src3.Domain;
                    dest2.SecurityHandle = dest3;
                }
                dest2.Value = src2.Value;
                dest1.SecureName = dest2;
            }
            src2 = src1.PermissionType;
            if (src2 is System.Nullable<Northwind.NET.Security.PermissionTypeEnum>)
            {
                dest2 = new System.Nullable<Northwind.SL.Security.PermissionTypeEnum>(default(Northwind.SL.Security.PermissionTypeEnum));
                dest1.PermissionType = dest2;
            }
            dest1.ContactName = src1.ContactName;
            dest1.ContactTitle = src1.ContactTitle;
            dest1.Address = src1.Address;
            dest1.City = src1.City;
            dest1.Region = src1.Region;
            dest1.PostalCode = src1.PostalCode;
            dest1.Country = src1.Country;
            dest1.Phone = src1.Phone;
            dest1.Fax = src1.Fax;
            dest1.HomePage = src1.HomePage;
            dest1.RowTimeStamps = src1.RowTimeStamps;
            if (src1.Products != null)
            {
                dest1.Products = new System.Collections.Generic.List<Northwind.SL.Model.Product>();
                for (int i1 = 0; i1 < src1.Products.Count; i1++)
                {
                    src2 = src1.Products[i1];
                    dest2 = new Northwind.SL.Model.Product();
                    dest2.ID = src2.ID;
                    src3 = src2.PermissionType;
                    if (src3 is System.Nullable<Northwind.NET.Security.PermissionTypeEnum>)
                    {
                        dest3 = new System.Nullable<Northwind.SL.Security.PermissionTypeEnum>(default(Northwind.SL.Security.PermissionTypeEnum));
                        dest2.PermissionType = dest3;
                    }
                    src3 = src2.SecureName;
                    if (src3 is Northwind.NET.Security.SecureString)
                    {
                        dest3 = new Northwind.SL.Security.SecureString();
                        src4 = src3.SecurityHandle;
                        if (src4 is Northwind.NET.Security.SecurityHandle)
                        {
                            dest4 = new Northwind.SL.Security.SecurityHandle();
                            dest4.Domain = src4.Domain;
                            dest3.SecurityHandle = dest4;
                        }
                        dest3.Value = src3.Value;
                        dest2.SecureName = dest3;
                    }
                    dest2.Name = src2.Name;
                    src3 = src2.SecurityHandle;
                    if (src3 is Northwind.NET.Security.SecurityHandle)
                    {
                        dest3 = new Northwind.SL.Security.SecurityHandle();
                        dest3.Domain = src3.Domain;
                        dest2.SecurityHandle = dest3;
                    }
                    dest2.SupplierID = src2.SupplierID;
                    dest2.CategoryID = src2.CategoryID;
                    dest2.QuantityPerUnit = src2.QuantityPerUnit;
                    dest2.UnitPrice = src2.UnitPrice;
                    dest2.UnitsInStock = src2.UnitsInStock;
                    dest2.UnitsOnOrder = src2.UnitsOnOrder;
                    dest2.ReorderLevel = src2.ReorderLevel;
                    dest2.Discontinued = src2.Discontinued;
                    dest2.RowTimeStamps = src2.RowTimeStamps;
                    dest1.Products.Add(dest2);
                }
            }
            dest0.Supplier = dest1;
        }
        return dest0;
    }
\$\endgroup\$
8
\$\begingroup\$

With AutoMapper (automapper.codeplex.com), your complete code could be flattened down to:

using SLProduct = Northwind.SL.Model.Product;
using NetProduct = Northwind.NET.Model.Product;

//

SLProduct slProduct = Mapper.Map<NetProduct, SLProduct>(nlProduct);

As you've said that the types use the same property types and names, you won't need to customise a mapping profile.

\$\endgroup\$
1
\$\begingroup\$

I don't think is either "Fair use" nor "Abuse" of dynamic; I think for your purpose, dynamic may be "Misused".

If all you want to accomplish is to map "two types have the identical names and property names, just different namespaces and different types", then you should probably use a mapper class with a function method that maps the types.

public static class ObjectMapper
{
    private static readonly Dictionary<Tuple<Type, Type>, object> Maps
        = new Dictionary<Tuple<Type, Type>, object>();

    /// <summary>
    /// <para>Add a new map</para>
    /// </summary>
    /// <typeparam name="TFrom">From Type</typeparam>
    /// <typeparam name="TTo">To Type</typeparam>
    /// <param name="map">Mapping delegate</param>
    public static void AddMap<TFrom, TTo>(Action<TFrom, TTo> map)
        where TFrom : class
        where TTo : class
    {
        Maps.Add(Tuple.Create(typeof(TFrom), typeof(TTo)), map);
    }

    /// <summary>
    /// <para>Map object data to another object</para>
    /// </summary>
    /// <typeparam name="TFrom">From type</typeparam>
    /// <typeparam name="TTo">To type</typeparam>
    /// <param name="from">From object</param>
    /// <param name="to">To object</param>
    public static void Map<TFrom, TTo>(TFrom from, TTo to)
    {
        var key = Tuple.Create(typeof(TFrom), typeof(TTo));
        var message = string.Format("No map defined for {0} => {1}", typeof(TFrom).Name, typeof(TTo).Name);

        if (!Maps.ContainsKey(key))
            throw new Exception(message);

        var map = (Action<TFrom, TTo>)Maps[key];

        if (map == null)
            throw new Exception(message);

        map(from, to);
    }
}

Alternatively, if you want to do something really generic to map a TInput instance to a TOutput instance without needing to write a specific mapper function for them, you can use reflection to create an extension method of object called CastTo like so:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

public static class TypeHelper
{
    public static TDestType CastTo<TDestType>(this object myType) where TDestType : new()
    {
        List<MemberInfo> tOrigMembers = myType.GetType().FindMembers(MemberTypes.Property | MemberTypes.Field,
                                                                     BindingFlags.GetProperty |
                                                                     BindingFlags.SetProperty |
                                                                     BindingFlags.SetField |
                                                                     BindingFlags.GetField |
                                                                     BindingFlags.Public |
                                                                     BindingFlags.NonPublic |
                                                                     BindingFlags.Instance,
                                                                     null, null).ToList();
        List<MemberInfo> tDestMembers = typeof (TDestType).FindMembers(MemberTypes.Property | MemberTypes.Field,
                                                                       BindingFlags.SetProperty |
                                                                       BindingFlags.SetField |
                                                                       BindingFlags.GetField |
                                                                       BindingFlags.Public |
                                                                       BindingFlags.NonPublic |
                                                                       BindingFlags.Instance,
                                                                       null, null).ToList();
        var destTypeInstance = new TDestType();
        foreach (MemberInfo m in tOrigMembers)
        {
            object value = (m is PropertyInfo)
                               ? ((PropertyInfo) m).GetValue(myType, null)
                               : ((FieldInfo) m).GetValue(myType);
            object enumValue;
            MemberInfo thisMember = FindMemberMatch(m.Name, value, tDestMembers, out enumValue);
            if (thisMember == null)
                continue;

            if (thisMember is PropertyInfo)
            {
                var pInfo = (PropertyInfo) thisMember;
                pInfo.SetValue(destTypeInstance, pInfo.PropertyType.IsEnum ? enumValue : value, null);
                continue;
            }
            if (thisMember is FieldInfo)
            {
                var fInfo = (FieldInfo) thisMember;
                fInfo.SetValue(destTypeInstance, fInfo.FieldType.IsEnum ? enumValue : value);
                continue;
            }
        }
        return destTypeInstance;
    }

    private static MemberInfo FindMemberMatch(string name, object value, IEnumerable<MemberInfo> members,
                                             out object enumValue)
    {
        enumValue = null;
        Type valueType = value == null ? typeof (object) : value.GetType();
        foreach (MemberInfo memberInfo in members)
        {
            if (memberInfo.Name != name)
                continue;
            if (memberInfo is FieldInfo)
            {
                var fieldInfo = (FieldInfo) memberInfo;
                if (fieldInfo.FieldType == valueType && fieldInfo.FieldType.IsAssignableFrom(valueType))
                    return fieldInfo;
                if (fieldInfo.FieldType.IsEnum && value != null &&
                    EnumIsAssignableFromValue(fieldInfo.FieldType, value))
                {
                    enumValue = Enum.Parse(fieldInfo.FieldType, value.ToString(), true);
                    return fieldInfo;
                }
                continue;
            }
            if (memberInfo is PropertyInfo)
            {
                var propertyInfo = (PropertyInfo) memberInfo;
                if (propertyInfo.PropertyType == valueType && propertyInfo.PropertyType.IsAssignableFrom(valueType))
                    return propertyInfo;
                if (propertyInfo.PropertyType.IsEnum && value != null &&
                    EnumIsAssignableFromValue(propertyInfo.PropertyType, value))
                {
                    enumValue = Enum.Parse(propertyInfo.PropertyType, value.ToString(), true);
                    return propertyInfo;
                }
                continue;
            }
        }
        return null;
    }

    private static bool EnumIsAssignableFromValue(Type enumType, object value)
    {
        return Enum.GetNames(enumType).Contains(value.ToString()) ||
               Enum.GetValues(enumType).OfType<object>().Contains(value);
    }
} 

To use it you would do

var myObjectOfTypeA = myObjectOfTypeB.CastTo<TypeA>();
\$\endgroup\$
  • 1
    \$\begingroup\$ Or as another alternative, you can make use of something like AutoMapper which has been designed from the ground up to do this very thing.... \$\endgroup\$ – Matthew Abbott Jun 2 '11 at 14:20
  • 1
    \$\begingroup\$ I didn't know about the AutoMapper. After looking at automapper.codeplex.com I'm in love. It even supports flattening! Thanks for the tip. \$\endgroup\$ – Michel Triana Jun 2 '11 at 14:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.