Example: Task list app
This quick guide walks you through recreating the simple TO-DOs (task list) app.
Revo.Examples.Todos is a simple application intended as an introduction to Revo framework. It showcases some of its most basic features including DDD-style event sourced aggregates and entities, commands and queries, projections and read models.
Using this simple application, one should be able to track his tasks to do. User can create task lists, to which tasks (to-dos) can be added (and later modified, deleted or marked as 'done').
You can also instantly download the complete application by cloning the Github repository: https://github.com/revoframework/Revo/tree/develop/Examples/Todos
1. Create a new ASP.NET Core application in Visual Studio
Open Visual Studio or any other compatible IDE and create a new project targeting ASP.NET Core 3.0 or newer and add to it references to NuGet packages Revo.Infrastructure, Revo.EFCore and Revo.AspNetCore.
We are going to write the application using EF Core, ASP.NET Core and either PostgreSQL, MSSQL or SQLite database (your choice), but it is also possible to easily adapt the example to other platform or database system with minor modifications.
Open the generated Startup.cs file with your ASP.NET Core's Startup
class and modify it so that it inherits from RevoStartup
. This adds a light-weight support for the ASP.NET Core platform and bootstraps the framework application.
You also have to implement a method called CreateRevoConfiguration()
which configures the framework. This is the place to modify many of configuration options the framework offers.
You also have to uncomment the correct line (15 - 17) depending on what database system you decided to use (PostgreSQL is recommended, but you can also start off with SQLite, for example).
2. Define domain model
First, we are going to define the domain model for our application. For the sake of simplicity, we are going to have only one aggregate root, which is going to be event-sourced (Revo also supports non-event-sourced aggregates and allows you to mix them in your domain models).
If you are feeling uncertain with the terminology used in this guide, I definitely recommend reading up on topics like domain-driven design (DDD) or event sourcing elsewhere first, as explaining these concepts is greatly beyond the scope of this documentation. Great book covering many practical aspects of these topics is Implementing Domain-Driven Design by Vaughn Vernon (2013), for example.
2.1. Aggregate and entities
Our only aggregate is going to be a task (to-do) list, which represents a list (e.g. a sticky note) to which individual tasks can be added. Each task list can also have its name. The task list entity (TodoList
), which is also the aggregate root, represents an entry point to interacting with the aggregate.
As you can see, we are only modifying the state of the aggregate using events, so that these modifications can later be saved in form of a sequence of events and then the state later again reloaded from these events.
The aggregate root defines one public constructor with parameters (for ensuring class invariants) and one protected constructor that takes just the aggregate ID - this one is needed for the framework to be able to load from the event store.
Note that we also defined a class ID of the aggregate root using the [DomainClassId]
attribute. This is an arbitrary GUID value (that must however be unique in your project) and is needed by Revo to identify the class when saving the aggregate.
We also need to define the entity representing a task that can be added to our list.
2.2 Domain events
We wouldn't be complete without the events. Note that their state is defined as immutable (good practice).
3. Writing data with commands
3.1. Commands
Next, we are going to implement the write-side of our application, enabling us to create and modify tasks and tasks lists. Let's start with command classes.
A command can be any POCO class that implements the ICommand
interface. Same as with events, we make its properties immutable. A single command always represents one write operation. Its scope can vary greatly depending on the needs of your consumers (here a future REST API that we are going to write), but it is usually a good practice that one command should always modify just one aggregate.
In Revo, however, this is just a recommendation, not a requirement, and the framework doesn't limit you in what you do in your command handlers (you don't even need to work with any aggregates, for example).
This concept of strictly segregating responsibilities of reading and writing (query handlers and command handlers) that Revo uses is called CQRS (command-query responsibility segregation). Related concept CQS (command-query separation) then means (simply put) that and operation always either returns data or modifies the data, not both (also asking a question should not change the answer).
We can also see we annotated some of command properties with the [Required]
validation attribute, which ensures that only commands with non-empty data can get passed to the command handlers.
3.2. Command handler
Now we need to implement the actual code that gets executed when our commands get send to the commands bus - we do that by defining a class implementing ICommandHandler<>
interfaces. By default, these handlers get auto-discovered and registered upon application startup, so it is enough to just define the class.
The command handler's constructor gets a reference to the repository, which is used for loading and storing domain aggregates (note that you can only get an aggregate root from it, not just any entity). When a command handler executes, the command handler pipeline automatically creates and commits a new unit of work (using pipeline filters in the background). This also means that an execution of single commands defines a strict transactional boundary.
Because the unit of work gets automatically commited at the end of a successful command execution, you don't need to explicitly call anything to save the repository.
4. Querying data from read model
Because we want to display the data (tasks and task lists) of our application on a simple web page, we also need a way to query the data we store in the database. Since directly querying individual events from the event store would be very cumbersome in our use case (it usually is), we are going to define a read model for our data.
4.1. Read model
Read model can be structured pretty much in any way we or our consumers (e.g. an UI or REST API) need (even denormalized, for example). Here, for the simplicity of this example, we are going to project the events into simple POCO classes persisted by Entity Framework Core ORM (but you can also use other, like Entity Framework 6, RavenDB document-database or your own).
[TablePrefix]
attribute is just Revo's convenience attribute which prefixes the names of the tables and columns and you don't need to use it if you don't like it.
4.2. Queries
Similarly to when we defined commands to modify our data, we need to defines queries to be able to query our read model. Here, GetTodoListsQuery
loosely corresponds to a single REST API endpoint we are going to implement.
Query is any class that implements the IQuery<T>
interface, where T
is the type of the result it returns. It can also have parameters (properties) like commands.
Since we don't like directly returning the read model the way it is stored by the ORM (e.g. with recursive references), we also defines DTO (data transfer objects) mapped by AutoMapper, but this is purely optional and you don't need to do it in your code and your queries can directly return your read model.
4.3. Query handler
To define what a query returns once it is executed, we are going to implement a query handler. Query handlers get also auto-discovered and registered upon startup. Note that Revo doesn't restrict how you work with your read model in any way and you can store your data in any way you like.
In contrary to our command handler, our query handler works with IReadRepository
(CRUD repository) instead of IRepository
(domain repository).
While you should use the (and only) domain IRepository
in command handlers to modify your aggregates, in your query handlers, you are going to need IReadRepository
which is just a thin read-only abstraction layer over an ORM (Entity Framework Core here in our case).
Compared to IReadRepository
(which behaves just like a thin wrapper over an CRUD-like ORM), domain IRepository
can also persist aggregates in other forms, e.g. event sourced aggregates to an event store. It also deals with other aspects of domain aggregates like publishing events to event bus.
4.4. Projector
Finally, we need to specify how our read model gets populated with the data from events emitted by the aggregates. To do so, we are going to implement a projector. To find out more about how projectors work, see Projections in the reference guide.
5. ASP.NET Core controller
We have now almost all the parts for a fully functional Revo application and need just one more thing - an endpoint to send the commands and queries to our application from. We can do this by implementing an ASP.NET Core controller.
We are deriving our controller from a convenience base class called CommandApiController
which does just one thing - gets an ICommandBus
dependency injected. A command bus can be used for sending commands and queries to an application.
6. Finish!
You have now implemented a complete (albeit simple) application using Revo framework. You can either use a REST API testing tool like Postman to manually send the requests to the API controller, or you can directly grab a simple JavaScript frontend UI that is implemented in this example's repository and copy it to your project, it's your choice.
Happy testing!
Last updated