C
C#2mo ago
Caleb Weeks

Data fetching in components?

I'm using Blazor Server. I have set up a partial class called DataContextComponent that uses DBContextFactory to create an instance of dbContext for the lifetime of that component. I thought this would be enough to keep components isolated, but I'm finding that if there is a parent and child component that both inherit from DataContextComponent and fetch data using EF Core, I get an error saying "a second operation was started on this context instance before a previous operation completed." Am I doing something wrong? I'd like to make reusable components that fetch their own data that I can use throughout the application. Is this an anti pattern? Thanks for the advice!
10 Replies
Joschi
Joschi2mo ago
This is what I'm using
c#
public abstract class UnitOfWorkComponentBase : ComponentBase, IDisposable, IAsyncDisposable
{
[Inject]
public required IDbContextFactory<SeasonContext> ContextFactory { get; init; }

protected SeasonContext Context { get; set; } = null!;

protected override async Task OnInitializedAsync()
{
Context = await ContextFactory.CreateDbContextAsync();
await base.OnInitializedAsync();
}

public void Dispose()
{
Context.Dispose();
}

public async ValueTask DisposeAsync()
{
await Context.DisposeAsync();
}
}
c#
public abstract class UnitOfWorkComponentBase : ComponentBase, IDisposable, IAsyncDisposable
{
[Inject]
public required IDbContextFactory<SeasonContext> ContextFactory { get; init; }

protected SeasonContext Context { get; set; } = null!;

protected override async Task OnInitializedAsync()
{
Context = await ContextFactory.CreateDbContextAsync();
await base.OnInitializedAsync();
}

public void Dispose()
{
Context.Dispose();
}

public async ValueTask DisposeAsync()
{
await Context.DisposeAsync();
}
}
And then you can just inherit it in your component with the @inherits directive. But you are responsible to ensure that the user cannot trigger two actions at the same time. For example if you have two buttons, both resulting in a database call, it would throw a concurrency exception, if the user clicks both buttons in rapid succession, before the first database call finishes.
Caleb Weeks
Caleb WeeksOP2mo ago
Thanks for the response! Somehow I missed this message until just now. My implementation looks very similar. It's been working for the most part, but when I tried fetching data in OnParametersSetAsync in the child component, I'm getting that error. If I move the data loading to OnInitializedAsync in the child component or comment out the data fetching in the parent component, the error goes away. So it has something to do with the interaction between the two components. Here's my version (essentially the same):
public partial class DataContextComponent : ComponentBase, IDisposable
{
[Inject] public IDbContextFactory<DataContext> DbFactory { get; set; }

protected DataContext dbContext;

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
dbContext ??= await DbFactory.CreateDbContextAsync();
}

public void Dispose() => dbContext.Dispose();
}
public partial class DataContextComponent : ComponentBase, IDisposable
{
[Inject] public IDbContextFactory<DataContext> DbFactory { get; set; }

protected DataContext dbContext;

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
dbContext ??= await DbFactory.CreateDbContextAsync();
}

public void Dispose() => dbContext.Dispose();
}
Joschi
Joschi2mo ago
It's probably not important, but why is your class partial? Also could you share a minimal example, of how to produce that error?
Caleb Weeks
Caleb WeeksOP2mo ago
It might be important... I'm fairly new to C# and OOP in general, so I thought I needed to make it partial so that the inheriting class had access to the protected dbContext. I'll put together a minimal example tomorrow morning.
Joschi
Joschi2mo ago
partial allows you to split a class between different files. Like you could now go into a diferent .cs file, redeclare the class and add some properties or methods. Most of the time it is only done to allow source generators to extend your class. protected as an accessibility modifier means "Only I and inheriting classes can access this variable". So that already accomplishes your goal. In Blazor partial is also used for codebehind files mycomponent.razor.cs. That is possible because the .razor files are actually being translated into a C# class behind the scenes by a source generator.
Caleb Weeks
Caleb WeeksOP2mo ago
Alright, I spent some time this morning making a minimal reproduction of the error and have determined that it's not caused by the parent and child components both using dbContext. It has more to do with OnParametersSetAsync being called twice in rapid succession on a component with a @bind-Value directive. Here's some code that causes the error: [Parent.razor]
@page "/Parent"

<Child @bind-Value="value" />

@code {
private int value;

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Task.Delay(1);
}
}
@page "/Parent"

<Child @bind-Value="value" />

@code {
private int value;

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await Task.Delay(1);
}
}
[Child.razor]
@using Core.Data
@inherits DataContextComponent

@code {
[Parameter] public int Value { get; set; }
[Parameter] public EventCallback<int> ValueChanged { get; set; }

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}

protected override async Task OnParametersSetAsync()
{
await dbContext.MaterialInstances.ToListAsync();
}
}
@using Core.Data
@inherits DataContextComponent

@code {
[Parameter] public int Value { get; set; }
[Parameter] public EventCallback<int> ValueChanged { get; set; }

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}

protected override async Task OnParametersSetAsync()
{
await dbContext.MaterialInstances.ToListAsync();
}
}
Joschi
Joschi2mo ago
If you use prerendering, then OnParameterSet will always be called twice. https://github.com/dotnet/aspnetcore/issues/30037 It will also always be rerun, when blazor believes the parameter may have changed. If your db call does not depend on the parameter I suggest moving it into OnInitializedAsync after the call to the base method. If it has to be in OnParameterSet you could create a CancellationToken using a CancellationTokenSource and cancel the previous db call. Or just create a new DbContext.
GitHub
OnParametersSetAsync is being invoked multiple times in certain sce...
Describe the bug I have a page which can be visited through out a parameter. in my case @page "/Test{CustomerId:int}" When I visit the page with a direct link within my blazor project OnP...
Caleb Weeks
Caleb WeeksOP2mo ago
Oh, that's an interesting idea. I could have a variation of the DataContextComponent that creates a new dbContext every time OnParametersSetAsync gets called. I think I'll look more into the Cancellation token approach. In this case, I need the value from the parameter in order to do the data fetching. Thanks for the help!
Joschi
Joschi2mo ago
If you create a new one every time you should just use the IDbContextFactory directly.
Caleb Weeks
Caleb WeeksOP2mo ago
That's true...
Want results from more Discord servers?
Add your server