DapperLib / DapperAOT

Build time tools in the flavor of Dapper
Other
357 stars 19 forks source link

QueryState copy by value on async method #27

Closed latop2604 closed 12 months ago

latop2604 commented 1 year ago

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 90701185fa726a680947a4216076b1f1223fa9d4

image image image image

Sorry for my (ChatGPT enhanced) English

mgravell commented 1 year ago

This is the result of the state-machine rewrite on the first await; fixing

mgravell commented 1 year ago

example that shows the problem is below; I'm investigating options

using System;
using System.Threading.Tasks;

// fine and dandy, dandy and fine
Console.WriteLine();
Console.WriteLine("sync");
var foo = new Foo(1);
Console.WriteLine($"After .ctor (1): {foo.State}"); // 1
foo.DoTheThing(2);
Console.WriteLine($"After work (2): {foo.State}"); // 2
foo.DoTheThing(3);
Console.WriteLine($"After work (3): {foo.State}"); // 3

// ho hum, mutations to state don't get preserved
Console.WriteLine();
Console.WriteLine("async (but actually sync)");
foo = new Foo(1);
Console.WriteLine($"After .ctor (1): {foo.State}"); // 1
await foo.DoTheThingAsync(true, 2);
Console.WriteLine($"After work (2): {foo.State}"); // 1 - expected 2
await foo.DoTheThingAsync(true, 3);
Console.WriteLine($"After work (3): {foo.State}"); // 1 - expected 3

// nor here
Console.WriteLine();
Console.WriteLine("true async");
foo = new Foo(1);
Console.WriteLine($"After .ctor (1): {foo.State}"); // 1
await foo.DoTheThingAsync(false, 2);
Console.WriteLine($"After work (2): {foo.State}"); // 1 - expected 2
await foo.DoTheThingAsync(false, 3);
Console.WriteLine($"After work (3): {foo.State}"); // 1 - expected 3

// fine again
Console.WriteLine();
Console.WriteLine("back to sync");
foo = new Foo(1);
Console.WriteLine($"After .ctor (1): {foo.State}"); // 1
foo.DoTheThing(2);
Console.WriteLine($"After work (2): {foo.State}"); // 2
foo.DoTheThing(3);
Console.WriteLine($"After work (3): {foo.State}"); // 3

struct Foo // intentionally mutable
{
    public Foo(int state) => State = state;
    public int State { get; private set; }

    public void DoTheThing(int state) => State = state;

    public async Task DoTheThingAsync(bool sync, int state)
    {
        if (!sync) { await Task.Yield(); }
        State = state;
    }
}