Microservice SDK for C#

Overview

This section describes how to develop and deploy microservices on top of Cumulocity IoT using the Microservice SDK for C#, and it contains:

To develop a microservice using the SDK for C#, the starting point is our Hello world tutorial.

Info: You can develop microservices for Cumulocity IoT with any IDE and build tool that you prefer, but this guide focuses on Cake (C# Make) and Visual Studio.

If you face any issue or need technical support, please use the Cumulocity IoT community at Stack Overflow. You will find there many useful questions and answers.

Upgrading the SDK

The latest supported SDK is based on .NET Core 3.1 and Visual Studio 2019 is required for supporting it. To migrate your current version (e.g. 9.20.0 or 1004.12.0) to a new one of the SDK, update all dependencies to the latest version, e.g. 1006.6.0, and update the project to .Net SDK 3.1. The developer who is upgrading the an existing project to the latest version of SDK (1006.6.0) must follow offical microsoft guidlines about the code changes to be done for upgrading.

For new projects, you shall use a new bundle of building scripts, and it is recommended to use Cumulocity.SDK.Microservices v1006.6.0 based on .Net Core 3.1.

Development prerequisites

To use the C# client libraries for development, you need to install .NET Core SDK for your development platform such as Windows or Linux (version 3.1 of the .NET Core SDK). Note that .NET Core Runtime and .NET Core SDK are different things.

Use the following command to verify the version of your .NET Core SDK:

$ dotnet --info

The output must show a version number later than “3.1.0” to implement the basic examples.

You also need a local Docker installation. Review the information at Docker for Windows: What to know before you install and install Docker For Windows. For Linux systems, follow the general installation on the Get Started with Docker webpage.

For .NET development, Microsoft provides a number of different images depending on what you are trying to achieve.

Depending on what you want to do, you need either the .NET Core SDK or the .NET Core Runtime.

Windows system requirements

Linux system requirements

Runtime prerequisites

The most important requirement is an installation of Docker 17.06 or later.

The recommended image for production is mcr.microsoft.com/dotnet/core/aspnet:<version> AS runtime as it contains the .NET Core (runtime and libraries) and it is optimized for running .NET Core applications.

Important: Cumulocity IoT supports only Linux containers. Nevertheless, for development – should you wish to do so – it is possible to use Windows containers.

The SDK is based on the package Cumulocity.SDK.Microservices and it has a dependency on:

Hello world tutorial

This example provides a step-by-step guide to develop a simple microservice in Cumulocity IoT. It uses Cake (C# Make), which is a cross-platform build automation system.

To start building .NET apps, you just need to download and install the .NET SDK. Follow the instructions on the download page for the last stable release or alternatively you can also try using 3.1.

If you use Linux, visit the MonoDevelop website for download packages and more details about our cross-platform IDE. Follow the instructions on the download page for the last stable release or alternatively you can also try using 6.8 or higher version of mono IDE. Note, that Mono-devel is required to compile code.

The initial script was used to create a demo, which makes it easier to create an example microservice project with dependency management and then deploy it on the server. The script attempts to download the package from the sources listed in the project file, and next a reference is added to the appropriate project file. In addition, the script creates the appropriate Dockerfile to take into account the naming of projects. Next it will create a Docker image based on a Dockerfile.

The application created in this way uses the ASP.NET Web API framework to create a web API. The API runs on an isolated web server called Kestrel and as a foreground job, which makes it work really well with Docker.

Building and deploying on Windows

In Windows systems, Powershell is installed by default. Download the script file to build a “Hello world” application. Manage the version of scripts and replace latest to the right version number.

Invoke-WebRequest  http://resources.cumulocity.com/cssdk/releases/microservicesdk-win-dev-latest.zip -OutFile microservicesdk-win-dev-latest.zip

The latest can be replaced by the version number, e.g. microservicesdk-lin-dev-{X.X.X}.zip.

Once you have downloaded the source, unzip the file.

Expand-Archive c:\microservicesdk-win-dev-latest.zip -DestinationPath c:\microservicesdk-win-dev-latest

Change the current folder and navigate to the microservicesdk-win-dev-latest folder.

cd microservicesdk-win-dev-latest

Verify the version of your .Net tools.

dotnet --version

Make sure to use the correct SDK version - 3.1.200 or define which .NET Core SDK version is used when you run .NET Core CLI commands as seen in the terminal output above.

dotnet new globaljson --sdk-version 3.1.200

Run the script create.ps1 to create a sample project, provide the name of the project and the API application.

./create.ps1

Execute the bootstrapper script to build the application and an image from a Dockerfile.

./build.ps1

After a successful build you will be provided with a ZIP file in the target directory. The ZIP can be deployed to the Cumulocity IoT platform as described in the Deployment section.

Running the microservice locally

In order to test the microservice calls to Cumulocity IoT, you can run the Docker container locally.

The microservice must be deployed to verify calls from Cumulocity IoT.

To run a microservice which uses Cumulocity IoT API locally you need the following:

You may also install the cURL utility. There are several ways to install it on Windows:

Assuming that Chocolatey is installed:

choco install curl

Step 1 - Create the application

If the application does not exist, create a new application on a platform:

POST <URL>/application/applications

HEADERS:
  "Authorization": "<AUTHORIZATION>"
  "Content-Type": "application/vnd.com.nsn.cumulocity.application+json"
  "Accept: application/vnd.com.nsn.cumulocity.application+json"

BODY:
  {
    "name": "<APPLICATION_NAME>",
    "type": "MICROSERVICE",
    "key": "<APPLICATION_NAME>-microservice-key"
  }

Example:

curl -X POST -s \
-d '{"name":"hello-microservice-1","type":"MICROSERVICE","key":"hello-microservice-1-key"}' \
-H "Authorization: <AUTHORIZATION>" \
-H "Content-Type: application/vnd.com.nsn.cumulocity.application+json" \
-H "Accept: application/vnd.com.nsn.cumulocity.application+json" \
"<URL>/application/applications"

Example response:

{
    "availability": "PRIVATE",
    "id": "<APPLICATION_ID>",
    "key": "<APPLICATION_NAME>-microservice-key",
    "manifest": {
        "imports": [],
        "noAppSwitcher": true
    },
    "name": "<APPLICATION_NAME>",
    "owner": {
        "self": "...",
        "tenant": {
            "id": "..."
        }
    },
    "requiredRoles": [],
    "roles": [],
    "self": "..",
    "type": "MICROSERVICE"
}

If the application has been created correctly, you can get the application ID from the response.

Step 2 - Acquire the microservice bootstrap user

GET <URL>/application/applications/<APPLICATION_ID>/bootstrapUser

HEADERS:
  "Authorization": <AUTHORIZATION>
  "Content-Type": application/vnd.com.nsn.cumulocity.user+json

Example response:

HTTP/1.1 200 Ok
Content-Type: application/vnd.com.nsn.cumulocity.user+json
{
  "tenant": "...",
  "name": "...",
  "password": "..."
}

Step 3 - Run the microservice locally

The image is already added to the local Docker repository during the build. List all the Docker repository images available:

docker images

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
api                 latest              a8298ed10cd9        16 hours ago        258MB

After you find the image in the list, run the Docker container for the microservice by providing the baseurl and the bootstrap user credentials:

docker run -e C8Y_BASEURL=<URL> -e C8Y_BOOTSTRAP_TENANT=<BOOTSTRAP_TENANT> -e C8Y_BOOTSTRAP_USER=<BOOTSTRAP_USERNAME> -e C8Y_BOOTSTRAP_PASSWORD=<BOOTSTRAP_USER_PASSWORD> -e C8Y_MICROSERVICE_ISOLATION=MULTI_TENANT -i -t <DOCKER_REPOSITORY_IMAGE>:<TAG>

Step 4 - Subscribe to the microservice

POST <URL>/tenant/tenants/<TENANT_ID>/applications

HEADERS:
  "Authorization": "<AUTHORIZATION>"

BODY:
  { "application":
      { "id": "<APPLICATION_ID>" }
  }

Example:

curl -X POST -d '{"application":{"id": "<APPLICATION_ID>"}}'  \
-H "Authorization: <AUTHORIZATION>" \
-H "Content-type: application/json" \
 "<URL>/tenant/tenants/<TENANT_ID>/applications"

Step 5 - Verify if the microservice is running

Now you can verify if your application is running by executing

curl -H "Authorization: <AUTHORIZATION>" \
  <URL>/service/hello/weatherforecast

The expected result is like:

[
  {
    "date": "2020-07-14T10:39:06.3395082+00:00",
    "temperatureC": 27,
    "temperatureF": 80,
    "summary": "Hot"
  },
  {
    "date": "2020-07-15T10:39:06.3408548+00:00",
    "temperatureC": 40,
    "temperatureF": 103,
    "summary": "Cool"
  },
  {
    "date": "2020-07-16T10:39:06.3408577+00:00",
    "temperatureC": 18,
    "temperatureF": 64,
    "summary": "Balmy"
  }
]

Running the application within the IDE

It is possible to check if the application communicates with the platform by defining relevant environmental variables in launchSettings.json. This file sets up the different launch environments that Visual Studio can launch automatically. Here is a snippet of the default launchSettings.json.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:3288/",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Api": {
      "commandName": "Project",
      "environmentVariables": {
        "SERVER_PORT": "47000",
        "C8Y_MICROSERIVCE_ISOLATION": "PER_TENANT",
        "C8Y_BASEURL": "<URL>",
        "C8Y_BASEURL_MQTT": "",
        "C8Y_TENANT": "",
        "C8Y_PASSWORD": "",
        "C8Y_USERNAME": "",
        "C8Y_BOOTSTRAP_TENANT": "<tenant>",
        "C8Y_BOOTSTRAP_USERNAME": "<username>",
        "C8Y_BOOTSTRAP_PASSWORD": "<password>"
      }
    }
  }
}

Microservice package and deploy

Cumulocity IoT provides you with an utility tool for easy microservice packaging, deployment and subscription. The script requires running Docker and can be found in the ZIP file microservicesdk-win-dev-latest.zip. Use the following command to download it.

Invoke-WebRequest  http://resources.cumulocity.com/cssdk/releases/microservicesdk-win-dev-latest.zip -OutFile microservicesdk-win-dev-latest.zip

To show all the functions are available, type

./microservice.ps1 --help

For further information refer to General aspects > Microservice utility tool in this guide.

Deployment

In addition, there is a deploy.ps1 script that uses credentials stored locally. Run the script in order to deploy the application. You must provide the correct URL and credentials in the settings.ini file.

To deploy a microservice application on the platform, you need the following:

Configure the settings.ini file as follows:

[deploy]
username=<tenant>/<user>
password=<password>
url=<tenanturl>
appname=sample_application

deploy.ps1

./deploy.ps1

Call the script with the .ini name

./deploy.ps1 -f settings.ini
./deploy.ps1 -an hello-world -f settings_alternative.ini
./deploy.ps1 -s <siteurl> -u <username> -p <password>  -an hello-world -f settings.ini

Improving the microservice

The application starts executing from the entry point public static void Main() in Program class where the host for the application is created. The following shows an example of a program created by create.sh.

namespace api
{
	using System.Net;
    using Microsoft.AspNetCore;

	public class Program
  {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }
        public static IWebHost BuildWebHost(string[] args) =>

                WebHost.CreateDefaultBuilder(args)
                .ConfigureLogging((hostingContext, logging) =>
                {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseStartup<Startup>()
                .UseKestrel(options =>
                {
                    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
                    var port = Environment.GetEnvironmentVariable("SERVER_PORT");
                    int portNumber = 8080;
                    if (Int32.TryParse(port, out portNumber))
                    {
                        options.Listen(IPAddress.Parse("0.0.0.0"), portNumber);
                    }
                    else
                    {
                        options.Listen(IPAddress.Parse("0.0.0.0"), 1);
                    }
                })
                .Build();
	}
}

The method BuildWebHost performs the following tasks:

An example application must include Startup class. As the name suggests, it is executed first when the application starts.

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCumulocityAuthentication(Configuration);
        services.AddPlatform(Configuration);
        services.AddSingleton<IApplicationService, ApplicationService>();
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddControllers(options => options.EnableEndpointRouting = false);

    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseAuthentication();
        app.UseBasicAuthentication();
        app.UseMvcWithDefaultRoute();
    }
}

Startup.cs responsibilities:

The Dockerfile file created by create.ps1 contains:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
WORKDIR /app
COPY ./publish/Web ./
ENV SERVER_PORT 4700
ENTRYPOINT ["dotnet", "api.dll"]

It also defines what goes on in the environment inside a container:

Platform API

It is possible to use the C# REST SDK as an extension. A developer can use it to perform basic operations against the platform. For hosted deployment, most of the properties are provided by the platform.

The API provides the following services:

For further information, refer to the Device SDK guide.

C# MQTT SDK

It is possible to use the C# MQTT SDK as a nuget-package. A developer can use it to perform basic operations against the platform. For further information, refer to MQTT examples in the Device SDK guide.

Building and deploying on Linux

use the wget command to download the script file to build a “Hello World” application.

$ sudo wget  http://resources.cumulocity.com/cssdk/releases/microservicesdk-lin-dev-latest.zip

The “latest” can be replaced by the version number, e.g. microservicesdk-lin-dev-<X.X.X>.zip.

Once you have downloaded the source, unzip the file.

$ unzip microservicesdk-lin-dev-latest.zip -d  microservicesdk-latest

Change the current folder, to navigate to a microservicesdk folder.

$ cd microservicesdk-latest

Run the script create.sh to create a sample project, provide the name of the project and the API application.

$ ./create.sh

Enter the solution name:
<demo>

Enter the name of a web API project:
<api>

For a working cake you need the build.sh or build.ps1 file to bootstrap cake and the build.cake file. build.sh and build.ps1 are bootstrapper scripts that ensure you have Cake and other required dependencies installed. The bootstrapper scripts are also responsible for invoking Cake. build.cake is the actual build script.

build.cake contains tasks representing a unit of work in Cake, and you may use them to perform specific work in a specific order:

Execute the bootstrapper script, to build the application and an image from a Dockerfile.

$ ./build.sh

Launch the Docker container with the command:

$ docker run -p 8999:4700 <imagename>:latest

Check the status of the application that is running inside the Docker container.

$ curl http://localhost:8999/weatherforecast

In order to deploy the application, run the deploy script. You must provide the correct URL and credentials in this script.

Microservice package and deploy

Cumulocity IoT provides you with an utility tool for easy microservice packaging, deployment and subscription. The script requires running Docker and can be found in cumulocity-examples:

Next, add execution permissions

$ chmod +x microservice

To show all options, type

$ ./microservice help

For further information, refer to General aspects > Packing in this guide.

Deployment

In addition, there is a deploy.ps1 script that uses credentials stored locally. In order to deploy the application run the deploy script. You must provide the correct URL and credentials in the settings.ini file.

To deploy a microservice application on an environment you need the following:

The settings.ini file contains:

[deploy]
username=<tenant>/<user>
password=<password>
url=<tenanturl>
appname=sample_application
$ ./deploy.sh
$ ./deploy.sh -f settings.ini
$ ./deploy.sh -s <URL> -u <username> -p <password> -an hello-world -f settings.ini

Improving the microservice

Now that you have done your first steps, check out the section Developing microservices to find out what else can be implemented. Review also the extended example in the GitHub repository to learn more features of the microservice SDK and REST API.

Developing microservices

The SDK is based on ASP.NET Core, a cross-platform, high-performance, open-source framework for building modern, cloud-based, Internet-connected applications. ASP.NET Core apps use a Startup class, which is named Startup by convention. The Startup class must include a Configure method to create the app’s request processing pipeline, and can optionally include a ConfigureServices method to configure the app’s services.

There are two possible deployment types on the platform:

For development and testing purposes, you can deploy a microservice on a local Docker container. The process is described below.

Microservice security

The Configure method is used to specify how the application responds to HTTP requests. The request pipeline is configured by adding middleware components to an IApplicationBuilder instance.

The UseAuthentication method adds a single authentication middleware component which is responsible for automatic authentication and the handling of remote authentication requests. It replaces all of the individual middleware components with a single, common middleware component. Since ASP.NET Security does not include Basic Authentication middleware, we must add custom Basic Authentication middleware.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	app.UseAuthentication();
	app.UseBasicAuthentication();
}

Next, each authentication scheme is registered in the ConfigureServices method of Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
	services.AddCumulocityAuthentication(Configuration);
}

Platform

The root interface for connecting to Cumulocity IoT from C# is called Platform. It provides access to all other interfaces of the platform, such as the inventory. In its simplest form, it is instantiated as follows.

To enable service providers to run microservices together with the platform, it is required to execute the registration procedure. During this process each microservice receives a dedicated bootstrap user to ensure that the microservice can be identified by the platform and can only access allowed resources.

The platform is registered with the dependency injection container. Services that are registered with the dependency injection (DI) container are available to the controllers.

public void ConfigureServices(IServiceCollection services)
{
	// ...
	 services.AddPlatform(Configuration);
}

The Configuration object represents a set of key/value application configuration properties.

public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
	Configuration = configuration;
}

In this way microservices receive very basic configuration. Besides the properties related to the isolation level, the microservices will receive the following variables:

Variable Description
C8Y_BASEURL URL which points to the core platform
C8Y_BASEURL_MQTT URL which points to the core platform with MQTT protocol
SERVER_PORT Port on which the microservice runs
C8Y_MICROSERVICE_ISOLATION Isolation level
C8Y_TENANT Application user tenant (available only for PER_TENANT isolation)
C8Y_USER Application user name (available only for PER_TENANT isolation)
C8Y_PASSWORD Application user password (available only for PER_TENANT isolation)
C8Y_BOOTSTRAP_TENANT Bootstrap user to get platform subscriptions
C8Y_BOOTSTRAP_USERNAME Bootstrap user to get platform subscriptions
C8Y_BOOTSTRAP_PASSWORD Bootstrap user to get platform subscriptions

Role-based authorization

Once a user has been authenticated, the next step is to check if the user is authorized to do what they are trying to do.

[Authorize]
public IActionResult Index()
{
  return View();
}

The authorize attribute is used to protect an action in a controller from being called. If no conditions have been specified, any user who is authenticated is able to perform the action.

To be more specific and allow only members of a certain role (in this case the ROLE_APPLICATION_MANAGEMENT_READ role) to perform actions in a controller, add the role as a requirement to the attribute like this:

[Authorize(Roles = "ROLE_APPLICATION_MANAGEMENT_READ")]
public class HomeController : Controller
{
    public HomeController(Platform platform)
    {

    }
}

Accessing HTTPContext in ASP.net Core

In earlier versions of .Net Core, IHttpContextAccessor was automatically registered. This was removed. You need to register it manually if you intend to use it inside services. IHttpContextAccessor is only intended for accessing the HttpContext in locations where it is not directly available.

public void ConfigureServices(IServiceCollection services)
{
    // ...
     services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

Building a scheduled task

In order to add a new scheduled task, add it as shown in the example below. All scheduled tasks should look similar to:

public class SomeTask : IScheduledTask
{
    public string Schedule => "0 1/6 * * *";

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        //...
    }
}

where the Schedule property is a cron expression and the ExecuteAsync() method is the work to execute asynchronously.

Then you can easily register scheduled tasks:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    // Add scheduled tasks
    services.AddSingleton<IScheduledTask, SomeTask>();
}

Microservice subscription

The following section refers to the user management as described under General aspects of microservices in Cumulocity IoT.

This SDK has a task CurrentApplicationSubscriptionsTask which only fetches a list of all subscriptions. The CurrentApplicationSubscriptionsTask is the IScheduledTask implementation which runs every hour:

services.AddSingleton<IScheduledTask, CurrentApplicationSubscriptionsTask>();

services.AddScheduler((sender, args) =>
{
    Debug.Write(args.Exception.Message);
    args.SetObserved();
});

It should get all subscriptions and make it available for any other part of your application to work with.

As you can see, the AddScheduler takes a delegate that handles unobserved exceptions. In our scheduler code, TaskFactory.StartNew() is used to run the task’s code. If there is an unhandled exception, you won’t see this exception. Therefore, you may want to do some logging. This is normally done by setting TaskScheduler.UnobservedTaskException, that is global for this case so added our own to specifically catch scheduled tasks unhandled exceptions.

The SDK allows you to subscribe to the event application subscriptions changed.

Start by getting the singleton instance of the hub:

var hub = MessageHub.Instance;

You can now use the hub to subscribe to any publication of a given type, in our case OnChangedSubscription.

public class HomeController : Controller
{
    private readonly MessageHub _hub;
    private readonly Guid _subscriptionToken;

    public HomeController(Platform platform,MessageHub hub)
    {
        _hub = hub;
        _subscriptionToken =   _hub.Subscribe<List<ChangedSubscription>>(OnChangedSubscription);
    }

    private void OnChangedSubscription(List<ChangedSubscription> obj)
    {

    }
}

Program class

In ASP.NET Core 3.1, the Program class is used to setup the IWebHost. This is the entry point to our application. The main method creates a host, builds and then runs it. The host then listens for HTTP requests.

There are multiple ways to configure the application.

Simplified configuration

By using the extension to IWebHost - UseMicroserviceApplication the configuration with Startup can be simplified.

UseMicroserviceApplication has an optional parameter by default true. This parameter indicates whether to create a health point.

public class Program
{
	public static void Main(string[] args)
	{
		BuildWebHost(args).Run();
	}

	public static IWebHost BuildWebHost(string[] args) =>
		WebHost.CreateDefaultBuilder(args)
			.UseKestrel(options =>
			{
				var port = Environment.GetEnvironmentVariable("SERVER_PORT");
				options.Listen(IPAddress.Parse("0.0.0.0"), Int32.TryParse(port, out var portNumber) ? portNumber : 8080);
			})
			.ConfigureLogging((hostingContext, logging) =>
			{
				logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
				logging.AddConsole().SetMinimumLevel(LogLevel.Information);
			})
			.UseMicroserviceApplication()
			.UseStartup<Startup>()
			.Build();
}

The minimum form of the Startup class may look like as follows:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      app.UseMvcWithDefaultRoute();
		}
}

Advanced configuration

In this case, the entire configuration must be carried out manually:

public class Program
{
	public static void Main(string[] args)
	{
	    BuildWebHost(args).Run();
	}

	public static IWebHost BuildWebHost(string[] args) =>

	    WebHost.CreateDefaultBuilder(args)
	    .UseKestrel()
	    .ConfigureLogging((hostingContext, logging) =>
	    {
	        logging.SetMinimumLevel(LogLevel.Warning);
	        logging.AddConsole();
	        logging.AddDebug();
	    })
	    .UseStartup<Startup>()
	    .UseKestrel(options =>
	    {
	        var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
	        var port = Environment.GetEnvironmentVariable("SERVER_PORT");
	        int portNumber = 8080;

	        if (Int32.TryParse(port, out portNumber)){
	            options.Listen(IPAddress.Parse("0.0.0.0"), portNumber);
	        }
	        else {
	            options.Listen(IPAddress.Parse("0.0.0.0"), 1);
	        }
	    })
	    .Build();
}

The Startup class may look like the following code:

public class Startup
{
	ILogger _logger;

	public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
	{
		Configuration = configuration;
		_logger = loggerFactory.CreateLogger<Startup>();
	}

	public IConfiguration Configuration { get; }

	public void ConfigureServices(IServiceCollection services)
	{
		_logger.LogDebug($"Total Services Initially: {services.Count}");

		services.AddMemoryCache();
		services.AddPlatform(Configuration);
		ConfigureServicesLayer(services);
		services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

		// Add scheduled tasks & scheduler
		services.AddSingleton<IScheduledTask, TimerTask>();
		services.AddScheduler((sender, args) =>
		{
			Debug.Write(args.Exception.Message);
			args.SetObserved();
		});

		//MVC
		services.AddControllers(options => options.EnableEndpointRouting = false);
		//services.Replace(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(TimedLogger<>)));
	}
	public virtual void ConfigureServicesLayer(IServiceCollection services)
	{
		services.AddCumulocityAuthentication(Configuration);
		services.AddSingleton<IApplicationService, ApplicationService>();
	}

	public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
	{
		app.UseAuthentication();
		app.UseBasicAuthentication();
		app.UseMvcWithDefaultRoute();
	}
}

Health check

Health monitoring can allow near-real-time information about the state of your containers and microservices. Health monitoring is critical to multiple aspects of operating microservices and is especially important when orchestrators perform partial application upgrades in phases.

For a service or web application to expose the health check endpoint, it has to enable the UseHealthChecks([url_for_health_checks]) extension method. This method goes at the WebHostBuilder level in the main method of the Program class of your ASP.NET Core service or web application, right after UseKestrel as shown in the code below.

public class Program
{
    public static void Main(string[] args)
    {
        BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureLogging((hostingContext, logging) =>{})
            .UseStartup<Startup>()
            .UseKestrel(options =>{})
            .UseHealthChecks("/health")
            .Build();
}

Each microservice exposes the endpoint /health. This endpoint is created by the library ASP.NET Core middleware. When this endpoint is invoked, it runs all the health checks that are configured in the AddHealthChecks method in the Startup class.

The UseHealthChecks method expects a port or a path. That port or path is the endpoint to use to check the health state of the service. For instance, the catalog microservice uses the path /health.

The basic flow is that you register your health checks in your IoC container. You register these health checks via a fluent HealthCheckBuilder API in your Startup‘s ConfigureServices method. This HealthCheckBuilder will build a HealthCheckService and register it as an IHealthCheckService in your IoC container.

Built-in platform health checks

The microservice is healthy if the platform is accessible via HTTP from the application. To check it, it is possible to use an action that is built-in.

.AddPlatformCheck();

After that, you add the health check actions that you want to perform in that microservice. These actions are basically dependencies on other microservices (HttpUrlCheck) or databases (currently SqlCheck* for SQL Server databases). You add the action within the Startup class of each ASP.NET microservice or ASP.NET web application.

Custom health check

It is also possible to make your own custom health check. However, to do that, derive from IHealthCheck and implement the interface. Below is an example of one that checks to make sure the C drive has at least 1 GB of free space.

public class CheckCDriveHasMoreThan1GbFreeHealthCheck : IHealthCheck
{
    public ValueTask<IHealthCheckResult> CheckAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        long freeSpaceInGb = GetTotalFreeSpaceInGb(@"C:\");
        CheckStatus status = freeSpaceInGb > 1 ? CheckStatus.Healthy : CheckStatus.Unhealthy;

        return new ValueTask<IHealthCheckResult>(HealthCheckResult.FromStatus(status, $"Free Space [GB]: {freeSpaceinGb}"));

    }

    private long GetTotalFreeSpaceInGb(string driveName)
    {
        foreach (DriveInfo drive in DriveInfo.GetDrives())
        {
            if (drive.IsReady && drive.Name == driveName)
            {
                return drive.TotalFreeSpace / 1024 / 1024 / 1024;
            }
        }
        throw new ArgumentException($"Invalid Drive Name {driveName}");
    }
}

Then in your ConfigureServices method, register the custom health check with adequate the lifetime of the service that makes sense for the health check and then add it to the AddHealthChecks registration that has been done before.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<CDriveHasMoreThan1GbFreeHealthCheck>();

    services.AddHealthChecks(checks =>
    {
        checks.AddCheck<CheckCDriveHasMoreThan1GbFreeHealthCheck>("C Drive has more than 1 GB Free");
    });

    services.AddMvc();
}

The following example combines built-in checking and custom checking:

public class Startup
{
    ILogger _logger;
    public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
    {
        Configuration = configuration;
        ...
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        ...
        // Add framework services
        services.AddHealthChecks(checks =>
        {
            checks.AddPlatformCheck();
            checks.AddCheck("long-running", async cancellationToken =>
            {
                await Task.Delay(1000, cancellationToken);
                return HealthCheckResult.Healthy("I ran too long");
            });
        });

        ...
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        ...
        app.UseMvcWithDefaultRoute();
    }
}

Cake

Cake is a cross platform build automation system, and it is built on top of Roslyn and the Mono Compiler which uses C# as the scripting language to do things like compiling code, copy files/folders, running unit tests, compress files and build NuGet packages.

The Cake script called build.cake has has the predefined tasks. These tasks represent a unit of work in Cake, and you use them to perform specific work in a specific order.

Authentication

The C# SDK also supports OAuth tokens. Authentication with OAuth is based on cookies technology, so the token has to be read from the request cookie header. Refer to General aspects > Security for more details.

You can find a microservice example in our GitHub repository to learn how to use OAuth tokens. The microservice configures REST endpoints (GET, PUT, POST, DELETE) using basic and OAuth authentication schemes, but it does not add any business logic as it is just for demonstration. The configuration is done by runtime and adds the authentication. Finally, a web port is created and starts listening on the specified port in the Properties/launchSettings.json file.

Note that when a request authenticated with OAuth arrives to the microservice, it must be verified using a token saved in the authorization cookie and with the X-XSRF-TOKEN header. A request in user scope with OAuth must pass the cookie and header to the platform.

The UseAuthentication method adds a single authentication middleware component which is responsible for automatic authentication and the handling of remote authentication requests.

To employ OAuth you need to add the following code to the ConfigureServices method in the Startup.cs file:

services.AddCumulocityAuthenticationAll(Configuration);

This adds support to OAuth and Basic authentication – by default OAuth is being used.

public static IServiceCollection AddCumulocityAuthenticationAll(this IServiceCollection services, IConfiguration configuration)
{
    services.AddAuthentication(OAuthAuthenticationDefaults.AuthenticationScheme)
        .AddBasicAuthentication<BasicCredentialVerifier>()
        .AddOAuthAuthentication<OAuthVerifier>();
    return services;
}

Moreover, you need to register the authentication middleware. To do so, call the UseAuthentication method within the Configure method in the Startup.cs file as shown below:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseAuthentication();            // to employ OAuth
    app.UseMvcWithDefaultRoute();
}

Important: Do not use the AddBasicAuthentication method if you want OAuth-based authentication for your microservice.

To use multiple authentication schemes, the Controllers class must be decorated as follows:

[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = AllSchemes)]
public class ValuesController : Controller
{
    public const string AllSchemes =
        BasicAuthenticationDefaults.AuthenticationScheme + ", " +
        OAuthAuthenticationDefaults.AuthenticationScheme;

    // REST endpoints methods ...

}

Refer to Authorize with a specific scheme in ASP.NET Core for more details.

Testing the microservice with OAuth tokens

Third-party tools such as Postman can be used to test your REST API. You need the cookie and X-XSRF-TOKEN headers which must be captured from the platform (e.g. by watching network requests in the browser). Note that the token expires within few minutes as configured by the administrator. Make sure you test this feature before the token expires. The cURL equivalent to test the microservice example is as follows:

curl 'http://<URL>/api/values' \
  -H 'X-XSRF-TOKEN: CHMkPIteHmTiihocpPwZ' \
  -H 'Cookie: authorization=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGwsImlzcyI6Im9hdXRoLWp3a3MubGF0ZXN0LnN0YWdlLmM4eS5pbyIsImF1ZCI6Im9hdXRoLWp3a3MubGF0ZXN0LnN0YWdlLmM4eS5pbyIsInN1YiI6ImRvbWluaWthIiwidGNpIjoiMjY0NjBjNGItZWJmNy00OGRlLWE1ZmMtYzkxZGJhZWM3MWFlIiwiaWF0IjoxNTk3ODk4OTcyLCJuYmYiOjAsImV4cCI6MTU5NzkwMjU3MiwidGZhIjpmYWxzZSwidGVuIjoidDU5MDA4IiwieHNyZlRva2VuIjoiQ0hNa1BJdGVIbVRpaWhvY3BQd1oifQ.laooVzd3jS2Vj9Pj86To1M1ONl7_m7bPX0cGH8dYnUltDu5jxwNjpaCy7L8Hei59VYB7euGO7qn0LeqNZGt9Nw; XSRF-TOKEN=CHMkPIteHmTiihocpPwZ' \
  --compressed

You can get the headers from the Network tab and replace them in the example above to hit the endpoint which returns 200 OK response code.

Troubleshooting

Some common problems and their solutions have been identified and documented below.

After deploying my microservice, requests to any endpoint return an error message “Microservice not available Connection refused”

After uploading the microservice, the internal deployment and container run may take a couple of minutes. Once completed, the error message will disappear.

I get a message error saying “The current .NET SDK does not support targeting .NET Core 3.1.”

This error will appear if you try running the SDK on Visual Studio 2017 or lower. The current version of the Cumulocity IoT Microservices SDK (1006.6.0) is compatible with .NET Core 3.1 and Visual Studio 2019, and it is a prerequisite for developing applications.