Why Repository Abstractions Usually Hurt Blazor Apps More Than Help “You didn’t decouple EF Core. You just hid it badly.”
“You didn’t decouple EF Core. You just hid it badly.”
I’ve built this architecture.
More than once.
Early in my Blazor projects, adding repository abstractions
felt like the responsible thing to do. The code looked clean. The layers were
clear. Code reviews were easy.
Then the app grew.
The team changed.
Performance started to matter.
That’s when the repository layer stopped helping—and started
getting in the way.
This article isn’t about theory. It’s about what I’ve
learned the hard way, maintaining long-lived Blazor applications.
Why I Used Repositories (And Why They Felt Right)
I didn’t introduce repositories because I was careless.
I did it because:
- Books
recommended them
- Talks
showcased them
- Juniors
expected them
- “Clean
Architecture” diagrams normalized them
And early on?
They worked.
That’s the trap.
The Year-Two Problem
The pain never shows up in month three.
It shows up when:
- Queries
need tuning
- Pages
get slower
- Features
overlap
- New
developers join
- Nobody
remembers how data really flows
I’d see code like this:
var order = await _repository.GetByIdAsync(id);
order.Complete();
await _repository.UpdateAsync(order);
And ask myself:
- What
does this query load?
- Is it
tracked?
- Does
it include relationships?
- Why is
this page slow?
The repository didn’t answer those questions.
It hid them.
The Decoupling Myth
Here’s the uncomfortable truth:
We were never decoupled from EF Core.
- EF
Core still tracked entities
- EF
Core still dictates performance
- EF
Core behavior still leaks everywhere
The repository didn’t remove the dependency.
It blurred responsibility.
And blurred responsibility is where bugs and performance
problems hide.
Generic Repositories Create Performance Blindness
Blazor apps are sensitive to data shape.
Generic repositories push teams toward:
- GetAll()
- Find()
- In-memory
filtering
- Accidental
over-fetching
When performance degrades, no one knows why—because the
query is abstracted away.
Visibility matters more than reuse in Blazor.
Cargo Cult “Clean Architecture”
One of the hardest lessons I’ve learned is how easily
patterns become rituals.
Junior developers were taught:
“This is how professional systems are built.”
So repositories multiplied.
Interfaces wrapped interfaces.
Services forwarded calls.
No one owned behavior.
Refactoring became risky—not because the domain was complex,
but because the architecture was.
That’s not clean architecture.
That’s architectural superstition.
The Question That Changed Everything
Eventually, I started asking one simple question:
What volatility does this abstraction protect me from?
In most Blazor apps:
- EF
Core is stable
- The
database isn’t changing
- The
domain is evolving
Abstracting EF didn’t protect me from change.
It slowed my response to it.
What I Do Now: Use EF Core Directly (But Correctly)
When I stopped hiding EF Core, I had to be more intentional.
And in Blazor, that means owning DbContext lifetime
explicitly.
Why DbContextFactory Is the Default in Blazor
Blazor components and services:
- Live
longer than HTTP requests
- Can
re-render repeatedly
- May
execute concurrently
DbContext is not thread-safe and not designed for
long-lived usage.
So today, I treat IDbContextFactory<TContext> as the
default.
Reading Data: Query Exactly What the UI Needs
public sealed class OrderSummaryService
{
private readonly
IDbContextFactory<AppDbContext> _dbFactory;
public OrderSummaryService(IDbContextFactory<AppDbContext>
dbFactory)
{
_dbFactory =
dbFactory;
}
public async
Task<OrderSummaryDto> GetAsync(Guid orderId)
{
await using var
db = await _dbFactory.CreateDbContextAsync();
return await
db.Orders
.AsNoTracking()
.Where(o
=> o.Id == orderId)
.Select(o
=> new OrderSummaryDto
{
OrderId = o.Id,
Number
= o.OrderNumber,
Status
= o.Status,
Total
= o.Lines.Sum(l => l.Quantity * l.UnitPrice)
})
.SingleAsync();
}
}
Why this works:
- No
over-fetching
- No
hidden includes
- No
tracking leaks
- Performance
is visible
When something is slow, I know exactly where to look.
Writing Data: Load, Mutate, Save
public sealed class CompleteOrderService
{
private readonly
IDbContextFactory<AppDbContext> _dbFactory;
public CompleteOrderService(IDbContextFactory<AppDbContext>
dbFactory)
{
_dbFactory =
dbFactory;
}
public async Task CompleteAsync(Guid
orderId)
{
await using var
db = await _dbFactory.CreateDbContextAsync();
var order = await
db.Orders
.SingleAsync(o => o.Id == orderId);
order.Complete();
await
db.SaveChangesAsync();
}
}
No repositories.
No Update() calls.
No guessing about the state.
EF Core already knows what changed.
Where This Code Lives Matters
This code does not live:
- In UI
components
- In
generic services
- In
shared utility layers
It lives:
- Near
the feature it supports
- Close
to the UI
- Inside
vertical slices
That proximity keeps reasoning cheap.
When I Still Use Repositories
I haven’t banned repositories.
I’ve narrowed them.
I use them when:
- There’s
a rich domain
- Aggregates
enforce invariants
- Persistence
rules matter
public interface OrderRepository
{
Task<Order> LoadForCompletion(Guid
id);
Task Save(Order
order);
}
This expresses business intent, not CRUD.
Testing Reality
I used to justify repositories for testing.
In practice:
- Fake
repositories lied
- Integration
tests caught real bugs
- SQLite
+ EF Core worked fine
Understanding beat abstraction.
The Real Cost I Didn’t Expect
The highest cost wasn’t performance.
It was mental overhead.
Every new developer had to reconstruct:
- Where the data came from
- What
queries actually ran
- Why
behavior differed
Good architecture reduces thinking.
Bad abstraction multiplies it.
Final Thought
Repository abstractions aren’t evil.
But in Blazor apps, they’re often optimistic architecture—designed
for a future that never arrives.
If I could give my younger self one rule, it would be this:
Favor visibility over abstraction.
Control lifetimes explicitly.
Let EF Core be honest.
Your Blazor app will be simpler.
Your performance issues will be obvious.
And year two will hurt a lot less.

Comments
Post a Comment