Introduction
Last article of the series: the application layer. We will talk about its role, how to write it in a functional style with just a few lines of code but also how to deal with asynchronism. For that, we will use all the concepts we have learned so far. Let's dive in and let's create sustainable software.
A refresher on the functional architecture
Functional architecture is a rigorous clean or onion architecture. Its domain and application layers are written by applying the principles of functional programming: immutability, function composition, etc.
A functional application layer
Remember, the application layer is an orchestrator between the infrastructure layer and the domain layer: the domain layer make the decisions and the infrastructure layer executes the necessary side effects (saving to a database, posting a message in a bus, ....) thanks to the application layer.
Let's take an example: adding a product to a catalog. The application layer will be responsible to perform the following steps:
Transform a
ProductDto
into a domainProduct
.Check if the product reference already exists.
Save the product to the database.
We want these steps to be written in a functional style by creating a pipeline. We will use for that techniques we've learned so far: containers, Bind()
and Map()
functions.
Creating the pipeline
Creating the AddProduct() function
Let's create this function. We will complete it all along this article. It will look like this :
public static async OperationResultDto<ProductDto> AddProduct(
AddProductDto addProductDto)
{
}
I have defined an OperationResult<T>
: it will contain a value if no error happened during the operation, else it will contain an error.
Railway-oriented programming
A word about railway-oriented programming first. As soon as something wrong happens in the flow, then it is switched towards an error path: the execution flow of the pipeline is interrupted and the error messages are returned.
In our pipeline, the execution flow will be automatically routed to the error path whenever an entity is in an invalid state, thanks to Map()
and Bind()
functions defined on the entities.
Here is a little drawing to help you understand. This programming style is extremely well explained by Scott Wlaschin here.
Identifying the signatures of the pipeline functions
First step to write our pipeline: identifying the signatures of the functions that we will use for each step.
To easily create our pipeline, we will use an Entity<Product>
as we've learned in the previous articles.
Its steps will be as follows:
We need to transform a
ProductDto
into a domainProduct
. The signature of this function will beProductDto -> Entity<Product>
.Next, we need to check if the product reference already exists. For that, we will create a function that takes a domain
Product
and a count of identical references as parameters. It will return anEntity<Product>
in a valid state if no other references have been found in the database, else it will be in an invalid state. So the function's signature will beProduct -> int -> Entity<Product>
.Next, we need to save the product. The saving will be done by a repository. It will take a domain
Product
as a parameter and will return the saved domain product as a response. The signature of such a function isProduct -> Product
.
Nevertheless, we have a problem to create our pipeline because the function's outputs are all different, thus preventing us to chain them using a fluent style. That is where Bind()
and Map()
functions will come into play.
First step: converting a ProductDto to Entity<Product>
This first step is an easy one. We need transform a ProductDto
into an Entity<Product>
.
To do that, we simply define an extension method on the ProductDto
class. I already have defined Reference
, Name
and Price
Value Objects as explained in the 4th article of this series. I then create the Product Entity using the guidelines of the previous article of this series.
public static class AddProductDtoExtensions
{
public static Entity<Product> ToProductEntity(this AddProductDto addProductDto)
{
return Product.CreateEmpty()
.WithReference(addProductDto.Reference)
.WithName(addProductDto.Name)
.WithPrice(addProductDto.Price);
}
}
If a value of the ProductDto
is not or incorrectly filled in, then the value objects will return an error when they are instantiated: they act as control gates and a big advantage of that is that we don't need to provide any model validation in the Dtos thus avoiding code duplication between the domain layer and the infrastructure layer. The entity will then be created in an invalid state and all subsequent steps in the flow will be skipped.
Now let's complete AddProduct()
function and let's call our new extension method to convert our AddProductDto
into an Entity<Product>
:
public static OperationResultDto<ProductDto> AddProduct(
AddProductDto addProductDto)
{
var p = addProductDto.ToProductEntity();
return p.Match(
Valid: (v) => new OperationResultDto<ProductDto>(v.ToProductDto()),
Invalid: (e) => new OperationResultDto<ProductDto>(e)
);
}
Second step: verifying if the product reference exists
Let's create our verification function. It is a decision-making function, so you need to put in the domain layer. It returns an invalid Entity<Product>
if the reference already exists:
public static Entity<Product> VerifyProductReference(Product product, int existingReferencesCount)
{
if (existingReferencesCount > 0)
return Entity<Product>.Invalid(new Error(string.Format(Messages.TheProductReferenceAlreadyExists, product.Reference.Value)));
else
return Entity<Product>.Valid(product);
}
In Grenat.Functional.DDD library, I added some implicit conversions to simplify writing. The following code is equivalent and shorter:
public static Entity<Product> VerifyProductReference(Product product, int existingReferencesCount)
{
if (existingReferencesCount > 0)
return new Error(string.Format(Messages.TheProductReferenceAlreadyExists, product.Reference.Value)); // No need do call Entity<Product>.Invalid()
else
return product;
}
But how does this function chain to the result of the previous step? The previous function outputs an Entity<Product>
, and we need a Product
. Well, we will use the Bind()
function. Remember, Bind()
takes a function and it applies it to the wrapped value of the Entity<T>
container.
Using Bind()
, we can add simply add this new step to our AddProduct()
function:
public static OperationResultDto<ProductDto> AddProduct(
AddProductDto addProductDto,
Func<string, int> CountProductReferences)
{
var p = addProductDto.ToProductEntity()
.Bind(VerifyProductReference, CountProductReferences(addProductDto.Reference));
return p.Match(
Valid: (v) => new OperationResultDto<ProductDto>(v.ToProductDto()),
Invalid: (e) => new OperationResultDto<ProductDto>(e)
);
}
Third step: saving the product
I did not want to introduce an Entity<Product>
container in the repository for that. It would have been useless to deal with an entity's state in the repository. The decision to save the product was already made by the domain layer, so the repository just have to execute. It does not need to verify the decision.
So the save function signature will be Product -> Product
. Here again how to chain it to the function of the previous step? We need a Product
whereas the previous step outputs an Entity<Product>
. What's more, we need to get back an Entity<Product>
to stay in the elevated world.
In that situation, Map()
becomes handy. It is the same as Bind()
, i.e. it takes a function and its parameters and it applies it to the wrapped value of the Entity<T>
container.
But instead, it lifts the function's output to the elevated world: Entity<Product>
, allowing us to stick to the elevated world.
So let's update the AddProduct()
function by adding this last step. I will not give the details of the SaveProduct()
function, it is a classical repository function.
public static OperationResultDto<ProductDto> AddProduct(
AddProductDto addProductDto,
Func<string, int> CountProductReferences
Func<Product, Product> SaveProduct)
{
var p = addProductDto.ToProductEntity()
.Bind(Domain.Services.VerifyProductReference, CountProductReferences(addProductDto.Reference))
.Map(SaveProduct);
return p.Match(
Valid: (v) => new OperationResultDto<ProductDto>(v.ToProductDto()),
Invalid: (e) => new OperationResultDto<ProductDto>(e)
);
}
Look how simple and clear is this code. Thanks to functional concepts, it deals with error harvesting and error management without having to write any conditional logic. If another step needs to be added to the flow, well... just insert it into the pipeline using Map()
, Bind()
and Entity<Product>
. Forget about errors, say goodbye to many bugs :)
Dealing with asynchrony
All functions working with infrastructure resources must be asynchronous. Therefore, they must work with Tasks
. But in C#, the syntax for representing functions returning Tasks is quite cumbersome… Func<Task<T>>, Func<T, Task<R>>
…
To make our life easier, I added an AsyncFunc
delegate to simplify this writing in Grenat.Functional.DDD:
Func<Task<T>>
is equivalent toAsyncFunc<T>
.Func<T, Task<R>>
is equivalent toAsyncFunc<T, R>
.Etc., etc…
I also added asynchronous versions of Map()
and Bind()
: MapAsync()
and BindAsync()
. So, an asynchronous version of AddProduct()
will be as follows:
public static async Task<OperationResultDto<ProductDto>> AddProduct(
AddProductDto addProductDto,
AsyncFunc<string, int> CountProductReferences,
AsyncFunc<Product, Product> SaveProduct)
{
var p = await addProductDto.ToProductEntity()
.BindAsync(VerifyProductReference, CountProductReferences(addProductDto.Reference))
.MapAsync(SaveProduct);
return p.Match(
Valid: (v) => new OperationResultDto<ProductDto>(v.ToProductDto()),
Invalid: (e) => new OperationResultDto<ProductDto>(e)
);
}
Improving performance with lazy evaluation
We have a performance problem with the previous code. It comes from this line of code:
.BindAsync(VerifyProductReference, CountProductReferences(addProductDto.Reference))
Here, the function CountProductReferences()
is called even if Entity<Product>
is in an invalid state because it is given as a parameter.
To correct this problem, you should give a function as a parameter. An overload of BindAsync()
exists for that in my library: it will call the given function only if Entity<T>
is valid. This behavior is called lazy evaluation.
The correct statement is as follows:
.BindAsync(VerifyProductReference, () => CountProductReferences(addProductDto.Reference))
Calling AddProduct from an ASP.Net Core controller
Finally, the code to call the application layers' AddProduct()
function from an ASP.Net Core controller could look like this:
[HttpPost]
public async Task<ActionResult> AddProduct(AddProductDto addProductDto)
{
var response = await CatalogOperations.AddProduct(
addProductDto,
_productRepository.CountExistingProductReferences,
_productRepository.AddProduct);
if (response.Success)
return Ok(response.Data);
else
return UnprocessableEntity(response.Errors);
}
As you can see, I pass functions as parameters instead of passing interfaces to AddProduct()
. This functional approach makes the application much easier to test.
Summary
Use functional techniques to write a very thin application layer: railway-oriented programming, containers (
Entity<T>
,ValueObject<T>
, ...) and their underlying operations:Map()
,Bind()
.Use my library Grenat.Functional.DDD to help you with that.
Don't forget to use asynchrony for better performance when handling out-of-process resources (databases, message bus, ....).
Think of lazy evaluation for better performance: when calling a function that needs to handle time-consuming resources, give a function reference as a parameter and don't call it in case of an error.
Final words about the series
This was the last article of this series. I hope you enjoyed it and that you learned new techniques for your daily C# DDD work. Feel free to contact me if you have any questions! :)
About the cover image
IC 4628, the Prawn nebula. This pretty nebula is found in the Scorpius constellation. It is visible with good conditions only in the southern hemisphere. Total exposure time is about 9 hours.