Skip to content

Entity Framework Core read-only contexts

Use Cratis Arc’s read-only Entity Framework Core contexts for code that should query data rather than save changes. Combine the base class with read-only registration to get automatic navigation loading, no-tracking reads, and SaveChanges guards. These guards catch accidental saves; use database permissions when you must prevent writes.

Derive your query context from ReadOnlyDbContext. This declaration assumes your application supplies a Customer entity:

using Cratis.Arc.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class StoreDbContext(DbContextOptions<StoreDbContext> options) : ReadOnlyDbContext(options)
{
public DbSet<Customer> Customers => Set<Customer>();
}

Register it through WithEntityFrameworkCore discovery or this explicit service-registration fragment:

services.AddReadOnlyDbContextWithConnectionString<StoreDbContext>("Data Source=store.db");

Import Microsoft.Extensions.DependencyInjection for service registration.

Mechanism Behavior
ReadOnlyDbContext inheritance Includes BaseDbContext conversions; overrides SaveChanges() and SaveChangesAsync(CancellationToken) to throw; enables eager loading for ordinary navigations
AddReadOnlyDbContext<T> Registers a pooled factory and scoped context; configures NoTracking and a SaveChanges interceptor; accepts any EF DbContext subclass
AddReadOnlyDbContextWithConnectionString<T> Adds provider detection to read-only registration; requires a BaseDbContext subclass

The base class and registration helpers do different work. Inheritance alone does not set NoTracking or override the Boolean acceptAllChangesOnSuccess SaveChanges overloads. Read-only registration adds an interceptor that rejects the SaveChanges path, including those overloads.

Neither mechanism prohibits raw SQL, ExecuteUpdate, ExecuteDelete, or writes through another context. Use database credentials with read-only permissions when preventing writes is a requirement.

ReadOnlyDbContext marks the navigation properties returned by EF’s GetNavigations() as auto-included, so EF loads those related entities without an explicit Include call. Inspect the generated queries for your model before relying on this default. The loop does not include EF skip navigations, which represent many-to-many relationships without an explicit join entity.

To disable this behavior globally, add this member to your derived context:

protected override bool IsEagerLoadingEnabled => false;

To skip auto-includes for an individual query:

var customers = await context.Customers.IgnoreAutoIncludes().ToListAsync();

To override a particular navigation, configure it after the base call so your setting takes precedence:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Customer>().Navigation(customer => customer.Orders).AutoInclude(false);
}

These are context/query fragments using your application’s entities. EF’s own rules for owned navigations still apply.

Assembly scanning selects exported, nonabstract ReadOnlyDbContext subclasses and excludes [IgnoreAutoRegistration] types. It does not discover every arbitrary DbContext.

services.AddReadModelDbContextsWithConnectionStringFromAssemblies(
"Data Source=store.db",
(serviceProvider, options) => options.EnableDetailedErrors(),
typeof(StoreDbContext).Assembly);

Continue with automatic database hookup to understand provider selection and pooled context registration. If clients need refreshed query results after changes, follow DbSet observation, including its provider setup and lifetime requirements.