SQL Server Blazor and EF core - Different threads concurrently using the same instance of DbContext

dwbf0jvd  于 2023-04-10  发布在  其他
关注(0)|答案(1)|浏览(120)

Blazor and EF core - Different threads concurrently using the same instance of DbContext - Problem

I'm trying to navigate to a Blazor component but the component loads twice.

@page "/MySettings"
@using eKurser_Blazor.Data.Services;
@using eKurser_Blazor.DataDB;
@inject UserService UserService
@inject AuthenticationStateProvider AuthenticationStateProvider

<h1>Get profile</h1>

@if (ErrorMessage != null)
{
    <div class="alert alert-danger">@ErrorMessage</div>
}

<form>
    <div class="form-group">
        <label for="firstName">Fname</label>
        <input type="text" class="form-control" id="firstName" @bind="@Fname" @value="@Fname">
    </div>
    <div class="form-group">
        <label for="lastName">Lname</label>
        <input type="text" class="form-control" id="lastName" @bind="@Lname" @value="@Lname">
    </div>
    <div class="form-group">
        <label for="email">E-mail</label>
        <input type="email" class="form-control" id="email" @bind="@Email" @value="@Email">
    </div>

</form>

This code will be executed twice.

protected override async Task OnInitializedAsync()
{
    var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
    var user = authState.User;
    string userIdClaim = user.FindFirst("UserID")?.Value;

    if (!string.IsNullOrEmpty(userIdClaim))
    {
        var _user = await UserService.GetUserByIdAsync(userIdClaim);
        if (_user != null)
        {

        }
    }
}

I have changed _Host.cshtml according to:

<component type="typeof(App)" render-mode="Server" />

Blazor rendering content twice

But I will end up with error:
Error: System.InvalidOperationException: There is already an open DataReader associated with this Connection which must be closed first. at Microsoft.Data.SqlClient.SqlCommand.<>c.b__208_0(Task 1 result) at System.Threading.Tasks.ContinuationResultTaskFromResultTask 2.InnerInvoke() at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)

UserService.cs

public class UserService
    {
        private readonly MyDbContext _dbContext;

        public UserService(MyDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<TblUser> AuthenticateAsync(string email, string password)
        {
            if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
            {
                return null;
            }

            TblUser user = null;

            try
            {
                user = await _dbContext.TblUsers.SingleOrDefaultAsync(x => x.Email == email);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
                return null;
            }

            if (user == null)
            {
            }

            if (user == null || !PasswordHash.ValidatePassword(password, user.Pwd))
            {
                return null;
            }

            return user;
        }
        public async Task<TblUser> GetUserByIdAsync(string userId)
        {
            if (string.IsNullOrEmpty(userId))
            {
                return null;
            }

            var user = await _dbContext.TblUsers.SingleOrDefaultAsync(x => x.UserId == userId);
            return user;
        }      

        private bool UserExists(string userId)
        {
            return _dbContext.TblUsers.Any(e => e.UserId == userId);
        }

Program.cs

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<UserService>();

builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthenticationStateProvider>());

var connString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContextFactory<MyDbContext>((DbContextOptionsBuilder option
    ) => option.UseSqlServer(connString));

// Add services to the container.
builder.Services.AddControllersWithViews();

If I Invoke the Method manually after the Component is loaded there is no problem, but I would of course like to load and present the data as automatically as the component is loaded.

<button type="submit" class="btn btn-primary" @onclick="GetUserInfo">Ladda</button>

   @code {
    private async Task GetUserInfo()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        string userIdClaim = user.FindFirst("UserID")?.Value;

        if (!string.IsNullOrEmpty(userIdClaim))
        {
            var _user = await UserService.GetUserByIdAsync(userIdClaim);
            if (_user != null)
            {

            }
        }
    }
}

UPDATE 1

Edited UserService.cs

private readonly IDbContextFactory<MyDbContext> _factory;

public UserService(IDbContextFactory<MyDbContext> factory)
{
    _factory = factory;
}

This removed the error, but the component still triggers OnInitializedAsync() twice. And this will show data first time, but after navigating to another page and back, var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); is empty.

Update 2

The issue for missing data on navigation or update was the incorrect implementation of saving the AuthenticationState. I installed Blazored.LocalStorage to save the State in localStorage. OnInitializedAsync() is still triggered twice

Update 3

I'm not really sure what I did but fixing the AuthenticationState and restarting VS solved my issue!

hrirmatl

hrirmatl1#

Your problems may not be directly related.

Let's deal with the DbContext problem first.

You are attempting to use the same DbContext in two operations at the same time. This is a common problem in asynchronous operations. You already have the IDbContextFactory loaded into DI, you just need to start using it to create transactional/unit of work contexts.

Here's your UserService :

public class UserService
    {
        private readonly IDbContextFactory _factory;

        public UserService(IDbContextFactory factory)
        {
            _factory = factory;
        }
        //...

        public async ValueTask SomeWorkAsync()
        {
            using var dbContext = _factory.CreateDbContext();

            // await do something with dbContext
        }
    }

On loading twice, there are three probable reasons OnInitializedAsync is loading twice:

  1. You are reloading the SPA, or this is your startup page.
  2. You are calling it manually.
  3. You are loading more than one instance of the component.

Where are you navigating to MySettings and how?

Update

You need to establish if the code is loading twice or you are loading two different components.

Add this to MySettings and check the

@code {
    public readonly Guid ComponentUid = Guid.NewGuid();

    protected override void OnInitialized()
    {
        Debug.WriteLine($"XXX OnInitialized called on Component Uid = {ComponentUid.ToString()}");
    }

    protected override Task OnInitializedAsync()
    {
        Debug.WriteLine($"XXX OnInitializedAsync called on Component Uid = {ComponentUid.ToString()}");
        //...
    }

}

And check the Output.

相关问题