C#:使用反射和自定义属性映射对象属性
我个人不太喜欢用特性(Attributes)和注解(Annotations)来装饰我的代码。大多数时候,我总觉得一定有更好的方法来完成我正在做的事情,或者觉得我某个地方的抽象已经出现了“泄露”。然而,有时,c
我个人不太喜欢用特性(Attributes)和注解(Annotations)来装饰我的代码。大多数时候,我总觉得一定有更好的方法来完成我正在做的事情,或者觉得我某个地方的抽象已经出现了“泄露”。
然而,有时,自定义特性(custom attributes)恰恰是解决问题的最佳工具,有时甚至是唯一实用的方法。
自定义特性的一个易于理解的用例可能是在数据访问层中将对象属性映射到数据库字段。在使用 Entity Framework 时,您无疑已经见过这种用法。在 EF 中,我们经常使用 System.ComponentModel.DataAnnotations 来装饰我们数据对象的属性。
图片,作者:Elizabeth Briel | 保留部分权利
在这里,我们将快速了解一下如何创建自己的自定义特性。
使用自定义特性提供提示或属性元数据
是的,EF 和 System.ComponentModel.DataAnnotations
命名空间提供了一种现成的实现方式,但首先,您可能会发现自己正在构建自己的数据访问层或工具;其次,这是一个易于理解的示例用例。
让我们看看如何实现我们自己的数据注解版本作为自定义特性。要在 C# 中创建自定义特性,我们只需创建一个继承自 System.Attribute
的类。例如,如果我们想实现自己的 [PrimaryKey]
特性来指示我们应用程序中某个类的特定属性代表数据库中的主键,我们可以创建以下自定义特性
public class PrimaryKeyAttribute : Attribute { }
现在,考虑我们应用程序中的一个类,即 Client
类。Client
类有一个 ClientId
属性,它对应于 Clients 数据库表中的主键
public class Client
{
public int ClientId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Email { get; set; }
}
我们可以简单地用我们新的特性来装饰 ClientId
属性,然后像下面的简单示例一样从代码中访问它
用自定义主键特性装饰 ClientId 属性
public class Client
{
[PrimaryKey]
public int ClientId { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Email { get; set; }
}
使用反射检查对象的属性和特性
然后,我们可以准备一个简单的控制台应用程序演示来查看它是如何工作的。首先,我们将采取更冗长的方式,使用 foreach
结构进行迭代,以便更清楚地了解发生了什么。然后我们将看看一个更简洁(也更有效)的基于 LINQ 的实现。
访问自定义属性的简单示例
static void WritePK<T>(T item) where T : new()
{
// Just grabbing this to get hold of the type name:
var type = item.GetType();
// Get the PropertyInfo object:
var properties = type.GetProperties();
Console.WriteLine("Finding PK for {0}", type.Name);
foreach(var property in properties)
{
var attributes = property.GetCustomAttributes(false);
foreach(var attribute in attributes)
{
if(attribute.GetType() == typeof(PrimaryKeyAttribute))
{
string msg = "The Primary Key for the {0} class is the {1} property";
Console.WriteLine(msg, type.Name, property.Name);
}
}
}
}
在上面的代码中,我们传入了一个泛型对象 T(这意味着,这个方法可以与任何域对象一起使用,以检查是否存在 [PrimaryKey]
特性)。我们首先使用 GetType()
方法获取对象的 Type
信息,然后调用 Type
实例的 GetProperties()
方法,它返回一个 PropertyInfo
对象数组。
接下来,我们遍历每个 PropertyInfo
实例,并调用 GetCustomAttributes()
方法,该方法将返回一个对象数组,代表该属性上找到的 CustomAttributes
。然后,我们可以检查每个 CustomAttribute
对象的类型,如果它是 PrimaryKeyAttribute
类型,我们就知道我们找到了一个代表我们数据库主键的属性。
LINQ 让一切更美好
我们可以使用 LINQ 重写上面的代码,以获得更紧凑、更有效的方法,如下所示
使用 LINQ 重写的 WritePk 方法
static void WritePK<T>(T item) where T : new()
{
var type = item.GetType();
var properties = type.GetProperties();
Console.WriteLine("Finding PK for {0}", type.Name);
// This replaces all the iteration above:
var property = properties
.FirstOrDefault(p => p.GetCustomAttributes(false)
.Any(a => a.GetType() == typeof(PrimaryKeyAttribute)));
if (property != null)
{
string msg = "The Primary Key for the {0} class is the {1} property";
Console.WriteLine(msg, type.Name, property.Name);
}
}
这个例子非常简单,但很好地说明了我们如何访问自定义特性来达到有用的目的。
我最近遇到的另一个例子是映射属性到数据库列。在创建通用数据访问工具时,您永远不知道数据库列将如何与您的域对象上的属性对齐。在我的例子中,我们需要动态构建一些 SQL,使用反射来获取对象属性,并映射到数据库。但是,数据库列名不一定与域对象上的属性名匹配。
在这种情况下,当列名与对象属性不同时,自定义特性是处理这种情况的一种方式(这是数据访问工具的抽象层被数据库侵入业务对象领域的一部分……)。
使用自定义特性映射属性到数据库列
前面的例子只是将自定义特性用作属性上的某种标签。特性也可以在需要时传递信息。让我们考虑一种将属性映射到特定数据库列名的方法。
同样,我们创建一个继承自 System.Attribute
的类,但这次我们将添加一个属性和一个构造函数
自定义 DbColumn 特性
public class DbColumnAttribute : Attribute
{
string Name { get; private set; }
public DbColumnAttribute(string name)
{
this.Name = name;
}
}
现在,假设您继承了一个需要集成到现有代码库中的数据库。将获取客户端信息的表使用全小写的列名,并在段之间使用下划线而不是驼峰命名法或 Pascal 命名法
列名与类属性不匹配的表 SQL
CREATE TABLE Clients (
client_id int IDENTITY(1,1) PRIMARY KEY NOT NULL,
last_name varchar(50) NOT NULL,
first_name varchar(50) NOT NULL,
email varchar(50) NOT NULL
);
现在您可以这样做
带有列名特性的 Client 类
public class Client
{
[PrimaryKey]
[DbColumn("client_id")]
public int ClientId { get; set; }
[DbColumn("last_name")]
public string LastName { get; set; }
[DbColumn("first_name")]
public string FirstName { get; set; }
[DbColumn("email")]
public string Email { get; set; }
}
您可以通过以下方式从代码中访问这些特性及其属性
从代码中读取自定义特性属性
static void WriteColumnMappings<T>(T item) where T : new()
{
// Just grabbing this to get hold of the type name:
var type = item.GetType();
// Get the PropertyInfo object:
var properties = item.GetType().GetProperties();
Console.WriteLine("Finding properties for {0} ...", type.Name);
foreach(var property in properties)
{
var attributes = property.GetCustomAttributes(false);
string msg = "the {0} property maps to the {1} database column";
var columnMapping = attributes
.FirstOrDefault(a => a.GetType() == typeof(DbColumnAttribute));
if(columnMapping != null)
{
var mapsto = columnMapping as DbColumnAttribute;
Console.WriteLine(msg, property.Name, mapsto.Name);
}
}
}
“但是 John,”你说,“Entity Framework 已经这样做了!”
没错。但现在您知道它是如何工作的。相信我,您可能并不总是有 EF 可用。此外,您会在“现实世界”的数据库中遇到列命名约定与 C# 类和属性命名约定不匹配的情况(在 PostgreSQL 数据库中工作五分钟,然后回来告诉我)。
在可能的情况下缓存使用反射调用的结果
一个简短的说明,虽然与上面的示例关系不大,但在实际应用程序设计中很重要。使用反射的调用可能会很昂贵。总的来说,现在的机器很快,并且通常情况下,偶尔调用 GetType()
和 GetCustomAttributes()
并不会那么重要。除非它们很重要。
例如,如果上面的代码在较大的应用程序上下文中被反复使用,那么最好在对象初始化(甚至在应用程序加载时)时遍历对象属性,并将每个对象的属性映射到其各自的列名,然后将它们全部存储在一个 Dictionary<string, string>
中。然后,在您的代码中需要映射的任何地方,您都可以通过将属性作为键来访问特定对象的名称。
如何以及在哪里执行此操作将很大程度上取决于您的操作以及应用程序的整体结构。有关我所说的内容的示例,请查看 Biggy 项目,我最近就不得不这样做。
自定义特性可以是架构上的权衡
我最近在做一个开源项目,项目维护者明智地指出,像上面这样的列映射特性是“数据库通过抽象层直接推挤上来”。这是真的。在将数据库列映射到对象属性的情况下,我们试图只解决所有对象关系映射(ORM)框架面临的年龄相仿的阻抗失配问题中的一个方面。它并不总是优雅的,但有时,它是唯一的方法。
这并不是说自定义特性仅在映射数据库列的上下文中才有用。有无数种潜在的用例。虽然我个人不喜欢用特性和注解来混乱我的代码,但有时这是解决问题的最佳方式。
下次您发现自己希望能够了解某个特定属性或方法的额外信息时,自定义特性是您工具箱中的又一个工具。
其他资源和感兴趣的项目
- ASP.NET MVC 5 Identity:实现基于组的权限管理
- ASP.NET MVC 5 Identity:扩展和修改角色
- ASP.NET MVC 中的路由基础
- C#:使用 LinqToExcel 查询 Excel 和 .CSV 文件
- C#:使用 DocX 以编程方式创建和操作 Word 文档