DEV Community

Wei
Wei

Posted on

Trace Dapper.NET Source Code - Fast Efficiency Key: Cache Principle

Github Link : Trace-Dapper.NET-Source-Code


10. One of the keys to Dapper's fast efficiency: Cache principle

Why can Dapper be so fast?

I introduced the dynamic use of Emit IL to establish the ADO.NET Mapping method, but this function cannot make Dapper the king of lightweight ORM efficiency.

Because the dynamic create method is Cost and time consuming action, simply using it will slow down the speed. But when it cooperates with Cache, it is different. By storing the established method in Cache, you can use the 『Space for time』 concept to speed up the efficiency of the query .

Then trace the Dapper source code. This time, we need to pay special attention to Identity and GetCacheInfo under the QueryImpl method.

image

Identity、GetCacheInfo

Identity mainly encapsulates the comparison Key attribute of each cache:

  • sql: distinguish different SQL strings
  • type: distinguish Mapping type
  • commandType: Responsible for distinguishing different databases
  • gridIndex: Mainly used in QueryMultiple, explained later.
  • connectionString: Mainly distinguish the same database manufacturer but different DB situation
  • parametersType: Mainly distinguish parameter types
  • typeCount: Mainly used in Multi Query multi-mapping, it needs to be used with the override GetType method, which will be explained later

Then match the cache type used by Dapper in the GetCacheInfo method. When ConcurrentDictionary<Identity, CacheInfo> using the TryGetValue method, it will first compare the HashCode and then compare the Equals features, such as the image source code.

image

Using the Key type Identity to override Equals implement the cache comparison algorithm, you can see the following Dapper implementation logic. As long as one attribute is different, a new dynamic method and cache will be created.

public bool Equals(Identity other)
{
  if (ReferenceEquals(this, other)) return true;
  if (ReferenceEquals(other, null)) return false;

  int typeCount;
  return gridIndex == other.gridIndex
    && type == other.type
    && sql == other.sql
    && commandType == other.commandType
    && connectionStringComparer.Equals(connectionString, other.connectionString)
    && parametersType == other.parametersType
    && (typeCount = TypeCount) == other.TypeCount
    && (typeCount == 0 || TypesEqual(this, other, typeCount));
}
Enter fullscreen mode Exit fullscreen mode

With this concept, the previous Emit version is modified into a simple Cache Demo :

public class Identity
{
    public string sql { get; set; }
    public CommandType? commandType { get; set; }
    public string connectionString { get; set; }
    public Type type { get; set; }
    public Type parametersType { get; set; }
    public Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType)
    {
        this.sql = sql;
        this.commandType = commandType;
        this.connectionString = connectionString;
        this.type = type;
        this.parametersType = parametersType;
        unchecked
        {
            hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this
            hashCode = (hashCode * 23) + commandType.GetHashCode();
            hashCode = (hashCode * 23) + (sql?.GetHashCode() ?? 0);
            hashCode = (hashCode * 23) + (type?.GetHashCode() ?? 0);
            hashCode = (hashCode * 23) + (connectionString == null ? 0 : StringComparer.Ordinal.GetHashCode(connectionString));
            hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0);
        }
    }

    public readonly int hashCode;
    public override int GetHashCode() => hashCode;

    public override bool Equals(object obj) => Equals(obj as Identity);
    public bool Equals(Identity other)
    {
        if (ReferenceEquals(this, other)) return true;
        if (ReferenceEquals(other, null)) return false;

        return type == other.type
          && sql == other.sql
          && commandType == other.commandType
          && StringComparer.Ordinal.Equals(connectionString, other.connectionString)
          && parametersType == other.parametersType;
    }
}

public static class DemoExtension
{
    private static readonly Dictionary<Identity, Func<DbDataReader, object>> readers = new Dictionary<Identity, Func<DbDataReader, object>>();

    public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql, object param = null) where T : new()
    {
        using (var command = cnn.CreateCommand())
        {
            command.CommandText = sql;
            using (var reader = command.ExecuteReader())
            {
                var identity = new Identity(command.CommandText, command.CommandType, cnn.ConnectionString, typeof(T), param?.GetType());

                // 2. If the cache has data, use it, and if there is no data, create a method dynamically and save it in the cache 
                if (!readers.TryGetValue(identity, out Func<DbDataReader, object> func))
                {
                    //The dynamic creation method 
                    func = GetTypeDeserializerImpl(typeof(T), reader);
                    readers[identity] = func;
                    Console.WriteLine(" No cache, create a dynamic method and put it in the cache ");
                }
                else
                {
                    Console.WriteLine(" Use cache ");
                }


                // 3. Call the generated method by reader, read the data and return 
                while (reader.Read())
                {
                    var result = func(reader as DbDataReader);
                    yield return result is T ? (T)result : default(T);
                }
            }

        }
    }

    private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
    {
        // ..
    }
}
Enter fullscreen mode Exit fullscreen mode

image

Top comments (0)