Repository: DapperLib/DapperAOT
Version: 1.0.48
Description
When a custom TypeHandler<T> is registered via [module: TypeHandler<T, THandler>], the generated CommandFactory does not use THandler.SetValue() for parameter binding. Instead, it generates p.Value = AsValue(typed.Property), which sets the raw custom type directly as the parameter value — causing SqlClient to throw:
System.ArgumentException: No mapping exists from object type MyCustomType to a known managed provider native type.
The TypeHandler<T> works correctly for result parsing (RowFactory uses Parse()), but is not used for parameter binding (CommandFactory uses AsValue() instead of SetValue()).
Reproduction
// Custom value type for typed SQL string parameters
public readonly record struct DbString(string? Value, bool IsAnsi, int Length, bool IsFixedLength);
// AOT TypeHandler
public sealed class DbStringHandler : Dapper.TypeHandler<DbString>
{
public override void SetValue(DbParameter parameter, DbString value)
{
parameter.Value = value.Value ?? (object)DBNull.Value;
parameter.DbType = value.IsAnsi ? DbType.AnsiString : DbType.String;
if (value.Length > 0) parameter.Size = value.Length;
}
public override DbString Parse(DbParameter parameter) => new(parameter.Value as string);
}
// Registration
[module: DapperAot]
[module: SqlSyntax(SqlSyntax.SqlServer)]
[module: TypeHandler<DbString, DbStringHandler>]
// Usage — flat parameter overload (intercepted)
await connection.ExecuteAsync(
"UPDATE Table SET Name = @Name WHERE Id = @Id",
new { Name = new DbString("test", IsAnsi: true, Length: 25, IsFixedLength: false), Id = 1 }
);
Expected behavior
The generated CommandFactory.AddParameters() should call DbStringHandler.SetValue(parameter, value) for DbString properties, producing:
p.ParameterName = "Name";
TypeHandlerInstance.SetValue(p, typed.Name); // sets DbType, Size, Value
ps.Add(p);
Actual behavior
The generated CommandFactory.AddParameters() produces:
p.ParameterName = "Name";
p.Direction = ParameterDirection.Input;
p.Value = AsValue(typed.Name); // sets raw DbString struct as Value — SqlClient throws
ps.Add(p);
No DbType or Size is set. The TypeHandler.SetValue() is never called.
Generated code excerpt
private sealed class CommandFactory2 : global::Dapper.CommandFactory<object?>
// <anonymous type: int Id, DbString Name>
{
public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args)
{
var typed = Cast(args, static () => new { Id = default(int), Name = default(DbString) });
var ps = cmd.Parameters;
DbParameter p;
p = cmd.CreateParameter();
p.ParameterName = "Id";
p.DbType = DbType.Int32;
p.Direction = ParameterDirection.Input;
p.Value = AsValue(typed.Id);
ps.Add(p);
p = cmd.CreateParameter();
p.ParameterName = "Name";
p.Direction = ParameterDirection.Input;
p.Value = AsValue(typed.Name); // <-- should use TypeHandler.SetValue() here
ps.Add(p);
}
}
Workaround
Use CommandDefinition overloads for calls with custom TypeHandler parameters. CommandDefinition falls back to vanilla Dapper which respects SqlMapper.AddTypeHandler():
// Works — vanilla Dapper path uses TypeHandler
await connection.ExecuteAsync(new CommandDefinition(sql, new { Name = new DbString(...) }));
// Fails — AOT intercepted path ignores TypeHandler
await connection.ExecuteAsync(sql, new { Name = new DbString(...) });
Selectively disable AOT on affected methods:
[DapperAot(false)]
public async Task Handle(MyEvent @event) { ... }
Environment
- Dapper.AOT 1.0.48
- Dapper 2.1.72
- .NET 10
- Microsoft.Data.SqlClient 7.0.0
Repository: DapperLib/DapperAOT
Version: 1.0.48
Description
When a custom
TypeHandler<T>is registered via[module: TypeHandler<T, THandler>], the generatedCommandFactorydoes not useTHandler.SetValue()for parameter binding. Instead, it generatesp.Value = AsValue(typed.Property), which sets the raw custom type directly as the parameter value — causingSqlClientto throw:The
TypeHandler<T>works correctly for result parsing (RowFactory usesParse()), but is not used for parameter binding (CommandFactory usesAsValue()instead ofSetValue()).Reproduction
Expected behavior
The generated
CommandFactory.AddParameters()should callDbStringHandler.SetValue(parameter, value)forDbStringproperties, producing:Actual behavior
The generated
CommandFactory.AddParameters()produces:No
DbTypeorSizeis set. TheTypeHandler.SetValue()is never called.Generated code excerpt
Workaround
Use
CommandDefinitionoverloads for calls with custom TypeHandler parameters.CommandDefinitionfalls back to vanilla Dapper which respectsSqlMapper.AddTypeHandler():Selectively disable AOT on affected methods:
Environment