Skip to content

Read only access

Mirko Da Corte edited this page Aug 25, 2026 · 8 revisions

A db context, or a single repository, can be configured read-only: every write on the underlying collections is denied at the driver boundary, while reads keep working normally. Use it to consume collections owned by another application, with no possibility of writing to them by mistake.

Configuring it

Make a whole context read-only from its options:

builder.Services.AddScriniumWithHangfire()
    .AddDbContext<IReadOnlySampleDbContext, ReadOnlySampleDbContext>(options =>
    {
        options.ConnectionString = connectionString;   // the owner application's database
        options.IsReadOnly = true;
    });

Or make a single repository read-only from its RepositoryOptions, keeping the rest of the context writable:

public IRepository<Cat, string> Cats { get; } = new Repository<Cat, string>(
    new RepositoryOptions<Cat>("cats") { IsReadOnly = true });

IRepository.IsReadOnly reports the effective flag — true when required by the repository's own options or by the context's — and repositories with different flags coexist on the same context.

What is denied, what keeps working

Enforcement lives on the guarded collection wrapper, so it also covers driver-level operations through AccessToCollectionAsync. A denied operation throws UnauthorizedAccessException.

Denied on a read-only collection Keeps working
Creates, replaces, deletes, bulk updates and upserts Finds, queries, cursors, counts
Aggregates writing to a collection ($out/$merge as last stage) and map-reduce with a collection output Read aggregates, inline map-reduce
Tracked saves (SaveChangesAsync) reaching the collection Loading and tracking models in memory
Index and search-index management: creations, drops, updates Index listing
Starting migrations and seeding (see below) Migration status and history reads

Lifecycle of a read-only context

The database lifecycle belongs to the owner application, so a read-only context stays out of it:

  • Seeding is skipped: SeedIfNeededAsync logs and returns false, so SeedDbContexts() at startup keeps working with a mix of writable and read-only contexts.
  • Migrations are denied: TryStartMigrationAsync returns null, and the Admin dashboard renders the context with a Read-only badge, without migration controls or status polling. Its Model schemas, Document structures and Missing origin references sections still work — counting and scanning are reads — telling you when the owner application has documents left on a deprecated schema, or dangling references; only the references removal, a write, is denied.
  • There are no locks: reading Engine.DbContextLock, and asking for a resource lock (GetResourceLock, or the TryAcquireResourceLockAsync and IsResourceLockedAsync facades), throw InvalidOperationException, since claiming a lock would write the lock collection of a database this context can only read. Nothing in Scrinium reaches for the db context lock on a read-only context — the works it coordinates are already denied; coordinate your own locking through a db context the application can write.

On a writable context, migrations skip the index steps of its read-only repositories: the indexes of a shared collection belong to the collection owner, and must not be dropped or rebuilt by a consumer.

The shared collection pattern

The consumer declares its own context over the owner's database, read-only:

public interface IReadOnlySampleDbContext : IDbContext
{
    IRepository<Cat, string> Cats { get; }
}

public class ReadOnlySampleDbContext : DbContext, IReadOnlySampleDbContext
{
    public IRepository<Cat, string> Cats { get; } = new Repository<Cat, string>("cats");

    protected override IEnumerable<IModelMapsCollector> ModelMapsCollectors =>
        [new ModelBaseMap(), new CatMap()];
}

When the consumer also owns collections of its own on the same database, use a single writable context and mark only the shared repositories read-only through their RepositoryOptions.

Note. The flag protects the ODM surface, not intentional bypasses: Engine.Database still hands out the raw driver database, like it does under exclusive access.


Next: Exclusive access for the other access limitation, or Repositories for the rest of RepositoryOptions.

Clone this wiki locally