The Occasionally Useful State Pattern

The most popular post on my blog last year by far was The Lowly Strategy Pattern is Still Useful, a little rewind on the very basic strategy pattern. I occasionally make a pledge to myself to try to write more about development fundamentals like that, but I’m unusually busy because it turns out that starting a new company is time consuming (who knew?). One topic I do want to hit is basic design patterns, so I’ll be occasionally spitting out these little posts when I can pull out a decent example from something from Marten or Wolverine.

According to Wikipedia, the “State Pattern” is:

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes.

https://en.wikipedia.org/wiki/State_pattern

I do still have my old hard copy of the real GoF book on my book shelf and don’t really feel ashamed about that in any way.

Let me just jump right into an example from the ongoing, soon to be released (I swear) Marten 7.0 effort. In Marten 7.0, when you issue a query via Marten’s IDocumentSession service like in this MVC controller:

    [HttpGet("/api/incidents/open")]
    public Task<IReadOnlyList<IncidentDetails>> GetOpen([FromServices] IDocumentSession session)
    {
        return session
            .Query<IncidentDetails>()
            .Where(x => x.Status != IncidentStatus.Closed)
            .ToListAsync();
    }

Marten is opening a database connection just in time, and immediately closing that connection as soon as the resulting ADO.Net DbDataReader is closed or disposed. We believe that this “new normal” behavior will be more efficient in most usages, and will especially help folks integrate Marten into Hot Chocolate for GraphQL queries (that’s a longer post when Marten 7 is officially released).

However, some folks will sometimes need to make Marten do one of a couple things:

  • Do consistent reads across multiple operations
  • Use the Marten session’s underlying database connection to read or write to PostgreSQL outside of Marten
  • Combine other tools like Dapper with Marten in the same shared database transaction

To that end, you can explicitly start a new database transaction for a Marten session like so:

    public static async Task DoStuffInTransaction(
        IDocumentSession session,
        CancellationToken token)
    {
        // This makes the session open a new database connection
        // and start a new transaction
        await session.BeginTransactionAsync(token);

        // do a mix of reads and write operations

        // Commit the whole unit of work and
        // any operations
        await session.SaveChangesAsync(token);
    }

As soon as that call to `IDocumentSession.BeginTransactionAsync() is made, the behavior of the session for every single subsequent operation changes. Instead of:

  1. Opening a new connection just in time
  2. Executing
  3. Closing that connection as soon as possible

The session is now:

  1. Attaching the generated command to the session’s currently open connection and transaction
  2. Executing

Inside the internals of the DocumentSession, you could simply do an if/then check on the current state of the session to see if it’s currently enrolled or not in a transaction or using an open connection, but that’s a lot of repetitive branching logic that would clutter up our code. Instead, we’re using the old “State Pattern” with a common interface like this:

public interface IConnectionLifetime: IAsyncDisposable, IDisposable
{
    IMartenSessionLogger Logger { get; set; }
    int CommandTimeout { get; }

    int Execute(NpgsqlCommand cmd);
    Task<int> ExecuteAsync(NpgsqlCommand command, CancellationToken token = new());

    DbDataReader ExecuteReader(NpgsqlCommand command);

    Task<DbDataReader> ExecuteReaderAsync(NpgsqlCommand command,
        CancellationToken token = default);

    DbDataReader ExecuteReader(NpgsqlBatch batch);

    Task<DbDataReader> ExecuteReaderAsync(NpgsqlBatch batch,
        CancellationToken token = default);

    void ExecuteBatchPages(IReadOnlyList<OperationPage> pages, List<Exception> exceptions);
    Task ExecuteBatchPagesAsync(IReadOnlyList<OperationPage> pages,
        List<Exception> exceptions, CancellationToken token);
}

One way or another, every operation inside of IDocumentSession that calls through to the database utilizes that interface above — and there’s a lot of different operations!

By default now, each IDocumentSession is created with a reference to our default flavor of IConnectionLifetime called AutoClosingLifetime. But when you call BeginTransactionAsync(), the session is creating an all new object with all new behavior for the newly started connection and transaction like this:

    public async ValueTask BeginTransactionAsync(CancellationToken token)
    {
        if (_connection is IAlwaysConnectedLifetime lifetime)
        {
            await lifetime.BeginTransactionAsync(token).ConfigureAwait(false);
        }
        else if (_connection is ITransactionStarter starter)
        {
            var tx = await starter.StartAsync(token).ConfigureAwait(false);
            await tx.BeginTransactionAsync(token).ConfigureAwait(false);
            
            // As you can see below, the session is completely swapping out its
            // IConnectionLifetime reference so that every operation will go 
            // now get the "already connected and in a transaction" state
            // logic
            _connection = tx;
        }
        else
        {
            throw new InvalidOperationException(
                $"The current lifetime {_connection} is neither a {nameof(IAlwaysConnectedLifetime)} nor a {nameof(ITransactionStarter)}");
        }
    }

And now, just to make this a little more concrete, here’s the logic of the AutoClosingLifetime when Marten is executing a single command asynchronously:

    public async Task<int> ExecuteAsync(NpgsqlCommand command,
        CancellationToken token = new())
    {
        Logger.OnBeforeExecute(command);
        await using var conn = _database.CreateConnection();
        await conn.OpenAsync(token).ConfigureAwait(false);

        try
        {
            command.Connection = conn;
            command.CommandTimeout = CommandTimeout;
            var returnValue = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false);
            Logger.LogSuccess(command);

            return returnValue;
        }
        catch (Exception e)
        {
            handleCommandException(command, e);
            throw;
        }
        finally
        {
            await conn.CloseAsync().ConfigureAwait(false);
        }
    }

And here’s the same functionality, but in the TransactionalConnection state where the connection is already open with a transaction started:

    public virtual async Task ApplyAsync(NpgsqlCommand command, CancellationToken token)
    {
        await EnsureConnectedAsync(token).ConfigureAwait(false);

        command.Connection = _connection;
        command.Transaction = Transaction;
        command.CommandTimeout = CommandTimeout;
    }

    public async Task<int> ExecuteAsync(NpgsqlCommand command,
        CancellationToken token = new())
    {
        await ApplyAsync(command, token).ConfigureAwait(false);

        Logger.OnBeforeExecute(command);

        try
        {
            var returnValue = await command.ExecuteNonQueryAsync(token)
                .ConfigureAwait(false);
            Logger.LogSuccess(command);

            return returnValue;
        }
        catch (Exception e)
        {
            handleCommandException(command, e);
            throw;
        }
    }

By using the “State Pattern”, we are able to remove a great deal of potentially repetitive and error prone if/then branching logic out of our code. That’s even more valuable when you consider that we have additional session state behavior for externally controlled transactions (the user pushes a shared connection and/or transaction into Marten) or for enlisting in ambient transactions. Many of the ancient GoF patterns were at heart, a way to head off potential bugs by reducing the amount if if/then branching code.

Last thing, many of you are going to correctly call out that the mechanical implementation is very similar to the old “Strategy” pattern. That’s certainly true, but I think the key is that the intent is a little different. The “State Pattern” is closely related to the usage of finite state machines where there’s a fixed set of operations that behave differently depending on the exact state. The Marten IDocumentSession transactional behavior qualifies as a “state pattern” in my book.

Not that I think it’s worth a lot of argument if you wanna just say it still looks like a “strategy”:-)

Leave a comment