ViewModel / Entity bidirection convention

Oct 1, 2011 at 6:47 PM
Edited Oct 1, 2011 at 10:49 PM

Thanks to the great work done on Value Injector and allowing it to be so extensible I've come up with a convention which allows for bi-directional auto mapping between ViewModels and Entities. Hopefully this will help others in need of a similar solution.

Here are some key points it offers.

1. Supports complex nested types & collections... i.e. Customer -> IList<Project> -> Project

2. Handles recursive objects... i.e.  Customer -> Project -> Customer

3. Handles add/update/delete of child objects of a collection by means of a Interface.

Here's the convention:

 

   public class CloneInjection : ConventionInjection
    {
        private List<ObjectHistory> _createdObjects = null;

        public CloneInjection()
        {
            _createdObjects = new List<ObjectHistory>();
        }

        private class ObjectHistory
        {
            public IReadOnlyIdentifier Source { get; set; }

            public IReadOnlyIdentifier Target { get; set; }

            public ObjectHistory(IReadOnlyIdentifier source, IReadOnlyIdentifier target)
            {
                this.Source = source;
                this.Target = target;
            }

            public Guid SourceId { get { return this.Source.Id; } }

            public Guid TargetId { get { return this.Target.Id; } }

            public Type SourceType { get { return this.Source.GetType(); } }
        }

        protected override bool Match(ConventionInfo c)
        {
            bool propertyMatch = c.SourceProp.Name == c.TargetProp.Name;
            bool sourceNotNull = c.SourceProp.Value != null;

            bool targetPropertyIdWritable = true;

            if (propertyMatch && c.TargetProp.Name == "Id" && !(c.Target.Value is IIdentifier))
                targetPropertyIdWritable = false;

            return propertyMatch && sourceNotNull && targetPropertyIdWritable;
        }

        private void AddObjectHistory(object source, object target)
        {
            if (source is IReadOnlyIdentifier && target is IReadOnlyIdentifier)
            {
                IReadOnlyIdentifier actualSource = source as IReadOnlyIdentifier;
                IReadOnlyIdentifier actualTarget = target as IReadOnlyIdentifier;

                if (!this.Exist(actualSource))
                    _createdObjects.Add(new ObjectHistory(actualSource, actualTarget));
            }
        }

        private bool Exist(IReadOnlyIdentifier source)
        {
            return this.FindHistoryObject(source) != null;
        }

        private ObjectHistory FindHistoryObject(IReadOnlyIdentifier source)
        {
            ObjectHistory history = null;

            if (source.Id != Guid.Empty) // Find by id first if exist.
            {
                history = _createdObjects.FirstOrDefault(o => o.SourceId == source.Id);
            }
            else // If the Id is empty then use equality comparision.
            {
                // Attempt to find by object equality (GetHashCode).
                history = _createdObjects.FirstOrDefault(o => o.Source.GetHashCode() == source.GetHashCode());
            }

            return history;
        }

        private object GetTargetFromHistory(object source)
        {
            object target = null;

            if (source != null && source is IReadOnlyIdentifier)
            {
                IReadOnlyIdentifier actualSource = source as IReadOnlyIdentifier;
                ObjectHistory history = this.FindHistoryObject(actualSource);

                if (history != null)
                    target = history.Target;
            }

            return target;
        }

        protected override object SetValue(ConventionInfo c)
        {
            this.AddObjectHistory(c.Source.Value, c.Target.Value);

            //for value types and string just return the value as is
            if (c.SourceProp.Type.IsValueType || c.SourceProp.Type == typeof(string))
                return c.SourceProp.Value;

            //handle arrays
            if (c.SourceProp.Type.IsArray)
            {
                var arr = c.SourceProp.Value as Array;
                var clone = arr.Clone() as Array;

                for (int index = 0; index < arr.Length; index++)
                {
                    var a = arr.GetValue(index);
                    if (a.GetType().IsValueType || a.GetType() == typeof(string)) continue;
                    clone.SetValue(Activator.CreateInstance(a.GetType()).InjectFrom(this, a), index);
                }
                return clone;
            }


            if (c.SourceProp.Type.IsGenericType)
            {
                //handle IEnumerable<> also ICollection<> IList<> List<>
                if (c.SourceProp.Type.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable)))
                {
                    Type targetChildType = c.TargetProp.Type.GetGenericArguments()[0];
                    if (targetChildType.IsValueType || targetChildType == typeof(string)) return c.SourceProp.Value;

                    return this.AddCollection(c, targetChildType);
                }

                //unhandled generic type, you could also return null or throw
                return c.SourceProp.Value;
            }

            //for simple object types create a new instace and apply the clone injection on it

            if (c.TargetProp.Type == typeof(System.Type))
                return c.SourceProp.Value;
            else
            {
                object target = this.GetTargetFromHistory(c.SourceProp.Value);

                if (target != null)
                    return target;
                else
                    return Activator.CreateInstance(c.TargetProp.Type)
                        .InjectFrom(this, c.SourceProp.Value);
            }
        }

        private object AddCollection(ConventionInfo c, Type targetChildType)
        {
            var list = c.TargetProp.Value;

            Type targetCollectionInterface = c.TargetProp.Type.GetInterface("ICollection`1");

            this.DeleteFromTargetCollection(c, targetCollectionInterface, targetChildType, list);
            this.AddOrUpdateTargetCollection(c, targetCollectionInterface, targetChildType, list);

            return list;
        }

        private void AddOrUpdateTargetCollection(ConventionInfo c, Type targetCollectionInterface, Type targetChildType, object list)
        {
            var addMethod = targetCollectionInterface.GetMethod("Add");
            foreach (IReadOnlyIdentifier sourceChild in c.SourceProp.Value as IEnumerable)
            {
                object child = null;
                bool found = this.FindInList(list, sourceChild.Id) != null;

                if (sourceChild.Id == Guid.Empty || !found)
                {
                    child = Activator.CreateInstance(targetChildType);
                    addMethod.Invoke(list, new[] { child });
                }
                else
                {
                    child = this.FindInList(list, sourceChild.Id);
                }

                child = child.InjectFrom(this, sourceChild);
            }
        }

        private void DeleteFromTargetCollection(ConventionInfo c, Type targetCollectionInterface, Type targetChildType, object list)
        {
            IEnumerable sourceList = c.SourceProp.Value as IEnumerable;

            List<object> childrenToDelete = new List<object>();

            var removeMethod = targetCollectionInterface.GetMethod("Remove");
            foreach (IReadOnlyIdentifier targetChild in list as IEnumerable)
            {
                bool found = this.FindInList(sourceList, targetChild.Id) != null;

                if (!found)
                    childrenToDelete.Add(targetChild);
            }

            foreach (object child in childrenToDelete)
                removeMethod.Invoke(list, new[] { child });
        }

        private object FindInList(object list, Guid id)
        {
            object child = null;

            foreach (IReadOnlyIdentifier current in list as IEnumerable)
            {
                if (current.Id == id)
                {
                    child = current;
                    break;
                }
            }

            return child;
        }
    }

 

The following are the interfaces your objects must implement. If your not fond of these interfaces, their easily changable...

 

 

    public interface IReadOnlyIdentifier
    {
        Guid Id { get; }
    }

 

 

    public interface IIdentifier : IReadOnlyIdentifier
    {
        Guid Id { get; set; }
    }
Feb 27, 2012 at 7:43 AM

Thanks! Very helpful.

Mar 15, 2012 at 5:29 AM

Would it be possible to post an example of how this is implemented? eg a test method with some objects that have collections.   I am having a little trouble getting it to work with collections of custom types so perhaps my setup is wrong.