0

We have a test WPF application (.NET Core 3.0) with only one button in center of the screen. The button have a click event and a method for its processing:

private async void CreateUsers(object sender, RoutedEventArgs e)
    {
        using (var context = new AsyncDbContext())
        {
            await context.Users.AddRangeAsync(new List<User>
                {
                     new User{Name="1"},
                     new User{Name="2"},
                     new User{Name="3"},
                     new User{Name="4"},
                     new User{Name="5"},
                     new User{Name="6"},
                     new User{Name="7"},
                     new User{Name="8"},
                });
            await context.SaveChangesAsync();
            MessageBox.Show("Done!");
        }
    }

This method is async, so we expecting, that UI will not freeze after button click. But, if we start the app and click button, the app freezes on 1-2 seconds. If we will make another clicks it works as expected without freezes.

After it we reboot our app and the situation come again.

  1. Database was created
  2. .Net framework work exactly the same
  3. DbContext:

    public class AsyncDbContext:DbContext
    {
        public DbSet<User> Users { get; set; }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=ServerName;Database=AsyncDb3; Trusted_connection=true");
        }
    }
    
  4. await Task.Delay(5000); work normally

  5. Wrapping these actions in await Task.Run(...) work normally

Why does dbcontext freeze WPF app each first click after reboot?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Oleg Skidan
  • 617
  • 6
  • 24
  • Adding async/await doesn't make your application asynchronous. And it freezes because it awaits until the operation is done and still running in the UI thread – OlegI Dec 20 '19 at 14:27
  • @OlegI 4 and 5 position work as expected. Await these operation must not block UI thread. So they don't. But why context does? – Oleg Skidan Dec 20 '19 at 14:33
  • Try to add `.ConfigureAwait(false)` to your Tasks it might solve the problem – OlegI Dec 20 '19 at 14:35
  • @OlegI not worked – Oleg Skidan Dec 20 '19 at 14:42
  • One thing that could be happening....(I am not sure it is) is that the first time the wpf app tries to create a thread to run this which takes time because creating threads is expensive. – Jonathan Alfaro Dec 20 '19 at 14:52

4 Answers4

0

Try putting a break point and step through each line.

My guess it that the line that blocks your UI is var context = new AsyncDbContext() which is still executed as synchronous code. My suggestion would be to setup your Context at the app startup, then reuse the same instance and only dispose it at the application shutdown.

The NET Core DI could help with this. Or you could use whichever IoC library you prefer

The reason why it doesn't (appear to) freeze the second time around is because DbContext would cache some things, which took a while to initialize the first time

Alternative (fast and dirty) method would be to wrap the Context constructor in a Task.Run call:

 var context = await Task.Run(()=> new AsyncDbContext())
Alex
  • 3,689
  • 1
  • 21
  • 32
  • 1
    First, `async void` - is normal and the only way to work with base events in WPF, because each event based on delegate, and almost all delegates in WPF have void return type. Second, use only one instance of dbcontext is not good idea, if application may work many hours or days. It's not a production app, and I want to understand only why it wotks like that? – Oleg Skidan Dec 20 '19 at 14:41
  • @OlegSkidan good point, haven't notices the method signature. – Alex Dec 20 '19 at 14:47
  • @OlegSkidan probably that's the reasons why it runs synchronously? Since method returns void it cannot be awaited by caller. And only spinning up new Task inside that method can solve the problem – OlegI Dec 20 '19 at 14:51
  • @OlegI but every method with "Async"-ending returns us a Task, isn't it? – Oleg Skidan Dec 20 '19 at 14:53
  • @OlegSkidan in your case it's void. So it doesn't return the Task. Thus it cannot be awaited. So the caller runs in synchronously. Async word only gives you the possibility to await within the method, it does nothing else except this – OlegI Dec 20 '19 at 14:54
  • @OlegI that is not true. Its is 100% wrong. Async methods that return void are async.... NOT Sync. https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming – Jonathan Alfaro Dec 20 '19 at 14:57
  • 1
    @OlegI if I changed my context call to `async Task.Delay(5000)` then UI thread not freezes. But method stays async void =) – Oleg Skidan Dec 20 '19 at 14:57
  • Even further microsoft allowed async void methods for the purpose of creating async event handlers...... – Jonathan Alfaro Dec 20 '19 at 14:58
  • Simply put, the line that blocks is still run exactly as if the method would have been `private void` (without any async in the signature). To fix this you will have to find a way to either build the context asynchronously, or build it once, and then reuse it. – Alex Dec 20 '19 at 14:59
  • @Alex maybe. Because if I use dbcontext in App.xaml class (start point of the app), it freezes only one time (at start), but it is not answer on my question - why? Maybe first call of dbcontext try to create HttpConnection? If so, than it try to create and open it synchronously? Will it close after few minutes/hours? Does this situation could happen in WebApi Core? =) – Oleg Skidan Dec 20 '19 at 15:03
  • @Alex your new suggestion were noticed by my in question (Task.Run) – Oleg Skidan Dec 20 '19 at 15:05
  • @OlegSkidan that's a lot of questions. the main point however is that the first call to a DbContext constructor takes some times which is noticeable because the main thread cannot process UI work anymore. more about this in this thread: https://stackoverflow.com/questions/44237272/why-is-creating-a-new-dbcontext-slower-than-a-dependency-injected-one – Alex Dec 20 '19 at 15:06
  • @OlegSkidan no need to wrap the whole thing, just the constructor. Or you could move that to a factory method – Alex Dec 20 '19 at 15:07
  • @Alex di not worked also. It is faster, but not enough. – Oleg Skidan Dec 20 '19 at 15:07
0
private async void CreateUsers(object sender, RoutedEventArgs e)
{
  Task.Factory.StartNew(()=>{
    using (var context = new AsyncDbContext())
    {
        await context.Users.AddRangeAsync(new List<User>
            {
                 new User{Name="1"},
                 new User{Name="2"},
                 new User{Name="3"},
                 new User{Name="4"},
                 new User{Name="5"},
                 new User{Name="6"},
                 new User{Name="7"},
                 new User{Name="8"},
            });
        await context.SaveChangesAsync();
        MessageBox.Show("Done!");
});
}
thegunn
  • 26
  • 3
  • It is the worst solution =) we don't need to create detailed task with options, so better to use Task.Run. and also it is not an answer on my question "why?" – Oleg Skidan Dec 21 '19 at 06:43
0

I've read some answers here, and i'd like to tell you about some encounters i've had with something that seems similar. I doubt it will be your answer, but it may help you out regardless so i think its okay to post.

2 things:

  1. When calling a task directly, e.g. 'asyn SomeFunc()' this will use the current SynchronisationContext to schedule the task. In WPF this will default to the dispatcher. This will dispatch the task to the main ui thread. In other words, if you start a task and it has a holdup of 5 seconds, your UI thread will be scheduled with work with a holdup of 5 seconds, and so you will notice an ui freeze of 5 seconds. You can instead opt to schedule this on the threadpool by using Task.Run. Make sure you try and test what your tasks are doing when and what on which thread, because it could get tricky. Also take note of the sync context.

  2. When using EF6 (this might differ from your version) the first ever database call will initialize the databse. This can, depending on your dbcontext take some time. I've also had a case where 2 threads would do 'the first' call around the same time, which cause the initial startup delay to be multiplicative (e.g. from 2 seconds with 1 thread to 4 with 2).

I feel like the issue you're encountering is a combination of both above, but be mindful as my experience is about .net 4.8 and ef6 and you're using the succesor of both of those.

sommmen
  • 6,570
  • 2
  • 30
  • 51
  • Once again i don't know if this helps but i hope you appreciate the time i took to write this :) – sommmen Apr 10 '20 at 08:02
0

It is not related to async or Tasks. The behavior you are seeing is arising from the fact that EF must generate and cache the model represented by a context upon first query. This happens before the async point in SaveChanges() since EF must figure out what queries to generate, hence the pause.

What you can do to help matters along is issue a "do nothing" query at application startup, perhaps in another thread, which causes this caching to happen upfront rather than as a result of some user action.

Tanveer Badar
  • 5,438
  • 2
  • 27
  • 32