In my last post, I talked about the theory of a microservice. Today it is going to be more practical. I will create two microservices using ASP .net core 3.1. Over the next posts., I will extend the microservices using CQRS, docker and docker-compose, RabbitMQ and automatic builds and deployments.
Create a Microservice using ASP .Net Core 3.1
You can find the code of the finished demo on GitHub. I will talk about the key aspects of the microservice but not about every detail. You will need at least a basic understanding of C# and ASP.NET Core.
To-do list for the Microservice
Our two microservice should satisfy the following requirements:
- Implement a Customer API with the following methods: create customer, update customer
- Implement an Order API with the following methods: create order, pay order, get all orders which had already been paid
- When a customer name is updated, it should also be updated in the Order API
- The APIs should not directly call each other to update data
- ASP .Net Core 3.1 Web API with DI/IoC
- Communication between microservices should be implemented through some kind of queue
- Use DDD and CQRS approaches with the Mediator and Repository Pattern
To keep it simple, I will use an in-memory database. During the implementation, I will point out what you have to change if you want to use a normal database. I will split up the full implementation. In this post, I will create the microservices with the needed features. In the following posts, I will implement Swagger, create a Docker container, set up RabbitMQ and explain CQRS and Mediator.
Structure of the Microservices
I created a solution for each microservice. You can see the structure of the microservices on the following screenshot.
Both microservice have exactly the same structure, except that the order solution has a Messaging.Receive project and the customer solution has a Messaging.Send project. I will use these projects later to send and receive data using RabbitMQ.
An important aspect of an API is that you don’t know who your consumers are and you should never break existing features. To implement versioning, I place everything like controllers or models in a v1 folder. If I want to extend my feature and it is not breaking the current behavior, I will extend it in the already existing classes. If my changes were to break the functionality, I will create a v2 folder and place the changes there. With this approach, no consumer is affected and they can implement the new features whenever they want or need them.
The API Project
The API project is the heart of the application and contains the controllers, validators, and models as well as the startup class in which all dependencies are registered.
Controllers in the API Project
I try to keep the controller methods as simple as possible. They only call different services and return a model or status to the client. They don’t do any business logic.
The _mediator.Send is used to call a service using CQRS and the Mediator pattern. I will explain that in a later post. For now, it is important to understand that a service is called and that a Customer is returned. In case of an exception, a bad request and an error message are returned.
My naming convention is that I use the name of the object, in that case, Customer. The HTTP verb will tell you what this action does. In this case, the post will create an object, whereas put would update an existing customer.
To validate the user input, I use the NuGet FluentValidations and a validator per model. Your validator inherits from AbstractValidator<T> where T is the class of the model you want to validate. Then you can add rules in the constructor of your validator. The validator is not really important for me right now and so I try to keep it simple and only validate that the first and last name has at least two characters and that the age and birthday are between zero and 150 years. I don’t validate if the birthday and the age match. This should be changed in the future.
In the Startup.cs, I register my services, validators and configure other parts of the application like AutoMapper, the database context or Swagger. This part should be self-explanatory and I will talk about Swagger or RabbitMQ later.
The Data project contains everything needed to access the database. I use Entity Framework Core, an in-memory database and the repository pattern.
In the database context, I add a list of customers that I will use to update an existing customer. The database context is created for every request, therefore updated or created customers will be lost after the request. This behavior is fine for the sake of this demo.
If you want to use a normal database, all you have to do is delete the adding of customers in the constructor and change the following line in the Startup class to take your connection string instead of using an in-memory database.
You can either hard-code your connection string in the Startup class or better, read it from the appsettings.json file.
I have a generic repository for CRUD operations which can be used for every entity. If I had a special use case for a specific entity, I would create a specific repository that would inherit from this generic one.
When I save or update an entity, I return the entity. When I get data, I always return an IQueryable. The IQueryable contains the query but hasn’t accessed the database yet. This can increase the performance since you can make an even more specific query and load only certain data instead of all.
The Domain project contains all entities and no business logic. In my microservice, this is only the Customer or Order entity.
The Messaging.Send project contains everything I need to send Customer objects to a RabbitMQ queue. I will talk about the specifics of the implementation in a later post.
The Service project is split into Command and Query. This is how CQRS separates the concerns of reading and writing data. I will go into the details in a later post. For now, all you have to know is that commands write data and queries read data. A query consists of a query and a handler. The query indicates what action should be executed and the handler implements this action. The same for the command
The handler often calls the repository to retrieve or change data.
For my tests, I like to create a test project for each normal project wheres the name is the same except that I add .Test at the end. I use xUnit, FakeItEasy, and FluentAssertions. Currently, there are no tests for the RabbitMQ logic.
Run the Microservices
In the previous section I only talked about the Customer service but the Order service has the same structure and should be easy to understand.
Now that the base functionality is set up, it is time to test both microservice. Before you can start them, you have to make two small changes to be actually able to start them. Currently, we have no queue and therefore the microservices will generate an exception. In the future, it would be nice if the microservices could work even without a queue.
Edit the Customer Service
Open the CustomerCommandHandler in the Service project and comment out the following line _customerUpdateSender.SendCustomer(customer);
This line is responsible for publishing the Customer to the queue
Edit the Order Service
In the Order API, you have to comment out services.AddHostedService<CustomerFullNameUpdateReceiver>(); in the Startup class of the API project.
This line would register a background service that listens to change in the queue and would pull these changes.
Test the Microservices
After you made the changes to both APIs, you can start them. This should display the Swagger GUI which gives you information about all actions and models and also lets you send requests. The GUI should be self-explanatory but I will talk more about it in my next post.
Today, I talked about the structure and the features of my microservices. This is just the beginning but both applications are working and could be already deployed. It is important to keep in mind that each microservice has its own data storage and is kept as simple as possible.
You can find the code of the finished demo on GitHub. In my next post, I will talk about Swagger and how you can use it to easily and quickly document your microservice while providing the opportunity to test requests.