Skip to content

Features Slices

César edited this page Jan 10, 2023 · 5 revisions

In order to keep features as isolated/encapsulated pieces of code that won't affect each other during development, we implement them by leveraging the pattern CQS/CQRS . This pattern will allow us to split features into 2 types: write operations that modify data (Commands) and read operations that don't modify data (Queries). Queries are simple actions that based on a set of parameters (or not) will retrieve a set of data. They are useful for retrieving listed or paged data as well as individual records. Commands, on the other side, are a bit more complex in the way that prior to modify or create any data we usually need to validate the input(s) and then perform the action. Also, if the intention is to modify some existing piece of data, normally one should ensure that this data actually exists prior to modify it.

MediatR is a very good library that helps us implement this pattern in a very easy way.

For Queries, we can then have them implemented like the following image:

Mediatr Query

A Request is triggered, a RequestHandler takes care of actually executing the code related and then the result of handler is a Response.

For Commands, we can have them implemented like the following image:

Mediatr Command

Similar to the Queries, for a Command a Request is triggered, but since MediatR offers the possibility to control the pipeline execution flow by including a PipelineBehavior before the RequestHandler, we use this to include some flow control logic to perform validations and checks prior to the RequestHandler execution so we don't have to repeat the same code in every single command handler. Then we can of course proceed with the execution of the RequestHandler or just return a Response directly.

Commands and Queries objects:

In order to follow DRY principle, Monaco offers some classes to simplify the general handling of Queries and Commands. Strictly speaking, the current implementation of these is by using C# Records. This is because Commands and Queries are expected to be immutable (once they've been built, they should remain the same during the entire execution of the request), so it makes sense to represent them as Records instead of Classes.

Queries

There are 4 base records that allow to simplify your Queries declaration and they all implement the interface IRequest<TResponse> to represent a request for MediatR:

  • QueryBase<T>: a generic record that will represent a query that will return whatever the user has declared. It receives a list of parameters represented by a list of key-value pairs that have a string key and a value of StringValues, meaning that it can contain a string or a list of strings. Useful for most of queries that need to receive any kind of parameters and return any kind of data. It contains several methods to facilitate the retrieval of strongly typed parameters from the parameters list.
  • QueryPagedBase<T>: it's a generic record that inherits from QueryBase<T> and always returns a Page<T> as result. Useful for receiving any kind of parameters and return a paged set of results.
  • QueryByIdBase<T>: it's a generic record that receives an ID as parameter and returns a single result in response. Useful for retrieving a record based on its ID.
  • QueryByKeyBase<T>: Similar to QueryByIdBase and QueryBase, but it offers a generic declaration of the lookup field to use for retrieving the record/s.

Commands

Monaco's commands pipeline execution integrate 2 kinds of validations mechanisms out-of-the-box:

  • Validate the incoming data/parameters to ensure that it's valid for the operation that the command represents.
  • In the case of having to retrieve and work with existing records, validate that the record to be retrieved actually exists in the DB

For this purpose, our Commands always return one of these 2 types:

  • ICommandResult: contains 2 fields to indicate the kind of validation error that might have occurred:
    • ValidationResult: A FluentValidation's ValidationResult that indicates whether the validation resulted a valid one or not, and a list of the possible errors that might have occurred.
    • ItemNotFound: a boolean property indicating whether the validation for checking the existance of an existing record was a failure or not.
  • ICommandResult<T>: Derives from ICommandResult to also include a T Result property to return whatever type of data might be desired to return. Useful for returning a 201-Created result on a REST API where it might be needed to return a DTO of the recently created record.

Based on these 2 different types of results, Monaco's Commands are also split into 2 types, which, similarly to the Queries, also implement IRequest<TResponse> to represent a request for MediatR:

  • CommandBase: It allows to optionally pass an ID as parameter or just use the default ctor. Can be used for either creation of new records or edition/deletion of existing ones and will only return an ICommandResult.
  • CommandBase<TResult>: exact same as CommandBase, but it will return an ICommandResult<TResult> instead. Useful for cases like the creation of a resource on a REST API and subsequent return of a 201-Created with the DTO of the newly created resource.

It's important to note that these records are meant to be mere helpers for resolving the most common use cases. By no means Monaco intends to restrict the implementation of Commands and Queries to these components and anyone is free to create new ones implementing straight from IRequest<TResponse>. In fact, it's what it's expected to be done for cases where there's no need of validating and responding to the caller, for example: when triggering commands from a message consumer in a Service Bus, which makes no sense to respond a validation message to the bus because there's no one listening anymore for the response.

Concurrency handling

Along with the default PipelineBehaviors for handling validations for any CommandBase-derived command, Monaco also includes another one for performing a few retries when the execution of such Commands throws a DbUpdateConcurrencyException, which means there has been a concurrent update when a given record and more than 1 process have been attempting to update the same record at the same time. The default behavior is to just perform a retry with exponential back-off, taking a bit longer to retry if the exception keeps occurring multiple times and up until 3 retries. This should allow for the most common scenarios to just retry and apply the most recent change and don't process incorrect data between the time it's been read and it's been effectively recorded into the DB.