Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Customized Casting With ObjectCaster

5.00/5 (3 votes)
24 Apr 2018CPOL7 min read 10.7K   108  
Library that provides custom casting functionality from one type to another between properties with disparate names and types

Introduction

We've all been there. You have a Person object, for example, that needs to be casted to an Employee object. The problem: these two types do not implement a common interface or inherit from a common base object. Even worse, the properties on the Person object whose values are used to set properties on the Employee object don't all match in type or name. So, you end up writing "conversion" code that looks something like this:

C#
void DoStuff(Person person)
{
     Employee _employee = new Employee();
     _employee.ID = person.PersonId.ToString();  //int to string
     _employee.FirstName = person.FName;
     //Do stuff with Employee object
}

And, you have similar conversion code repeated throughout your application. The worst part about this, besides repeated code, is that your methods no longer implement a single behavior. They now have messy casting code (often somewhere in the middle) as well as code for the targeted behavior.

A quick search on CodeProject will find a host of libraries that will do conversions of this sort - but only when properties between the two objects have the same name and type. The ObjectCaster library bridges this gap and also provides for cleaner code by separating casting/data formatting concerns from business logic.

Basic Implementation

ObjectCaster works by using an implementation of the TypeCastBase<TFrom, TTo> base class in which implementers define property mappings between the two object types involved in the casting operation. Implementers are not required to define a mapping for every property - only the ones they care about. Callers then call the static ObjectCaster.Cast() method, passing in one or more TypeCastBase objects. Here's a basic example:

C#
//Entity classes
class Person
{
     public int PersonId { get; set; }
     public string FName { get; set; }
     public string LName { get; set; }
}

class Employee
{
     public int ID { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
}

//TypeCastBase implementation
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
     //Abstract method implementation
     public override void SetMappings()
     {
          Map(p => p.PersonId, e => e.ID);
          Map(p => p.FName, e => e.FirstName);
          Map(p => p.LName, e => e.LastName);
     }
}

//Calling code
class Client
{
    void DoStuff(Person person)
    {
         //Cast to employee
         Employee _emp = ObjectCaster.Cast<Employee>(person, new PersonToEmployeeCast());
         //Do stuff with employee object
    }
}

In the simple example noted above, we've merely moved our conversion code into a separate class by creating an implementation of TypeCastBase. We passed in a type parameter that specifies what we want to convert from (Person) and what we want to convert to (Employee). Then, we implemented the abstract method SetMappings(). In this method, properties from a Person object are mapped to properties on an Employee object by using the Map() method like this:

C#
Map(p => p.PersonId, e => e.ID);

which reads: map Person.PersonId property to Employee.ID property.

Mapping Properties with Incompatible Types

Let's modify our code so that the Employee.ID property is a string:

C#
class Employee
{
     public string ID { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
}

Now, we have to convert the int value Person.PersonId to the string value Employee.ID. This can be done using the OnCast property handler. So, we'll need to modify our PersonToEmployeeCast object:

C#
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
     //Abstract method implementation
     public override void SetMappings()
     {
          Map(p => p.PersonId, e => e.ID).SetCastOperation(Id_OnCast);
          Map(p => p.FName, e => e.FirstName);
          Map(p => p.LName, e => e.LastName);
     }
     
     void Id_OnCast(PropertyCastEventArgs e)
     {
          //Get Person.PersonId value
          int _personId = (int)e.FromValue;
          //Don't set To property if personId < 0
          e.DontHandle = personId <= 0;

          //Cast PersonId to string
          string _employeeId = _personId.ToString();
          //Set Employee.Id
          e.NewValue = _employeeId;
        }
     }
}

Note that the Map() call for PersonId now is suffixed with the code SetCastOperation(Id_OnCast). This causes the event handler, Id_OnCast, to be called during the casting operation. This event handler is passed a PropertyCastEventArgs which contains the property value being casted, e.FromValue. When OnCast is handled, the ObjectCaster object expects the event handler to tell it what value to set the "to" property to by setting the e.NewValue property. As you can tell, any kind of calculated value can be set in this event handler. Note: Every property mapping defined in SetMappings() can have its own OnCast event handler.

The PropertyCastEventArgs has two other properties, one not shown above: e.ToValue and e.DontHandle. In the example above, e.ToValue will contain the default value of Employee.ID - a null string. However, it is possible to use an existing object for the "to" object when calling ObjectCaster.Cast. In that case, e.ToValue will contain whatever Employee.ID was already set to prior to the Cast() call.

Set e.DontHandle to true if you don't want the ObjectCaster to set that property. In the above example, casting only happens if Person.PersonId > 0.

Deep Casting

This section will cover how to handle casting properties from one type to another. Let's modify our entities so that Person has a PersonAddress object and Employee has an EmployeeAddress object:

C#
class PersonAddress
{
     public int HouseNumber { get; set; }
     public string StreetName { get;set; }
}
class EmployeeAddress
{
     public int Number { get; set; }
     public string Street { get;set; }
}

class Person
{
     public int PersonId { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
     public PersonAddress Addrs { get; set; }
}
class Employee
{
     public string ID { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
     public EmployeeAddress Adress { get; set; }
}

In order to cause Person.Addrs to be casted to Employee.Address, we have to create another TypeCastBase object that defines the property mappings between PersonAddress and EmployeeAddress:

C#
PersonAddressToEmployeeAddressCast : TypeCastBase<PersonAddress,EmployeeAddress>
{
     public override SetMappings()
     {
           Map(pa => pa.HouseNumber, ea => ea.Number);
           Map(pa => pa.StreetName, ea => ea.Street);
     }
}

And now, our call to ObjectCaster.Cast() will change, as follows:

C#
class Client
{
  void DoStuff(Person person)
  {
       //Create cast collection
       List<TypeCastBase> _casts = new List<TypeCastBase>() 
           { new PersonToEmployeeCast(), new PersonAddressToEmployeeAddressCast() };
       //Cast to employee
       Employee _emp = ObjectCaster.Cast<Employee>(person, _casts);
       //Do stuff with employee object
  }
}

The ObjectCaster.Cast() method allows a collection of TypeCastBase objects to be passed to it. When this is done, every TypeCastBase object is potentially used for each level of casting. This is important to note: if PersonAddress had a Person property and EmployeeAddress had an Employee property, and they were both mapped to each other in the PersonAddressToEmployeeCast.SetMappings() method, the first TypeCastBase object we defined would be used to do this casting.

Custom Instantiation

Sometimes, you need to handle instantiating the "to" object instead of allowing the ObjectCaster to instantiate it for you. This is true in particular when the "to" object does not have a parameterless constructor or is an interface type: ObjectCaster cannot instantiate these types of objects. To do this, you have two options.

1. Handle the OnInstantiate Event

Let's modify our PersonToEmployeeCast to do this:

C#
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
     public PersonToEmployeeCast()
     {
          OnInstantiate = employee_instantiate;
     }
     void employee_instantiate(InstantiateEventArgs e)
     {
          //Set new object
          e.NewObject = new Employee();
     }
     //...
     //...
}

In the event handler, we set e.NewObject to the new object. ObjectCaster will then proceed with casting based on the mappings defined in the TypeCastBase object. The InstantiateEventArgs class also provides a FromObject property, which holds a reference to the From object, and a TToObject property, which is a Type object representing the type the "to" object is expected to be.

2. Pass a "to" Object to ObjectCaster.Cast()

Alternatively, you can use one of the Cast() overloads that allows an already instantiated "to" object to be passed to it, such as this overload:

C#
ObjectCaster.Cast<Person, Employee>(_person, new Employee(), _casts);

The above method could also be helpful if some of the properties on the Employee object are set elsewhere, before the ObjectCaster.Cast() method is reached.

Collections

Collection properties are any property that implements IEnumerable. These properties are handled in one of several ways:

  1. If OnCast is handled for the property mapping, the value of the two properties is set to whatever the value of the e.NewValue is set to.
  2. If OnCast is not handled and only one of the properties are an IEnumerable, an exception is thrown.
  3. If there is no TypeCastBase passed to Cast() that maps the From property's element type to the To property's element type, then:
    1. If the To property is writable and is assignable by the From property, the To property is set to the value of the From property (reference only).
    2. If the To property is writable and not assignable by the From property, or if the To property is not writable period, the To property must implement either ICollection or IList. If it doesn't, an exception will be thrown. If it does, the collection is cleared and each element from the From property is added to the To property.
  4. If there is a TypeCastBase passed to Cast() that maps the From property's element type to the To property's element type, then:
    1. If the To property does not implement either ICollection or IList, an exception is thrown.
    2. The To collection is cleared and each element from the From property is casted using the TypeCastBase and added to the To property. Note that, as usual, the full list of TypeCastBases passed to Cast() is potentially used when casting each of the enumerable's elements.

Additional Notes

For very simple implementations of OnCast, it is cleaner to use an anonymous delegate like this:

C#
public override void SetMappings()
{
     Map(p => p.PersonId, e => e.ID).SetCastOperation(e => e.NewValue = e.FromValue.ToString());
}

And, then, you don't have to implement the OnCast event handler.

Non-typed implementations are available for the TypeCastBase object. For example, the SetMappings() method also allows using property names to specify mappings by using the Map2() method:

C#
public override void SetMappings()
{
     Map2("FName", "FirstName");
}

But, if you are not going to use the strongly typed Map() method, you might want to consider using the non-generic TypeCastBase object from which the generic TypeCastBase object derives. Note that the ObjectCaster.Cast() will accept any implementation of the non-generic TypeCastBase. Here's an example:

C#
class personToEmployee : TypeCastBase
{
    public override Type FromType
    {
        get { return typeof(Person); }
    }

    public override Type ToType
    {
        get { return typeof(Employee); }
    }

    public override void SetMappings()
    {
        Map2("PersonId", "ID");
        Map2("FName", "FirstName");
        Map2("LName", "LastName");
    }
}

Note that the Map() method is not available on the non-generic TypeCastBase object; only the Map2() method is. Also, the FromType and ToType properties have to be implemented.

Conclusion

This library is somewhat slow because it uses reflection. However, I am in the process of building an extension that uses Expression objects instead, which will be significantly faster. One advantage of using Expression objects is that implementers will be able to do something like this in the future:

C#
MyTypeCast : TypeCastBase<Person,Employee>
{
     public override Map()
     {
          //Assume Employee now has a Number property
          Map(p => p.Addrs.HouseNumber, e => e.Number);
     }
}

In other words, they'll be able to specify a property that is several levels down and map to another property that is on any level. If Person.Addrs was null in the above example, Employee.Number would be set to its default value. Or, if one of the members specified in the To expression was null, the To property would not be set - unless that member had been instantiated based on one of the other Map() calls.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)