Multi-Tenanted Documents

Marten supports multi-tenancy to provide data isolation between tenants, aka groups of users. In effect, this allows scoping storage operations, such as persisting and loading data, so that no tenant can access data of others. Marten provides multi-tenancy at the logical level, by associating data records with a tenant identifier. In addition, multi-tenancy through separate databases or schemas is planned.

By default, Marten operates in single-tenancy mode (TenancyStyle.Single) with multi-tenancy disabled.

Once configured for multi-tenancy, Marten exposes it via sessions (IQuerySession, IDocumentSession) scoped to specific tenants, as well as various overloads to saving operations that accept a tenant identifier.

Scoping Sessions to Tenancy

The following sample demonstrates scoping a document session to tenancy identified as tenant1. With multi-tenancy enabled, the persisted User objects are then associated with the tenancy of the session.

// Write some User documents to tenant "tenant1"
using (var session = theStore.OpenSession("tenant1"))
{
    session.Store(new User { Id = "u1", UserName = "Bill", Roles = new[] { "admin" } });
    session.Store(new User { Id = "u2", UserName = "Lindsey", Roles = new string[0] });
    session.SaveChanges();
}

snippet source | anchor

// Write some User documents to tenant "tenant1"
using (var session = theStore.OpenSession("tenant1"))
{
    session.Store(new User { Id = "u1", UserName = "Bill", Roles = new[] { "admin" } });
    session.Store(new User { Id = "u2", UserName = "Lindsey", Roles = new string[0] });
    session.SaveChanges();
}

snippet source | anchor

// Write some User documents to tenant "tenant1"
using (var session = store.OpenSession("tenant1"))
{
    session.Store(new User { UserName = "Bill" });
    session.Store(new User { UserName = "Lindsey" });
    session.SaveChanges();
}

snippet source | anchor

As with storing, the load operations respect tenancy of the session.

// When you query for data from the "tenant1" tenant,
// you only get data for that tenant
using (var query = store.QuerySession("tenant1"))
{
    query.Query<User>()
        .Select(x => x.UserName)
        .ToList()
        .ShouldHaveTheSameElementsAs("Bill", "Lindsey");
}

snippet source | anchor

Lastly, unlike reading operations, IDocumentSession.Store offers an overload to explicitly pass in a tenant identifier, bypassing any tenancy associated with the session. Similar overload for tenancy exists for IDocumentStore.BulkInsert.

Default Tenancy

With multi-tenancy enabled, Marten associates each record with a tenancy. If no explicit tenancy is specified, either via policies, mappings, scoped sessions or overloads, Marten will default to Tenancy.DefaultTenantId with a constant value of *DEFAULT*.

The following sample demonstrates persisting documents as non-tenanted, under default tenant and other named tenants then querying them back in a session scoped to a specific named tenant and default tenant.

using var store = DocumentStore.For(opts =>
{
    opts.DatabaseSchemaName = "mixed_multi_tenants";
    opts.Connection(ConnectionSource.ConnectionString);
    opts.Schema.For<Target>().MultiTenanted(); // tenanted
    opts.Schema.For<User>(); // non-tenanted
    opts.Schema.For<Issue>().MultiTenanted(); // tenanted
});

store.Advanced.Clean.DeleteAllDocuments();

// Add documents to tenant Green
var greens = Target.GenerateRandomData(10).ToArray();
store.BulkInsert("Green", greens);

// Add documents to tenant Red
var reds = Target.GenerateRandomData(11).ToArray();
store.BulkInsert("Red", reds);

// Add non-tenanted documents
// User is non-tenanted in schema
var user1 = new User {UserName = "Frank"};
var user2 = new User {UserName = "Bill"};
store.BulkInsert(new[] {user1, user2});

// Add documents to default tenant
// Note that schema for Issue is multi-tenanted hence documents will get added
// to default tenant if tenant is not passed in the bulk insert operation
var issue1 = new Issue {Title = "Test issue1"};
var issue2 = new Issue {Title = "Test issue2"};
store.BulkInsert(new[] {issue1, issue2});

// Create a session with tenant Green
using (var session = store.QuerySession("Green"))
{
    // Query tenanted document as the tenant passed in session
    session.Query<Target>().Count().ShouldBe(10);

    // Query non-tenanted documents
    session.Query<User>().Count().ShouldBe(2);

    // Query documents in default tenant from a session using tenant Green
    session.Query<Issue>().Count(x => x.TenantIsOneOf(Tenancy.DefaultTenantId)).ShouldBe(2);

    // Query documents from tenant Red from a session using tenant Green
    session.Query<Target>().Count(x => x.TenantIsOneOf("Red")).ShouldBe(11);
}

// create a session without passing any tenant, session will use default tenant
using (var session = store.QuerySession())
{
    // Query non-tenanted documents
    session.Query<User>().Count().ShouldBe(2);

    // Query documents in default tenant
    // Note that session is using default tenant
    session.Query<Issue>().Count().ShouldBe(2);

    // Query documents on tenant Green
    session.Query<Target>().Count(x => x.TenantIsOneOf("Green")).ShouldBe(10);

    // Query documents on tenant Red
    session.Query<Target>().Count(x => x.TenantIsOneOf("Red")).ShouldBe(11);
}

snippet source | anchor

In some cases, You may want to disable using the default tenant for storing documents, set StoreOptions.DefaultTenantUsageEnabled to false. With this option disabled, Tenant (non-default tenant) should be passed via method argument or SessionOptions when creating a session using document store. Marten will throw an exception DefaultTenantUsageDisabledException if a session is created using default tenant.

Configuring Tenancy

The three levels of tenancy that Marten supports are expressed in the enum TenancyStyle with effective values of:

  • Single, no multi-tenancy
  • Conjoined, multi-tenancy through tenant id
  • Separate, multi-tenancy through separate databases or schemas

Tenancy can be configured at the store level, applying to all documents or, at the most fine-grained level, on individual documents.

Tenancy Through Policies

Tenancy can be configured through Document Policies, accessible via StoreOptions.Policies. The following sample demonstrates setting the default tenancy to TenancyStyle.Conjoined for all documents.

storeOptions.Policies.AllDocumentsAreMultiTenanted();
// Shorthand for
// storeOptions.Policies.ForAllDocuments(_ => _.TenancyStyle = TenancyStyle.Conjoined);

snippet source | anchor

Tenancy At Document Level & Policy Overrides

Tenancy can be configured at a document level through document mappings. This also enables overriding store-level configurations applied through Document Policies. The following sample demonstrates setting, through StoreOptions the tenancy for Target to TenancyStyle.Conjoined, making it deviate from the configured default policy of TenancyStyle.Single.

storeOptions.Policies.ForAllDocuments(x => x.TenancyStyle = TenancyStyle.Single);
storeOptions.Schema.For<Target>().MultiTenanted();

snippet source | anchor

Implementation Details

At the moment, Marten implements two modes of tenancy, namely single tenancy and conjoined multi-tenancy.

Conjoined Tenancy

The conjoined (TenancyStyle.Conjoined) multi-tenancy in Marten is implemented by associating each record with a tenant identifier. As such, Marten does not guarantee or enforce data isolation via database access privileges.

Effects On Schema

Once enabled, TenancyStyle.Conjoined introduces a tenant_id column to Marten tables. This column, of type varchar with the default value of *DEFAULT* (default tenancy), holds the tenant identifier associated with the record. Furthermore, Marten creates an index on this column by default.

A unique index may optionally be scoped per tenant (see unique indexes).