DEV Community

loading...
Cover image for Trace Dapper.NET Source Code - Strongly Typed Mapping - Reflection version

Trace Dapper.NET Source Code - Strongly Typed Mapping - Reflection version

Wei
Web/.NET/Azure/Java/Feynman Technique enthusiast
・5 min read

Github Link : https://github.com/shps951023/Trace-Dapper.NET-Source-Code


4. Strongly Typed Mapping Part1: ADO.NET vs. Dapper

Next is the key function of Dapper, Strongly Typed Mapping. Because of the difficulty, it will be divided into multiple parts for explanation.

In the first part, compare ADO.NET DataReader GetItem By Index with Dapper Strongly Typed Query, check the difference between the IL and understand the main logic of Dapper Query Mapping.

With the logic, how to implement it, I use three techniques in order: Reflection、Expression、Emit implement three versions of the Query method from scratch to let readers understand gradually.

ADO.NET vs. Dapper

First use the following code to trace the Dapper Query logic

class Program
{
  static void Main(string[] args)
  {
    using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
    {
      var result = cn.Query<User>("select N'Wei' Name , 25 Age").First();
      Console.WriteLine(result.Name);
      Console.WriteLine(result.Age);
    }
  }
}

public  class  User
{
  public string Name { get; set; }
  public int Age { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Here we need to focus on the Dapper.SqlMapper.GenerateDeserializerFromMap method, it is responsible for the logic of Mapping, you can see that a large number of Emit IL technology is used inside.

20191004012713.png

To understand this IL logic, my way: "You should not go directly to the details, but check the complete IL first" As for how to view it, you need to prepare the il-visualizer open source tool first, which can view the IL generated by DynamicMethod at Runtime.

It supports vs 2015 and 2017 by default. If you use vs2019 like me

  1. Need to manually extract the %USERPROFILE%\Documents\Visual Studio 2019 path below
  2. .netstandard2.0 The project needs to be created netstandard2.0 and unzipped to this folder image

Finally reopen visaul studio and run debug, enter the GetTypeDeserializerImpl method, click the magnifying glass> IL visualizer> view the Runtime generated IL code for the DynamicMethod

image

The following IL can be obtained

IL_0000 : LDC . i4 .0    
IL_0001 : stloc .0     
IL_0002 : newobj      Void . ctor () / Demo . Customer 
IL_0007 : stloc .1     
IL_0008 : ldloc .1     
IL_0009 : after         
IL_000a : LDC . i4 .0    
IL_000b : stloc .0     
IL_000c : ldarg .0     
IL_000d : LDC . i4 .0   
IL_000e: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0013: dup        
IL_0014: stloc.2    
IL_0015: dup        
IL_0016: isinst     System.DBNull
IL_001b: brtrue.s   IL_0029
IL_001d: unbox.any  System.String
IL_0022: callvirt   Void set_Name(System.String)/Demo.User
IL_0027: br.s IL_002b
IL_0029: pop        
IL_002a: pop        
IL_002b: dup        
IL_002c: ldc.i4.1   
IL_002d: stloc.0    
IL_002e: ldarg. 0    
IL_002f: ldc.i4.1   
IL_0030: callvirt   System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0035: dup        
IL_0036: stloc.2    
IL_0037: dup        
IL_0038: isinst     System.DBNull
IL_003d: brtrue.s IL_004b
IL_003f: unbox.any  System.Int32
IL_0044: callvirt   Void set_Age(Int32)/Demo.User
IL_0049: br.s IL_004d
IL_004b: pop        
IL_004c: pop        
IL_004d: stloc.1    
IL_004e: leave IL_0060
IL_0053: ldloc.0    
IL_0054: ldarg. 0    
IL_0055: ldloc.2    
IL_0056: call       Void ThrowDataException(System.Exception, Int32, System.Data.IDataReader, System.Object)/Dapper.SqlMapper
IL_005b: leave IL_0060
IL_0060: ldloc.1    
IL_0061: ret        
Enter fullscreen mode Exit fullscreen mode

To understand this IL, you need to understand how it ADO.NET DataReader fast way to read data will be used GetItem By Index , such as the following code

public static class DemoExtension
{
  private static User CastToUser(this IDataReader reader)
  {
    var user = new User();
    var value = reader[0];
    if(!(value is System.DBNull))
      user.Name = (string)value;
    var value = reader[1];
    if(!(value is System.DBNull))
      user.Age = (int)value;      
    return user;
  }

  public static IEnumerable<User> Query<T>(this IDbConnection cnn, string sql)
  {
    if (cnn.State == ConnectionState.Closed) cnn.Open();
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
        while (reader.Read())
          yield return reader.CastToUser();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then look at the IL code generated by this Demo-CastToUser method

DemoExtension.CastToUser:
IL_0000:  nop         
IL_0001:  newobj      User..ctor
IL_0006:  stloc.0     // user
IL_0007:  ldarg.0     
IL_0008:  ldc.i4.0    
IL_0009:  callvirt    System.Data.IDataRecord.get_Item
IL_000E:  stloc.1     // value
IL_000F:  ldloc.1     // value
IL_0010:  isinst      System.DBNull
IL_0015:  ldnull      
IL_0016:  cgt.un      
IL_0018:  ldc.i4.0    
IL_0019:  ceq         
IL_001B:  stloc.2     
IL_001C:  ldloc.2     
IL_001D:  brfalse.s   IL_002C
IL_001F:  ldloc.0     // user
IL_0020:  ldloc.1     // value
IL_0021:  castclass   System.String
IL_0026:  callvirt    User.set_Name
IL_002B:  nop         
IL_002C:  ldarg.0     
IL_002D:  ldc.i4.1    
IL_002E:  callvirt    System.Data.IDataRecord.get_Item
IL_0033:  stloc.1     // value
IL_0034:  ldloc.1     // value
IL_0035:  isinst      System.DBNull
IL_003A:  ldnull      
IL_003B:  cgt.un      
IL_003D:  ldc.i4.0    
IL_003E:  ceq         
IL_0040:  stloc.3     
IL_0041:  ldloc.3     
IL_0042:  brfalse.s   IL_0051
IL_0044:  ldloc.0     // user
IL_0045:  ldloc.1     // value
IL_0046:  unbox.any   System.Int32
IL_004B:  callvirt    User.set_Age
IL_0050:  nop         
IL_0051:  ldloc.0     // user
IL_0052:  stloc.s     04 
IL_0054:  br.s        IL_0056
IL_0056:  ldloc.s     04 
IL_0058:  ret     
Enter fullscreen mode Exit fullscreen mode

It can be compared with the IL generated by Dapper shows that it is roughly the same (the differences will be explained later), which means that the logic and efficiency of the two operations will be similar, which is Dapper efficiency is close to the native ado.net the reasons why.

5. Strongly Typed Mapping Part2: Reflection version

In the previous ado.net Mapping example, we found a serious problem with there is no way to share multiple classes of methods, and each new class requires a code rewrite. To solve this problem, write a common method that does different logical processing for different classes during the Runtime.

There are three main implementation methods: Reflection, Expression, and Emit. Here, I will first introduce the simplest method: "Reflection". I will use reflection to simulate Query to write code from scratch to give readers a preliminary understanding of dynamic processing concepts. (If experienced readers can skip this article)

Logic:

  1. Use generics to pass dynamic class
  2. Use Generic constraints new() to create objects dynamically
  3. DataReader need to use attribute string name is used as Key , you can use Reflection to get the attribute name of the dynamic type and get the database data through the DataReader this[string parameter]
  4. Use PropertyInfo.SetValue to dynamically assign database data to objects

Finally got the following code:

public static class DemoExtension
{
  public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new()
  {
    using (var command = cnn.CreateCommand())
    {
      command.CommandText = sql;
      using (var reader = command.ExecuteReader())
        while (reader.Read())
          yield return reader.CastToType<T>();
    }
  }

  // 1. Use generics to pass dynamic class 
  private  static  T  CastToType < T >( this  IDataReader  reader ) where  T : new ()
  {
    // 2. Use `Generic constraints new()` to create objects dynamically
    var  instance  =  new  T ();

    // 3.DataReader need to use  `attribute string name is used as Key` , you can use Reflection to get the attribute name of the dynamic type and get the database data through the `DataReader this[string parameter]`
    var  type  =  typeof ( T );
     var  props  =  type . GetProperties ();
     foreach ( var  p  in  props )
    {
      var val = reader[p.Name];

      // 4. Use PropertyInfo.SetValue to dynamically assign database data to objects 
      if ( ! ( Val  is  System . DBNull ))  
         p . SetValue ( instance , val );
    }

    return instance;
  }
}
Enter fullscreen mode Exit fullscreen mode

The advantage of the Reflection version is that the code is simple, but it has the following problems

  1. The attribute query should not be repeated, and it should be ignored if it is not used. Example: If the class has N properties, SQL means to query 3 fields, and the ORM PropertyInfo foreach N times is not 3 times each time. And Dapper specially optimized this logic in Emit IL: 「Check how much you use, not waste」.
    image

  2. Efficiency issues:

  • The reflection efficiency will be slower. the solution will be introduced later: 「Key Cache + Dynamic Create Method」 exchange space for time.

  • Using the string Key value will call more GetOrdinal methods, you can check the official MSDN explanation its efficiency is worse than Index value .

image

Discussion (0)