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 operationoperationId
is the identifier of this operation holding all data togetheroperationName
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.
[…] Our journey to F#: bye-bye fluent syntax Version 2 (Urs Enzler) […]