5
\$\begingroup\$

Here is a utility that supports ETL/merging in Entity Framework.

If it's not appealing as a general purpose tool, why?

If it is appealing as a general purpose tool, how might the design be made better?

Also, I'm not the world's most experienced programmer when it comes to writing re-usable code with paramaterized types - any feedback on that aspect would be appreciated.

public static class MergeUtility
{
    /// <summary>
    /// TODO: documentation
    /// </summary>
    /// <typeparam name="TItem">The type of the source items.</typeparam>
    /// <typeparam name="TContainer">The type of the destination container.</typeparam>
    /// <typeparam name="TTarget">The type of the destination items.</typeparam>
    /// <param name="items">A function returning an enumerable set of source items.</param>
    /// <param name="container">A function returning a destination container.</param>
    /// <param name="sourceKey">The name of the property on each source item who's value uniquely identifies it in the whole set.</param>
    /// <param name="targetSet">The name of the property on the target container who's value is the ObjectSet containing the target items.</param>
    /// <param name="targetKey">The name of the property on each target item who's value uniquely identifies it in the whole set.</param>
    /// <param name="save">A function called when the merge is completed to save the changes in the target container.</param>
    public static void Merge<TItem, TContainer, TTarget>(
        Func<IEnumerable<TItem>> items,
        Func<TContainer> container,
        string sourceKey,
        string targetSet,
        string targetKey,
        Action<TContainer> save)
        where TItem : class
        where TContainer : IDisposable
        where TTarget : class, new()
    {
        Log<TItem, TContainer, TTarget>();

        var source = items();

        using (var destination = container())
        {
            var target = destination.MemberValue<ObjectSet<TTarget>>(targetSet);

            var existing = target.ToDictionary(c => c.MemberValue<object>(targetKey));

            var mapper = new Mapper<TItem, TTarget>();

            foreach (var item in source)
            {
                var id = item.MemberValue<object>(sourceKey);

                TTarget entity;

                if (existing.ContainsKey(id))
                {
                    entity = existing[id];
                }
                else
                {
                    entity = new TTarget();
                    target.AddObject(entity);
                }

                mapper.MapProperties(item, entity, false);
            }

            save(destination);
        }
    }

    public static void Merge<TSource, TSourceContainer, TTarget, TTargetContainer>(
        EntitySource<TSource, TSourceContainer> source, 
        EntityTarget<TTarget, TTargetContainer> target)
        where TSource : class
        where TSourceContainer : IDisposable
        where TTarget : class, new()
        where TTargetContainer : IDisposable
    {
        using (var container = source.Container())
        {
            Merge<TSource, TTargetContainer, TTarget>(
                () => container.MemberValue<ObjectSet<TSource>>(source.Set),
                target.Container,
                source.Key,
                target.Set,
                target.Key,
                target.Save);
        }
    }

    public static void Merge<TSource, TContainer, TTarget>(
        Source<TSource> source,
        EntityTarget<TTarget, TContainer> target)
        where TSource : class
        where TContainer : IDisposable
        where TTarget : class, new()
    {
        Merge<TSource, TContainer, TTarget>(
            source.Items,
            target.Container,
            source.Key,
            target.Set,
            target.Key,
            target.Save);
    }

    public static void Merge<T1, T2, T3>()
        where T1 : class
        where T2 : class, new()
        where T3 : IDisposable
    {
        Merge(
            ServiceLocator.GetInstance<Source<T1>>(),
            ServiceLocator.GetInstance<EntityTarget<T2, T3>>());
    }     

    private static void Log<TSource, TContainer, TTarget>()
        where TSource : class
        where TContainer : IDisposable
        where TTarget : class, new()
    {
        Console.WriteLine("MERGE");
        Console.WriteLine("       Source: " + typeof(TSource).FullName);
        Console.WriteLine("    Container: " + typeof(TContainer).FullName);
        Console.WriteLine("       Target: " + typeof(TTarget).FullName);
    }
}

This is how I'm using it in my code:

class Program
{
    static void Main(string[] args)
    {
        Configuration.Initialize();
        new Program().ExtractAndStageCakeAffiliates();
    }

    public Program()
    {
        MapperItem.AddConversion<Int32, Decimal>(c => Convert.ToDecimal(c));
        MapperItem.AddConversion<Int32, String>(c => c.ToString());
        MapperItem.AddConversion<Boolean, String>(c => c.ToString());
        MapperItem.AddConversion<Int16, String>(c => c.ToString());
        MapperItem.AddConversion<DateTime, String>(c => c.ToString());
        MapperItem.AddConversion<Decimal, String>(c => c.ToString());
    }

    public void ExtractAndStageCakeAffiliates()
    {
        MergeUtility.Merge(
            ServiceLocator.GetInstance<Source<Cake.Data.Wsdl.ExportService.Affiliate>>(),
            ServiceLocator.GetInstance<EntityTarget<Cake.Model.Staging.CakeAffiliate, Cake.Model.Staging.CakeStagingEntities>>());
    }

    public void LoadCakeAffiliates()
    {
        MergeUtility.Merge(
            ServiceLocator.GetInstance<EntitySource<Cake.Model.Staging.CakeAffiliate, Cake.Model.Staging.CakeStagingEntities>>(),
            ServiceLocator.GetInstance<EntityTarget<Cake.Model.CakeAffiliate, Cake.Model.CakeContainer>>());
    }

    public void RefreshCakeAffiliates()
    {
        this.ExtractAndStageCakeAffiliates();
        this.LoadCakeAffiliates();
    }
}

And here's the DI setup:

internal class Configuration
{
    internal static void Initialize()
    {
        IUnityContainer container = new UnityContainer()

                // Extract from web service
            .RegisterInstance<Source<Cake.Data.Wsdl.ExportService.Affiliate>>(
                new Source<Cake.Data.Wsdl.ExportService.Affiliate>(
                    () => ServiceLocator.GetInstance<Cake.Data.Wsdl.ICakeWebService>().ExportAffiliates(),
                    "affiliate_id"))

                // Load to staging
            .RegisterInstance<EntityTarget<Cake.Model.Staging.CakeAffiliate, Cake.Model.Staging.CakeStagingEntities>>(
                new EntityTarget<Cake.Model.Staging.CakeAffiliate, Cake.Model.Staging.CakeStagingEntities>(
                    () => new Cake.Model.Staging.CakeStagingEntities(),
                    "CakeAffiliates",
                    "Affiliate_Id",
                    c => c.SaveChanges()))

                // Extract from staging
            .RegisterInstance<EntitySource<Cake.Model.Staging.CakeAffiliate, Cake.Model.Staging.CakeStagingEntities>>(
                new EntitySource<Cake.Model.Staging.CakeAffiliate, Cake.Model.Staging.CakeStagingEntities>(
                    () => new Cake.Model.Staging.CakeStagingEntities(),
                    "CakeAffiliates",
                    "Affiliate_Id"))

                // Load to production
            .RegisterInstance<EntityTarget<Cake.Model.CakeAffiliate, Cake.Model.CakeContainer>>(
                new EntityTarget<Cake.Model.CakeAffiliate, Cake.Model.CakeContainer>(() =>
                    new Cake.Model.CakeContainer(),
                    "CakeAffiliates",
                    "Affiliate_Id",
                    c => c.SaveChanges())) 
            ;

And the object mapper:

    /// <summary>
    /// This Mapper can transfer the values bwtween two existing objects, the source and the destination.
    /// 
    /// Property names are matched after being normalized:
    ///    1. Underscores are removed (foo_bar_id becomes foobarid).
    ///    2. Converted to uppercase (foobarid becomes FOOBARID)
    /// </summary>
    /// <typeparam name="S"></typeparam>
    /// <typeparam name="T"></typeparam>
    public class Mapper<S, T>
    {
        List<MemberInfo> targetMembers = new List<MemberInfo>();

        private List<string> ignoreList = new List<string>();
        public List<string> IgnoreList
        {
            get { return ignoreList; }
            set { ignoreList = value; }
        }

        public Mapper()
        {
            this.targetMembers.AddRange(typeof(T).GetProperties());
            this.targetMembers.AddRange(typeof(T).GetFields());
        }

        /// <summary>
        /// Transfer the values bwtween two existing objects, the source and the destination.
        /// </summary>
        /// <param name="source">The object from which property values will be obtained.</param>
        /// <param name="target">The object who's properties recieve the value of their matching property in the <paramref name="source"/></param>
        /// <param name="failIfNotMatched">When a property in the <paramref name="source"/> does not match to a property in the <paramref name="target"/>
        /// and <paramref name="failIfNotMatched"/> is TRUE, a <c>TargetNotMatchedException</c> will be thrown.  Otherwise the unmatched property is ignored.< </param>
        /// <param name="mapInheritedMembers">When <paramref name="mapInheritedMembers"/> is TRUE the set of source properties will include properties which
        /// are inherited.  Otherwise only the properties of the most derived type are mapped.</param>
        public void MapProperties(S source, T target, bool failIfNotMatched = true, bool mapInheritedMembers = false)
        {
            BindingFlags bindingFlags = mapInheritedMembers
                ? BindingFlags.Public | BindingFlags.Instance
                : BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance;

            foreach (PropertyInfo property in source.GetType()
                                                    .GetProperties(bindingFlags)
                                                    .Where(c => !IgnoreList.Contains(c.Name)))
            {
                try
                {
                    var sourceField = Factory.Get<MapperItem>(property, source);
                    var targetField = Factory.Get<MapperItem>(MatchToTarget(property), target);

                    targetField.Assign(sourceField);
                }
                catch (TargetNotMatchedException noMatch)
                {
                    if (failIfNotMatched)
                    {
                        throw noMatch;
                    }
                }
            }
        }

        private MemberInfo MatchToTarget(MemberInfo member)
        {
            var exactMatch = this.targetMembers.Where(c => c.Name == member.Name);
            if (exactMatch.FirstOrDefault() != null)
            {
                return exactMatch.First();
            }

            var sameAlphaChars = this.targetMembers.Where(c => Normalize(c.Name) == Normalize(member.Name));
            if (sameAlphaChars.FirstOrDefault() != null)
            {
                return sameAlphaChars.First();
            }

            throw new TargetNotMatchedException(member, typeof(T));
        }

        private static string Normalize(string input)
        {
            string normalized = input.Replace("_", "").ToUpper();
            return normalized;
        }
    }

MapperItem:

    /// <summary>
    /// Encapsulates an item to be mapped and supports conversion from the souce type to the destination type.
    /// </summary>
    public class MapperItem
    {
        private MemberInfo memberInfo;
        private object target;
        private Type type;
        private static Dictionary<Tuple<Type, Type>, Func<object, object>> Conversions = new Dictionary<Tuple<Type, Type>, Func<object, object>>();

        /// <summary>
        /// Constructor. TODO: improve comment
        /// </summary>
        /// <param name="member"></param>
        /// <param name="target"></param>
        public MapperItem(MemberInfo member, object target)
        {
            this.memberInfo = member;
            this.target = target;
            this.type = this.memberInfo.UnderlyingType();
        }

        /// <summary>
        /// Transfers the value from one mapper item to the other while applying type conversion.
        /// </summary>
        /// <param name="source"></param>
        public void Assign(MapperItem source)
        {
            this.memberInfo.Assign(this.target, source.Convert(this.type));
        }

        /// <summary>
        /// Allows arbitrary conversions.
        /// </summary>
        /// <typeparam name="S"></typeparam>
        /// <typeparam name="T"></typeparam>
        /// <param name="converter"></param>
        public static void AddConversion<S, T>(Func<object, object> converter)
        {
            Conversions.Add(Tuple.Create(typeof(S), typeof(T)), converter);
        }

        private object Value
        {
            get
            {
                return this.memberInfo.Value(this.target);
            }
        }

        private object Convert(Type convertToType)
        {
            object converted = null;

            if (this.Value == null)
            {
                return converted;
            }
            else if (convertToType.IsAssignableFrom(this.type))
            {
                converted = this.Value;
            }
            else
            {
                var conversionKey = Tuple.Create(this.type, convertToType);

                if (Conversions.ContainsKey(conversionKey))
                {
                    converted = Conversions[conversionKey](this.Value);
                }
                else
                {
                    throw new Exception(convertToType.Name + " is not assignable from " + this.type.Name);
                }
            }

            return converted;
        }
    }

Reflection extensions:

public static class ReflectionExtensions
{
    public static Type UnderlyingType(this MemberInfo member)
    {
        Type type;
        switch (member.MemberType)
        {
            case MemberTypes.Field:
                type = ((FieldInfo)member).FieldType;
                break;
            case MemberTypes.Property:
                type = ((PropertyInfo)member).PropertyType;
                break;
            case MemberTypes.Event:
                type = ((EventInfo)member).EventHandlerType;
                break;
            default:
                throw new ArgumentException("member must be if type FieldInfo, PropertyInfo or EventInfo", "member");
        }
        return Nullable.GetUnderlyingType(type) ?? type;
    }

    public static object Value(this MemberInfo member, object target)
    {
        if (member is PropertyInfo)
        {
            return (member as PropertyInfo).GetValue(target, null);
        }
        else if (member is FieldInfo)
        {
            return (member as FieldInfo).GetValue(target);
        }
        else
        {
            throw new Exception("member must be either PropertyInfo or FieldInfo");
        }
    }

    public static void Assign(this MemberInfo member, object target, object value)
    {
        if (member is PropertyInfo)
        {
            (member as PropertyInfo).SetValue(target, value, null);
        }
        else if (member is FieldInfo)
        {
            (member as FieldInfo).SetValue(target, value);
        }
        else
        {
            throw new Exception("destinationMember must be either PropertyInfo or FieldInfo");
        }
    }

    public static T MemberValue<T>(this object source, string memberName)
    {
        return (T)source.GetType().GetMember(memberName)[0].Value(source);
    }
}
\$\endgroup\$
  • \$\begingroup\$ Is there a suitable *Contrib project? \$\endgroup\$ – Bill Barry Apr 26 '12 at 21:06
  • 1
    \$\begingroup\$ I would remove the DI library dependency, if it exists. Generally, reusable code has as few dependencies as possible. \$\endgroup\$ – Frank Hileman Mar 27 '15 at 21:52
2
\$\begingroup\$

In this object method I would change a couple of things

    private object Convert(Type convertToType)
    {
        object converted = null;

        if (this.Value == null)
        {
            return converted;
        }
        else if (convertToType.IsAssignableFrom(this.type))
        {
            converted = this.Value;
        }
        else
        {
            var conversionKey = Tuple.Create(this.type, convertToType);

            if (Conversions.ContainsKey(conversionKey))
            {
                converted = Conversions[conversionKey](this.Value);
            }
            else
            {
                throw new Exception(convertToType.Name + " is not assignable from " + this.type.Name);
            }
        }

        return converted;
    }

I would return directly and get rid of the Object variable.

private object Convert(Type convertToType)
{
    if (this.Value == null)
    {
        return null;
    }
    else if (convertToType.IsAssignableFrom(this.type))
    {
        return this.Value;
    }
    else
    {
        var conversionKey = Tuple.Create(this.type, convertToType);

        if (Conversions.ContainsKey(conversionKey))
        {
            return Conversions[conversionKey](this.Value);
        }
        else
        {
            throw new Exception(convertToType.Name + " is not assignable from " + this.type.Name);
        }
    }
    return null;
}

we were discussing this question in The Second Monitor and thought that it would be a good idea to turn this method into a Generic Method like so

private T Convert<T>(objectToConvert)
{
    if (this.Value == null)
    {
        return null;
    }
    else if (convertToType.IsAssignableFrom(this.type))
    {
        return this.Value;
    }
    else
    {
        var conversionKey = Tuple.Create(this.type, convertToType);

        if (Conversions.ContainsKey(conversionKey))
        {
            return Conversions[conversionKey](this.Value);
        }
        else
        {
            throw new Exception(convertToType.Name + " is not assignable from " + this.type.Name);
        }
    }
    return null;
}
\$\endgroup\$

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.