Creating MicroService with .Net 5

Creating MicroService with .Net 5

In this Article we will be create a MicroService with .Net 5

You can watch the full video on Youtube

You can get the source code from GitHub: github.com/mohamadlawand087/v23-MicroService

So what we will cover today:

  • Ingredients
  • How to create .NET 5 MicroService
  • Interact with your MicroService endpoints
  • Manage configuration and secrets
  • Handle errors
  • Add healthChecks to our service
  • Adjust logs for development and production

As always you will find the source code in the description down below. Please like, share and subscribe if you like the video. It will really help the channel

Ingredients

Creating our MicroService application

dotnet new webapi -o SampleService --no-https

The reason we disabled https is to make it simple and usually in a MicroService architecture handling SSL is done on infrastructure level.

Let us prepare our API , we will use https://www.worldcoinindex.com/apiservice to get all of the latest crypto currency prices

Now let us start developing, we will first create a class that will allow us to read the configuration which is in our case the API Key, since we don't want to store this information inside our source code.

In the root directory we will create a new folder called Configurations and inside that folder will create a class called ServiceSettings

public class ServiceSettings
{
    // This will represent the Url
    public string CoinsPriceUrl { get; set; }

    // This will represent the api key
    public string ApiKey { get; set; }
}

Now let us update our appsettings.json which represent out configuration store for the url.

"ServiceSettings": {
    "CoinsPriceUrl" : "www.worldcoinindex.com/apiservice"
  },

We should never add the secrets into our appsettings for this reason we will be using .net secret manager. To initiate it we will use the following

dotnet user-secrets init
dotnet user-secrets set ServiceSettings:ApiKey 6zvcCSO2DhUdUNMaIEEk4KTjCNfDuR8ZggM

Now we need to register our configuration inside our startup class so the application will be able to know about the host url as well the secrets keys that we have.

Inside the startup class we need to add the following

// This will allow us read the configurations from the appsettings as well the 
// secret key store
services.Configure<ServiceSettings>(Configuration.GetSection(nameof(ServiceSettings)));

The next step is to create an API client which will allow us to connect with our API.

We will be utilising RestSharp API client to do all of our calls, so we need to install our nuget package into our application

dotnet add package RestSharp

Inside our root folder will create a folder called Services and inside the Services folder will create the ApiClient class and its interface

public interface IApiClient
{
    CoinsInfo ConnectToApi(string currency);
}
public class ApiClient : IApiClient
{
    private readonly ServiceSettings _settings;

    public ApiClient(IOptions<ServiceSettings> settings)
    {
        _settings = settings.Value;
    }

    public CoinsInfo ConnectToApi(string currency)
    {
        var client = new RestClient($"https://{_settings.CoinsPriceUrl}/ticker");
        var request = new RestRequest(Method.GET);
        request.RequestFormat = DataFormat.Json;

        request.AddParameter("key", _settings.ApiKey, ParameterType.GetOrPost);
        request.AddParameter("label", "ethbtc-ltcbtc-BTCBTC-eosbtc", ParameterType.GetOrPost);
        request.AddParameter("fiat", currency, ParameterType.GetOrPost);

        var response = client.Get(request);

        var markets =  JsonSerializer.Deserialize<CoinsInfo>(response.Content);

        return markets;
    }

    // Utilising the .Net 5 records features
    public record Market (string Label, string Name, double Price);
    public record CoinsInfo (Market[] Markets);
}

Now we need to update our startup class with the new service so we can use it. Inside our startup class let us add the following

services.AddScoped<IApiClient, ApiClient>();

Now its time to create our controller, inside our controller folder will create a new class called CoinsMarketController

[ApiController]
[Route("[controller]")]
public class CoinsMarketController : ControllerBase
{
    private readonly ILogger<CoinsMarketController> _logger;
    private readonly IApiClient _apiClient;

    public CoinsMarketController(
        ILogger<CoinsMarketController> logger,
        IApiClient apiClient)
    {
        _logger = logger;
        _apiClient = apiClient;
    }

    [HttpGet]
    [Route("{currency}")]
    public IActionResult Get(string currency)
    {
        var result = _apiClient.ConnectToApi(currency);

        return Ok(result);
    }
}

Now one of the main features of MicroServices is that they are reliable, And with this external dependency there might be a chance for this service to go down due to the 3rd party service.

One of the features we can add to our service to make more reliable is retrys so in case the call has failed we automatically retry the request.

We can utilise the Polly extension from Microsoft, to do that we need to add a package

dotnet add package Microsoft.Extensions.Http.Polly

Polly will allow us to do transient error policies which will allow us to define how do we want the error to be handled.

To implement these policies we need to go to ApiClient class and update it to the following.

public class ApiClient : IApiClient
{
    private readonly ServiceSettings _settings;
    private readonly ILogger<ApiClient> _logger;
    private Policy<IRestResponse> retryPolicy;

    private static readonly List<HttpStatusCode> invalidStatusCodes = new List<HttpStatusCode> {
        HttpStatusCode.BadGateway,
        HttpStatusCode.Unauthorized,
        HttpStatusCode.InternalServerError,
        HttpStatusCode.RequestTimeout,
        HttpStatusCode.BadRequest,
        HttpStatusCode.Forbidden,
        HttpStatusCode.GatewayTimeout
    };

    public ApiClient(IOptions<ServiceSettings> settings, ILogger<ApiClient> logger)
    {
        _logger = logger;
        _settings = settings.Value;
    }

    public CoinsInfo ConnectToApi(string currency)
    {
                // We will use the Exponential backoff
        retryPolicy = Policy                
            .HandleResult<IRestResponse>(resp => invalidStatusCodes.Contains(resp.StatusCode))                
            .WaitAndRetry(10, i => TimeSpan.FromSeconds(Math.Pow(2, i)), (result, timeSpan, currentRetryCount, context) =>
            {
                _logger.LogError($"Request failed with {result.Result.StatusCode}. Waiting {timeSpan} before next retry. Retry attempt {currentRetryCount}");
            });

        var client = new RestClient($"https://{_settings.CoinsPriceUrl}/ticker");
        var request = new RestRequest(Method.GET);
        request.RequestFormat = DataFormat.Json;

        request.AddParameter("key", _settings.ApiKey, ParameterType.GetOrPost);
        request.AddParameter("label", "ethbtc-ltcbtc-BTCBTC-eosbtc", ParameterType.GetOrPost);
        request.AddParameter("fiat", currency, ParameterType.GetOrPost);

        var policyResponse = retryPolicy.ExecuteAndCapture(() =>
        {
            var response = client.Get(request);
            return response;
        });
        if(policyResponse.Result != null)
        {
            var markets =  JsonSerializer.Deserialize<CoinsInfo>(policyResponse.Result.Content);

            return markets;
        } else
        {
            return null;
        }
        // var response = client.Get(request);

        // var markets =  JsonSerializer.Deserialize<CoinsInfo>(response.Content);

        // return markets;
    }

    // Utilising the .Net 5 records features
    public record Market (string Label, string Name, double Price);
    public record CoinsInfo (Market[] Markets);
}

So this is a very good way and a standard way to keep our MicroService Reliable, but this is solution is executing after the request is being processed, and the best way to check for a potential downtime is to do that before the service execute. And to do that we will need to utilise health check.

So how do we utilise health check, we need to do the following. First we need to add the HealthCheck Service inside our startup class ConfigureServices method

services.AddHealthChecks();

Then we need to update our middlewear to utilise the HealthCheck and create an endpoint for it, which basically adds a route to check the health

endpoints.MapHealthChecks("/health");

Now we need to add the HealthCheck for the external endpoint as the basic HealthCheck will only check the status of our app without triggering 3rd party services.

Our external endpoint HealthCheck will do a ping on the api to check we get a response or not.

We start by adding a new folder called HealthChecks and inside that folder we add a new class called CoinsInfoHealthCheck and will add the following code.

// We need to implement the IHealthCheck interface and 
// implement the method
public class CoinsInfoHealthCheck : IHealthCheck
{
    private readonly ServiceSettings _settings;

    public CoinsInfoHealthCheck(IOptions<ServiceSettings> settings)
    {
        _settings = settings.Value;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        // This is default implementation
        Ping ping = new();
        var reply = await ping.SendPingAsync(_settings.CoinsPriceUrl);

        if(reply.Status != IPStatus.Success)
            return HealthCheckResult.Unhealthy();

        return HealthCheckResult.Healthy();
    }
}

Now we need to register this HealtCheck in our startup class.

now we need to configure logs to output json only to make easy to send the logs to our host log manager in an MicroServices Architecture. Inside our Program.Cs we need to add the following to CreateHostBuilder

.ConfigureLogging((context, logging) => {
                    if(context.HostingEnvironment.IsProduction())
                    {
                        logging.ClearProviders();
                        logging.AddJsonConsole();
                    }
                })

Thank you for reading.