Opening Sessions

IDocumentStore is the root of Marten usage, but most Marten usage in code will start with one of the session types that can be created from an IDocumentStore. The following diagram explains the relationship between the different flavors of session and the root store:

DocumentStore and Session Types

While there are sections below describing each session in more detail, at a high level the different types of sessions are:

CreationRead/WriteIdentity MapDirty Checking
IDocumentStore.QuerySession()Read OnlyNoNo
IDocumentStore.OpenSession()Read/WriteYesNo
IDocumentStore.DirtyTrackedSession()Read/WriteYesYes
IDocumentStore.LightweightSession()Read/WriteNoNo

Read Only QuerySession

For strictly read-only querying, the QuerySession is a lightweight session that is optimized for reading. The IServiceCollection.AddMarten() configuration will set up a DI registration for IQuerySession, so you can inject that into classes like this sample MVC controller:

public class GetIssueController: ControllerBase
{
    private readonly IQuerySession _session;

    public GetIssueController(IQuerySession session)
    {
        _session = session;
    }

    [HttpGet("/issue/{issueId}")]
    public Task<Issue> Get(Guid issueId)
    {
        return _session.LoadAsync<Issue>(issueId);
    }

    [HttpGet("/issue/fast/{issueId}")]
    public Task GetFast(Guid issueId)
    {
        return _session.Json.WriteById<Issue>(issueId, HttpContext);
    }

}

snippet source | anchor

If you have an IDocumentStore object though, you can open a query session like this:

using var store = DocumentStore.For(opts =>
{
    opts.Connection("some connection string");
});

using var session = store.QuerySession();

var badIssues = await session.Query<Issue>()
    .Where(x => x.Tags.Contains("bad"))
    .ToListAsync();

snippet source | anchor

Identity Map Mechanics

Identity Map:

Ensures that each object gets loaded only once by keeping every loaded object in a map. Looks up objects using the map when referring to them.

-- Martin Fowler

Marten's IDocumentSession implements the Identity Map pattern that seeks to cache documents loaded by id. This behavior can be very valuable, for example, in handling web requests or service bus messages when many different objects or functions may need to access the same logical document. Using the identity map mechanics allows the application to easily share data and avoid the extra database access hits -- as long as the IDocumentSession is scoped to the web request.

public void using_identity_map()
{
    var user = new User { FirstName = "Tamba", LastName = "Hali" };
    theStore.BulkInsert(new[] { user });

    // Open a document session with the identity map
    using (var session = theStore.OpenSession())
    {
        session.Load<User>(user.Id)
            .ShouldBeTheSameAs(session.Load<User>(user.Id));
    }
}

snippet source | anchor

Do note that using the identity map functionality can be wasteful if you aren't able to take advantage of the identity map caching in a session. In those cases, you may want to either use the IDocumentStore.LightweightSession() which forgos the identity map functionality, or use the read only IQuerySession alternative. RavenDb users will note that Marten does not (yet) support any notion of Evict() to manually remove documents from identity map tracking to avoid memory usage problems. Our hope is that the existence of the lightweight session and the read only interface will alleviate the memory explosion problems that you can run into with naive usage of identity maps or the dirty checking when fetching a large number of documents.

The Identity Map functionality is applied to all documents loaded by Id or Linq queries with IQuerySession/IDocumentSession.Query<T>(). Documents loaded by user-supplied SQL in the IQuerySession.Query<T>(sql) mechanism bypass the Identity Map functionality.

Ejecting Documents from a Session

If for some reason you need to completely remove a document from a session's identity map and unit of work tracking, as of Marten 2.4.0 you can use the IDocumentSession.Eject<T>(T document) syntax shown below in one of the tests:

[Fact]
public void demonstrate_eject()
{
    var target1 = Target.Random();
    var target2 = Target.Random();

    using (var session = theStore.OpenSession())
    {
        session.Store(target1, target2);

        // Both documents are in the identity map
        session.Load<Target>(target1.Id).ShouldBeTheSameAs(target1);
        session.Load<Target>(target2.Id).ShouldBeTheSameAs(target2);

        // Eject the 2nd document
        session.Eject(target2);

        // Now that 2nd document is no longer in the identity map
        SpecificationExtensions.ShouldBeNull(session.Load<Target>(target2.Id));

        session.SaveChanges();
    }

    using (var session = theStore.QuerySession())
    {
        // The 2nd document was ejected before the session
        // was saved, so it was never persisted
        SpecificationExtensions.ShouldBeNull(session.Load<Target>(target2.Id));
    }
}

snippet source | anchor

Connection Handling

Marten uses a single connection to the Postgresql database in each IQuerySession or IDocumentSession. The connection is only opened on the first call to the database, but after that remains open until the IQuerySession/IDocumentSession is disposed. A couple things to note:

  • It's imperative that any IQuerySession/IDocumentSession opened is disposed in order to recover and reuse connections to the underlying database
  • Because the connection is "sticky" to the session, you can utilize serializable transactions. In the future, Marten will also enable you to opt into locking documents read from the session.

There is no place within Marten where it keeps a stateful connection open across sessions.

Command Timeouts

By default, Marten just uses the underlying timeout configuration from the Npgsql connection string. You can though, opt to set a different command timeout per session with this syntax:

public void ConfigureCommandTimeout(IDocumentStore store)
{
    // Sets the command timeout for this session to 60 seconds
    // The default is 30
    using (var session = store.OpenSession(new SessionOptions {Timeout = 60}))
    {

    }
}

snippet source | anchor

Unit of Work Mechanics

TIP

The call to IDocumentSession.SaveChanges() tries to batch all the queued updates and deletes into a single ADO.Net call to PostgreSQL. Our testing has shown that this technique is much faster than issuing one ADO.Net call at a time.

At this point, the IDocumentSession is the sole unit of work for transactional updates -- but that may change later as Marten outgrows its origin as a replacement for RavenDb. As shown before, document sessions come in three flavors (lightweight, identity map tracking, and identity map + dirty checking), but there are only two modes of change tracking:

  1. Lightweight and the standard "identity map" sessions require users to do all the change tracking manually and tell the IDocumentSession what documents have changed
  2. The "dirty checking" session tries to determine which documents loaded from that IDocumentSession has any changes when IDocumentSession.SaveChanges() is called

INFO

When using a Guid/CombGuid, Int, or Long identifier, Marten will ensure the identity is set immediately after calling IDocumentSession.Store on the entity.

TODO -- Need to talk about SaveChanges / SaveChangesAsync here!

Adding Listeners

See Diagnostics and Instrumentation for information about using document session listeners.

Enlisting in Existing Transactions

Before Marten 2.4.0, a Marten IDocumentSession always controlled the lifecycle of its underlying database connection and transaction boundaries. With the 2.4.0+ release, you can pass in an existing transaction or connection, direct Marten to enlist in an ambient transaction scope, and even direct Marten on whether or not it owns the transaction boundaries to override whether or not SaveChanges/SaveChangesAsync will commit the underlying transaction.

Do note that the transaction scope enlisting is only available in either the full .Net framework (> .Net 4.6) or applications targeting Netstandard 2.0.

public void samples(IDocumentStore store, NpgsqlConnection connection, NpgsqlTransaction transaction)
{
    // Use an existing connection, but Marten still controls the transaction lifecycle
    var session1 = store.OpenSession(new SessionOptions
    {
        Connection = connection
    });

    // Enlist in an existing Npgsql transaction, but
    // choose not to allow the session to own the transaction
    // boundaries
    var session2 = store.OpenSession(new SessionOptions
    {
        Transaction = transaction,
        OwnsTransactionLifecycle = false
    });

    // This is syntactical sugar for the sample above
    var session3 = store.OpenSession(SessionOptions.ForTransaction(transaction));

    // Enlist in the current, ambient transaction scope
    using (var scope = new TransactionScope())
    {
        var session4 = store.OpenSession(SessionOptions.ForCurrentTransaction());
    }

    // or this is the long hand way of doing the options above
    using (var scope = new TransactionScope())
    {
        var session5 = store.OpenSession(new SessionOptions
        {
            EnlistInAmbientTransactionScope = true,
            OwnsTransactionLifecycle = false
        });
    }
}

snippet source | anchor

Transaction Isolation Level

New in v0.7 is the ability to configure the transaction isolation level when opening a new IDocumentSession by supplying the optional isolationLevel argument. As of v0.9.2, the default level is ReadCommitted.

As one of the use cases that spawned this feature, say that you are using the Saga pattern in a service bus architecture. When handling a message with this pattern, you typically want to load some kind of persisted state for the long running saga, do some work, then persist the updated saga state. If you need to worry about serializing the messages for a single saga, you might want to use serializable transactions like this:

public class MySagaState
{
    public Guid Id;
}

public void execute_saga(IDocumentStore store, Guid sagaId)
{
    // The session below will open its connection and start a
    // serializable transaction
    using (var session = store.DirtyTrackedSession(IsolationLevel.Serializable))
    {
        var state = session.Load<MySagaState>(sagaId);

        // do some work against the saga

        session.SaveChanges();
    }
}

snippet source | anchor

Manual Change Tracking

The first step is to create a new DocumentSession with the IDocumentStore.LightweightSession() (or IDocumentStore.OpenSession()):

public void lightweight_document_session(IDocumentStore store)
{
    using (var session = store.LightweightSession())
    {
        var user = new User { FirstName = "Jeremy", LastName = "Miller" };

        // Manually adding the new user to the session
        session.Store(user);

        var existing = session.Query<User>().Single(x => x.FirstName == "Max");
        existing.Internal = false;

        // Manually marking an existing user as changed
        session.Store(existing);

        // Marking another existing User document as deleted
        session.Delete<User>(Guid.NewGuid());

        // Persisting the changes to the database
        session.SaveChanges();
    }
}

snippet source | anchor

Do note that Marten's Store() method makes no distinctions between inserts and updates. The Postgresql functions generated by Marten to update the document storage tables perform "upserts" for you. Anytime a document is registered through IDocumentSession.Store(document), Marten runs the "auto-assignment" policy for the id type of that document. See identity for more information on document id's.

Automatic Dirty Checking Sessions

In the case an IDocumentSession opened with the dirty checking enabled, the session will try to detect changes to any of the documents loaded by that session. The dirty checking is done by keeping the original JSON fetched from Postgresql and using Newtonsoft.Json to do a node by node comparison of the JSON representation of the document at the time that IDocumentSession is called.

public void tracking_document_session(IDocumentStore store)
{
    using (var session = store.DirtyTrackedSession())
    {
        var user = new User { FirstName = "Jeremy", LastName = "Miller" };

        // Manually adding the new user to the session
        session.Store(user);

        var existing = session.Query<User>().Single(x => x.FirstName == "Max");
        existing.Internal = false;

        // Marking another existing User document as deleted
        session.Delete<User>(Guid.NewGuid());

        // Persisting the changes to the database
        session.SaveChanges();
    }
}

snippet source | anchor

Do be aware that the automated dirty checking comes with some mechanical cost in memory and runtime performance.