Giter Site home page Giter Site logo

dapperaot's People

Contributors

camiloterevinto avatar deaglegross avatar erjanmx avatar mgravell avatar nickcraver avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dapperaot's Issues

Generated code set invalid `DbParameter.Size` with Oracle database

Describe the bug

I am trying to write my code with Dapper.AOT. When a simple query is executed with Oracle provider,

var result = await connection.QueryAsync<string>(
    "SELECT DISTINCT MY_ROW FROM MY_VIEW WHERE XN = :arg AND XQM = :xqm",
    new { xn, xqm });

...the following exception is thrown:

Unhandled exception. System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
   at Oracle.ManagedDataAccess.Client.OracleParameter.set_Size(Int32 value)
   at Dapper.AOT.<dapper_aot_repro_231207_generated>F693EEE48F7739B4ABF5C1A3B26A697291B752118758EC20DF1C53DCA2012F7F5__DapperGeneratedInterceptors.CommandFactory0.AddParameters(UnifiedCommand& cmd, Object args) in C:\Users\bs\Code\Tests\dapper-aot-repro-231207\Dapper.AOT.Analyzers\Dapper.CodeAnalysis.DapperInterceptorGenerator\dapper-aot-repro-231207.generated.cs:line 52
   at Dapper.CommandFactory`1.Initialize(UnifiedCommand& cmd, String sql, CommandType commandType, T args) in /_/src/Dapper.AOT/CommandFactory.cs:line 195
   at Dapper.CommandFactory`1.GetCommand(DbConnection connection, String sql, CommandType commandType, T args) in /_/src/Dapper.AOT/CommandFactory.cs:line 186
   at Dapper.AOT.<dapper_aot_repro_231207_generated>F693EEE48F7739B4ABF5C1A3B26A697291B752118758EC20DF1C53DCA2012F7F5__DapperGeneratedInterceptors.CommonCommandFactory`1.GetCommand(DbConnection connection, String sql, CommandType commandType, T args) in C:\Users\bs\Code\Tests\dapper-aot-repro-231207\Dapper.AOT.Analyzers\Dapper.CodeAnalysis.DapperInterceptorGenerator\dapper-aot-repro-231207.generated.cs:line 26
   at Dapper.Command`1.GetCommand(TArgs args) in /_/src/Dapper.AOT/CommandT.cs:line 79
   at Dapper.Command`1.QueryBufferedAsync[TRow](TArgs args, RowFactory`1 rowFactory, Int32 rowCountHint, CancellationToken cancellationToken) in /_/src/Dapper.AOT/CommandT.Query.cs:line 72
   at Dapper.Command`1.QueryBufferedAsync[TRow](TArgs args, RowFactory`1 rowFactory, Int32 rowCountHint, CancellationToken cancellationToken) in /_/src/Dapper.AOT/CommandT.Query.cs:line 98
   at Dapper.DapperAotExtensions.AsEnumerableAsync[TValue](Task`1 values) in /_/src/Dapper.AOT/DapperAotExtensions.cs:line 23
   at Program.<Main>$(String[] args) in C:\Users\bs\Code\Tests\dapper-aot-repro-231207\Program.cs:line 12
   at Program.<Main>$(String[] args) in C:\Users\bs\Code\Tests\dapper-aot-repro-231207\Program.cs:line 12
   at Program.<Main>(String[] args)

When I look at the generated code, I see that the Size property is set to -1, which caused the exception.

// ...
private sealed class CommandFactory0 : CommonCommandFactory<object?> // <anonymous type: string xn, string xqm>
{
    // ...
    public override void AddParameters(in global::Dapper.UnifiedCommand cmd, object? args)
    {
        var typed = Cast(args, static () => new { xn = default(string), xqm = default(string) }); // expected shape
        var ps = cmd.Parameters;
        global::System.Data.Common.DbParameter p;
        p = cmd.CreateParameter();
        p.ParameterName = "xqm";
        p.DbType = global::System.Data.DbType.String;
        p.Size = -1;   // HERE
        p.Direction = global::System.Data.ParameterDirection.Input;
        p.Value = AsValue(typed.xqm);
        ps.Add(p);
        // ...
    }
    // ...
}
// ...

Everything works fine without using Native AOT.

  • Database backend: Oracle
  • Dapper version: 2.1.24
  • Dapper.AOT version: 1.0.23
  • Oracle.ManagedDataAccess.Core version: 3.21.120

To Reproduce

The reproducible project is located here

Expected behavior

The query works.

Additional context

  • .NET SDK version: 8.0.100

Analyzer: detect SELECT column name problems

Note: requires knowing whether we're binding columns via position or name

if binding by name, SELECT columns should be inspected for:

  • missing names
  • duplicate names (ordinal case-insensitive)
  • hard to pronounce names (spaces, hyphens, etc)?

DbString - analyzer and runtime

DbString is a Dapper type that allows metadata to be conveyed for string types.

However:

  • it is allocatey
  • it isn't touched by AOT today
  • what it achieves is the same as DbValue]

Suggestions:

  1. we need to verify that [DbValue] allows the size and type to be conveyed into DbParameter
  2. we should emit a warning to use [DbValue] instead
  3. we should probably treat DbString as a recognized type, and configure things correctly
  4. for this last, I would propose (if any DbString are encountered) writing a helper method into the output (we have some prior art that copies entire files into the output; that would be fine) with a method that takes a DbParameter and DbString (global:: qualified) and does the right things:
  • if the entire DbString is null, just set Value to DbNull
  • set the size if appropriate
  • set the type if appropriate
  • set the value using AsValue, or a (object)value.Value ?? DbNull if easier

The reason we can't put this into the library is that we can't ref Dapper directly because of the Dapper/Dapper.StrongName duality - hence we need to implement it in codegen

Analyzer: parameterization anti-patterns

examples:

  • interpolated strings: conn.QueryFirst<Customer>($"select * from customers where Id={id}")
  • concatenation with variables conn.QueryFirst<Customer>("select * from customers where Id=" + id)
  • in both cases, we'll grudgingly permit constant string values; I'd like to permit other constant values, but culture rules make that impossible

(applies in all SQL scenarios, not just QueryFirst)

should probably be a warning, category "Security", something like "Data values should not be concatenated into SQL - use parameters instead"; it is hard to offer an auto rewrite here because the specific parameter syntax is provider specific - but we might know the provider (we have some stuff for that already; that's a distant second to spotting it, though)

[DAP226] False positive: DATEADD

Describe the bug
When using SqlConnection and querying data using the built-in MSSQL-function DATEADD parameter datepart is falsely identified as a table column causing a false DAP226 warning when used in a query with joins.

Dapper.Advisor: 1.0.10

To Reproduce

// DAP226: FROM expressions with multiple elements should qualify all columns; it is unclear where 'YEAR' is located
public static async Task DateAddWithJoin(SqlConnection connection) =>
    await connection.QueryAsync(
        """
        SELECT DATEADD(YEAR, 1, t.Year) AS NextYear
        FROM MyTable t
        JOIN MyOtherTable o ON o.Id = t.Id
        """
    );

Detect pre-reqs and emit diagnostics

  • <LangVersion>11</LangVersion> (or higher)
  • <Features>interceptors</Features>

2 separate diagnostics; also, do not emit anything if langver is too low (currently: we do)

note: it should only emit these if at least one Dapper call is detected - suggest adding a new flag that is AotNotEnabled, and all (non-zero) are AotNotEnabled: do the thing; perhaps implement as optional , flags = None on the simple ctor, and this flags = flags | DoNotGenerate

SQL hint: multi-table queries

  1. recommend all column refs be fully qualified
  2. validate that all qualifiers are valid tables or aliases
  3. in the case of joins, validate that the join expression mentions both an incoming and outgoing qualifier

Suggestion: db schema as "additional file"; apply validation

context: https://twitter.com/IanStockport/status/1702313925810024716

imagine a "dotnet tool":

dapper schema {conn-key} 

which:

  • resolved dev-time connection-key conn-key
  • connected to the database
  • probed the server-kind
  • dumped the schema to a file such as dbschema.txt
  • optional: find the csproj, check the additional file exists; if not, add it

Where dbschema.txt could be something like (is there prior art here? plain text?):

(note on order: objects are invariant alphabetical; columns/parameters/etc are positional)

default schema dbo
server version 16.0.1000.6

table dbo.Foo
column Id int notnull identity readonly
column Name string
column Label string computed readonly

table someSchema.Bar
column Id int notnull identity readonly

view someView
column Id notnull null
...

we would then load the dbschema.txt and use it to validate commands at build time, without needing constant SQL access. Schema should be implied via "default schema" when omitted.

Possible checks:

  • object exists
  • column exists (including views and functions)
  • capitalization
  • appropriate data types
  • treat views as immutable
  • don't allow update/insert to readonly columns
  • correct parameters on functions
  • correct function kind (scalar vs tabular UDF)
  • system functions vs server version
  • stored procedure parameters (columns are probably a stretch, unless we get the sp_helptext-etc source and parse that too?)
  • server version syntax rules (currently we always assume "latest")

Rewriting the db schema would be a case of re-running the tool, with the changes visible in source control.

Note: advanced SQL analysis is currently limited to SQL Server; we should probably at least not assume that when connecting (use the provider model), but... whatever works.

[DAP231] False positive: Full table aggregation

Describe the bug

When using an aggregation function e.g. MIN/MAX an a full table a false DAP231 warning is given

Dapper.Advisor: 1.0.10

To Reproduce

// DAP231: SELECT for single row without WHERE or (TOP and ORDER BY)
public static async Task<int> SelectMax(SqlConnection connection) =>
    await connection.QueryFirstOrDefaultAsync<int>("SELECT MAX(SomeColumn) FROM MyTable");

Source generation seemingly not working when using minimal APIs and top level statements

Describe the bug

When building a sample application using minimal APIs and top level statements (sample linked below), it seems like the source generation isn't working as expected. Even though adding the [module:DapperAot], compiling produces Warning DAP005 : 1 candidate Dapper methods detected, but none have Dapper.AOT enabled, and running the application results in the expected errors, due to not being able to use dynamic code generation under Native AOT.

If I move the code to a static class and map the endpoint, then the warning disappears and everything works as expected.

I imagine this isn't a very relevant issue, as in non-sample code, it's unlikely to have queries in top level statements, but just in case it's an unknown issue (or maybe I'm using it wrong), I thought of reporting.

Where are you seeing this?

  • what Dapper/Dapper.StrongName version? 2.1.21
  • what Dapper.AOT/Dapper.Advisor version? 1.0.16
  • if relevant: what database backend? Npgsql 8.0.0-rc.2
  • dotnet --version: 8.0.100

To Reproduce
Sample code
Note: out of the box this code works, because the direct route to code is commented out and replaced with a static class, but reverting things, we can see the reported issue.

Expected behavior

Source generation works regardless of where the queries are used.

However, if supporting this scenario is not worth the effort, maybe include it as a known limitation in the docs.

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Add any other context about the problem here.

[DAP025] False positive: SELECT INTO

Describe the bug
When using SqlConnection and selecting data from one table into another using ExecuteAsync a false DAP025 warning is triggered.

Dapper.Advisor: 1.0.10

To Reproduce

// DAP025: The command has a query that will be ignored
public static async Task SelectInto(SqlConnection connection) =>
    await connection.ExecuteAsync("SELECT [Test] INTO #MyTemporaryTable FROM MyTable");

AsyncAccessorDataReader never sets Current

Describe the bug

The AsyncAccessorDataReader never seems to take the Current value from the IAsyncEnumerator and set it's own Current value with it, when ReadAsync is called. This means that as soon as the SqlBulkCopy tries to get a value for the first row, it encounters a null value, triggering a NullReferenceException.

Where are you seeing this?

  • Dapper AOT 1.0.23

To Reproduce

IAsyncEnumerable<Customer> customers = ...
var reader = TypeAccessor.CreateDataReader(customers);
using var table = new SqlBulkCopy(connection);
table.EnableStreaming = true;
table.WriteToServer(TypeAccessor.CreateDataReader(customers,
    [nameof(Customer.Name), nameof(Customer.CustomerNumber)]));

Support non-public constructors

We already support annotated custom constructors when they're accessible to the generator. We can extend that.

Note: only impacts private and protected constructor usage; all others should continue using direct

This can be implemented acceptably using reflection (ideally optimized via Expression) or [UnsafeAccessor] (net8+ only); example

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;

static class P {
    static void Main()
    {
        var obj = CreateBar(42);
        Console.WriteLine(obj.A);

        obj = CreateBar(96);
        Console.WriteLine(obj.A);
    }
#if NET8_0_OR_GREATER

    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    static extern Bar CreateBar(int a);
#else
    private static Func<int, Bar>? s_CreateBar;
    static Bar CreateBar(int a) => SomeUtilityHelper.GetConstructor(ref s_CreateBar)(a);
#endif
}

static class SomeUtilityHelper // in DapperAOT - maybe in RowFactory?
{
    public static TDelegate GetConstructor<TDelegate>(ref TDelegate? field) where TDelegate : Delegate
    {
        return field ?? SlowCreate(ref field);

        static TDelegate SlowCreate(ref TDelegate? field)
        {
            var signature = typeof(TDelegate).GetMethod(nameof(Action.Invoke));
            if (signature?.ReturnType is null || signature.ReturnType == typeof(void))
            {
                throw new InvalidOperationException("No target-type found");
            }
            var methodArgs = signature.GetParameters();
            var argTypes = Array.ConvertAll(methodArgs, p => p.ParameterType);
            var ctor = signature.ReturnType.GetConstructor(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, argTypes);
            if (ctor is null)
            {
                throw new InvalidOperationException("No suitable constructor found matching "
                    + string.Join<Type>(", ", argTypes));
            }
            var args = Array.ConvertAll(methodArgs, p => Expression.Parameter(p.ParameterType, p.Name));
            field = Expression.Lambda<TDelegate>(Expression.New(ctor, args), args).Compile();
            return field;
        }
    }
}
class Bar
{
    private readonly int x;
    public int A => x;
    private Bar(int a) => x = a;
}

CS9206 'An interceptor cannot be declared in the global namespace' compiler error after upgrading to .NET 8 RC2

Describe the bug

Code which ran perfectly with RC1 of .NET 8 does not compile any more. Getting CS9206 'An interceptor cannot be declared in the global namespace' compiler errors.

To Reproduce
Download the repository and compile the UsageLinker and UsageBenchmark projects. They don't build with RC2

Expected behavior
No error, worked fine with RC1

Screenshots
image

Additional context
Using Visual Studio 2022 Version 17.8.0 Preview 4.0

Query text passed over string variable is not picked up by Analyzer

Example:

public async Task<List<Symbol>> GetAllSymbolsVariable()
{
    var sql = "select * from Symbol";
    var results = await Conn().QueryAsync<Symbol>(sql); // no warning
    return results.AsList();
}
public async Task<List<Symbol>> GetAllSymbolsInline()
{
    var results = await Conn().QueryAsync<Symbol>("select * from Symbol"); // has warning
    return results.AsList();
}

Build error "The type 'InterceptsLocationAttribute' exists in both ...."

Describe the bug

Build error :
"The type 'InterceptsLocationAttribute' exists in both 'test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' and 'test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" is thrown for simple dotnet8 console project.

Where are you seeing this?

  • what Dapper/Dapper.StrongName version? 2.1.24
  • what Dapper.AOT/Dapper.Advisor version? 1.0.31
  • if relevant: what database backend? SQL Server, Microsoft.Data.SqlClient 5.1.2

To Reproduce

  • Create a new dotnet 8 console project
  • Put "[module: DapperAot]" below the usings
  • Create a string for the SQL query
  • Create a class for dapper to map the query to
  • Put a query method in the class querying the database as such:
 using (var connection = new SqlConnection(connectionString))
{
    await connection.OpenAsync();
    using (var tran = await connection.BeginTransactionAsync(IsolationLevel.Snapshot))
    {
        var returned= await connection.QueryAsync<Class>(Query, transaction: tran);
    }
}

Expected behavior
Soultion builds with no error.
Screenshots

Additional context
"InterceptorsPreviewNamespaces" csproj element exists in PropertyGroup
Dotnet version:

dotnet --info
.NET SDK:
 Version:           8.0.100
 Commit:            57efcf1350
 Workload version:  8.0.100-manifests.71b9f198 

Npgsql parameter rewrite

There are two parameter models in Npgsql - ordinal and nominal; we use nominal, but the ordinal API is much more efficient; we could ingest nominal const sql and rewrite it as ordinal

this should include splitting semi-colon multi-statements into DbBatch when possible

tasks:

  • basic generalized SQL parser
  • SQL syntax tests for postgresql peculiarities
  • basic exploration of batch concept
  • "real" batch concept (UnifiedBatch, should work with/without DbBatch API)
  • codegen (including turn-off-and-onable)
  • benchmark:
    • vanilla Dapper, parameterized single batch (Npgsql param rewrite)
    • vanilla Dapper, parameterized multi-batch (Npgsql param rewrite+split)
    • AOT Dapper, rewrite enabled, parameterized single batch, DbBatch disabled
    • AOT Dapper, rewrite enabled, parameterized multi batch, DbBatch disabled
    • AOT Dapper, rewrite enabled, parameterized single batch, DbBatch enabled
    • AOT Dapper, rewrite enabled, parameterized multi batch, DbBatch enabled
  • use parser in place of the regex in the regular code path? as an option with fallback?
  • docs
  • performance tuning of parser (I have a plan, don't ask; kind of a rewrite of the parser) (note this is not critical path)

"lacks WHERE" - shouldn't apply if JOIN

The join us effectively a restriction UNLESS it is the "always" side of an outer join

Basic tier: omit for JOIN
Middle tier: omit for INNER JOIN
Top tier: omit if not the weak side of OUTER

Custom DbType for parameters not working.

I tried to set DbType using DbValue attribute for parameters and looks like it is not use DbType property

[DapperAot(enabled: true)]
public static class UsersSqlQieries
{
    public sealed class UserIncrementParams
    {
        [DbValue(Name = "userId")]
        public int UserId { get; set; }

        [DbValue(Name = "date", DbType = System.Data.DbType.Date)]
        public DateTime Date { get; set; }
    }

    public static async Task IncrementAsync(DbConnection connection, int userId)
    {
        var date = DateTime.Today;

        await connection.ExecuteAsync("""
            UPDATE [dbo].[table]
            SET [column] = ([column] + 1) 
            WHERE [Id] = @userId and [Date] = @date
            """, new UserIncrementParams() { UserId = userId, Date = date });
    }
}

and generated code:

 file static class DapperGeneratedInterceptors
{
  [global::System.Runtime.CompilerServices.InterceptsLocationAttribute("....cs", 25, 30)]
  internal static global::System.Threading.Tasks.Task<int> ExecuteAsync4(this global::System.Data.IDbConnection cnn, string sql, object? param, global::System.Data.IDbTransaction? transaction, int? commandTimeout, global::System.Data.CommandType? commandType)
  {
      // Execute, Async, HasParameters, Text, KnownParameters
      // takes parameter: global::App.Services.CompiledQueries.Sql.UsersSqlQieries.UserIncrementParams
      // parameter map: Date UserId
      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(), CommandFactory2.Instance).ExecuteAsync((global::App.Services.CompiledQueries.Sql.UsersSqlQieries.UserIncrementParams)param!);
  
  }
}
  private sealed class CommandFactory2 : global::Dapper.CommandFactory<global::App.Services.CompiledQueries.Sql.UsersSqlQieries.UserIncrementParams>
  {
      internal static readonly CommandFactory2 Instance = new();
      public override void AddParameters(in global::Dapper.UnifiedCommand cmd, global::App.Services.CompiledQueries.Sql.UsersSqlQieries.UserIncrementParams args)
      {
          var ps = cmd.Parameters;
          global::System.Data.Common.DbParameter p;
          p = cmd.CreateParameter();
          p.ParameterName = "userId";
          p.DbType = global::System.Data.DbType.Int32;
          p.Direction = global::System.Data.ParameterDirection.Input;
          p.Value = AsValue(args.UserId);
          ps.Add(p);

          p = cmd.CreateParameter();
          p.ParameterName = "date";
          p.DbType = global::System.Data.DbType.DateTime;  //problem is here, DbType from DbValueAttribute not used 
          p.Direction = global::System.Data.ParameterDirection.Input;
          p.Value = AsValue(args.Date);
          ps.Add(p);

      }
      public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, global::App.Services.CompiledQueries.Sql.UsersSqlQieries.UserIncrementParams args)
      {
          var ps = cmd.Parameters;
          ps[0].Value = AsValue(args.UserId);
          ps[1].Value = AsValue(args.Date);

      }
      public override bool CanPrepare => true;

  }

As I understood the problem in this method

Should it be?

  public DbType? GetDbType(out string? readerMethod)
        {
            var dbType = IdentifyDbType(CodeType, out readerMethod);
            if (TryGetAttributeValue(_dbValue, "DbType", out int explicitType, out bool isNull))
            {
                var preferredType = isNull ? (DbType?)null : (DbType)explicitType;
                if (preferredType != dbType)
                {   // only preserve the reader method if this matches
                    readerMethod = null;
                    dbType = preferredType; //added line
                }
            }
            return dbType;
        }

QueryState copy by value on async method

Hello

I'm trying DapperAOT with the new interceptor mode and I spotted an issue that occurs only in async mode.

The problem occurs in Command<TArgs>.QueryBufferedAsync when invoking await state.ExecuteReaderAsync(...).
This method is supposed to assign the Reader field of the QueryState struct. However, it seem that the struct is copied (by value) during the async call, causing the reader to be set on a separate instance. When the execution returns to Command<TArgs>.QueryBufferedAsync, the original QueryState remains unchanged, and the Reader field stays null.

I attempted to modify QueryState from a struct to a class (and replaced all instances of QueryState state = default; with QueryState state = new();), and it worked.

Although I'm not expert enough to resolve this issue on my own, I hope my findings will be helpful to you.

Main branch commit 9070118

image
image
image
image

Sorry for my (ChatGPT enhanced) English

Hello just trying to esatablish how to use this as there are no code examples

I have seen that the code is obviously not constructed the same as normal dapper
I have tried this with the [DapperAot] attribute over my model
var programs = new List();

    var con = new SqlConnection(_connectionString);
    con.Open();
    var list = new List<Program>();
    var test = con.Command<Program>("SELECT Name, Acronym FROM Programs");
    var test2 = test.Execute(programs);

but it returns nothing, there is no "Query method off of sql connection like normal Dapper. How doo i get this to work on just very basic simple terms. Can you please give a ver basic code example of getting DapperAot to work, there is nothing in the readme

Warn about ambiguous properties/fields

See DapperLib/Dapper#1993

The properties FirstName and First_Name are ambiguous. Dapper (core, haven't checked AOT) prefers properties over fields, but there was a glitch where ambiguous properties could get conflated.

We should warn if:

  • any two fields on the same type have the same name after normalisation
  • any two properties on the same type have the same name after normalisation

We should also verify that AOT prefers properties over fields (this isn't a warning: this is a library requirement)

This analyzer applies with+without AOT enabled.

Generated code results in compile error for types that have required properties

Describe the bug

If I have a type like:

public class Todo
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public bool IsComplete { get; set; }
}

And a method like:

[DapperAot]
public async Task<Todo> DapperAot()
{
    const string sql = """
        INSERT INTO Todos(Title, IsComplete)
        Values(@Title, @IsComplete)
        RETURNING *
        """;
    await using var connection = _dataSource.CreateConnection();
    IDbConnection dbConnection = connection;
    var createdTodo = await dbConnection.QuerySingleAsync<Todo>(sql, _todo);

    return createdTodo;
}

I get a compile error: CS9035 Required member 'Todo.Title' must be set in the object initializer or attribute constructor.

Snippet of generated code that causes the error:

public override global::Todo Read(global::System.Data.Common.DbDataReader reader, global::System.ReadOnlySpan<int> tokens, int columnOffset, object? state)
{
    global::Todo result = new();
    ...

Non-trivial construction

Right now, the materializer is akin to:

TheType obj = new();
while (...)
{
    switch (...)
    {
        case 4223423:
            obj.Foo = /* read the value */
            break;
    }
}
return obj;

This only works for trivial construction; it does not support more complex scenarios:

  1. custom constructors
  2. init-only properties
  3. read-only fields (deferred for now, as this also requires materializer colocation)

A custom constructor (at most one per type) is identified by:

  • ignore parameterless constructors and constructors that are marked [DapperAot(false)]
  • if only a single constructor remains, it is selected whether or not it is marked [DapperAot]/[DapperAot(true)]
  • if multiple constructors remain, and multiple are marked [DapperAot]/[DapperAot(true)], a generator error is emitted and no constructor is selected
  • if multiple constructors remain, and exactly one is marked [DapperAot]/[DapperAot(true)], it is selected
  • in all other cases, no constructor is selected

If a custom constructor is selected, the parameters are checked against the member list using normalized / insensitive rules; if any paired members do not match field type (question: nullability?), a generator error is emitted and the constructor is deselected


Init-only properties are any non-static properties that have the init modifier, for example public string Name { get; init; }


In any of these scenarios, we must collect the values into locals, and defer construction; I would propose simply value0, value1, etc where the index is the position of the discovered member; each should be assigned default, awaiting values from the database; we then collect fields into these variables instead of the object:

int value0 = default;
string? value1 = default;
DateTime value2 = default;

while (...)
{
    switch (...)
    {
        case 4223423:
            value1 = /* read the value */
            break;
    }
}
// construction not shown yet

The construction step depends on whether a custom constructor is selected, and whether the parameters for such custom constructor covers all members; there are 3 possible outcomes:

  1. custom constructor covers all members
return new TheType(value0, value2, value1);

(noting that the parameter order does not necessarily match our declaration order, so it is not necessarily strict order)

  1. custom constructor covers some but not all members (which may or may not include init-only properties)
return new TheType(value0, value2)
{
    Foo = value1,
};
  1. no custom constructor (and which by elimination: must include at least one init-only property)
return new TheType
{
    Foo = value1,
};

Implementation notes:

DRY:

  • ideally the 3 non-trivial construction techniques (with/without constructor, with/without additional members) should not use 3 separate paths (it is fine to keep the original simple construction TheType obj = new() line separate, for simplicity; see †)
  • ideally the field read loop should not be duplicated between simple/custom construction; it should just change the target of the read statement between obj.TheMember = vs value42 =

†: the 4th quadrant in that with/without matrix is: simple construction, so: already handled - i.e. without constructor, everything is additional members, none of which are init-only - the difference being that in that simple scenario, we're not using the stack to gather the values - we're pushing them directly onto the object

System.NullReferenceException: Object reference not set to an instance of an object. - error

I have a Roslyn Source Code Generators examples with code on GitHub - https://github.com/ignatandrei/rsCG_examples

The code is pretty simple , but it gives the error
System.NullReferenceException: Object reference not set to an instance of an object.

The line is

__dapper__result = global::Dapper.TypeReader.TryGetReader<global::DapperExampleDAL.Person[]>()!.Read(__dapper__reader, ref __dapper__tokenBuffer);

You can find the example at https://github.com/ignatandrei/rsCG_examples , folder later_aotdapper\src\dapperexample
Please help

Support [Column]

Currently we support names (parameter and column) via [DbValue], however there is also a well-known [System.ComponentModel.DataAnnotations.Schema.ColumnAttribute] that is commonly used.

I'm hesitant to use this blindly, because that would change existing behavior, so this should be triggered via a new UseColumnAttributeAttribute (yes, the naming is weird - see also XmlAttributeAttribute) which would apply at all levels (like most DapperAOT attributes):

  • feature should only apply if [UseColumnAttribute] or [UseColumnAttribute(true)] is in play
  • [UseColumnAttribute(false)] explicitly disables
  • only Name would be used; we aren't interested in Order etc
  • if we see a [Column(...)] with a Name but don't see any explicit on/off flag, emit a warning so the user knows to opt in/out
  • works similar to [DbValue] with Name, but: doesn't impact parameters

See also https://stackoverflow.com/questions/77073286/can-dapper-mappings-be-automated-with-attributes

Mapping inherited class properties

Right now when using a base class for your entities like this

public abstract class EntityBase
{
    public long Id { get; set; }
}

public class Entity1 : EntityBase
{
    public string Name { get; set; }
}

Then the Id property will not be read by the code generated.

It only works when putting the Id directly into Entity1 (otherwise it never gets mapped, always default 0 for any query). I think it´s fairly common to want to enforce consistent naming of ids and timestamps. Although, this can be enforced with interfaces too I suppose (doing that now instead).

The above should either work, or a the analyzer should give you a warning about this (in case this is not gonna be implemented anytime soon). I don´t yet know enough about compile time generators to know how hard it is to figure out that there is a base class and walk back up the class tree.

Adding benchmarks to the documentation

First of all, Thank you very very much @mgravell for your hugely-appreciated efforts.
.NET would not be the same without you.
This library is very great and very important, and I can not stop introducing my friends and colleagues to it.

I would just suggest adding benchmarks to the documentation to show how important this library is.
It will be the easiest way for people to understand its importance by seeing how faster and less memory this approach provides.

Thank you a lot again

Using explicit constructor for DynamicParameters type causes DAP214 error

Using something similar to the following causes a couple of DAP214 variable not declared errors, when infact the code works as expected (results returned):

var parameters = new DynamicParameters();
parameters.Add("@StartDate", "2023-06-01");
parameters.Add("@EndDate", "2023-06-30");

When I switch to an anonymous type for the parameters everything is fine - no errors.

var parameters = new 
{
   parameters.Add("@StartDate", "2023-06-01").
   parameters.Add("@EndDate", "2023-06-30")
}

Stack overflow in source generator?

I tried to setup one of the ASP.NET Core benchmarks app's (this one) to use Dapper.AOT but it appears to be causing a stack overflow with lots of the following printed to output during build:

C:\Program Files\dotnet\sdk\8.0.100\Roslyn\Microsoft.CSharp.Core.targets(84,5): error :    at Dapper.Internal.Inspection.InvolvesTup
leType(Microsoft.CodeAnalysis.ITypeSymbol, Boolean ByRef) [D:\src\GitHub\aspnet\Benchmarks\src\BenchmarksApps\TodosApi\TodosApi.cspr
oj]

Visual Studio freezes and then disappears.

  • Dapper version 2.0.123
  • Dapper.AOT version 1.0.23
  • Npgsql

To Reproduce
Project that I was updating is: https://github.com/aspnet/Benchmarks/tree/damianedwards/dapper.aot/src/BenchmarksApps/TodosApi

Expected behavior
Expect it to work.

Screenshots
See error above.

Additional context
none

Primitive types break row-reader emit

things like Query<int> end up emitting a row-reader with a broken outdent level

  1. fix the broken outdent! (suggest counter validation for indent and brace level, checked end of each reader) - probably a "if there's fields" scenario
  2. special-case types that should be handled directly - similar to identifydbtype (don't forget ideal/fallback mode)
  3. possibly also consider things like DbGeography in that? can that be generalized?

new tests suggested:

  • Query<int>
  • Query<int?>
  • Query<string>
  • Query<string?>
  • Query<DbGeography>
  • Query<DbGeography?>

Handling Relationships, their mappers as well as their configuration

I have thought of generating the parser/materializers AOT as well for quite some time even more with the rise of Source Generators. However I was never able to figure out how one would implement it. Therefor I am curious on how you would solve this, especially in terms of the configuration.

  • How will the user be able to configure the method with joins in a type safe manner?
  • Will the parser of a method with a join be automatically generated?

Missing brackets from IN clause causes Error - code still works

SQL string looks something like "... WHERE [DividendId] IN @ids ORDER BY [SecId];"

This caused error DAP206 Incorrect syntax near @ids, but the code executes as expected without having the brackets around the IN parameter,

Should this be an Error or a Warning - if the code still works, it feels like a Warning, and the correction an advisory.

Generator: major refactor (do not store semantic model)

The incremental generator is stashing parts of the semantic model; it must not - we should create custom types (struct records? value tuples? must have equality support) and fully expand everything during parse, so we never store anything except our own data

Detect trivial DynamicParameters

for example, see #48

  • if the non-trivial constructor is used: don't warn
  • any Add usage other than just name+value: don't warn
  • any branched logic around what gets added: don't warn

Detect missing [DapperAot]

It would be nice if a non-zero number of Dapper methods detected, but zero marked [DapperAot], resulted in a single message of the form (assuming pre-reqs met): "Dapper.AOT found candidate methods, but is not enabled at any level; consider adding [DapperAot] at the method, type, module or assembly level"

Ensure parameter false positives are detected

See DapperLib/Dapper#1914 and DapperLib/Dapper#1971

In analyzer mode (not in generator mode), we should be able to detect the following queries as problematic, i.e. the true parameters are not the same as Dapper is going to assume:

select 'this ? looks like OLE DB'

and

select 'this ?looks? like pseudo-positional

The problem here is that the runtime SQL parser in Dapper detects both of these as meaning the wrong thing; they aren't parameters. Our existing parameter handler in TSQL should be able to detect this.

Enum Invalid cast from 'System.Int64' (works without AOT)

The following create statement with an Enum in the class will fail with Invalid cast from 'System.Int64'.

I´m using sqlite, the query that fails looks like this

public async Task<long> CreateAsync(AppUser newUser)
{
    await using var connection = new SqliteConnection(settings.ConnectionString);

    const string sql =
        $"""
          INSERT INTO AppUser
          (Username, Password, UserRole) VALUES
          (
            @{nameof(AppUser.Username)},
            @{nameof(AppUser.Password)},
            @{nameof(AppUser.UserRole)}
          ) RETURNING *;
          """;
        
    var insertedRows = await connection.QueryAsync<AppUser>(sql, newUser);

    return insertedRows.FirstOrDefault()?.Id ?? -1;
}

The Enum is mapped to an INTEGER column in Sqlite. Without AOT the mapping works just fine.

Adding a TypeHandler like this does not seem to work either

internal class UserRoleConverter : SqlMapper.TypeHandler<UserRole>
{
    public override UserRole Parse(object value)
    {
        // theoretically this should fix it as casting to enums only works if it´s int32 afaik, but it never gets called
        var int32Value = Convert.ToInt32(value);
        
        return (UserRole) int32Value;
    }

    public override void SetValue(IDbDataParameter parameter, UserRole value)
    {
        parameter.Value = (int)value;
    }
}

// Registered on startup
SqlMapper.AddTypeHandler(new UserRoleConverter());

I am aware that this query in particular parsing the enum would be avoidable entierly, because I only need the Id back, but this would just be a fix for this one query and it would still be an issue for virtual everything else. It just happens to be the first thing that runs in virtually every single test I have.


There is this issue for regular dapper DapperLib/Dapper#259

So, does that mean if dapper AOT does not handle enums out of the box it just won´t be possible to make it work? I could probably work around this with a class that imitates enums and some explicit converters, but I would rather not.


Tried to create a simple wrapper class like this

public class UserRoleColumn(UserRole userRole)
{
    public UserRole Value { get; set; } = userRole;
}

with this type converter

public class UserRoleColumnConverter : SqlMapper.TypeHandler<UserRoleColumn>
{
    public override void SetValue(IDbDataParameter parameter, UserRoleColumn? value)
    {
        ArgumentNullException.ThrowIfNull(value);

        parameter.Value = (int) value.Value;
    }

    public override UserRoleColumn? Parse(object value)
    {
        var userRole = (UserRole) Convert.ToInt32(value);
        return new UserRoleColumn(userRole);
    }
}

Also does not work throws No mapping exists from object type UserRoleColumn to a known managed provider native type in the same place.


On further inspection SqlMapper.AddTypeHandler is not yet AOT friendly. It breaks on publish so wether it´s used or not by the code generated does not even matter because you can´t register them.

Analyzer raises DAP019 Sql Parameters error for parameters used inside query only

In a few Dapper SQL queries, I have query level parameters which are used as variables inside the query scope and don't require any values to be passed in as arguments to the Dapper in methods like QueryAsync.

DECLARE @ActiveWidgets TABLE  (Id int)
INSERT @ActiveWidgets SELECT Id FROM Widgets WHERE Active = 1

This is raising the error DAP019 SQL parameters were detected, but no parameters are being supplied

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.