65.9K
CodeProject is changing. Read more.
Home

Updating Business Models With Only Differing Property Values

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4 votes)

Mar 27, 2016

CPOL

4 min read

viewsIcon

10760

downloadIcon

97

An automated approach to updating business models intelligently, by updating a destination model with a source model only on properties whose values differ.

Introduction

Often times, we want to update an object with properties from another object, whether or not the object types are the same. Here is a quick, tested, low footprint project that allows you to update object properties with only those that differ between the source and the destination, without loading a giant one size fits all framework. In addition, the code allows you to specify whether default values are considered, and omission of properties.

Background

This very small project was devised while I was developing on a Web API project, allowing me to pass in partial objects, dynamics, and such in controllers.

Using the Code

A simple example would have us make two classes, we will call them TestClassOne and TestClassTwo. We will then call the IntelligentModelUpdater.ApplyOnlyChangedProperties method with the two instances we created. Keep in mind that the two classes share nothing in common in regard to interfaces or base types, also keep in mind that this is completely irrelevant in every case, the code ignores class signatures.

C#

    class Program
    {
        static void Main(string[] args)
        {
            TestClassOne objOne = new TestClassOne()
            {
                Age = 28,
                Name = "Jennifer"
            };

            TestClassTwo objTwo = new TestClassTwo()
            {
                Age = 34,
                Name = "Jennifer"
            };

            IntelligentModelUpdater.ApplyOnlyChangedProperties(objOne, objTwo);

            //assert our changes
            Console.WriteLine(string.Format("objOne and 
            objTwo had different age values, objTwo should now have an age of {0} it is now {1}",
                objOne.Age, objTwo.Age));

            Console.ReadLine();
        }
    }

    class TestClassOne
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }

    class TestClassTwo
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }

VB.NET

    Module Main

        Sub Main()
            Dim objOne As New TestClassOne() With {.Age = 28, .Name = "Jennifer"}
            Dim objTwo As New TestClassTwo() With {.Age = 34, .Name = "Jennifer"}

            IntelligentModelUpdater.ApplyOnlyChangedProperties(objOne, objTwo)

            'assert our changes
            Console.WriteLine(String.Format("objOne and objTwo had different age values, _
            objTwo should now have an age of {0} it is now {1}", objOne.Age, objTwo.Age))

            Console.ReadLine()

        End Sub

        Class TestClassOne
            Public Property Name() As String
                Get
                    Return m_Name
                End Get
                Set(value As String)
                    m_Name = value
                End Set
            End Property
            Private m_Name As String

            Public Property Age() As Integer
                Get
                    Return m_Age
                End Get
                Set(value As Integer)
                    m_Age = value
                End Set
            End Property
            Private m_Age As Integer
        End Class

        Class TestClassTwo
            Public Property Name() As String
                Get
                    Return m_Name
                End Get
                Set(value As String)
                    m_Name = value
                End Set
            End Property
            Private m_Name As String

            Public Property Age() As Integer
                Get
                    Return m_Age
                End Get
                Set(value As Integer)
                    m_Age = value
                End Set
            End Property
            Private m_Age As Integer
        End Class

    End Module

Running the prior code produces the following output:

Quote:

objOne and objTwo had different age values, objTwo should now have an age of 28 it is now 28

We can also alternatively replace objOne with a dynamic object, that specifies an Age property. Our program output would be the same, and a quick debug step through would indicate that the Name property on objTwo remains to be "Jennifer" since by default the code does not copy over default values.

C#

            dynamic objOne = new { Age = 28 };

            TestClassTwo objTwo = new TestClassTwo()
            {
                Age = 34,
                Name = "Jennifer"
            };

VB.NET

            Dim objOne = New With {.Age = 28}
            Dim objTwo As New TestClassTwo() With {.Age = 34, .Name = "Jennifer"}

So with those examples, you get the gest of the feature here. The IntelligentModelUpdater static class (Module in VB) contains the sole method plus overloads to perform this operation. It is not feature rich but I have included the ability to omit unwanted properties, whether or not to perform recursive changes when complex property types are involved, and whether or not to update the destination object with properties whose values are default.

Let's take a look at an example that uses recursive property comparison and property omission.

C#

        static void Main(string[] args)
        {
            Person objOne = new Person()
            {
                Age = 21,
                Name = "Tyler",
                Friends = new List<string>() { "Bill", "Tim", "Rick" },
                OwnedVehicle = new Car()
                {
                    Color = "Blue",
                    MilesDriven = 2000
                }
            };

            Person objTwo = new Person()
            {
                Age = 34,
                Name = "Ashley",
                Friends = new List<string>() { "Sarah", "Tyler", "Stephanie" },
                OwnedVehicle = new Car()
                {
                    Color = "Light Brown",
                    MilesDriven = 100000
                }
            };

            //update objTwo with objOne, but ignore the Age and Name properties.
            IntelligentModelUpdater.ApplyOnlyChangedProperties(objOne, objTwo, (x) => x.Age, (x) => x.Name);

            //assert our changes
            Console.WriteLine(string.Format("Assert that objOne and 
            objTwo have different ages. {0}", objOne.Age != objTwo.Age));
            Console.WriteLine(string.Format("Assert that objOne and 
            objTwo have different names. {0}", objOne.Name != objTwo.Name));

            Console.WriteLine(string.Format("Assert that objOne and objTwo share the same friends. {0}",
                objOne.Friends.Equals(objTwo.Friends)));

            Console.WriteLine(string.Format("Assert that objOne and 
            objTwo's OwnedVehicle complex property share the same properties. {0}",
                objOne.OwnedVehicle.Color == objTwo.OwnedVehicle.Color 
                && objOne.OwnedVehicle.MilesDriven == objTwo.OwnedVehicle.MilesDriven));

            Console.ReadLine();
        }

VB.NET

  Sub Main(args As String())

            Dim objOne As New Person() With { _
                .Age = 21, _
                .Name = "Tyler", _
                .Friends = New List(Of String)() From { _
                    "Bill", _
                    "Tim", _
                    "Rick" _
                }, _
                .OwnedVehicle = New Car() With { _
                    .Color = "Blue", _
                    .MilesDriven = 2000 _
                } _
            }

            Dim objTwo As New Person() With { _
                .Age = 34, _
                .Name = "Ashley", _
                .Friends = New List(Of String)() From { _
                    "Sarah", _
                    "Tyler", _
                    "Stephanie" _
                }, _
                .OwnedVehicle = New Car() With { _
                    .Color = "Light Brown", _
                    .MilesDriven = 100000 _
                } _
            }

            'update objTwo with objOne, but ignore the Age and Name properties.
            IntelligentModelUpdater.ApplyOnlyChangedProperties_
            (objOne, objTwo, Function(x) x.Age, Function(x) x.Name)

            'assert our changes
            Console.WriteLine(String.Format("Assert that objOne and _
            objTwo have different ages. {0}", objOne.Age <> objTwo.Age))
            Console.WriteLine(String.Format("Assert that objOne and _
            objTwo have different names. {0}", objOne.Name <> objTwo.Name))

            Console.WriteLine(String.Format("Assert that objOne and _
            objTwo share the same friends. {0}", objOne.Friends.Equals(objTwo.Friends)))

            Console.WriteLine(String.Format("Assert that objOne and _
            objTwo's OwnedVehicle complex property share the same properties. {0}", _
            objOne.OwnedVehicle.Color = objTwo.OwnedVehicle.Color _
            AndAlso objOne.OwnedVehicle.MilesDriven = objTwo.OwnedVehicle.MilesDriven))

            Console.ReadLine()
        End Sub

The prior code produces the following output:

Quote:

Assert that objOne and objTwo have different ages. True
Assert that objOne and objTwo have different names. True
Assert that objOne and objTwo share the same friends. True
Assert that objOne and objTwo's OwnedVehicle complex property share the same properties. True

An example showing the default behavior when handling default values.

C#

  class Program
    {
        static void Main(string[] args)
        {
            Task taskOne = new Task()
            {
                WasComplete = false,
                Employee = "Bridget",
                Manager = null,
                TaskCompletedComments = ""
            };

            Task taskTwo = new Task()
            {
                WasComplete = true,
                Manager = "Dan",
                TaskSize = 10,
                TaskCompletedComments = "Task completed but not reviewed."
            };

            IntelligentModelUpdater.ApplyOnlyChangedProperties(taskOne, taskTwo);

            //assert our changes
            Console.WriteLine("Assert that taskTwo.WasCompleted is false. {0}", taskTwo.WasComplete);
            Console.WriteLine("Assert that taskTwo.Employee is now the value 'Bridget'. {0}",
                taskTwo.Employee == "Bridget");

            Console.WriteLine("Assert that taskTwo.Manager is still 
            'Dan'. {0}", taskTwo.Manager == "Dan");
            Console.WriteLine("Assert that taskTwo.TaskSize remained 
            at the value of '10'. {0}", taskTwo.TaskSize == 10);
            Console.WriteLine("Assert that taskTow.TaskCompletedComments 
            remained at value 'Task completed but not reviewed.' {0}",
                taskTwo.TaskCompletedComments == "Task completed but not reviewed.");

            Console.ReadLine();
        }
    }

    class Task
    {
        public bool WasComplete { get; set; }

        public string Employee { get; set; }

        public string Manager { get; set; }

        [DefaultValue("")]
        public string TaskCompletedComments { get; set; }

        public int TaskSize { get; set; }
    }

VB.NET

 Module Main
        Sub Main(args As String())
            Dim taskOne As New Task() With { _
                .WasComplete = False, _
                .Employee = "Bridget", _
                .Manager = Nothing, _
                .TaskCompletedComments = "" _
            }

            Dim taskTwo As New Task() With { _
                .WasComplete = True, _
                .Manager = "Dan", _
                .TaskSize = 10, _
                .TaskCompletedComments = "Task completed but not reviewed." _
            }

            IntelligentModelUpdater.ApplyOnlyChangedProperties(taskOne, taskTwo)

            'assert our changes
            Console.WriteLine("Assert that taskTwo.WasCompleted is false. _
            {0}", taskTwo.WasComplete)
            Console.WriteLine("Assert that taskTwo.Employee is now the value _
            'Bridget'. {0}", taskTwo.Employee = "Bridget")

            Console.WriteLine("Assert that taskTwo.Manager is still _
            'Dan'. {0}", taskTwo.Manager = "Dan")
            Console.WriteLine("Assert that taskTwo.TaskSize remained at _
            the value of '10'. {0}", taskTwo.TaskSize = 10)
            Console.WriteLine("Assert that taskTow.TaskCompletedComments remained _
            at value 'Task completed but not reviewed.' {0}", _
            taskTwo.TaskCompletedComments = "Task completed but not reviewed.")

            Console.ReadLine()
        End Sub

        Class Task
            Public Property WasComplete() As Boolean
                Get
                    Return m_WasComplete
                End Get
                Set(value As Boolean)
                    m_WasComplete = value
                End Set
            End Property
            Private m_WasComplete As Boolean

            Public Property Employee() As String
                Get
                    Return m_Employee
                End Get
                Set(value As String)
                    m_Employee = value
                End Set
            End Property
            Private m_Employee As String

            Public Property Manager() As String
                Get
                    Return m_Manager
                End Get
                Set(value As String)
                    m_Manager = value
                End Set
            End Property
            Private m_Manager As String

            <DefaultValue("")> _
            Public Property TaskCompletedComments() As String
                Get
                    Return m_TaskCompletedComments
                End Get
                Set(value As String)
                    m_TaskCompletedComments = value
                End Set
            End Property
            Private m_TaskCompletedComments As String

            Public Property TaskSize() As Integer
                Get
                    Return m_TaskSize
                End Get
                Set(value As Integer)
                    m_TaskSize = value
                End Set
            End Property
            Private m_TaskSize As Integer
        End Class
    End Module

The prior code produces the following output:

Quote:

Assert that taskTwo.WasCompleted is false. True
Assert that taskTwo.Employee is now the value 'Bridget'. True
Assert that taskTwo.Manager is still 'Dan'. True
Assert that taskTwo.TaskSize remained at the value of '10'. True
Assert that taskTow.TaskCompletedComments remained at value 'Task completed but not reviewed.' True

Behavior Highlights

I've designed the code to fit the most IDEAL behavior for a given case. Should a behavior not be ideal for your purposes, take a dive in the source.

  • The destination object and source object need not have the same properties. However, there is an argument in the base method which when true will indicate to throw an exception should a source property not be present on the destination, but not the other way around.
  • Bools (bool in C#, boolean in VB) are not checked for default value when doing default value comparisons. This is because of the fundamental nature of the bool type.
  • When operating, the code does reflection everytime on both types passed, no type information is cached.
  • During the recursive operation, the following types are ignored for property searches: Value Types, Arrays, and anything that implements IEnumerable or IEnumerator. When these types are encountered, they are compared with object.Equals.
  • Regarding the last bullet: If you pass in any of the aforementioned types in the method call directly, the code attempts a property scan no matter the object type.
  • Omitted properties are always case sensitive if passed using the overload that accepts them as a string param array, this is in regards to VB.NET.

File Downloads

The included files are project files for both the VB version and C# version. The source files contain the last example and source for the IntelligentModelUpdater and utility class that is used to retrieve default values during runtime. The projects are currently configured to .NET Framework version 4.5.1.