.NET Emit 入門教程:第七部分:實戰專案1:將 DbDataReader 轉實體

路过秋天發表於2024-04-29

前言:

經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的效能。

隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大夥分析或使用在專案中。

ORM 實現的三個通用階段:

第一階段:

在以往新手入門寫 ORM 實現的時候,往往會藉助程式碼生成器,來針對整個資料庫,生成一個一個的基礎增刪改查。

用程式碼生成器提前生成針對性的方法,執行效率高,但開發效率有可維護性低。

第二階段:

隨著對程式進一步的理解,可能會進化的使用反射來替代程式碼生成器,可以簡化掉大量的生成式程式碼。

但該方向正好相反,執行效率低,開發效率和可維護性高,透過對反射屬性加以快取,可以改善執行效率問題。

第三階段:

今天給出的專案示例是:

透過 Emit 實現 ORM 中常用的,透過 ADO.NET 的 DataReader 流讀取資料庫資料,並將其讀取到實體類 這一例子。

透過該方法,可以即有高的執行效率,同時又保持開發效率和可維護性。

下面看基礎示例:

示例程式碼:

以下示例程式碼,取自 CYQ.Data

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using CYQ.Data.Table;
using CYQ.Data.Tool;
using CYQ.Data.SQL;
using System.Data.Common;

namespace CYQ.Data.Emit
{
    /// <summary>
    /// DbDataReader 轉實體
    /// </summary>
    internal static partial class DbDataReaderToEntity
    {

        static Dictionary<Type, Func<DbDataReader, object>> typeFuncs = new Dictionary<Type, Func<DbDataReader, object>>();

        private static readonly object lockObj = new object();

        internal static Func<DbDataReader, object> Delegate(Type t)
        {
            if (typeFuncs.ContainsKey(t))
            {
                return typeFuncs[t];
            }
            lock (lockObj)
            {
                if (typeFuncs.ContainsKey(t))
                {
                    return typeFuncs[t];
                }
                DynamicMethod method = CreateDynamicMethod(t);
                var func = method.CreateDelegate(typeof(Func<DbDataReader, object>)) as Func<DbDataReader, object>;
                typeFuncs.Add(t, func);
                return func;
            }
        }

        /// <summary>
        /// 構建一個ORM實體轉換器(第1次構建有一定開銷時間)
        /// </summary>
        /// <param name="entityType">轉換的目標型別</param>
        private static DynamicMethod CreateDynamicMethod(Type entityType)
        {


            #region 建立動態方法

            var readerType = typeof(DbDataReader);
            Type convertToolType = typeof(ConvertTool);
            MethodInfo getValue = readerType.GetMethod("get_Item", BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(string) }, null);
            MethodInfo changeType = convertToolType.GetMethod("ChangeType", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(object), typeof(Type) }, null);
            MethodInfo getTypeFromHandle = typeof(Type).GetMethod("GetTypeFromHandle");

            DynamicMethod method = new DynamicMethod("DbDataReaderToEntity", typeof(object), new Type[] { readerType }, entityType);
            var constructor = entityType.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { }, null);

            ILGenerator gen = method.GetILGenerator();//開始編寫IL方法。
            if (constructor == null)
            {
                gen.Emit(OpCodes.Ret);
                return method;
            }
            var instance = gen.DeclareLocal(entityType);//0 : Entity t0;
            gen.DeclareLocal(typeof(object));//1 string s1;
            gen.DeclareLocal(typeof(Type));//2   Type t2;
            gen.Emit(OpCodes.Newobj, constructor);
            gen.Emit(OpCodes.Stloc_0, instance);//t0= new T();

            List<PropertyInfo> properties = ReflectTool.GetPropertyList(entityType);
            if (properties != null && properties.Count > 0)
            {
                foreach (var property in properties)
                {
                    SetValueByRow(gen, getValue, changeType, getTypeFromHandle, property, null);
                }
            }
            List<FieldInfo> fields = ReflectTool.GetFieldList(entityType);
            if (fields != null && fields.Count > 0)
            {
                foreach (var field in fields)
                {
                    SetValueByRow(gen, getValue, changeType, getTypeFromHandle, null, field);
                }
            }

            gen.Emit(OpCodes.Ldloc_0, instance);//t0 載入,準備返回
            gen.Emit(OpCodes.Ret);
            #endregion

            return method;
        }
        private static void SetValueByRow(ILGenerator gen, MethodInfo getValue, MethodInfo changeType, MethodInfo getTypeFromHandle, PropertyInfo pi, FieldInfo fi)
        {
            Type valueType = pi != null ? pi.PropertyType : fi.FieldType;
            string fieldName = pi != null ? pi.Name : fi.Name;

            Label labelContinue = gen.DefineLabel();//定義迴圈標籤;goto;

            gen.Emit(OpCodes.Ldarg_0);//載入 reader 物件
            gen.Emit(OpCodes.Ldstr, fieldName);//設定欄位名。
            gen.Emit(OpCodes.Callvirt, getValue);//reader.GetValue(...)
            gen.Emit(OpCodes.Stloc_1);//將索引 1 處的區域性變數載入到計算堆疊上。

            gen.Emit(OpCodes.Ldloc_1);//將索引 1 處的區域性變數載入到計算堆疊上。
            gen.Emit(OpCodes.Brfalse_S, labelContinue);//if(!a){continue;}

            //-------------新增:o=ConvertTool.ChangeType(o, t);
            if (valueType.Name != "Object")
            {
                gen.Emit(OpCodes.Ldtoken, valueType);//這個卡我卡的有點久。將後設資料標記轉換為其執行時表示形式,並將其推送到計算堆疊上。
                //下面這句Call,解決在 .net 中無法獲取Type值,拋的異常:嘗試讀取或寫入受保護的記憶體。這通常指示其他記憶體已損壞。
                gen.Emit(OpCodes.Call, getTypeFromHandle);
                gen.Emit(OpCodes.Stloc_2);

                gen.Emit(OpCodes.Ldloc_1);//o
                gen.Emit(OpCodes.Ldloc_2);
                gen.Emit(OpCodes.Call, changeType);//Call ChangeType(o,type);=> invoke(o,type) 呼叫由傳遞的方法說明符指示的方法。
                gen.Emit(OpCodes.Stloc_1); // o=GetItemValue(ordinal);
            }
            //-------------------------------------------
            SetValue(gen, pi, fi);
            gen.MarkLabel(labelContinue);//繼續下一個迴圈
        }

        private static void SetValue(ILGenerator gen, PropertyInfo pi, FieldInfo fi)
        {
            if (pi != null && pi.CanWrite)
            {
                gen.Emit(OpCodes.Ldloc_0);//實體物件obj
                gen.Emit(OpCodes.Ldloc_1);//屬性的值 objvalue
                EmitCastObj(gen, pi.PropertyType);//型別轉換
                gen.EmitCall(OpCodes.Callvirt, pi.GetSetMethod(), null); // Call the property setter
            }
            if (fi != null)
            {
                gen.Emit(OpCodes.Ldloc_0);//實體物件obj
                gen.Emit(OpCodes.Ldloc_1);//屬性的值 objvalue
                EmitCastObj(gen, fi.FieldType);//型別轉換
                gen.Emit(OpCodes.Stfld, fi);//對實體賦值 System.Object.FieldSetter(String typeName, String fieldName, Object val)
            }
        }
        private static void EmitCastObj(ILGenerator il, Type targetType)
        {
            if (targetType.IsValueType)
            {
                il.Emit(OpCodes.Unbox_Any, targetType);
            }
            else
            {
                il.Emit(OpCodes.Castclass, targetType);
            }
        }
    }

}

示例程式碼使用示例:

private static List<T> ReaderToListEntity<T>(DbDataReader reader)
{
    List<T> list = new List<T>();
    var func = DbDataReaderToEntity.Delegate(typeof(T));
    while (reader.Read())
    {
        object obj = func(reader);
        if (obj != null)
        {
            list.Add((T)obj);
        }
    }
    return list;
}

示例程式碼使用示例重點講解:

1、Emit 實現中,接收 DbDataReader 做為引數,它是各種 DataReader 的基類:

可以適應不同的資料庫型別,如果新手使用只是針對某一資料庫型別,也可以修改為:SqlDataReader 或 MySqlDataReader 等。

2、Emit 實現中,僅實現讀取當前行資料的功能,而讀取多行,是在外層封裝(即使用示例的封裝方法)實現:

這樣的好處是可以簡化 Emit 的部分實現,同時又保留高效的效能。

3、Emit 實現中,涉及到三個外部方法:

A:List<PropertyInfo> properties = ReflectTool.GetPropertyList(entityType);

該方法是 CYQ.Data 的內部的實現,以快取反射的屬性,可以用以下程式碼替代:

PropertyInfo[] pInfo = t.GetProperties();

B:List<FieldInfo> fields = ReflectTool.GetFieldList(entityType);

該方法是 CYQ.Data 的內部的實現,以快取反射的屬性,可以用以下程式碼替代:

FieldInfo[] pInfo = t.GetFields();

C:ConvertTool.ChangeType 方法:

方法原型如下,實現全品類型別的安全轉換:

public static object ChangeType(object value, Type t)

如果生成的實體類和資料庫型別保持一致,則可以不需要進行型別轉換,加型別轉換,是為了可以相容資料庫欄位型別和實體類屬性型別的不同。

該方法的高效實現,可以參考:ConverTool

總結:

Emit 雖然活躍在 ORM 和 動態代理的領域,但掌握它, 並在合適的場景使用它,則可以獲得更高效的解決方案。

當然,前提是你需要對程式 “效能” 有清晰的追求。

相關文章