API Gateways for Microservices

Photo by Growtika on Unsplash

API Gateways for Microservices

Implementing API gateways in ASP.NET with Ocelot.

Introduction

In the ever-evolving landscape of software development, building robust and scalable APIs is essential for creating seamless communication between different software system components. One powerful tool that plays a crucial role in managing and optimizing API traffic is the API Gateway. In this blog, we'll explore the world of API Gateways in C# and how they contribute to the efficiency and effectiveness of modern applications.

Benefits of API Gateways

An API Gateway is a central point for managing and orchestrating API requests and responses. It is a gateway between client applications and the underlying microservices or server-side components. API Gateways offer a range of functionalities, including:

  1. Simplified Client-Side Logic: API Gateways abstract the complexities of backend services, providing a unified entry point for clients. This simplifies client-side logic by eliminating the need for clients to interact directly with multiple microservices.

  2. Routing and Load Balancing: Distributing incoming requests across multiple servers to ensure optimal resource utilization and prevent overloading.

  3. Authentication and Authorization: Enforcing security measures by validating and authorizing requests before they reach the intended services.

  4. Rate Limiting: Controlling the rate at which requests are allowed, preventing abuse or misuse of the API.

  5. Request Transformation: Modifying requests or responses to adhere to a specific format or version, facilitating compatibility between different API versions.

  6. Logging and Monitoring: Collecting and analyzing data related to API traffic for monitoring, debugging, and analytics purposes.

There are many flavors of API gateways, including cloud services like AWS gateway, Azure API management, etc., and on-prem solutions like Ocelot, an open-source library. We would be implementing our gateway using Ocelot. To learn more about this library's features and coverage, please look at the reference available here: https://ocelot.readthedocs.io/en/latest/index.html.

Important Terminology in Ocelot Configuration

  • Routes

    • Description: Defines a route that maps requests to /service1/{everything} upstream to /api/service1/{everything} downstream. Supports both GET and POST methods, is case-insensitive, and requires authentication with specified scopes.

    • JSON:

        "Routes": [
          {
            "DownstreamPathTemplate": "/api/service1/{everything}",
            "UpstreamPathTemplate": "/service1/{everything}",
            "UpstreamHttpMethod": [ "GET", "POST" ],
            "RouteIsCaseSensitive": false,
            "AuthenticationOptions": {
              "AuthenticationProviderKey": "IdentityServer",
              "AllowedScopes": [ "api1.read", "api1.write" ]
            }
          },
          // Additional routes...
        ]
      
  • AuthenticationOptions

    • Description: Specifies authentication settings for a route, indicating the use of the "IdentityServer" provider and allowing access only to clients with "api1.read" and "api1.write" scopes.

    • JSON:

        "AuthenticationOptions": {
          "AuthenticationProviderKey": "IdentityServer",
          "AllowedScopes": [ "api1.read", "api1.write" ]
        }
      
  • LoadBalancerOptions

    • Description: Configures load balancing using the round-robin strategy for distributing requests among multiple instances of a downstream service.

    • JSON:

        "LoadBalancerOptions": {
          "Type": "RoundRobin"
        }
      
  • RateLimitOptions

    • Description: Enables rate limiting for clients "client1" and "client2," allowing 10 requests per second with a period of 1 second.

    • JSON:

        "RateLimitOptions": {
          "ClientWhitelist": [ "client1", "client2" ],
          "EnableRateLimiting": true,
          "Period": "1s",
          "Limit": 10
        }
      
  • QoSOptions

    • Description: Configures Quality of Service (QoS), allowing 5 exceptions before circuit breaking for 30 seconds, with a timeout value of 5 seconds.

    • JSON:

        "QoSOptions": {
          "ExceptionsAllowedBeforeBreaking": 5,
          "DurationOfBreak": "30s",
          "TimeoutValue": "5000"
        }
      
  • GlobalConfiguration

    • Description: Sets the base URL for the API gateway as "http://api-gateway/" and specifies the key used to identify requests as "OcRequestId."

    • JSON:

        "GlobalConfiguration": {
          "BaseUrl": "http://api-gateway/",
          "RequestIdKey": "OcRequestId"
        }
      
  • ServiceDiscoveryProvider

    • Description: Configures Ocelot to use Consul as the service discovery provider, connecting to Consul server at "consul-server" on port 8500.

    • JSON:

        "ServiceDiscoveryProvider": {
          "Type": "Consul",
          "Host": "consul-server",
          "Port": 8500
        }
      
  • FileConfiguration

    • Description: Represents the overall Ocelot configuration file structure, including route configurations, global settings, and middleware configurations.

    • JSON:

        {
          "Routes": [
            // Route configurations...
          ],
          "GlobalConfiguration": {
            // Global settings...
          },
          "MiddlewareConfiguration": {
            // Middleware configurations...
          }
        }
      
  • MiddlewareConfiguration

    • Description: Configures middleware settings, including pre-authentication and pre-routing middleware options in the Ocelot processing pipeline. Additional middleware configurations can be added as needed.

    • JSON:

        "MiddlewareConfiguration": {
          "PreAuthenticationMiddleware": {
            // Pre-authentication middleware settings...
          },
          "PreRoutingMiddleware": {
            // Pre-routing middleware settings...
          },
          // Additional middleware configurations...
        }
      

Implementing API Gateways in C

Our implementation would be done for a fictional e-commerce application comprising the following services: Orders service, Payment service, and Shipping service.

Let's get started!

  1. Implement our respective microservices and APIs

    • Create three new API projects: Orders.Api , Payments.Api , and Shippings.Api

    • Add the following lines of code to the Program.cs file of each project.

      For brevity, most implementation details have been excluded. Please take a look at the GitHub repository to see the full code.

    • Orders.Api

      ```csharp using Amazon.SQS; using AutoMapper; using SagaPattern.Commons; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Payments.Api.Commands; using Payments.Api.Data; using Payments.Api.Dtos; using Payments.Api.Entities; using Payments.Api.Events.ExternalEvents; using Payments.Api.Interfaces; using Payments.Api.Service;

      var builder = WebApplication.CreateBuilder(args);

      // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();

      builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddDbContext(options => { options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddEventHandlers(typeof(Program)); builder.Services.AddCommandHandlers(typeof(Program)); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.Configure(builder.Configuration.GetSection("QueueSettings"));

      var app = builder.Build();

      using var serviceScope = app.Services.CreateScope(); var dbContext = serviceScope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync();

      // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

      app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers();

      app.ListenForSqsEvents(new[] { nameof(OrderCreatedEvent), nameof(ShippingCancelledEvent) });

      app.MapPost("api/payments", async ([FromServices] IMapper mapper, [FromServices] ICommandHandler commandHandler, [FromBody] PaymentDetailsForCreateDto paymentDetails) => { var payment = mapper.Map(paymentDetails); await commandHandler.HandleAsync(new ProcessPaymentCommand(payment));

      return StatusCodes.Status201Created; });

app.MapDelete("api/payments/{paymentId:guid}", async ( [FromRoute] Guid paymentId, [FromServices] ICommandHandler commandHandler) => { await commandHandler.HandleAsync(new DeletePaymentCommand(paymentId));

return StatusCodes.Status204NoContent; });

app.Run();


    * `Payments.Api`

        ```csharp
        using AutoMapper;
        using Commons;
        using Microsoft.AspNetCore.Mvc;
        using Microsoft.EntityFrameworkCore;
        using Payments.Api.Commands;
        using Payments.Api.Data;
        using Payments.Api.Dtos;
        using Payments.Api.Entities;
        using Payments.Api.Interfaces;
        using Payments.Api.Service;

        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddControllers();
        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        builder.Services.AddAutoMapper(typeof(Program));
        builder.Services.AddDbContext<AppDbContext>(options =>
        {
            options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));
        });
        builder.Services.AddScoped<IPaymentRepository, PaymentRepository>();
        builder.Services.AddScoped<IPaymentProcessor, PaymentProcessor>();
        builder.Services.AddEventHandlers(typeof(Program));
        builder.Services.AddCommandHandlers(typeof(Program));

        var app = builder.Build();

        using var serviceScope = app.Services.CreateScope();
        var dbContext = serviceScope.ServiceProvider.GetRequiredService<AppDbContext>();
        await dbContext.Database.MigrateAsync();

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();
        app.UseAuthorization();
        app.MapControllers();

        app.MapPost("api/payments",
            async ([FromServices] IMapper mapper,
                [FromServices] ICommandHandler<ProcessPaymentCommand> commandHandler,
                [FromBody] PaymentDetailsForCreateDto paymentDetails) =>
            {
                var payment = mapper.Map<PaymentDetail>(paymentDetails);
                await commandHandler.HandleAsync(new ProcessPaymentCommand(payment));

                return StatusCodes.Status201Created;
            });


        app.MapDelete("api/payments/{paymentId:guid}", async (
            [FromRoute] Guid paymentId,
            [FromServices] ICommandHandler<DeletePaymentCommand> commandHandler) =>
        {
            await commandHandler.HandleAsync(new DeletePaymentCommand(paymentId));

            return StatusCodes.Status204NoContent;
        });

        app.Run();
  • Shippings.Api

      using AutoMapper;
      using Microsoft.AspNetCore.Mvc;
      using Microsoft.EntityFrameworkCore;
      using Commons;
      using Shipping.Api.Commands;
      using Shipping.Api.Data;
      using Shipping.Api.Dtos;
      using Shipping.Api.Entities;
      using Shipping.Api.Interfaces;
      using Shipping.Api.Services;
    
      var builder = WebApplication.CreateBuilder(args);
    
      // Add services to the container.
    
      builder.Services.AddControllers();
      // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
      builder.Services.AddEndpointsApiExplorer();
      builder.Services.AddSwaggerGen();
    
      builder.Services.AddAutoMapper(typeof(Program));
      builder.Services.AddDbContext<AppDbContext>(options =>
      {
          options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"));
      });
      builder.Services.AddScoped<IShippingRequestRepository, ShippingRequestRepository>();
      builder.Services.AddEventHandlers(typeof(Program));
      builder.Services.AddCommandHandlers(typeof(Program));
    
      var app = builder.Build();
      using var serviceScope = app.Services.CreateScope();
      var dbContext = serviceScope.ServiceProvider.GetRequiredService<AppDbContext>();
      await dbContext.Database.MigrateAsync();
    
      // Configure the HTTP request pipeline.
      if (app.Environment.IsDevelopment())
      {
          app.UseSwagger();
          app.UseSwaggerUI();
      }
    
      app.UseHttpsRedirection();
      app.UseAuthorization();
      app.MapControllers();
    
      app.MapPost("api/shipping",
          async (
              [FromServices] IMapper mapper,
              [FromServices] ICommandHandler<CreateShippingRequestCommand> commandHandler,
              [FromBody] ShippingRequestForCreateDto shippingRequestDto) =>
          {
              var shippingRequest = mapper.Map<ShippingRequest>(shippingRequestDto);
              await commandHandler.HandleAsync(new CreateShippingRequestCommand(shippingRequest));
    
              return StatusCodes.Status201Created;
          });
    
      app.MapDelete("api/shipping/{shippingRequestId:guid}",
          async ([FromServices] ICommandHandler<DeleteShippingRequestCommand> commandHandler,
              [FromRoute] Guid shippingRequestId) =>
          {
              await commandHandler.HandleAsync(new DeleteShippingRequestCommand(shippingRequestId));
    
              return StatusCodes.Status204NoContent;
          });
    
      app.Run();
    
  1. Create a new project for the API gateway

  2. Configure our API gateway

    • Install Ocelot NuGet Package:

      In your ASP.NET Core project, install the Ocelot NuGet package using the package manager console or the Package Manager UI:

        dotnet add package Ocelot
      
    • Configure Ocelot inStartup.cs:

      In the Startup.cs file, configure Ocelot to the DI pipeline:

        builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange:true);
        builder.Services.AddOcelot(builder.Configuration);
      

      Add Ocelot to the request middleware:

        await app.UseOcelot();
      
    • Create a new file called Ocelot.json. This file would contain the configurations for our API gateway.

    • Add configurations to our Ocelot.json file.

        {
          "Routes": [
            {
              "DownstreamPathTemplate": "/api/payments/{paymentId}",
              "DownstreamHostAndScheme":"https",
              "DownstreamHostAndPorts": {
                "Host": "localhost",
                "Ports": 7178
              },
              "UpstreamPathTemplate": "/api/payments/{paymentId}",
              "UpstreamHttpMethod": [
                "DELETE"
              ]
            },
            {
              "DownstreamPathTemplate": "/api/payments",
              "DownstreamHostAndScheme":"https",
              "DownstreamHostAndPorts": {
                "Host": "localhost",
                "Ports": 7178
              },
              "UpstreamPathTemplate": "/api/payments",
              "UpstreamHttpMethod": [
                "POST"
              ]
            },
            {
              "DownstreamPathTemplate": "/api/shippings/{shippingId}",
              "DownstreamHostAndScheme":"https",
              "DownstreamHostAndPorts": {
                "Host": "localhost",
                "Ports": 7282
              },
              "UpstreamPathTemplate": "/api/shippings/{shippingId}",
              "UpstreamHttpMethod": [
                "DELETE"
              ]
            },
            {
              "DownstreamPathTemplate": "/api/shippings",
              "DownstreamHostAndScheme":"https",
              "DownstreamHostAndPorts": {
                "Host": "localhost",
                "Ports": 7282
              },
              "UpstreamPathTemplate": "/api/shippings",
              "UpstreamHttpMethod": [
                "POST"
              ]
            },
            {
              "DownstreamPathTemplate": "/api/orders/{orderId}",
              "DownstreamHostAndScheme":"https",
              "DownstreamHostAndPorts": {
                "Host": "localhost",
                "Ports": 7154
              },
              "UpstreamPathTemplate": "/api/orders/{orderId}",
              "UpstreamHttpMethod": [
                "DELETE"
              ]
            },
            {
              "DownstreamPathTemplate": "/api/orders",
              "DownstreamHostAndScheme":"https",
              "DownstreamHostAndPorts": {
                "Host": "localhost",
                "Ports": 7154
              },
              "UpstreamPathTemplate": "/api/orders",
              "UpstreamHttpMethod": [
                "POST"
              ]
            }
          ],
          "GlobalConfiguration": {
            "BaseUrl": "https://localhost:7166/"
          }
        }
      
  3. Testing the whole flow.

    • Run our services, including the API Gateway project.

    • Call the endpoints via the API gateway URL to verify the requests are being passed downstream to our microservices.

      We get a 201 Created response. Logs from our Orders.Api also shows it responding to the call from the API gateway.

Conclusion

API gateways are an essential component of modern software architecture, providing a centralized and efficient way to manage API traffic. They simplify client-side logic, enhance security, improve performance, and streamline the versioning process. By utilizing the API gateways pattern in C#, developers can create high-performing and future-proof systems that are scalable and resilient. Ocelot, an open-source library, simplifies the process of implementing API gateways in .NET. You can find the code for this article here.