Functional DDD with C# Part 6: the application layer

Functional DDD with C# Part 6: the application layer

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:

  1. Transform a ProductDto into a domain Product.

  2. Check if the product reference already exists.

  3. 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:

  1. We need to transform a ProductDto into a domain Product. The signature of this function will be ProductDto -> Entity<Product>.

  2. 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 an Entity<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 be Product -> int -> Entity<Product>.

  3. 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 is Product -> 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 to AsyncFunc<T>.

  • Func<T, Task<R>> is equivalent to AsyncFunc<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.

See it in large size in my portfolio.