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; }
}
|
|
|
|
Thanks! Very helpful.
|
|
|
|
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.
|
|