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:
void DoStuff(Person person)
{
Employee _employee = new Employee();
_employee.ID = person.PersonId.ToString();
_employee.FirstName = person.FName;
}
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:
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; }
}
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
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);
}
}
class Client
{
void DoStuff(Person person)
{
Employee _emp = ObjectCaster.Cast<Employee>(person, new PersonToEmployeeCast());
}
}
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:
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
:
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:
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
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)
{
int _personId = (int)e.FromValue;
e.DontHandle = personId <= 0;
string _employeeId = _personId.ToString();
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:
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
:
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:
class Client
{
void DoStuff(Person person)
{
List<TypeCastBase> _casts = new List<TypeCastBase>()
{ new PersonToEmployeeCast(), new PersonAddressToEmployeeAddressCast() };
Employee _emp = ObjectCaster.Cast<Employee>(person, _casts);
}
}
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:
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
public PersonToEmployeeCast()
{
OnInstantiate = employee_instantiate;
}
void employee_instantiate(InstantiateEventArgs e)
{
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:
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:
- 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. - If
OnCast
is not handled and only one of the properties are an IEnumerable
, an exception is thrown. - If there is no
TypeCastBase
passed to Cast()
that maps the From
property's element type to the To
property's element type, then:
- 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). - 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.
- If there is a
TypeCastBase
passed to Cast()
that maps the From
property's element type to the To
property's element type, then:
- If the
To
property does not implement either ICollection
or IList
, an exception is thrown. - 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:
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:
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:
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:
MyTypeCast : TypeCastBase<Person,Employee>
{
public override Map()
{
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.