views:

71

answers:

3

Hi guys, has anyone come across any scenario wherein you needed to merge one object with another object of same type, merging the complete object graph. for e.g. If i have a person object and one person object is having first name and other the last name, some way to merge both the objects into a single object.

public class Person
{
  public Int32 Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

public class MyClass
{
   //both instances refer to the same person, probably coming from different sources
   Person obj1 = new Person(); obj1.Id=1; obj1.FirstName = "Tiju";
   Person obj2 = new Person(); ojb2.Id=1; obj2.LastName = "John";


   //some way of merging both the object
   obj1.MergeObject(obj2); //??
   //obj1.Id // = 1
   //obj1.FirstName // = "Tiju"
   //obj1.LastName // = "John"
}

I had come across such type of requirement and I wrote an extension method to do the same.

    public static class ExtensionMethods
{
    private const string Key = "Id";



    public static IList MergeList(this IList source, IList target)
    {
        Dictionary itemData = new Dictionary();

        //fill the dictionary for existing list
        string temp = null;
        foreach (object item in source)
        {
            temp = GetKeyOfRecord(item);
            if (!String.IsNullOrEmpty(temp))
                itemData[temp] = item;
        }

        //if the same id exists, merge the object, otherwise add to the existing list.

        foreach (object item in target)
        {
            temp = GetKeyOfRecord(item);
            if (!String.IsNullOrEmpty(temp) && itemData.ContainsKey(temp))
                itemData[temp].MergeObject(item);
            else
                source.Add(item);
        }

        return source;
    }




    private static string GetKeyOfRecord(object o)
    {
        string keyValue = null;
        Type pointType = o.GetType();
        if (pointType != null)
            foreach (PropertyInfo propertyItem in pointType.GetProperties())
            {
                if (propertyItem.Name == Key)
                { keyValue = (string)propertyItem.GetValue(o, null); }
            }
        return keyValue;
    }




    public static object MergeObject(this object source, object target)
    {
        if (source != null && target != null)
        {
            Type typeSource = source.GetType();
            Type typeTarget = target.GetType();

            //if both types are same, try to merge
            if (typeSource != null && typeTarget != null && typeSource.FullName == typeTarget.FullName)
                if (typeSource.IsClass && !typeSource.Namespace.Equals("System", StringComparison.InvariantCulture))
                {
                    PropertyInfo[] propertyList = typeSource.GetProperties();

                    for (int index = 0; index < propertyList.Length; index++)
                    {
                        Type tempPropertySourceValueType = null;
                        object tempPropertySourceValue = null;
                        Type tempPropertyTargetValueType = null;
                        object tempPropertyTargetValue = null;

                        //get rid of indexers
                        if (propertyList[index].GetIndexParameters().Length == 0)
                        {
                            tempPropertySourceValue = propertyList[index].GetValue(source, null);
                            tempPropertyTargetValue = propertyList[index].GetValue(target, null);
                        }
                        if (tempPropertySourceValue != null)
                            tempPropertySourceValueType = tempPropertySourceValue.GetType();
                        if (tempPropertyTargetValue != null)
                            tempPropertyTargetValueType = tempPropertyTargetValue.GetType();



                        //if the property is a list
                        IList ilistSource = tempPropertySourceValue as IList;
                        IList ilistTarget = tempPropertyTargetValue as IList;
                        if (ilistSource != null || ilistTarget != null)
                        {
                            if (ilistSource != null)
                                ilistSource.MergeList(ilistTarget);
                            else
                                propertyList[index].SetValue(source, ilistTarget, null);
                        }

                        //if the property is a Dto
                        else if (tempPropertySourceValue != null || tempPropertyTargetValue != null)
                        {
                            if (tempPropertySourceValue != null)
                                tempPropertySourceValue.MergeObject(tempPropertyTargetValue);
                            else
                                propertyList[index].SetValue(source, tempPropertyTargetValue, null);
                        }
                    }
                }
        }
        return source;
    }


}

However, this works when the source property is null, if target has it, it will copy that to source. IT can still be improved to merge when inconsistencies are there e.g. if FirstName="Tiju" and FirstName="John"

Any commments appreciated.

Thanks TJ

A: 

Your code looks very yucky. Are you sure you need such a generic model? Are you planning to merge more than Person objects alone? And if so, are the merge requirements exactly the same? If you need to merge other types, changes are that they have to be merged in a different way. Perhaps you should choose a design without reflection. Here is an other idea:

public interface IMergable<T>
{
    T MergeWith(T other);
}

public interface IEntity
{
    object EntityId { get; }
}

public class Person : IMergable<Person>, IEntity
{
    public int Id { get; set; }

    object IEntity.EntityId { get { return this.Id; } }

    public Person MergeWith(Person other)
    {
        var mergedperson = new Person();

        // Do merging here, and throw InvalidOperationException
        // when objects  can not be merged.

        return mergedPerson;
    }
}

Yur extension method would look something like this:

public static IEnumerable<T> MergeList<T>(this IEnumerable<T> left,
    IEnumerable<T> right)
    where T : IMergable<T>, IEntity
{
    return
       from leftEntity in left
       from rightEntity in right
       where leftEntity.EntityId.Equals(rightEntity.EntityId)
       select leftEntity.MergeWith(rightEntity);
}
Steven
Nice design. Is there a reason `IEntity.EntityId` is of type object and not int? I guess this won't work when the underlying type actually is a value type, because `leftEntity.EntityId == rightEntity.EntityId` is a comparison of the boxed value references that would always evaluate to false.
0xA3
@0xA3: good point. I fixed this. I changed that line to `leftEntity.EntityId.Compare(rightEntity.EntityId)`. The reason why `Entityid` is an object, because I didn't know if the OP's entities ids where always of type `int`. If that's the case, code will become a bit simpler of course.
Steven
You could make `EntityId` of type `IEquatable<T>` and use the `Equals()` method. Note that `object` doesn't have a `Compare()` method.
0xA3
Very sharp. I changed it to `Object.Equals`. That should work :-). Also nice note about `IEquatable<T>`.
Steven
pls see my post below
Tiju John
A: 

Nice approach Steven,

yes, i will be merging objects of the same type. Yes, i need a more generic model.

The reason being my architecture doesn't allow me inheritance relationships as these objects will be simple DTO's which are only lightweight objects just sent to the UI clint to bind to. so these kind of relationships is out of question.

This logic will be required at the client side and to be exact, my data will be coming from different sources, say one from system1, and another from system2, both will return same type object, with different values, i just want a way to merge both the objects so that the complete information can be shown to the client UI. There will not be any saves from UI back to the system. The code should be generic and should not contain any type specific merge logic. There is no problem when one property is null in one object and not in another. the problem arises when both will have different values of same properties.

for e.g. one source says person lives in USA and other says person lives in France. what to do in such scenario. although this is basically a question to the BA, but maybe some kind of credibility index for each source, mechanism may work.

If Type has a list of other Dto's, then the MergeList will not only merge the matching records, it will also add non matching records to the list.

any comments on making the code nicer :-)

Tiju John
Does this mean that even implementing interfaces on these DTOs is out of the question? What about decorating them with an attribute, is that OK? Or are any changes out of the question? (Are they auto generated?) My provided `MergeList` method is a bit naive, it does a join. You also need records only in the left, and records only in the right?
Steven
you are right, no interfaces, and they are auto generated. I cannot use Attributes as at these DTO are created as proxies at client (my case silverlight) as these types are exposed through WCF web service. Correct me if i am wrong, I think .net specific information like attributes cannot be passed over to proxies. I want all possible records as long as they have different Key
Tiju John
A: 

Not a direct answer, but anonymous classes types are worth thinking of:

return new {
  FirstName = "Peter",
  LastName = "Pen"
};

For more information, this article nicely explains the feature; there's more on msdn and wikipedia.

To sum up:

  • They can only inherit from object,
  • their only members are private fields each with a matching read/write property.
modosansreves
can you please elaborate a bit..
Tiju John