Our journey to F#: bye-bye fluent syntax Version 2

In the previous blog post “bye-bye fluent syntax”, I wrote about a way how to implement an equivalent to a fluent syntax in F#, without classes and interfaces, using discriminated unions.

Steve Gilham pointed me to a much simpler solution (many thanks again!). And it looks like this:

Operations

In our code, we have the concept of an “operation”. Yeah, not the best name to make the concept clear. An operation contains all the things that have to be done when the backend is triggered – for example, by a call from the client.

An operation consists of some actions that have to be done with every operation:

  • auditing of who issued the operation
  • logging when the operation started and whether it succeeded or failed
  • I case of failure, rollback all side-effects already executed (we use compensation, not transactions)

And some actions are specific to every operation – the actual business domain actions. For example, storing the time when I came to work. Remember, we build a time tracking system.

Setting up an operation in code, typically looks like this:

let createActivityStructure =
    // ... left out the arguments ... =
    asyncResult {
        // ... some code left out that gets 
        // some data and performs validation... 

        do! OperationRunner.start
                operationLogger
                rollbackOperationMessageSender
                operationId
                OperationNames.CreateActivityStructure
                operationMetadata
           |> OperationRunner.run
                  (addTranslations translationModule created.ActivityStructureId operationData.Translations application)
           |> OperationRunner.logAndPersistEvent
                  persistActivityStructureEvent
                  DomainEvent.ActivityStructureEvent
                  created
           |> OperationRunner.finish
    }

OperationRunner.start

OperationRunner.start sets up the operation runner by creating a context that holds everything needed during the operation, does some logging and returns the context so that it can be used like in a fluent syntax.

type RunState =
| Context of OperationGuid * OperationLogger * IRollbackOperationMessageSender
| Failure // pass-through state

let start
    (operationLogger : OperationLogger)
    (rollbackOperationMessageSender : IRollbackOperationMessageSender)
    operationId
    operationName
    (operationMetadata : OperationMetadata) =
    async {
        do! operationLogger.Log
                {
                    OperationId = operationId
                    OperationName = operationName
                    AuthenticationUserId = operationMetadata.AuthenticationUser
                    ApplicantId = operationMetadata.Applicant
                    AffectedEmployee = None
                    ApplicationSource = operationMetadata.ApplicationSource
                    Application = operationMetadata.Application
                }

        return Context (operationId, operationLogger, rollbackOperationMessageSender)
    }

OperationRunner.start takes some dependencies:

  • operationLogger is a C# class that provides logging functionality. We have a mix of C# and F# so most shared code is in C#.
  • rollbackOperationMessageSender allow us to send a message to the service bus that will compensate all actions already done in this operation
  • operationId is the identifier of this operation holding all data together
  • operationName is the name of the operation and is used to select the corresponding compensation algorithm (every operation has its own compensation algorithm).
  • operationMetadata contains some general information like current time and who is the authenticated user

The RunState tracks whether we are still running successfully or not. If we are still on track to success, the Context contains the data needed for executing the steps of the operation. If something failed, Failure is used.

OperationRunner.run

To actually run something inside the operation, I call the run function:

let private execute (action : Async<unit>) (runState : RunState) : Async<RunState> =
    match runState with
    | Failure -> Failure |> Async.singleton
    | Context (operationId, operationLogger, rollbackOperationMessageSender) ->
       async {
            try
                do! action
                return runState
            with
            | _ ->
                do! operationLogger.Error(operationId)
                do! rollbackOperationMessageSender.RollbackOperation operationId |> Async.AwaitTask
                return Failure
       }

let run action runState =
    Async.bind (execute action) runState

The run function takes some action that has to be executed of the form Async<unit>. So we can run anything that is async and returns unit. We will see later that we have some more sophisticated alternatives.

The actual work is done by the execute function that first looks whether the operation has already failed, in which case the failure is passed through and returned. Otherwise, it tries to execute the action. If the action succeeds, then we return the Context. If the action fails, the failure is logged, compensation is triggered, and Failure is returned so that the following actions are skipped.

Async.bind is use so that I can pass runState : Async<RunState> to the execute function, which takes runState : RunState. It “unwraps” the async value.

OperationRunner.logAndPersistEvents

Our system uses event sourcing to store most of its data, so we persist events. In order to be able to compensate an operation – either to reverse already persisted data in a failed operation or to undo an unwanted operation – we log the events along with the operation ID.

let logAndPersistEvents persist elevate events runState =
    let logAndPersistEvents'' operationId events operationLogger =
        async {
            let elevated = events |> List.map elevate
            do! OperationLog.storeEventsToOperationLog operationLogger operationId elevated
            do! persist events
        }

    let logAndPersistEvents' events runState =
        match runState with
        | Failure -> Failure |> Async.singleton
        | Context (operationId, operationLogger, _) ->
            execute (logAndPersistEvents'' operationId events operationLogger) runState

    if not (events |> List.isEmpty) then
        Async.bind (logAndPersistEvents' events) runState
    else
        runState

The OperationRunner.logAndPersistEvents function first checks whether there are any events to be persisted. If not, the runState is just passed through. Otherwise, the Context is decomposed inside logAndPersistEvents to get access to the operationId and the logger so that the events can be stored. We have a discriminated union over all event types, that contains the type of the event (discriminated union case) and the ID of the event. The elevate function transforms an event into this discriminated union. Once transformed, the events can be passed to the OperationLogger.storeEventsToOperationLog function. Finally, the events are stored by passing them to the persist function.

OperationRunner.finish

Finally, the finish function ends the operation by logging success when the operation was run successfully. In case of a failure, that was already logged in the failing step:

let finish runState =
    let finish' runState =
        async {
            match runState with
            | Failure -> ()
            | Context (operationId, operationLogger, _) ->
                do! operationLogger.Success operationId
        }

    Async.bind finish' runState

Conlusions

This approach to translating a C# fluent syntax to F# is much simpler and more flexible than the one I described in the earlier post. The syntax can be extended more easily than when using discriminated unions.

This blog post is made possible with the support of Time Rocket, the product this journey is all about. Take a look (German only).

Find all blog posts about our journey to F# here.

About the author

Urs Enzler

1 comment

By Urs Enzler

Recent Posts