Updating a class property value is synchronous code and therefore any consequential async awaited calls can be an issue. The recommended workaround is to implement OnPropertyChanged event for the class. Alternatively you can use StateHasChanged() in Blazor code to get around, for example, making async Entity Framework calls in synchronous code.

An issue that raises its head from time to time is having an async property. This occurs, for example, where a property Set requires a call to Entity Framework to update that property in an entity. Discussions maintains that this is not the way to do it. What you should do is implement OnChange async Task for the class, activate that for the property Set, and use an EF call in the OnChange handler to update the entity..

A requirement arose in a Blazor app to log which user has assessed a page because it contains confidential user information. The page is of course protected by various layers of restrictive accessibility including admin restrictions and a confirmation dialog that they are entitled to access the page. This requires an Entity Core code-first write to the database log table of the user, date-time and which page was access when the page is first rendered. The approach would be to determine the current user, from app data, when the page is opened and send that to service for logging.

Typically an async call is required such as:

    await service.LogAccess(user);

but this must be within an async Task, which class property code is not.

One approach is to synchronously make the call from the property Set and wait for its completion such as:

    service.LogAccess(user).GetAwaiter().GetResult()

but this is generally accepted to be bad coding. It can lead to deadlock and other issues. For a clarification see [1] below. Further, with Entity Framework you get error messages saying that the DBContext is in another thread (the UI) and is closed. See the EF tracking error message below.

In this Blazor app I recently came up with another way of doing this, although it may be suboptimal from an esoteric perspective. On each page there is a custom control that gets the logged in user and displays a message to the user. This appears near the top of each page. The control has a UserChanged event that is actioned by the control once it determines the logged in user from app data. The handler for this event in the Razor page code then sets the user local variable for subsequent activity on the page. It has StateHasChanged() code which assures that the page UI is updated when this event fires.

The “trick” is in the OnAfterRenderAsync() page method. By introducing a normally false boolean variable GotUser that is set just before the StateHasChanged call above.

    bool GotUser=false;
    User user = null;
    void  UserChanged(User u)
    {
        if (u != user)
        {
            user = u;
            GotUser=true;
            StateHasChanged();
        }
    }

The UserChanged() code

In OnAfterRenderAsync outside of the if(first){} code this causes the log to be sent if and only if GotUser is set.

    protected override async Task  OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        if (firstRender)
        {
            ...
        }
        if(GotUser)
        {
            GotUser=false;
            await service.LogAccess(user);
        }
    }

The OnAfterRenderAsync() code

As this await is within an async Task it will run in an async manner. Also it makes sure that the EF call is by the UI thread.

Note that calling the log update directly from the UserChanged event handler, after making it an async Task, results in the following EF Sql Tracking error messages:

The EF Tracking Error Message

Microsoft.EntityFrameworkCore.Update: Error: An exception occurred in the database while saving changes for context type 'AthsEssAVHelpers.Data.ApplicationDbContext'.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(DbContext _, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)

[1] Re: .GetAwaiter().GetResult() “Main method in Console apps is an exception to this rule; it is perfectly appropriate to use there. Many code samples use it in this way.” StackOverflow ref. That’s where I’ve seen it used in Microsoft IoT Hub samples.


Conclusions

  • Class property code is synchronous and therefore awaited updates in a property Set is an issue.
    • Getting around this by appending .GetAwaiter().GetResult() to a database update call (without the preceding await) is a poor coding pattern with UI code. It can lead to deadlocks.
  • In general, use the OnPropertyChanged pattern for when the property Set requires an async call, such as in the example on GitHub at: djaus2/PropOnChange
    • There is now a .NET Intearctive Notebook page Notebook.dib there enabling running the app without downloading and building it.
    • The handler in calling code can be an async method so as the awaited database update can be called without the .GetAwaiter().GetResult() appendage.
  • In making an EF call to update a database entity, generally this is done on the UI thread (in Blazor) and so an update that is not directly initiated by the user is bound to fail.
    • This can be resolved with Blazor by updating the Razor page property value, set a flag (that is initially clear), and immediately call StateHasChanged(); in that sequence of code.
    • Then in OnAfterRenderAsync() in the Razor page, if the flag is set, clear it and then make the awaited async database enity update call.

    :)


 TopicSubtopic
   
 This Category Links 
Category:Blazor Index:Blazor
<  Prev:   Blazor Helpers App Members