DapperLib / DapperLib/DapperAOT

CommandFactory does not use registered TypeHandler for parameter binding

Open
#173 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
472
Forks
43
Avg merge
1d 4h
Merged PRs (30d)
18

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the CommandFactory generation path and compare its parameter-binding handling with the RowFactory path that already calls TypeHandler.Parse(). Reproduce the flat parameter overload case, then verify that generated AddParameters() uses the registered handler's SetValue() for custom types and that the resulting parameter receives the handler-defined value, DbType, and size.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.