views:

125

answers:

3

...apart from the obvious looping through the list and a dirty great case statement!

I've turned over a few Linq queries in my head but nothing seems to get anywhere close.

Here's the an example DTO if it helps:

    class ClientCompany
    {
      public string Title { get; private set; }
      public string Forenames { get; private set; }
      public string Surname { get; private set; }
      public string EmailAddress { get; private set; }
      public string TelephoneNumber { get; private set; }
      public string AlternativeTelephoneNumber { get; private set; }
      public string Address1 { get; private set; }
      public string Address2 { get; private set; }
      public string TownOrDistrict { get; private set; }
      public string CountyOrState { get; private set; }
      public string PostCode { get; private set; }
    }

We have no control over the fact that we're getting the data in as KV pairs, I'm afraid.

and whilst there is an effective mapping of each KV pair to each property and I do know the keys in advance they're not named the same as the DTO.

A: 

If you can get the data to look like ['Title':'Mr', 'Forenames':'John', 'Surname':'Doe',...], then you should be able to JSON deserialize the kvp into your source object.

Jarrett Meyer
+1  A: 

Here is an elegant, extensible, maintainable and blazingly fast solution for loading DTOs from Dictionaries.

Create a console app and add these two files. The rest is self documenting.

The salient points:

  • simple, brief and maintainable mapping classes.
  • efficient object rehydration using dynamic methods.

NOTE: If you copied the previous DynamicProperties.cs, you will want to get this one. I added a flag to allow generation of private setters that was not in the previous version.

Cheers.

program.cs

using System.Collections.Generic;
using System.Diagnostics;
using Salient.Reflection;

namespace KVDTO
{
    /// <summary>
    /// This is our DTO
    /// </summary>
    public class ClientCompany
    {
        public string Address1 { get; private set; }
        public string Address2 { get; private set; }
        public string AlternativeTelephoneNumber { get; private set; }
        public string CountyOrState { get; private set; }
        public string EmailAddress { get; private set; }
        public string Forenames { get; private set; }
        public string PostCode { get; private set; }
        public string Surname { get; private set; }
        public string TelephoneNumber { get; private set; }
        public string Title { get; private set; }
        public string TownOrDistrict { get; private set; }
    }


    /// <summary>
    /// This is our DTO Map
    /// </summary>
    public sealed class ClientCompanyMapping : KeyValueDtoMap<ClientCompany>
    {
        static ClientCompanyMapping()
        {
            AddMapping("Title", "Greeting");
            AddMapping("Forenames", "First");
            AddMapping("Surname", "Last");
            AddMapping("EmailAddress", "eMail");
            AddMapping("TelephoneNumber", "Phone");
            AddMapping("AlternativeTelephoneNumber", "Phone2");
            AddMapping("Address1", "Address1");
            AddMapping("Address2", "Address2");
            AddMapping("TownOrDistrict", "City");
            AddMapping("CountyOrState", "State");
            AddMapping("PostCode", "Zip");
        }
    }


    internal class Program
    {
        private const string Address1 = "1243 Easy Street";
        private const string CountyOrState = "Az";
        private const string EmailAddress = "[email protected]";
        private const string Forenames = "Sky";
        private const string PostCode = "85282";
        private const string Surname = "Sanders";
        private const string TelephoneNumber = "800-555-1212";
        private const string Title = "Mr.";
        private const string TownOrDistrict = "Tempe";

        private static void Main(string[] args)
        {
            // this represents our input data, some discrepancies
            // introduced to demonstrate functionality of the map

            // the keys differ from the dto property names
            // there are missing properties
            // there are unrecognized properties
            var input = new Dictionary<string, string>
                {
                    {"Greeting", Title},
                    {"First", Forenames},
                    {"Last", Surname},
                    {"eMail", EmailAddress},
                    {"Phone", TelephoneNumber},
                    // missing from this input {"Phone2", ""},
                    {"Address1", Address1},
                    // missing from this input {"Address2", ""},
                    {"City", TownOrDistrict},
                    {"State", CountyOrState},
                    {"Zip", PostCode},
                    {"SomeOtherFieldWeDontCareAbout", "qwerty"}
                };


            // rehydration is simple and FAST

            // instantiate a map. You could store instances in a dictionary
            // but it is not really necessary for performance as all of the
            // work is done in the static constructors, so no matter how many
            // times you 'new' a map, it is only ever built once.

            var map = new ClientCompanyMapping();

            // do the work. 
            ClientCompany myDto = map.Load(input);





            // test
            Debug.Assert(myDto.Address1 == Address1, "Address1");
            Debug.Assert(myDto.Address2 == null, "Address2");
            Debug.Assert(myDto.AlternativeTelephoneNumber == null, "AlternativeTelephoneNumber");
            Debug.Assert(myDto.CountyOrState == CountyOrState, "CountyOrState");
            Debug.Assert(myDto.EmailAddress == EmailAddress, "EmailAddress");
            Debug.Assert(myDto.Forenames == Forenames, "Forenames");
            Debug.Assert(myDto.PostCode == PostCode, "PostCode");
            Debug.Assert(myDto.Surname == Surname, "Surname");
            Debug.Assert(myDto.TelephoneNumber == TelephoneNumber, "TelephoneNumber");
            Debug.Assert(myDto.Title == Title, "Title");
            Debug.Assert(myDto.TownOrDistrict == TownOrDistrict, "TownOrDistrict");
        }
    }

    /// <summary>
    /// Base mapper class.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class KeyValueDtoMap<T> where T : class, new()
    {
        private static readonly List<DynamicProperties.Property> Props;
        private static readonly Dictionary<string, string> KvMap;

        static KeyValueDtoMap()
        {
            // this property collection is built only once
            Props = new List<DynamicProperties.Property>(DynamicProperties.CreatePropertyMethods(typeof(T)));
            KvMap=new Dictionary<string, string>();
        }

        /// <summary>
        /// Adds a mapping between a DTO property and a KeyValue pair
        /// </summary>
        /// <param name="dtoPropertyName">The name of the DTO property</param>
        /// <param name="inputKey">The expected input key</param>
        protected static void AddMapping(string dtoPropertyName,string inputKey)
        {
            KvMap.Add(dtoPropertyName,inputKey);
        }

        /// <summary>
        /// Creates and loads a DTO from a Dictionary
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public T Load(Dictionary<string, string> input)
        {
            var result = new T();
            Props.ForEach(p =>
                {
                    string inputKey = KvMap[p.Info.Name];
                    if (input.ContainsKey(inputKey))
                    {
                        p.Setter.Invoke(result, input[inputKey]);
                    }
                });
            return result;
        }
    }
}

DynamicProperties.cs

/*!
 * Project: Salient.Reflection
 * File   : DynamicProperties.cs
 * http://spikes.codeplex.com
 *
 * Copyright 2010, Sky Sanders
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * See LICENSE.TXT
 * Date: Sat Mar 28 2010 
 */

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace Salient.Reflection
{
    /// <summary>
    /// Gets IL setters and getters for a property.
    /// </summary>
    public static class DynamicProperties
    {
        #region Delegates

        public delegate object GenericGetter(object target);

        public delegate void GenericSetter(object target, object value);

        #endregion

        public static IList<Property> CreatePropertyMethods(Type T)
        {
            var returnValue = new List<Property>();

            foreach (PropertyInfo prop in T.GetProperties())
            {
                returnValue.Add(new Property(prop));
            }
            return returnValue;
        }


        public static IList<Property> CreatePropertyMethods<T>()
        {
            var returnValue = new List<Property>();

            foreach (PropertyInfo prop in typeof (T).GetProperties())
            {
                returnValue.Add(new Property(prop));
            }
            return returnValue;
        }


        /// <summary>
        /// Creates a dynamic setter for the property
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <returns></returns>
        /// <source>
        /// http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
        /// </source>
        public static GenericSetter CreateSetMethod(PropertyInfo propertyInfo)
        {
            /*
            * If there's no setter return null
            */
            MethodInfo setMethod = propertyInfo.GetSetMethod(true);
            if (setMethod == null)
                return null;

            /*
            * Create the dynamic method
            */
            var arguments = new Type[2];
            arguments[0] = arguments[1] = typeof (object);

            var setter = new DynamicMethod(
                String.Concat("_Set", propertyInfo.Name, "_"),
                typeof (void), arguments, propertyInfo.DeclaringType);
            ILGenerator generator = setter.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
            generator.Emit(OpCodes.Ldarg_1);

            if (propertyInfo.PropertyType.IsClass)
                generator.Emit(OpCodes.Castclass, propertyInfo.PropertyType);
            else
                generator.Emit(OpCodes.Unbox_Any, propertyInfo.PropertyType);

            generator.EmitCall(OpCodes.Callvirt, setMethod, null);
            generator.Emit(OpCodes.Ret);

            /*
            * Create the delegate and return it
            */
            return (GenericSetter) setter.CreateDelegate(typeof (GenericSetter));
        }


        /// <summary>
        /// Creates a dynamic getter for the property
        /// </summary>
        /// <param name="propertyInfo"></param>
        /// <returns></returns>
        /// <source>
        /// http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
        /// </source>
        public static GenericGetter CreateGetMethod(PropertyInfo propertyInfo)
        {
            /*
            * If there's no getter return null
            */
            MethodInfo getMethod = propertyInfo.GetGetMethod(true);
            if (getMethod == null)
                return null;

            /*
            * Create the dynamic method
            */
            var arguments = new Type[1];
            arguments[0] = typeof (object);

            var getter = new DynamicMethod(
                String.Concat("_Get", propertyInfo.Name, "_"),
                typeof (object), arguments, propertyInfo.DeclaringType);
            ILGenerator generator = getter.GetILGenerator();
            generator.DeclareLocal(typeof (object));
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
            generator.EmitCall(OpCodes.Callvirt, getMethod, null);

            if (!propertyInfo.PropertyType.IsClass)
                generator.Emit(OpCodes.Box, propertyInfo.PropertyType);

            generator.Emit(OpCodes.Ret);

            /*
            * Create the delegate and return it
            */
            return (GenericGetter) getter.CreateDelegate(typeof (GenericGetter));
        }

        #region Nested type: Property

        public class Property
        {
            public GenericGetter Getter;
            public PropertyInfo Info;
            public GenericSetter Setter;

            public Property(PropertyInfo info)
            {
                Info = info;
                Setter = CreateSetMethod(info);
                Getter = CreateGetMethod(info);
            }
        }

        #endregion
    }
}
Sky Sanders
Nice bit of code there but as I know the keys and their mapping to the properties in advance I don't think I need the dynamic property access that supplies. Cheers though.
@weevie - yeah, you are halfway there with mappings, but the salient point of this class, in this context, is SPEED baby!
Sky Sanders
That'll teach me to speed read the code. _Might_ need this to be performant so this might end up being more useful. I'll give this a try and see the difference it makes. I was going down the same route that Sergej suggested below which I might still use for it's brevity and simplicity if I don't need the performance.
@Weevie There are some very elegant options open here that may not require the maintenance of so much code. Why don't you update the question with a full example of the domain object and the mapping information you have as well as the payload that will be used to rehydrate the objects.
Sky Sanders
@weevie - when I saw you use the words 'brevity and simplicity' to describe @Sergej's suggestion I nearly choked on my donut. Let's put style and maintenance aside and just agree that the code will not/would not work. ;-) I have provided an industrial grade reference implementation for your viewing pleasure. Cheers.
Sky Sanders
A: 

Alternatively if you don't want to go with reflection you could use this (this wont' work blazingly fast):

var ccd = new List<KeyValuePair<string, string>>();
ccd.Add(new KeyValuePair<string, string>("Title", ""));
ccd.Add(new KeyValuePair<string, string>("Forenames", ""));
ccd.Add(new KeyValuePair<string, string>("Surname", ""));
ccd.Add(new KeyValuePair<string, string>("EmailAddress", ""));
ccd.Add(new KeyValuePair<string, string>("TelephoneNumber", ""));
ccd.Add(new KeyValuePair<string, string>("AlternativeTelephoneNumber", ""));
ccd.Add(new KeyValuePair<string, string>("Address1", ""));
ccd.Add(new KeyValuePair<string, string>("Address2", ""));
ccd.Add(new KeyValuePair<string, string>("TownOrDistrict", ""));
ccd.Add(new KeyValuePair<string, string>("CountyOrState", ""));
ccd.Add(new KeyValuePair<string, string>("PostCode", ""));

var data = new List<List<KeyValuePair<string, string>>> { ccd, ccd, ccd };

var companies = from d in data
                select new ClientCompany {
                    Title = d.FirstOrDefault(k => k.Key == "Title").Value,
                    Forenames = d.FirstOrDefault(k => k.Key == "Forenames").Value,
                    Surname = d.FirstOrDefault(k => k.Key == "Surname").Value,
                    EmailAddress = d.FirstOrDefault(k => k.Key == "EmailAddress").Value,
                    TelephoneNumber = d.FirstOrDefault(k => k.Key == "TelephoneNumber").Value,
                    AlternativeTelephoneNumber = d.FirstOrDefault(k => k.Key == "AlternativeTelephoneNumber").Value,
                    Address1 = d.FirstOrDefault(k => k.Key == "Address1").Value,
                    Address2 = d.FirstOrDefault(k => k.Key == "Address2").Value,
                    TownOrDistrict = d.FirstOrDefault(k => k.Key == "TownOrDistrict").Value,
                    CountyOrState = d.FirstOrDefault(k => k.Key == "CountyOrState").Value,
                    PostCode = d.FirstOrDefault(k => k.Key == "PostCode").Value,
                };
Sergej Andrejev
do you think this will work with private setters as shown in the question?
Sky Sanders
Nothing will work with private setters without reflection. It's just to get you an idea.
Sergej Andrejev