Dapper.AOT Documentation
repository·main·Indexed 19 days ago
https://github.com/dapperlib/dapperaotA build-time code generation tool for ADO.NET that optimizes Dapper usage via Ahead-of-Time (AOT) code generation. It uses .NET 8+ interceptors to replace reflection-based Dapper calls with high-performance generated code, improving cold-start performance and compatibility with linkers and trimmers. The project includes Dapper.AOT for full optimization and Dapper.Advisor for usage guidance and analysis. Key features include support for named tuple types, bulk inserts via TypeAccessor, factory methods, and the DbBatch API.
What's inside Dapper.AOT
- Dapper.AOT is a set of build-time tools designed to optimize Dapper usage. It uses 'interceptors' to weave AOT (Ahead-of-Time) implementations into your existing Dapper code. This process replaces standard Dapper calls—which rely on reflection and ref-emit—with high-performance code that bypasses those mechanisms. Additionally, it provides usage guidance to help improve how Dapper is utilized in your project.
Overview of Dapper.AOT
mainDapper.AOT is a tool designed to simplify working with ADO.NET by generating the necessary code for command preparation, invocation, and result parsing during the build process. It is intended for developers who want to execute SQL directly without the overhead and ceremony of a full ORM (like EF or LLBLGenPro), while avoiding the manual, error-prone boilerplate typically associated with raw ADO.NET.SQL Server (TSQL) analysis limitations in Dapper.Advisor
mainThe advanced SQL analysis tools in Dapper.Advisor are currently limited to SQL Server (TSQL). The tool identifies the connection type viaSqlConnectionto trigger these specific analysis capabilities.Handle datepart tokens in SQL functions
mainWhen using T-SQL date functions like
DATEADDorDATEPART, the first argument must be a datepart token (e.g.,year,month,day).Important Constraint: These datepart tokens are special literal values. They cannot be parameterized using standard SQL parameters (e.g.,
@param) and they cannot be derived from a column value. They must be provided as hardcoded tokens within the SQL string.-- Correct usage: token is a literal DATEPART(year, GETDATE()) -- Incorrect usage: tokens cannot be parameterized -- DATEPART(@datepart, GETDATE()) -- Incorrect usage: tokens cannot be column values -- DATEPART(my_column, GETDATE())Avoid using types in the Dapper.AOT.Internal namespace
mainTheDapper.AOT.Internalnamespace contains types that are required for the generated AOT (Ahead-of-Time) code to function correctly. However, these types are not intended for direct consumption by application developers. Using them in your user code may lead to breaking changes in future updates. All types in this namespace are marked with the[Obsolete]attribute to discourage usage.Understand null literal behavior in bitwise/operator expressions
mainIn
dapperaot, operations involving anullliteral do not always yieldnull. While most operations involving anullvalue (from a parameter or local variable) evaluate tonullas expected, there are specific exceptions when using null literals directly in expressions.Specifically, the following patterns involving null literals may yield results other than
null:null >> 2(Right shift with null literal)2 >> null(Right shift with null literal)null & null(Bitwise AND with null literals, though this typically requires at least one operand to be something other than a 'hard' null to behave predictably).
Note: This behavior applies specifically to null literals. If a variable or parameter contains a
nullvalue, the expression will evaluate tonullfollowing standard expectations.// Exceptions apply to literals: var result1 = null >> 2; var result2 = 2 >> null; var result3 = null & null; // Note: behavior varies if operands are not 'hard' nulls // Standard behavior applies to null values in variables: int? myValue = null; var result4 = myValue >> 2; // Evaluates to nullDifference between Dapper.Advisor and Dapper.AOT
mainDapper provides two distinct tools depending on your requirements:
- Dapper.Advisor: Provides guidance and analysis on your Dapper usage (via analyzers) without changing how the code executes. It works with both C# and VB.
- Dapper.AOT: Includes everything in
Dapper.Advisorbut adds build-time code generation and runtime library code to enable AOT (Ahead-of-Time) compatibility. It works with C# only.
Use
Dapper.AOTif you need to run your code in environments that disallow runtime code generation or if you want to improve cold-start performance and compatibility with linkers/trimmers.Distinguish between scalar variables and table variables in SQL
mainWhen writing SQL, ensure you distinguish between scalar variables (which hold a single typed value) and table variables (which behave like temporary tables).
Scalar variables cannot be used in clauses that expect a table structure. You cannot use
INSERT,UPDATE,DELETE, orSELECT * FROMon a scalar variable. Instead, you must reference the variable directly in theSELECTlist to retrieve its value.-- Bad: Attempting to treat a scalar variable as a table declare @id int = 0; select * from @id; -- Good: Selecting the scalar variable directly declare @id int = 0; select @id;How Row Factories materialize results
mainA
RowFactoryis responsible for converting aDbDataReaderinto your target object (e.g., aProduct). It uses a two-step process to ensure high performance:Tokenize: This runs once per result set. It inspects the columns returned by the database and identifies how to handle each. It generates a set oftokensthat represent the mapping between database columns and object properties. For each column, it generates two paths: one for the ideal data type and one for type-flexible coercion.Read: This runs once per row. It iterates through the columns using the tokens provided byTokenizeto efficiently populate the object's fields or properties using theDbDataReader.
// Example RowFactory structure private sealed class RowFactory0 : global::Dapper.RowFactory<global::UsageLinker.Product> { public override object? Tokenize(global::System.Data.Common.DbDataReader reader, global::System.Span<int> tokens, int columnOffset) { // Identifies column types and assigns tokens } public override global::UsageLinker.Product Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan<int> tokens, int columnOffset, object? state) { // Uses tokens to populate the Product object } }Understand SQL variable scope and declaration behavior
mainSQL variables have a scope that extends to "anywhere later in the code." This leads to two important behaviors:
No block-level re-declaration: You cannot re-declare a local variable inside a conditional branch (like an
IF...ELSEblock) if it would conflict with the outer scope or if you are attempting to create a scoped version of a variable.Assignment vs. Declaration: While you cannot re-declare a variable inside a branch, you can assign a value to an existing variable declared earlier in the batch from within a branch.
Invalid: Re-declaring in branches
if -- some test begin declare @id int -- more code end else begin declare @id int -- more code endValid: Assigning to a previously declared variable
declare @id int; if -- some test begin set @id = 1; end else begin set @id = 2; end -- @id is accessible here as well select @id;How Dapper.AOT works via Interceptors
mainDapper.AOT leverages the .NET 8+ build SDK feature called "interceptors". During the build process, interceptors detect specific Dapper method calls (e.g.,QuerySingleOrDefault<Customer>(...)) and redirect them to generated methods that have compatible signatures. These replacement methods are generated entirely at build time, allowing for Ahead-of-Time (AOT) compatibility without changing your existing source code.Correctly using SQL table-variables
mainWhen working with SQL table-variables, do not attempt to treat them as scalar variables. You cannot use
SETorSELECTfor direct assignment to the variable itself, nor can youSELECTthe variable as if it were a single value. Instead, you must treat the table-variable as a data source and use theFROMclause to select specific columns from it.Incorrect pattern: Attempting to select the variable directly. Correct pattern: Selecting specific columns
FROMthe table-variable.-- Bad: Treating a table-variable like a scalar declare @t table (Id int not null); insert @t (Id) values (42); select @t; -- Good: Selecting columns FROM the table-variable declare @t table (Id int not null); insert @t (Id) values (42); select Id from @t;