DEV Community

Mauricio Contreras
Mauricio Contreras

Posted on • Edited on

Increasing your .NET Core App's Resilience: Building a Faulting API

Introduction

Imagine you're building an app that depends heavily on consuming some API that you don't own. How do you handle errors while making requests? Maybe the API couldn't respond for a couple of seconds, and now it's working again (transient errors), or maybe the service is down and no answer will be given back for a while.

Alt Text

We will be covering both scenarios on this post series, we will be simulating a faulting API that responds correctly a couple of times and then stops working for a predefined time and we will look at how to implement a retry mechanism to try sending a request again and what to do if the service does not respond after some failed intents.

So let's start coding!

I created this sample using the .NET Core API Template from Visual Studio 2019, the "Weather Forecast".

Alt Text

Our sample uses AspNetCoreRateLimit nuget to restrict API calls by IP to simulate the "Out of service" state. You should definitely check it out, it has different ways to restrict API calls, and it's easy to get it to work.

Once you installed the NuGet library you can add it to your Startup.cs

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // needed to load configuration from appsettings.json
    services.AddOptions();

    // needed to store rate limit counters and ip rules
    services.AddMemoryCache();

    //load general configuration from appsettings.json
    services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));

    //load ip rules from appsettings.json
    services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));

    // inject counter and rules stores
    services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
    services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();

    // https://github.com/aspnet/Hosting/issues/793
    // the IHttpContextAccessor service is not registered by default.
    // the clientId/clientIp resolvers use it.
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

    // configuration (resolvers, counter key builders)
    services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

    services.AddControllers();
}

Enter fullscreen mode Exit fullscreen mode

Don't forget to also add app.UseIpRateLimiting(); to your Configure Method, mine looks like this:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIpRateLimiting();

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
Enter fullscreen mode Exit fullscreen mode

After setting up everything you can go and modify your API's appsettings.json and add the following:

btw: don't forget to modify the appsettings.Development.json as I always do

  "IpRateLimiting": {
    "EnableEndpointRateLimiting": false,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 500,
    "QuotaExceededMessage": "Oops, Something very bad went wrong and the server couldn't respond.",
    "GeneralRules": [
      {
        "Endpoint": "*",
        "Period": "5s",
        "Limit": 3
      }
    ]
  }
Enter fullscreen mode Exit fullscreen mode

This will be the IpRateLimit that will be used on the services you just defined on startup.cs, it will Limit the APIs calls to 3 requests per 5 seconds only. If you receive a 4th request in between those 5 seconds, you'll get a 500 error (you can set whatever HtppStatusCode you want) and a message with a response of "Oops, Something very bad went wrong and the server couldn't respond."

Note that in GeneralRules you can add as many rules you want, in my case for this demo purposes I just declared that I would apply one rule to all my endpoints "Endpoint": "*", but you can create rules for one specific endpoint like "Endpoint": "get:/api/values".

That would be for our faulting API. You can now manually test the endpoint to check how it behaves.

Now let's work on our client app that will consume the faulting API.

We will be creating a .NET Core 3.0 Worker Service that will be making requests repeatedly to our faulting API and printing on the console all the results.

For that, I started by creating a new project from the template 'Worker Service'.

Alt Text

Then at Program.cs at the CreateHostBuilder method, we add the HttpClientFactory service for our API Requests.

services.AddHttpClient<Worker>();
Enter fullscreen mode Exit fullscreen mode

Our Program class should look like this:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace NETCore_ClientConsole
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHttpClient();
                    services.AddHostedService<Worker>();
                });
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to edit our Worker.cs class

First, we need to create a new variable for our Client Factory

private readonly IHttpClientFactory _clientFactory;
Enter fullscreen mode Exit fullscreen mode

Then we can inject it to our Worker constructor like this:

public Worker(ILogger<Worker> logger, IHttpClientFactory clientFactory)
{
    _logger = logger;
    _clientFactory = clientFactory;
}
Enter fullscreen mode Exit fullscreen mode

Note that our Worker class inherits from BackgroundService, it will let us override some methods that we need for execution such as StartAsync, StopAsync, and ExecuteAsync.

We need to override the StartAsync method to create our Http Client, like this:

   public override Task StartAsync(CancellationToken cancellationToken)
        {
            client = _clientFactory.CreateClient();
            return base.StartAsync(cancellationToken);
        }
Enter fullscreen mode Exit fullscreen mode

We already defined that every time the service starts it should create our Http Client, but what if our service stops, for that, we need to override the StopAsync method, we need to tell the service that when it stops, it also Disposes our Http Client.

public override Task StopAsync(CancellationToken cancellationToken)
        {
            client.Dispose();
            return base.StopAsync(cancellationToken);
        }
Enter fullscreen mode Exit fullscreen mode

And finally, we implement our logic in the ExecuteAsync task, it will make repeatedly request calls delayed by 1 second to our Faulting API that we created previously http://localhost:59316/weatherforecast while we don't stop the process.

If the request success, we will log a message letting us know that our API responded ok. If not, it will log a Request Failed message with the respective code status. (Remember we defined that it should be a 500 code status?)

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var result = await client.GetAsync("http://localhost:59316/weatherforecast");
                if (result.IsSuccessStatusCode)
                {
                    _logger.LogInformation($"Request Succeed with code status: {result.StatusCode}");
                }
                else
                {
                    _logger.LogInformation($"Request Failed with code status: {result.StatusCode}");
                }
                await Task.Delay(1000, stoppingToken);
            }
        }
Enter fullscreen mode Exit fullscreen mode

Our final Worker class should look like this:

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace NETCore_ClientConsole
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly IHttpClientFactory _clientFactory;
        private HttpClient client;

        public Worker(ILogger<Worker> logger, IHttpClientFactory clientFactory)
        {
            _logger = logger;
            _clientFactory = clientFactory;
        }

        public override Task StartAsync(CancellationToken cancellationToken)
        {
            client = _clientFactory.CreateClient();
            return base.StartAsync(cancellationToken);
        }

        public override Task StopAsync(CancellationToken cancellationToken)
        {
            client.Dispose();
            return base.StopAsync(cancellationToken);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var result = await client.GetAsync("http://localhost:59316/weatherforecast");
                if (result.IsSuccessStatusCode)
                {
                    _logger.LogInformation($"Request Succeed with code status: {result.StatusCode}");
                }
                else
                {
                    _logger.LogInformation($"Request Failed with code status: {result.StatusCode}");
                }
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

We did it! now you can run our Worker Service by hitting F5!
psst! don't forget to set up both projects, the API and the Service.

Alt Text

Summary

That's all for now! you already learned to create a Faulting API by restricting incoming API calls and responding with different HTTP responses.
Also, we created a Worker Service client to consume our API and see how it behaves after several API calls.

I hope you enjoyed this as much as me while writing this.
In the next chapter I'll be adding different Resilience patterns to our client like Retry and Circuit Breaker, so Subscribe!

You can find the full working sample at GitHub ❀

GitHub logo maurobytes / NETCore-FaultingAPI

.NET Core 3.0 API throttled on purpose with Worker Service as a client application

ASP.NETCore Resilience Sample

This sample demonstrates how to create a Faulting Web API utilizing .NET Core 3.0 and a throttling library.

The solution has two projects, the Faulting API itself and a Worker Service that calls the API to make repeated requests and logs the responses obtained.

You can follow along this post I made at the Dev Community to learn how I built this sample

This sample was made with:






Top comments (0)