-
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
176 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
# Generated Code | ||
|
||
The code generated by `Dapper.AOT` can be viewed in Visual Studio in the solution explorer, by expanding: | ||
|
||
- (your project) | ||
- Dependencies | ||
- Analyzers | ||
- Dapper.AOT.Analyzers | ||
- Dapper.CodeAnalysis.DapperInterceptorGenerator | ||
- (your project).generated.cs | ||
|
||
You can double-click on this `.cs` file to see the contents. We'll use the code from [Getting Started](/gettingstarted) to illustrate. | ||
|
||
The code is broken down into three main pieces: | ||
|
||
- interceptors | ||
- row factories | ||
- command factories | ||
|
||
## Interceptors | ||
|
||
Interceptors are how Dapper.AOT changes your code; for example, in response to the *source* code (the bit you write): | ||
|
||
``` csharp | ||
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: | ||
|
||
``` csharp | ||
[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: | ||
|
||
``` csharp | ||
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: | ||
|
||
- `Tokenize` looks at the columns returned from the database and identifies how to handle each (assuming it is recognized at all); for each expected column we generate two paths - one for the ideal case | ||
where the data-type is what we expected, and one for when we might need some more flexibility in coercing data; this happens once per *result*, not per *row* (although in the case of `QueryFirst` this | ||
difference is perhaps moot) | ||
- `Read` iterates though the columns of the data-reader and populates a record, using the token data that we reported during `Tokenize`; this happens once per *row* | ||
|
||
## 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: | ||
|
||
``` csharp | ||
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`](https://learn.microsoft.com/dotnet/api/system.data.common.dbcommand) and [`DbBatchCommand`](https://learn.microsoft.com/dotnet/api/system.data.common.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). | ||
|
||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters