PDF.NET开发框架一直是号称“无需反射”的,因为它的ORM框架(PDF.NET不仅仅是一个ORM框架,详细请见官网)中实体类的设计很特别,不需要反射就能够获知映射的字段信息,我们用实际的例子来说明下。
假设有这样一个数据库LocalDb中有一个表Table_User ,如下图:
图中的数据库用PDF.NET集成开发工具打开,该工具可以在官网找到下载地址。找到该表后,在左边的表名称树节点或者右边的查询窗口,鼠标右键菜单上,找到生成实体类的功能,具体过程这里不做演示了,因为这不是本文的主题。
下面,我们看看生成的实体类:
[Serializable()]
public partial class Table_User : EntityBase {
public Table_User()
{
TableName = "Table_User";
EntityMap = EntityMapType.Table;
//IdentityName = "标识字段名";
IdentityName = "UID";
//PrimaryKeys.Add("主键字段名");
PrimaryKeys.Add("UID");
}
protected override void SetFieldNames()
{
PropertyNames = new string[] { "UID", "Name", "Sex", "Height", "Birthday" };
}
/// <summary>
///
/// </summary>
public System.Int32 UID
{
get { return getProperty<System.Int32>("UID"); }
set { setProperty("UID", value); }
}
/// <summary>
///
/// </summary>
public System.String Name
{
get { return getProperty<System.String>("Name"); }
set { setProperty("Name", value, 50); }
}
/// <summary>
///
/// </summary>
public System.Boolean Sex
{
get { return getProperty<System.Boolean>("Sex"); }
set { setProperty("Sex", value); }
}
/// <summary>
///
/// </summary>
public System.Single Height
{
get { return getProperty<System.Single>("Height"); }
set { setProperty("Height", value); }
}
/// <summary>
///
/// </summary>
public System.DateTime Birthday
{
get { return getProperty<System.DateTime>("Birthday"); }
set { setProperty("Birthday", value); }
}
}
在实体类的构造函数中,下面几个属性指明了表的一些特性:
TableName = "Table_User"; 表示实体类映射的表名称;
EntityMap = EntityMapType.Table; 表示实体类的映射类型是一个表,当然还可以是视图、存储过程、函数等;
//IdentityName = "标识字段名";
IdentityName = "UID";
//PrimaryKeys.Add("主键字段名");
PrimaryKeys.Add("UID");
这个不用多说,有注释了。注意主键可以设置多个的。
protected override void SetFieldNames() 该方法说明了实体类映射的哪些字段。
public System.Int32 UID { get { return getProperty<System.Int32>("UID"); } set { setProperty("UID", value); } } UID属性的Get和Set方法也很简单,看名字就知道它的功能了。注意属性中映射了字段名称,比如数据库的字段是UID,那么属性改个名字,象下面这样写也是完全可以的:
public System.Int32 UserId { get { return getProperty<System.Int32>("UID"); } set { setProperty("UID", value); } }
因此,从总体上来说,PDF.NET实体类的结构很简单,比起EF的DbFirst方式和其它ORM框架的实体类来说,要简单很多,所以我一般情况下都是手写实体类,但是对于不是很熟悉框架的朋友来说,如果没有代码工具,要手写还是比较麻烦,毕竟属性的Get和Set访问器还是要多写一行代码。
如果我们将实体类先抽象出来一个接口,然后让框架根据该接口,自动继承EntityBase基类和实现接口的属性方法,那该多好啊!
PS:这个想法我已经想了好几年了,但总觉得不是很有必要。现在,CodeFirst越来越流行了,都是先定义实体类,然后在定义或者自动创建数据库。同样,PDF.NET的广大用户也要求能够更简单的使用框架,跟上时代潮流。所以,我最近才付诸实际行动。
我们用一点反射和一点Emit,来完成这个过程:
反射得到构造函数和属性定义:
//得到类型生成器
TypeBuilder typeBuilder = modBuilder.DefineType(newTypeName, newTypeAttribute, newTypeParent, newTypeInterfaces);
typeBuilder.AddInterfaceImplementation(targetType);
//定义构造函数
BuildConstructor(typeBuilder, newTypeParent, targetType.Name);
//以下将为新类型声明方法:新类型应该override基类型的所以virtual方法
PropertyInfo[] pis = targetType.GetProperties();
List<string> propertyNames = new List<string>();
foreach (PropertyInfo pi in pis)
{
propertyNames.Add(pi.Name);
//属性构造器
PropertyBuilder propBuilder = typeBuilder.DefineProperty(pi.Name,
System.Reflection.PropertyAttributes.HasDefault,
pi.PropertyType,
null);
MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final;
//构造Get访问器
MethodBuilder getPropMethodBuilder = typeBuilder.DefineMethod("get_" + pi.Name,
getSetAttr,
pi.PropertyType,
Type.EmptyTypes);
GeterIL(pi.Name, newTypeParent, pi.PropertyType, getPropMethodBuilder);
//构造Set访问器
MethodBuilder setPropMethodBuilder = typeBuilder.DefineMethod("set_" + pi.Name,
getSetAttr,
null,
new Type[] { pi.PropertyType });
SeterIL(pi.Name, newTypeParent, pi.PropertyType, setPropMethodBuilder);
//添加到属性构造器
propBuilder.SetGetMethod(getPropMethodBuilder);
propBuilder.SetSetMethod(setPropMethodBuilder);
}
MethodBuilder SetFieldNamesBuilder = typeBuilder.DefineMethod("SetFieldNames", MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig);
SetFieldNamesIL(newTypeParent, SetFieldNamesBuilder, propertyNames.ToArray());
//真正创建,并返回
Type resuleType=typeBuilder.CreateType();
Emit方式得到属性访问器的具体构造过程:
/// <summary>
/// 构造Get访问器
/// </summary>
/// <param name="propertyName"></param>
/// <param name="baseType"></param>
/// <param name="propertyType"></param>
/// <param name="methodBuilder"></param>
void GeterIL(string propertyName, Type baseType, Type propertyType, MethodBuilder methodBuilder)
{
MethodInfo getProperty = null;
MethodInfo[] ms = typeof(EntityBase).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);
foreach (MethodInfo info in ms)
{
if (info.Name == "getProperty" && info.IsGenericMethod)
{
getProperty = info;
break;
}
}
getProperty = getProperty.MakeGenericMethod(propertyType);
var ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldstr, propertyName);
ilGenerator.Emit(OpCodes.Call, getProperty);
ilGenerator.Emit(OpCodes.Ret);
}
/// <summary>
/// 构造Set访问器
/// </summary>
/// <param name="propertyName"></param>
/// <param name="baseType"></param>
/// <param name="propertyType"></param>
/// <param name="methodBuilder"></param>
void SeterIL(string propertyName, Type baseType, Type propertyType, MethodBuilder methodBuilder)
{
MethodInfo setProperty =null;//= baseType.GetMethod("setProperty", BindingFlags.Instance | BindingFlags.NonPublic);
MethodInfo[] ms = typeof(EntityBase).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);
foreach (MethodInfo info in ms)
{
if (info.Name == "setProperty" )
{
if (info.GetParameters().Length == 2)
{
setProperty = info;
break;
}
}
}
var ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldstr, propertyName);
ilGenerator.Emit(OpCodes.Ldarg_1);
//是否是值类型
if (propertyType.IsValueType)
ilGenerator.Emit(OpCodes.Box, propertyType);
ilGenerator.Emit(OpCodes.Call, setProperty);
ilGenerator.Emit(OpCodes.Ret);
}
在上面的IL代码方法中,EntityBase 的 getProperty 和setProperty 方法有泛型实现和重载,所以只有遍历实体类所有的方法。
写Emit代码也不是想象中的那么复杂,基本过程就是先手工写好C#代码,编译得到Exe或者Dll,然后用ILDASM或反编译工具,得到IL代码,最后就是看着IL代码,用Emit一个个对应发出代码,就行了。
OK,我们将这个代码封装到一个EntityBuilder类中,定一个构造实体类的方法
private static Dictionary<Type, Type> dictEntityType = new Dictionary<Type, Type>();
private static object sync_lock = new object();
/// <summary>
/// 根据接口类型,创建实体类的实例
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T CreateEntity<T>() where T:class
{
Type targetType = null;
Type sourceType = typeof(T);
if (sourceType.BaseType == typeof(EntityBase)) //如果本身是实体类,则不生成
{
targetType = sourceType;
}
else
{
if (!dictEntityType.TryGetValue(sourceType, out targetType))
{
lock (sync_lock)
{
if (!dictEntityType.TryGetValue(sourceType, out targetType))
{
EntityBuilder builder = new EntityBuilder(sourceType);
targetType = builder.Build();
dictEntityType[sourceType] = targetType;
}
}
}
}
T entity = (T)Activator.CreateInstance(targetType);
return entity;
}
万事俱备,只欠东风!
下面,我们将前面的实体类抽象出一个接口ITable_User :
public interface ITable_User
{
DateTime Birthday { get; set; }
float Height { get; set; }
string Name { get; set; }
bool Sex { get; set; }
int UID { get; set; }
}
再做一点准备工作,在应用程序配置文件里面配置一下连接,框架默认取最后一个配置:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="local"
connectionString="Data Source=.;Initial Catalog=LocalDB;Integrated Security=True"
providerName="SqlServer" />
</connectionStrings>
</configuration>
然后像下面这样使用实体类并查询:
static void TestDynamicEntity()
{
ITable_User user = EntityBuilder.CreateEntity<ITable_User>();
//如果接口的名称不是"ITableName" 这样的格式,那么需要调用 MapNewTableName方法指定
//((EntityBase)user).MapNewTableName("Table_User");
OQL qUser = OQL.From((EntityBase)user).Select(user.UID, user.Name, user.Sex).END;
List<ITable_User> users = EntityQuery.QueryList<ITable_User>(qUser, MyDB.Instance);
}
在代码中,只需要
EntityBuilder.CreateEntity<ITable_User>(); 这样的方式,定义一个实体类的接口,就自动创建了我们的实体类,是不是非常简单了?
有了实体类,然后可以像普通实体类那样来使用ORM查询语言--OQL,不过原来的EntityQuery泛型实体查询类得改进下,才可以支持“动态实体类”的查询。
当前功能已经在PDF.NET Ver 4.6.4.0525 版本实现,之前的版本,大家可以去开源项目下载:http://pwmis.codeplex.com
这里说的“动态实体类”是通过程序在运行时动态创建得到实体类,而不是预先在源码中写好的实体类。对本方案而言,使用动态实体类有以下几点约束:
如果你不想有这些约束,或者想灵活映射字段和属性,那么还是手写实体类吧,多写一行代码,象本文开头示例的那个实体类一样。
-----------------------------------------