Together with .NET 6 came tooling to help developers build better architectures. Projects like Dapr and example project like eShop On Containers help tremendously with building well-designed and well-architected platforms.
So where can .NET 6 help in building great architectures? There are a few concepts in .NET that help simplify some things; but not to worry, .NET is not pushing you into any direction. We still have full flexibility to architect our applications however we see fit. What we do have is numerous syntax concepts that help keep our code small and readable.
Record Types
The entity
This is a very basic example of an entity from an application build using the Domain-Driven-Design principles. It inherits from Entity, which has an Id property to give us a uniquely identifiable property, and it is an IAggregateRoot, which means that this object can be stored and retrieved on its own. Entities who are not an IAggregateRoot are not meant to exist by themselves; they depend on other objects to be a member of.
Let’s say we need to fetch a list of events to show in our frontend; not using DTOs would mean that we could possibly fetch hundreds of events with all Attendee and Address details, while maybe all we want to do is show a list of upcoming events. To simply, list all events that would be too much data. Instead, we use a DTO to simplify the object that goes over the wire according to the use case we need.
DTO for listing events
DTO as a record
That is one line of code to replace all the auto-properties. A record is a shorthand for writing a class, but there is more to it. Equality, for example, in a normal class, two variables of the same reference type are equal when they point to the same reference. With a record, they are equal when they have the same value. In this case, a class that only contains properties. Another difference is that a record is immutable. The complete documentation on records can be found here https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record.
IL output for a record and a class
As you can see, the outputs are identical, meaning records are known to the C# compiler but not to the runtime.
Value-type records
IL output for a record struct and a class
Struct records follow the same rules as normal structs. Structs are often used because they are cheaper and memory-wise because they are value-typed. This often results in better performance. They do have limitations when compared to classes, for example, structs don’t allow inheritance. A major difference between records and record structs is that record structs are not immutable by default; they can be if we mark them as readonly.
Monolith Architecture
In this example, we have a web client and a mobile client; both speak to the same API that in turn is connected to a data store. Depending on the size of the application, this API can potentially be huge. Let’s say there is one part of the API that is seeing intense usage and is slowing the entire API down. To solve this, we would need to scale the entire API or move it to a server with more power. Even worse, the entire system can go down because of a bottleneck in one place.
Another disadvantage of monolith services is maintainability. One big service containing all business logic is hard to maintain or even to keep an overview of what is where in the source code.
However, not everything is bad about monolith architecture. Depending on the size and complexity of your application, this might still be the right choice for you as microservices create extra layers of complexity besides the advantages they bring.
Microservices
There is a lot to like about a Microservices-oriented architecture. The split responsibilities mean that we can scale the parts where scaling is needed instead of just pumping more memory into the virtual server. We can create gateways per client so that only the absolute necessary parts of the backend platform are exposed and so on. It also brings with it added complexity and cost; since each service is basically its own application, we need a lot of application servers; all of those servers need to be maintained. Even if we went with a container orchestration system like Kubernetes, we get extra overhead, and exactly this is the danger of overengineering or over-architecting an application. Microservices are a great architecture pattern, but they are not the silver bullet for all applications; depending on your use case, a monolith application might be just fine.
Microservices work great in a Domain-Driven-Design (DDD) or Clean Architecture (CA) scenario. The scope of a microservice can, in most cases, map to a bounded context. Domain-Driven-Design and Clean Architecture are widely popular design patterns for enterprise applications. They both give the domain model responsibility for changes and nicely decouple read and write requests. Both are really great patterns to add to your arsenal as a developer.
A bounded context is a functional block of your application that can be isolated. For example, the orders of a webshop can contain products, customers, purchases, and so on. That isolated block of orders functionality can be a bounded context. However, just like with Microservices, DDD and CA have their place in larger applications. Don’t overengineer; use the right tool for the job instead of using a sledgehammer to drive a nail in a wooden board.
If you are interested in learning more about Clean Architecture or Domain-Driven-Design, I can advise you to take a look at the e-book of eshop on containers or the Practical Event-Driven Microservices Architecture book available from Apress.
Container Orchestration
We have talked about containers, specifically Docker-based containers, in the ASP.NET chapter. Containers and Microservices are a great match, if there is an orchestrator. A container orchestrator is a tool that manages a set of different container images and how they relate to each other. Can they communicate? Over what port? Which containers get exposed outside of the cluster? And so on. The most common orchestrators are Kubernetes and Docker Compose.
Kubernetes
One of the nodes is the control plane: the node that controls the cluster. Communication to and from the control plane happens over the Kubernetes API.
A deployed container on a node is called a Pod. For this example, we will create a Pod from one of the services in eShop On Containers. eShop On Containers is an open source reference architecture by Microsoft; it can be found at https://github.com/dotnet-architecture/eShopOnContainers. The reason we are using this as an example is because the eShop is a container-ready Microservices architecture. It fits quite right with the topic we are dealing with at the moment.
Creating a new deployment to Kubernetes
An example Kubernetes file
Listing 9-8 shows an example of a Kubernetes file that spins up a pod of the catalog API.
Docker Compose
Example of Docker Compose file
Running Docker Compose in the background
The Docker Compose file can be further expanded by adding volumes for persistent storage or network capabilities; all the information on how to do that can be found at the official Docker Compose documentation.
Dapr
The Distributed Application Runtime (Dapr) provides APIs that simplify microservice connectivity. The complete documentation for Dapr is found at https://docs.dapr.io/. It is a Microsoft-owned open-source project that can help simplify the management of large distributed systems. Consider it a “Microservices toolkit.” Dapr provides capabilities such as service-to-service communication, state management, publish/subscribe messaging pattern, observables, secrets, and so on. All these capabilities are abstracted away by Dapr’s building blocks. Dapr by itself is large enough to fill an entire book; what I want to do here is give you an idea of what Dapr is about so you can determine for yourself if you can use it in your project.
Installing Dapr
Installing Dapr CLI
Now we have everything set up, we can get to work. Dapr works according to the sidecar pattern. Meaning that we don’t have to include all components and code in our own application; we only need to make Dapr API calls that go to the sidecar that is attached to our application. That sidecar abstracts all logic away from us.
The sidecar pattern is a design pattern where components of an application are deployed into separate processes or containers. This provides isolation and encapsulation.
Dapr State Management
Let’s use the Dapr state management component as an example. State management in Dapr is done by default through Redis Cache. Dapr abstracts the logic of setting up Redis and calling its APIs away from us. We only need to call Dapr APIs to get state management up and running.
Calling Dapr state management
Launching the application using Dapr CLI
This was just one very simple example of Dapr. The major advantage is that Dapr takes a bunch of components and principles and bundles them into one developer model. We only need to develop against the Dapr API; everything else is handled by the runtime.
Wrapping Up
.NET has always been a framework that promotes good, clean architectures, and it continues that trend with .NET 6. Open-source reference projects like eShop On Containers help guide developers and application architects in finding the best architecture for their projects. Frameworks like Dapr can help ease the struggles of managing all the different building blocks in distributed applications. But as always, there is no one-size-fits-all. Look at the project you want to build from a higher, abstracter place, and choose the right architecture for the job. Not everything is suited for a complex DDD setup; don’t overengineer but keep things simple.