.NET Microservices: API Contracts
For more than 2 years I have been working to break an ASP.NET MVC monolith application into smaller pieces, something that could be called microservices. Context separation, contract versioning with NuGet packages, contract testing with Pact.Net, automatic deployments with Ansible, Terraform for basic infrastructure on AWS are some of the things we’ve explored and adopted for our .Net platform.
We stopped using the XML transform of the web.config and embraced Ansible templates for configuring different clients and environments. There is a road map to make the web app completely client side so now our front website is a mixture of React.js and ASP.NET MVC, while our build process is an interesting combination of Gulp and MSBuild running on TFS.
Some of the decisions that we took were good. For example, moving our Session items to Redis forced us to pay more attention to what kind of stuff goes into Session. Others, such as Entity Framework’s database migration option, were pretty bad. That caused applications receiving their first request to check if they had to apply any database changes. It seemed like a good idea in the beginning. But after we used it for a while we realised it lacked backward compatibility, so goodbye to blue-green deployments.
Divide into contexts
First thing when you start the breaking process is to see your contexts clearly. This way you can decide from the beginning what services to create.
As an example we will take a car renting application for individuals, which is pretty close to the kind of platform we are building now. Here you can distinguish from the start two different contexts. First, there’s the user context with actions such as register/login (using a Facebook/Gmail account) or edit user details. Secondly, there’s the renting part that includes creating a new rental, editing or cancelling an existing one.
Usually I noticed that the step of context separation was pretty well understood and applied. My colleagues paid pretty good attention to separation logic, isolation of the external services, and the identification of changing parts. There were many debates and a lot of energy consumed around it. So far the decisions were good enough, even after a few years. And there were many discussions on where to start from. Do we want to go with the part that changes often, because you get the most benefits, or the part which changes rarely because it is less risky. Both options are viable, so you should definitely start with what looks like the low-hanging fruit in your context.
Create the contracts
So on the car rental platform you have 2 services, users and rentals, each with their contract. The main web application is now a client for both of them. The first question is what do you do about the contract projects. The first option is to use the same library for both server and client, which would clearly mean coupling, and some issues when building the projects (even though on TFS it doesn’t look like an issue). The other solution is to use separate libraries, one set of contracts for the client and another set for the server. While this involves code duplication, it’s somehow acceptable for a microservices approach.
Actually, at this point both options would work as long as you understand the implications, with some advantages for the first option. There is actually a third option, but that is helpful when you have more services and clients using them. We are going to discover it further on. In our case we chose option 1, but the reason was that we saw code duplication a major issue to avoid, while high coupling is not necessary a problem when you come from a monolith world.
Version the contracts
Once you start having more services and clients using them things get complicated. Any option you would choose would show its limits. On the car rental service we’ve talked about earlier, let’s say we have the front app and the back-office app. These are using the car rental service to create and edit rentals. Then you can have a notification service that emails the users 1–2 weeks before the renting starts which is also a client for the rental service. And one more application that reconciles the consumed rentals with invoices from the suppliers which also needs data from the car rental service.
With 4 clients to our rental service and having picked option 1, same contract library for all, the coupling between client and server will force us to deploy the clients and the server on each change of the contract. We can at some point decide not to deploy some of the clients, which means we are starting to version our contract library. Some of the clients will remain with an older version, while at least one client and the server go with the new one. While this seems to be a step in the right direction, we are still not out of the coupling completely. There are still projects using/building the same contract so it would be better if we could stop using exactly the same project.
Option number 2 involves different duplicated libraries, one for each client and one for the server. This will allow you to deploy separately any of the clients and the server. And this might sound OK when you have just one client for your service. Once you keep adding and duplicating the library you will get the feeling something’s not right. You will not know exactly where one client version is and what he misses from the server contracts. And this might lead you to start comparing the contracts.
Decouple the contracts
Besides coupling, option 1 doesn’t have many benefits for the build process. Imagine making a change in the contract. You need to modify all the projects using it and it can become really painful when there are too many. The build definitions can also get more complicated the more shared contracts you have in your solution.
In our case we started to create NuGet packages from our shared library. All clients and the service are using those packages. You end up with a solution only for the contract. We have a separate build on TFS which creates the package and deploys it to your private NuGet server with a powershell script. Each new version of the contract becomes a NuGet package hosted on our server. This solves the coupling and makes it easier to upgrade to a newer version directly from Visual Studio.
Different versions of NuGet packages
Once you go on the package path, start changing all your contracts into packages, don’t leave any references to projects. Because the worst case scenario would be to get to a mix of packages and shared projects, especially for the same contract.
You can either start on the packages path from day one, or you can start with shared projects and get to the packages later, but you still need to get there. Having 40+ services is totally different than having less than 10, so if you know you will get fast to a big number then going with packages seems better. But if you start small and the pace of creating new services is not that big you could start first with shared contracts and move to packages later. Chose your battles wisely because the move to a microservices architecture will provide many challenges.
Everything seems to be much better now, but there is still one question. How do you know that every new version of the contract you create will be compatible with all the clients? And for such tests we are using something like pact and its .NET version, which I will share with you in a different post.