DapperAOT

Generated Code

The code generated by Dapper.AOT can be viewed in Visual Studio in the solution explorer, by expanding:

You can double-click on this .cs file to see the contents. We’ll use the code from Getting Started to illustrate.

The code is broken down into three main pieces:

Interceptors

Interceptors are how Dapper.AOT changes your code; for example, in response to the source code (the bit you write):

public static Product GetProduct(DbConnection connection, int productId) => connection.QueryFirst<Product>(
    "select * from Production.Product where ProductId=@productId", new { productId });

we see, in the generated file:

[global::System.Runtime.CompilerServices.InterceptsLocationAttribute("C:\\Code\\DapperAOT\\test\\UsageLinker\\Product.cs", 14, 92)]
internal static global::UsageLinker.Product QueryFirst1(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType)
{
    // Query, TypedResult, HasParameters, SingleRow, Text, AtLeastOne, BindResultsByName, KnownParameters
    // takes parameter: <anonymous type: int productId>
    // parameter map: productId
    // returns data: global::UsageLinker.Product
    global::System.Diagnostics.Debug.Assert(!string.IsNullOrWhiteSpace(sql));
    global::System.Diagnostics.Debug.Assert((commandType ?? global::Dapper.DapperAotExtensions.GetCommandType(sql)) == global::System.Data.CommandType.Text);
    global::System.Diagnostics.Debug.Assert(param is not null);

    return global::Dapper.DapperAotExtensions.Command(cnn, transaction, sql, global::System.Data.CommandType.Text, commandTimeout.GetValueOrDefault(), CommandFactory0.Instance).QueryFirst(param, RowFactory0.Instance);

}

The [InterceptsLocation(...)] usage here tells the build SDK to “intercept” the method call in the specified file/line/column (.QueryFirst<Product>(...) in our case); this means that instead of calling Dapper’s QueryFirst<T> method, we actually call this generated code (it needs to have the same signature, note). The generated code uses a new API - DapperAotExtensions.Command, which works similarly to Dapper, but note that the generated code passes in CommandFactory0.Instance and RowFactory0.Instance - this is our command factory and row factory for this method.

Row Factories

A row factory is the code that deals with materializing results - in this case, creating a Product from the DbDataReader that ADO.NET provides, pushing data into the appropriate Product fields/properties. We see:

private sealed class RowFactory0 : global::Dapper.RowFactory<global::UsageLinker.Product>
{
    internal static readonly RowFactory0 Instance = new();
    private RowFactory0() {}
    public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span<int> tokens, int columnOffset)
    {
        for (int i = 0; i < tokens.Length; i++)
        {
            int token = -1;
            var name = reader.GetName(columnOffset);
            var type = reader.GetFieldType(columnOffset);
            switch (NormalizedHash(name))
            {
                case 2521315361U when NormalizedEquals(name, "productid"):
                    token = type == typeof(int) ? 0 : 25; // two tokens for right-typed and type-flexible
                    break;
                case 2369371622U when NormalizedEquals(name, "name"):
                    token = type == typeof(string) ? 1 : 26;
                    break;
                // snip, more columns here
            }
            tokens[i] = token;
            columnOffset++;

        }
        return null;
    }
    public override global::UsageLinker.Product Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan<int> tokens, int columnOffset, object? state)
    {
        global::UsageLinker.Product result = new();
        foreach (var token in tokens)
        {
            switch (token)
            {
                case 0:
                    result.ProductID = reader.GetInt32(columnOffset);
                    break;
                case 25:
                    result.ProductID = GetValue<int>(reader, columnOffset);
                    break;
                case 1:
                    result.Name = reader.GetString(columnOffset);
                    break;
                case 26:
                    result.Name = GetValue<string>(reader, columnOffset);
                    break;
                // snip, more columns here
            }
            columnOffset++;

        }
        return result;

    }

}

There are two fundamental operations Dapper.AOT uses for parsing rows:

Command Factories

A command factory is responsible for preparing a command for use with ADO.NET; most of the work here happens behind the scenes, so the generated code usually just has to handle parameters; we can see:

private sealed class CommandFactory0 : global::Dapper.CommandFactory<object?> // <anonymous type: int productId>
{
    internal static readonly CommandFactory0 Instance = new();
    public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args)
    {
        var typed = Cast(args, static () => new { productId = default(int) }); // expected shape
        var ps = cmd.Parameters;
        global::System.Data.Common.DbParameter p;
        p = cmd.CreateParameter();
        p.ParameterName = "productId";
        p.DbType = global::System.Data.DbType.Int32;
        p.Direction = global::System.Data.ParameterDirection.Input;
        p.Value = AsValue(typed.productId);
        ps.Add(p);

    }
    public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, object? args)
    {
        var typed = Cast(args, static () => new { productId = default(int) }); // expected shape
        var ps = cmd.Parameters;
        ps[0].Value = AsValue(typed.productId);

    }
    public override bool CanPrepare => true;

}

Our original source code used an anonymous type; anonymous types cannot be referenced directly, even in “interceptor” code, so instead in this case we generate a command factory that processes object?. We need to generate code that configures the ADO.NET parameters from this object (we might also have code for in-place updating of parameters, post-processing parameters, etc). The first thing we need to do is to use the Cast call to get the value back as the anonymous type, so that we can access the values. Then it adds them following the usual ADO.NET rules. Note that UnifiedCommand here is a Dapper.AOT device that provides a common API over the DbCommand and DbBatchCommand APIs, so that this one method can work for both regular commands and the new “batch” command API.

Final notes

The generated code isn’t scary. It might be lengthy for large projects, but each part is fairly simple and looks more or less like hand-written ADO.NET code; even the specialized hashing of the column names is clear, since we always need to check the actual column name (because of the risk of hash collisions). Depending on your exact usage, there may be some additional pieces that we haven’t explored here, but the intent is usually fairly clear (and the generated code contains explanatory comments, as shown).

The code looks a little unusual because it eschews using directives, instead preferring to fully-qualify types (global::System.Data.IDbConnection etc). This is because when dealing with arbitrary user code it is impossible to rule out the chance of conflicts and ambiguities with user types. When generating code, using fully qualified syntax simply makes a lot of sense.

Using this approach:

  1. you don’t need to change your code; your existing Dapper code works, but now with AOT
  2. we don’t need to generate any code at runtime - all the code to handle queries, rows and parameters: exists
  3. we don’t need a strategy cache; each call knows in advance which row factories and command factories will be used
  4. linkers and trimmers can see all the code that is being used, including code that isn’t needed by your application code but which is used by the generated Dapper.AOT code
  5. consumers can have confidence that nothing nefarious is going on in the generated code