Introduction
In the previous blog post, I discussed the difference between workflows and attempts. In summary, a workflow accepts a command as input, and as a result, emits a list of domain events. Thus, an attempt also accepts a command as input, but instead, returns a status. Furthermore, the status of an attempt requires the caller to pipe the status into another function to generate domain events. This post will cover how domain events get processed so that they can inflict side effects within an application.
Domain Events
What exactly are domain events? Domain events can be viewed as historical observations related to a domain.
The following are domain events that could occur inside the Access domain:
module Events = type RegistrationValidationEvent = | FormValidated of ValidatedForm | FormNotValidated of UnvalidatedForm type RegistrationSubmissionEvent = | RegistrationSucceeded of DataTransfer.Profile | RegistrationFailed of ValidatedForm type LoginEvent = | LoggedIn of Provider | FailedToConnect of Credentials | FailedToAuthenticate of Credentials type LogoutEvent = | LoggedOut of Provider | LogoutFailed of Provider
When a user attempts to submit their credentials for login, one of these domain events could occur:
type LoginEvent = | LoggedIn of Provider | FailedToConnect of Credentials | FailedToAuthenticate of Credentials
Application
An application’s role is to respond to domain events. Hence, domain events are useless unless acted upon. Therefore, when a login attempt is successful, a LoggedIn event will be broadcasted to an application’s domain event handlers.
The following is an example of an attempt that results in domain events:
then Login { Email=email; Password=password } |> attempt login |> ResultOf.Login |> Are.Login.events
Login events are broadcasted at the end:
… |> attempt login |> ResultOf.Login |> Are.Login.events |> broadcast
Here’s how a broadcast function can be implemented:
let broadcast events = events.Head::events.Tail |> List.iter (fun event -> sideEffects.ForLoginAttempt |> handle event)
Here’s what the handle function could look like:
let handle event handlers= handlers.Head::handlers.Tail |> List.iter(fun handle -> handle event)
Domain event handlers
Domain event handlers are the primary subscribers to domain events. For example, when a LoggedIn event gets broadcasted, a page navigation handler can display a portal page.
The following example shows how navigation can take place from a LoginEvent case:
let navigate' = function | LoggedIn provider -> Application.Current |> navigate (portalPage provider.Profile) provider | FailedToConnect credentials -> Application.Current |> navigate errorPage credentials.Email | FailedToAuthenticate _ -> ()
Here’s an implementation for page navigation:
let navigate page context (app:Application) = try app.MainPage Debug.WriteLine(ex.Message); raise ex
So far, I elaborated on the segregation between the processing of a command that will emit domain events versus the actual domain event handlers that inflict side effects. To make an application useful, side effects are usually required. Such side effects would involve writing to a database or navigating to a page. The next section will cover configuring domain event handlers.
Designing Side Effects
As discussed earlier, an application’s role is to respond to domain events by executing side effects. Such side effects include page navigation, data persistence, and logging. In order for these side effects to occur, domain event handlers must first subscribe to domain events. Once subscribed, a domain event handler can execute a side effect from a domain event.
The following reflects how we can define domain event handlers for a login attempt:
module Login = type SideEffects = { ForLoginAttempt : (LoginEvent -> unit) nonempty }
The ForLoginAttempt property is a nonempty list of functions that takes a LoginEvent as input and outputs a unit. Note that a function will usually have side effects if it returns unit. Therefore, we can view the ForLoginAttempt property as a nonempty list of domain event handlers for a login attempt.
Domain event handlers can be defined as a dependency within our specification library:
module Login = … type SideEffects = { ForLoginAttempt : (LoginEvent -> unit) nonempty } type Dependencies = { … SideEffects : SideEffects }
The definition of Login.dependencies could be the following:
module Login = open System.Diagnostics let dependencies = let log' = function | LoggedIn user -> Debug.WriteLine(sprintf "Login successful:\n %A" user) | FailedToConnect credentials -> Debug.WriteLine(sprintf "Error: Unable to connect to server:\n %A" credentials) | FailedToAuthenticate credentials -> Debug.WriteLine(sprintf "Warning: Unable to authenticate user:\n %A" credentials) let navigate' = function | LoggedIn provider -> Application.Current |> navigate (portalPage provider.Profile) provider | FailedToConnect credentials -> Application.Current |> navigate errorPage credentials.Email | FailedToAuthenticate _ -> () let handlers = { Head=log'; Tail=[navigate'] } let sideEffects = { ForLoginAttempt= handlers } let attempt = { Login= TestAPI.mockLogin } { SideEffects= sideEffects; Attempt= attempt }
We could then inject these domain event handlers into a view-model as a dependency:
MainPage = new LoginPage { BindingContext = new ViewModel(Login.dependencies) };
Conclusion
In this post, I discussed how we can add side effect definitions to our specification library for a given domain event. In addition, I described how a domain event handler is essentially a function type that returns a unit. I then described how to inject domain event handlers as view-model dependencies so that they can inflict side effects inside our application. So what does the file solution look like when we model an application this way?
The saga continues…