As .NET developers, we often reach for Entity Framework Core (EF Core) as our default data access layer. Typically, we spin up a single AppDbContext, register it in our Dependency Injection (DI) container, and inject it everywhere.

For a Proof of Concept (PoC) or a small app, this is fine. But as your application scales—whether it’s a high-traffic Blazor Server app, a complex MAUI Hybrid solution, or a REST API—using a single context for everything can become a bottleneck.

There is a simple architectural pattern that yields massive benefits in performance, security, and scalability: Splitting your Database Context into two—a standard Write Context and a specialized Read-Only Context.

Here is why you should consider this approach and how to implement it.


1. Performance: The “No-Tracking” Default

By default, EF Core uses Change Tracking. When you query data, EF constructs a snapshot of the entities. When you call SaveChanges(), it compares the current state against the snapshot to generate SQL updates.

This is expensive. It consumes memory and CPU cycles.

When you are simply populating a UI grid in Blazor or returning a JSON list in an API, you don’t need change tracking. You aren’t going to update those specific objects.

While you can add .AsNoTracking() to every LINQ query, it is brittle. Developers forget.
With a dedicated ReadOnlyDbContext, you configure this behavior globally:

Now, every query runs lean by default. No snapshots, no tracking overhead.

2. Scalability: Utilizing Read Replicas

This is the strongest argument for enterprise applications.

Most applications follow the 80/20 rule: 80% of database operations are reads, and 20% are writes.
Modern databases (PostgreSQL, SQL Server, Aurora) support Read Replicas. You have one Primary node for writing and several Read-Only nodes for querying.

If you only have one AppDbContext, you are forced to point it at the Primary node, creating a bottleneck.

By splitting your contexts, you can configure different connection strings in your appsettings.json:

  • WriteContext Points to Primary Node (Port 5432)
  • ReadOnlyContext Points to Replica Node (Port 5433)

This instantly offloads 80% of your traffic away from your transactional database, allowing your application to scale horizontally at the database layer with almost no code changes in your business logic.

3. Security: The Principle of Least Privilege

Security is best implemented in layers (Defense in Depth).

If a hacker manages to perform a SQL Injection attack (rare with EF, but possible with raw SQL interpolation) or compromises a connection string in a specific microservice, you want to limit the blast radius.

The database user credentials used for your ReadOnlyDbContext should have SELECT permissions only.

If someone tries to execute a cheeky DROP TABLE Users or UPDATE Admin SET Password via the Read-Only context, the database engine itself will reject the command. You cannot achieve this protection with a single context that requires write permissions.

4. Code Intent and Architecture (CQRS-Lite)

Command Query Responsibility Segregation (CQRS) is a powerful pattern, but implementing it fully with MediatR and separate Read/Write models can be overkill for many projects.

Using two contexts offers a “CQRS-Lite” experience. It forces the developer to think about intent:

“Am I changing state, or am I just looking at data?”

If you inject IReadOnlyAppDbContext, the intent is clear: This method does not modify state. It makes code reviews easier and prevents accidental side effects where a “Get” method secretly modifies data.


Implementation Strategy

You don’t need to duplicate your DbSet properties. Use inheritance.

Step 1: The Base Context (Write)

Step 2: The Read-Only Context

Step 3: Registration (Program.cs)

Note for Blazor Server / Hybrid Developers

If you are working with Blazor Server or .NET MAUI Blazor Hybrid, remember that DbContext is not thread-safe and Scoped lifetimes can be tricky with SignalR circuits.

You should use IDbContextFactory instead. You can register factories for both:


Conclusion

Splitting your DbContext is a high-reward, low-effort architectural decision. It prepares your application for scaling (Read Replicas), optimizes performance by default (No Tracking), and hardens your security posture (Least Privilege).

Next time you initialize a project in Visual Studio or Rider, consider creating that ReadOnlyUser right from the start. Your future infrastructure will thank you.