使用 ObjectCaster 进行自定义类型转换





5.00/5 (3投票s)
提供从一种类型到另一种类型(属性名称和类型不同)的自定义类型转换功能的库。
引言
我们都遇到过这种情况。例如,您有一个 Person
对象,需要将其转换为 Employee
对象。问题是:这两种类型都没有实现共同的接口,也没有继承自共同的基对象。更糟糕的是,Person
对象上用于设置 Employee
对象属性的值的属性,它们的类型或名称并不完全匹配。因此,您最终会编写类似下面的“转换”代码
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
}
而且,您在应用程序中到处都有类似的转换代码。这方面最糟糕的,除了代码重复之外,是您的方法不再实现单一行为。它们现在有了混乱的类型转换代码(通常在中间),以及针对目标行为的代码。
在 CodeProject 上搜索一下,您会找到大量此类转换库,但它们仅在两个对象之间的属性名称和类型相同时才起作用。ObjectCaster
库弥合了这一差距,并通过将类型转换/数据格式化问题与业务逻辑分开,提供了更清晰的代码。
基本实现
ObjectCaster
的工作原理是使用 TypeCastBase<TFrom, TTo>
基类的实现,在其中实现者定义类型转换操作中涉及的两种对象类型之间的属性映射。实现者不需要为每个属性定义映射——只关心他们关心的属性。调用者然后调用 static ObjectCaster.Cast()
方法,传入一个或多个 TypeCastBase
对象。这是一个基本示例
//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
}
}
在上面提到的简单示例中,我们只是通过创建 TypeCastBase
的实现,将转换代码移到了一个单独的类中。我们传入了一个类型参数,指定了我们想从什么类型转换(Person
)以及想转换到什么类型(Employee
)。然后,我们实现了 abstract
方法 SetMappings()
。在此方法中,通过使用 Map()
方法将 Person
对象中的属性映射到 Employee
对象中的属性,如下所示
Map(p => p.PersonId, e => e.ID);
其含义是:将 Person.PersonId
属性映射到 Employee.ID
属性。
映射类型不兼容的属性
让我们修改代码,使 Employee.ID
属性为 string
类型
class Employee
{
public string ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
现在,我们需要将 int
类型的值 Person.PersonId
转换为 string
类型的值 Employee.ID
。这可以使用 OnCast
属性处理程序来完成。因此,我们需要修改我们的 PersonToEmployeeCast
对象
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;
}
}
}
请注意,PersonId
的 Map()
调用现在后面跟着代码 SetCastOperation(Id_OnCast)
。这会在类型转换操作期间调用事件处理程序 Id_OnCast
。此事件处理程序会收到一个 PropertyCastEventArgs
,其中包含正在转换的属性值 e.FromValue
。当处理 OnCast
时,ObjectCaster
对象期望事件处理程序通过设置 e.NewValue
属性来告诉它要设置“to
”属性的值。正如您所见,在此事件处理程序中可以设置任何类型的计算值。**注意**:在 SetMappings()
中定义的每个属性映射都可以有自己的 OnCast
事件处理程序。
PropertyCastEventArgs
还有另外两个属性,上面未显示:e.ToValue
和 e.DontHandle
。在上面的示例中,e.ToValue
将包含 Employee.ID
的默认值——一个 null string
。但是,在使用 ObjectCaster.Cast
时,可以使用一个现有对象作为“to
”对象。在这种情况下,e.ToValue
将包含在调用 Cast()
之前 Employee.ID
已设置为的任何值。
将 e.DontHandle 设置为 true
,如果您不希望 ObjectCaster
设置该属性。在上面的示例中,仅当 Person.PersonId > 0
时才发生类型转换。
深层类型转换
本节将介绍如何处理将一个类型的属性转换为另一个类型。让我们修改我们的实体,使 Person
包含一个 PersonAddress
对象,而 Employee
包含一个 EmployeeAddress
对象
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; }
}
为了将 Person.Addrs
转换为 Employee.Address
,我们需要创建另一个 TypeCastBase
对象,该对象定义 PersonAddress
和 EmployeeAddress
之间的属性映射
PersonAddressToEmployeeAddressCast : TypeCastBase<PersonAddress,EmployeeAddress>
{
public override SetMappings()
{
Map(pa => pa.HouseNumber, ea => ea.Number);
Map(pa => pa.StreetName, ea => ea.Street);
}
}
现在,我们对 ObjectCaster.Cast()
的调用将如下改变
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
}
}
ObjectCaster.Cast()
方法允许将 TypeCastBase
对象集合传递给它。执行此操作时,每个 TypeCastBase
对象都可能用于每一级类型转换。这一点很重要:如果 PersonAddress
有一个 Person
属性,而 EmployeeAddress
有一个 Employee
属性,并且它们都在 PersonAddressToEmployeeCast.SetMappings()
方法中相互映射,那么我们将使用第一个定义的 TypeCastBase
对象来执行此类型转换。
自定义实例化
有时,您需要处理“to
”对象的实例化,而不是让 ObjectCaster
为您实例化它。当“to
”对象没有无参数构造函数或是一个接口类型时,尤其如此:ObjectCaster
无法实例化这些类型的对象。为此,您有两种选择。
1. 处理 OnInstantiate 事件
让我们修改我们的 PersonToEmployeeCast
来实现这一点
class PersonToEmployeeCast : TypeCastBase<Person, Employee>
{
public PersonToEmployeeCast()
{
OnInstantiate = employee_instantiate;
}
void employee_instantiate(InstantiateEventArgs e)
{
//Set new object
e.NewObject = new Employee();
}
//...
//...
}
在事件处理程序中,我们将 e.NewObject
设置为新对象。然后,ObjectCaster
将基于 TypeCastBase
对象中定义的映射继续进行类型转换。InstantiateEventArgs
类还提供了一个 FromObject
属性,它保存对 From
对象的引用,以及一个 TToObject
属性,它是一个 Type
对象,表示“to
”对象预期是什么类型。
2. 将“to”对象传递给 ObjectCaster.Cast()
或者,您可以使用允许将已实例化的“to
”对象传递给它的 Cast()
重载之一,例如此重载
ObjectCaster.Cast<Person, Employee>(_person, new Employee(), _casts);
如果 Employee
对象上的某些属性在到达 ObjectCaster.Cast()
方法之前已在别处设置,则上述方法也会很有用。
集合
集合属性是指实现 IEnumerable
的任何属性。这些属性的处理方式如下
- 如果为属性映射处理了
OnCast
,则两个属性的值将被设置为e.NewValue
的值。 - 如果未处理
OnCast
且只有一个属性是IEnumerable
,则会抛出异常。 - 如果没有传递给
Cast()
的TypeCastBase
将From
属性的元素类型映射到To
属性的元素类型,那么- 如果
To
属性是可写的并且可以由From
属性赋值,则To
属性将设置为From
属性的值(仅引用)。 - 如果
To
属性是可写的但不能由From
属性赋值,或者To
属性根本不可写,则To
属性必须实现ICollection
或IList
。如果未实现,则会抛出异常。如果实现了,则清除集合并将From
属性的每个元素添加到To
属性。
- 如果
- 如果传递给
Cast()
的TypeCastBase
将From
属性的元素类型映射到To
属性的元素类型,那么- 如果
To
属性未实现ICollection
或IList
,则会抛出异常。 To
集合将被清除,并且From
属性的每个元素都将使用TypeCastBase
进行类型转换并添加到To
属性。请注意,像往常一样,传递给Cast()
的完整TypeCastBases
列表在转换可枚举项的每个元素时都可能被使用。
- 如果
其他说明
对于非常简单的 OnCast
实现,使用匿名委托会更简洁,如下所示
public override void SetMappings()
{
Map(p => p.PersonId, e => e.ID).SetCastOperation(e => e.NewValue = e.FromValue.ToString());
}
然后,您就不必实现 OnCast
事件处理程序了。
TypeCastBase
对象提供了非泛型实现。例如,SetMappings()
方法还允许使用属性名称来指定映射,方法是使用 Map2()
方法
public override void SetMappings()
{
Map2("FName", "FirstName");
}
但是,如果您不打算使用强类型 Map()
方法,则可以考虑使用非泛型 TypeCastBase
对象,泛型 TypeCastBase
对象是从它派生出来的。请注意,ObjectCaster
.Cast() 将接受任何非泛型 TypeCastBase
的实现。这是一个例子
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");
}
}
请注意,Map()
方法在非泛型 TypeCastBase
对象上不可用;只有 Map2()
方法可用。此外,还必须实现 FromType 和 ToType 属性。
结论
此库有些慢,因为它使用了反射。然而,我正在开发一个使用 Expression 对象的扩展,该扩展将大大提高速度。使用 Expression 对象的一个优点是,实现者将来可以执行类似的操作
MyTypeCast : TypeCastBase<Person,Employee>
{
public override Map()
{
//Assume Employee now has a Number property
Map(p => p.Addrs.HouseNumber, e => e.Number);
}
}
换句话说,他们将能够指定一个嵌套在多层之下的属性,并映射到任何级别的另一个属性。如果在上面的示例中 Person.Addrs
为 null
,则 Employee.Number
将设置为其默认值。或者,如果 To
表达式中指定的成员之一为 null
,则 To
属性将不会被设置——除非该成员已根据其他 Map()
调用进行了实例化。