A Simple Webservice

We’ll build a simple .NetCore webservice. We’ll discover that when we thought our service was ready, we’ve already missed a lot of vital things necessary for it to be considered production ready!

So, when starting out with a microservice, I start with an empty MVC API project. This gives me a container and some boilerplate code, that sets up the HTTP handler pipelines, dependency injection etc. I’ll post links to the relevant commits, git tags etc. as I go through all the steps and solely highlight relevant code here in the post! e60fe8ad shows this initial setup.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace StronglyTyped.SampleService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}

This code shows how the API will be hosted: As you can see, it’s a simple console application and also shares the console lifetime, which means, that pressing CTRL+C will terminated the service gracefully. In terms of ASP.Net Core this means, that no more inbound connections will be accepted while all currently running requests will be processed to completion. So here one first thing to note:

Lesson No. 1: Always have your services listen for a termination signal so you can terminate in an orderly fashion without forcing the OS to abort your process! (Graceful termination)

Next, let’s look at how MVC ist set up:

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
namespace StronglyTyped.SampleService
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

We also have a default controller which is the part of our application that will be invoked by MVC to handle our requests:

 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<string>> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }

The [Route("api/[controller]")] attribute on the controller defines that base route of what request paths can be handled by this controller. This path can be extended by every handling function ([Http*] attributed methods). And here we have our first issue: Leaking internal through your interfaces. It’s a form of a leaky abstraction. The abstraction at hand should be the HTTP-REST interface, and this interface says that invoking:

GET /api/values HTTP/1.1
Host: localhost:5000

…our service must return values! And it does! For now anyways. But the issue is, that an interna like the naming of the controller is part of the interface. This causes the interface to change, when when we rename this (the controllers types name) interna. This can often be observed with the information that is returned as well. Let’s consider the following sample of a controller:

 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[Route("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<Value>> Get()
    {
        return new [] { new Value { ID = "test", Label = "Content" } };
    }
}

public class Value 
{
    public string Id { get; set; }
    public string Label { get; set; }
}

In this sample we’ve fixed the obvious problem and made a static route. The controllers naming will no longer interfere with how the request in handled. But we’re returning a structured value as a response to GET /api/values. This sample is valid code and will return exactly what we’re looking for:

0
1
2
3
4
5
[
    {
        "Id": "test",
        "Label": "Content"
    }
]

And again, renaming the field Id in class Value to Identifier will break our interface and leave at least one of your customers very angry.

Lesson No. 2: Always have stable interfaces that do not depend on internal structure of your code!

This goal is actually quite simple to achieve: Just define our interface before your write your implementation and explicitly name and structure everything that crosses your system boundaries. To define our HTTP-REST interfaces, it has become common to supply OpenApi definitions (formerly known under the name Swagger).

Does this mean I can never change anything about my interfaces? Yes. Interfaces MUST be kept stable but there is a way we can sort of “change” an interface without actually changing it: We can version it!

How do we version HTTP-Rest interfaces? We need some way of identifying the version of the interface for every request that arrives in our system. This can be:

That is just a few examples of how service interfaces can be versioned. We now have a stable HTTP-Rest interface and know how to version it. We can now gracefully stop our service and have stable and versioned interface in commit e6b2251. But besides that we also need to get configuration into our service. For now, we don’t have a way to do this - let’s change that and use the ConfgurationBuilder and it’s extensions to read configuraiton for am JSON file:

16
17
18
19
20
21
22
23
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();
        }

For most use cases this should be enough. But if we have multiple environments into which we want to deploy our code, we’re going to need different configuration for each of those environments:

16
17
18
19
20
21
22
23
24
    public class Startup
    {
        public Startup(IConfiguration configuration, IHostingEnvironment environment)
        {
            Configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{environment.EnvironmentName}", optional: true)
                .Build();
        }

Now we can ship the environment specific configurations in a separate file and the configuration that is the same for all environments, we can ship in the appsettings.json file. When running applications in orchestration systems like Kubernetes, the possibility to override settings through environment variables and CLI arguments can also be a valuable an comes almost for free:

16
17
18
19
20
21
22
23
24
25
26
    public class Startup
    {
        public Startup(IConfiguration configuration, IHostingEnvironment environment)
        {
            Configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{environment.EnvironmentName}", optional: true)
                .AddEnvironmentVariables()
                .AddConfiguration(configuration)
                .Build();
        }
13
14
15
16
17
18
19
20
21
22
23
24
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .ConfigureAppConfiguration(x => { x.AddCommandLine(args); });
    }

This concludes the topic on configurability of our microservice. We have a service which can assume multiple environments and adopt configuration according to them. We can inject configuration through files, environment variables and cli arguments in 64b4db1.

Despite of the csharp examples, all of the things we’ve touched on, are not language specific and there are ways of implementing them in all major languages.

We’ve learnt to:

  1. have our services terminate gracefully

  2. keep our interfaces stable and separated from service-interna

  3. get configuration into our services through files, environment variablesa and cli arguments