65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2018年4月20日

CPOL

7分钟阅读

viewsIcon

11299

downloadIcon

177

提供从一种类型到另一种类型(属性名称和类型不同)的自定义类型转换功能的库。

引言

我们都遇到过这种情况。例如,您有一个 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;
        }
     }
}

请注意,PersonIdMap() 调用现在后面跟着代码 SetCastOperation(Id_OnCast)。这会在类型转换操作期间调用事件处理程序 Id_OnCast。此事件处理程序会收到一个 PropertyCastEventArgs,其中包含正在转换的属性值 e.FromValue。当处理 OnCast 时,ObjectCaster 对象期望事件处理程序通过设置 e.NewValue 属性来告诉它要设置“to”属性的值。正如您所见,在此事件处理程序中可以设置任何类型的计算值。**注意**:在 SetMappings() 中定义的每个属性映射都可以有自己的 OnCast 事件处理程序。

PropertyCastEventArgs 还有另外两个属性,上面未显示:e.ToValuee.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 对象,该对象定义 PersonAddressEmployeeAddress 之间的属性映射

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 的任何属性。这些属性的处理方式如下

  1. 如果为属性映射处理了 OnCast,则两个属性的值将被设置为 e.NewValue 的值。
  2. 如果未处理 OnCast 且只有一个属性是 IEnumerable,则会抛出异常。
  3. 如果没有传递给 Cast()TypeCastBaseFrom 属性的元素类型映射到 To 属性的元素类型,那么
    1. 如果 To 属性是可写的并且可以由 From 属性赋值,则 To 属性将设置为 From 属性的值(仅引用)。
    2. 如果 To 属性是可写的但不能由 From 属性赋值,或者 To 属性根本不可写,则 To 属性必须实现 ICollectionIList。如果未实现,则会抛出异常。如果实现了,则清除集合并将 From 属性的每个元素添加到 To 属性。
  4. 如果传递给 Cast()TypeCastBaseFrom 属性的元素类型映射到 To 属性的元素类型,那么
    1. 如果 To 属性未实现 ICollectionIList,则会抛出异常。
    2. 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.Addrsnull,则 Employee.Number 将设置为其默认值。或者,如果 To 表达式中指定的成员之一为 null,则 To 属性将不会被设置——除非该成员已根据其他 Map() 调用进行了实例化。

© . All rights reserved.