11

Firstly I believe that the first time is just a condition to see this blocking more clearly. For next times, somehow it still blocks the UI slightly but not obvious like when not using async.

I can say that because I can see the difference between using that QueryAsync and a simple wrapping code with Task.Run(() => connection.Query<T>) which works fine and of course much better than QueryAsync (in UX).

The code is just simple like this:

public async Task<IEnumerable<Item>> LoadItemsAsync(){
  using(var con = new OracleConnection(connectionString)){
     var items = await con.QueryAsync<dynamic>("someQuery");
     return items.Select(e => new Item { ... });
  }
}
//in UI thread, load items like this:
var items = await LoadItemsAsync();

The code working fine (without blocking UI) is like this:

public async Task<IEnumerable<Item>> LoadItemsAsync(){
  using(var con = new OracleConnection(connectionString)){
     var items = await Task.Run(() => con.Query<dynamic>("someQuery"));
     return items.Select(e => new Item { ... });
  }
}
//in UI thread, load items like this:
var items = await LoadItemsAsync();

I know that Task.Run() is not actually async to the detail but at least it puts the whole work to another thread and makes the UI free from being blocked and frozen.

I guess this might be a bug in Dapper, please take sometime to test this. I'm not so sure how to exactly reproduce this, but if possible please try a Winforms project, a fairly large Oracle database and of course as I said you can see it the most obviously by the first time querying (so be sure to run the clearing-cache query against the Oracle server before each test).

Finally if you have some explanation and solution to this (of course without using Task.Run), please share in your answer.

Hopeless
  • 4,397
  • 5
  • 37
  • 64
  • The query will run `async`, the deserialization won't be, so that is probably where you're getting some jitter from. With `Task.Run()`, the entire operation runs on a background thread, deserialization included. – JohanP Aug 02 '18 at 04:33
  • @JohanP but really what I've tested (for loading items with filter) returns empty result. So deserialization would not be performed at all, right? Also if it involves deserialization, why the next times (with many items returned), it is not blocked like in the first time. – Hopeless Aug 02 '18 at 05:58
  • Did you try to look at the Diagnostic tools in VS just to see how much memory it takes to load in `QueryAsync` vs `Task.Run`? – Jeric Cruz Aug 09 '18 at 09:55

2 Answers2

9

With async await you can free and use UI thread only during execution of a truly asynchronous operation (for example, asynchronous IO or a task delegated to a thread pool thread). In your case, methods that utilze Oracle driver (ODP.NET) are not truly asynchronous. See Can the Oracle Managed Driver use async/wait properly? discussion on Stack Overflow.

If you want to offload work from a UI thread to increase responsiveness, simply use Task.Run():

var items = await Task.Run(() => LoadItems());

Use of any other mechanism such as Task.ConfigureAwait(false), or synchronization context replacement combined with Task.Yield() will result in use of an additional thread pool thread too, but it will free UI thread later.

For more information check:

Leonid Vasilev
  • 11,910
  • 4
  • 36
  • 50
  • I had tried using `Task.ConfigureAwait(false)` before and still it blocks the UI, not tried `Task.Yield` yet. But do you mean that the `QueryAsync` implementation of Dapper really depends on the underlying ado.net driver? So SQL Server ado.net driver works but not for Oracle driver? Actually I've not tried this with SQL Server instead so I'm not sure about this (I remember that one time in another project there was one strange thing when I using Dapper to SQL Server, the `Task.Run` blocks UI while `QueryAsync` actually works (not block UI)), but this time for Oracle, it's different. – Hopeless Aug 07 '18 at 07:10
  • Dapper uses an `IDbConnection` implementation instance that you pass to it for sure. In your case it is a `OracleConnection` instance. Also Dapper uses an `IDbCommand` implementation instance that an `IDbConnection` generates. – Leonid Vasilev Aug 07 '18 at 08:43
  • First time code execution is slower in many situations because of the Just-in-time compilation (this is why performance tests often contain warm up iteration that is not measured) and because various caches are empty. Also be sure to test Release build. – Leonid Vasilev Aug 07 '18 at 08:44
  • really as I mentioned in the question, the first-time long-waiting here is almost caused by the Oracle server (I've even tried executing the raw query directly using SQL Developer), so that would not be the problem of Dapper or .NET code. I've found your answer pointing to the right cause of this and it's fairly helpful, but I still let it open for more possible better answers before choosing the best. Thank you – Hopeless Aug 07 '18 at 09:08
  • Oracle clients do not support async. I believe the actual client provided by Oracle would have to support it for it to be async. – Daniel Aug 19 '18 at 06:00
0

I think what you're seeing here might be due to a bug in .net, rather than dapper. The bug is:

https://github.com/Microsoft/dotnet/issues/579

The issue you're maybe seeing is that dapper.Query will internally call SqlDataReader.ReadAsync which can sometimes result in sync/blocking behaviour. This bug is due to be fixed in .net 4.7.3 but the latest pre-release builds still seem to contain this issue.

Also see: How do I make SqlDataReader.ReadAsync() run asynchronously?

Sam Shiles
  • 10,529
  • 9
  • 60
  • 72
  • I'm not so sure if this is related to my problem, because what working here is `Query` (used with `Task.Run`), while what not working here is `QueryAsync`. Although as I already said in one comment of mine to the Leonid Vasilev's answer, the `QueryAsync` seems to work for SQL Server ado.net driver (as I remember from one project I've worked in before), it's strange that in that case `Task.Run(() => con.Query(...` did not work (and that was why I switched to using `QueryAsync` instead and found that it working). But for Oracle driver, looks like it goes the opposite way. – Hopeless Aug 07 '18 at 09:14
  • Was there any other solution to this? I have upgraded to .net framework v4.8 and still get a hang on QueryAsync in my own code. I have even tried `ConfigureAwait` from my controller method down and it never returns. – trevster344 Jun 12 '20 at 21:57