API gateway using ocelot in asp.net core application

7 minute read

Introduction

API Gateway is an API management tools that sits between a client application and backend application. It agregates different services, maintain load balancing and work as reverse proxy. Ocelot is an api managment tool which is very powerful and best fit for .net application.

Tools and technologies used

  • Visual Studio 2022
  • .NET 6.0
  • In Memory Database
  • Entity Framework
  • ASP.NET Core Web API
  • C#
  • Ocelot

Implementation

Step 1: Create solution and projects.

  • Create a solution name APIGateway
  • Add 4 new web api project, name - Catalog.API, Location.API, Ordering.API and BFF.Web in the solution.

Here, BFF.Web project will act as API Gateway.

Step 2: Install nuget packages.

  • Install following nuget package in Catalog.API Project
PM> Install-Package Microsoft.EntityFrameworkCore.InMemory
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools
  • Install following nuget package in Ordering.API Project
PM> Install-Package Microsoft.EntityFrameworkCore
PM> Install-Package Microsoft.EntityFrameworkCore.InMemory
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools
  • Install following nuget packages in BFF.Web Project
PM> Install-Package Ocelot
PM> Install-Package Ocelot.Cache.CacheManager

Step 3: Organize Catalog.API Project

  • Create a Product model class in Catalog.API/Model folder

Product.cs


using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Catalog.API.Model
{
    public class Product
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string Name { get; set; }

        public string Description { get; set; }

        public decimal Price { get; set; }

        public int AvailableStock { get; set; }

        public int RestockThreshold { get; set; }
    }
}

  • Create a CatalogContext class in Catalog.API/Db folder

CatalogContext.cs


using Catalog.API.Model;
using Microsoft.EntityFrameworkCore;

namespace Catalog.API.Db
{
    public class CatalogContext : DbContext
    {
        public CatalogContext(DbContextOptions<CatalogContext> options) : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
        }

        public DbSet<Product> Products { get; set; }
    }
}

  • Modify Program.cs file as follows

using Catalog.API.Db;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddDbContext<CatalogContext>(opt => opt.UseInMemoryDatabase("CatalogDB"));

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

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

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

  • Create a conroller class name ProductsController in Catalog.API/Controllers folder

CatalogContoller.cs

#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Catalog.API.Db;
using Catalog.API.Model;

namespace Catalog.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly CatalogContext _context;

        public ProductsController(CatalogContext context)
        {
            _context = context;
        }

        // GET: api/Products
        [HttpGet("GetAll")]
        public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
        {
            return await _context.Products.ToListAsync();
        }

        // GET: api/Products/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Product>> GetProduct(int id)
        {
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            return product;
        }

        // PUT: api/Products/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("Edit/{id}")]
        public async Task<IActionResult> PutProduct(int id, Product product)
        {
            if (id != product.Id)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ProductExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Products
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost("Add")]
        public async Task<ActionResult<Product>> PostProduct(Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetProduct", new { id = product.Id }, product);
        }

        // DELETE: api/Products/5
        [HttpDelete("Delete/{id}")]
        public async Task<IActionResult> DeleteProduct(int id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool ProductExists(int id)
        {
            return _context.Products.Any(e => e.Id == id);
        }
    }
}

Step 4: Organize Ordering.API Project

  • Create a Order model class in Ordering.API/Model folder

Order.cs


namespace Ordering.API.Models
{
    public class Order
    {
        public int Id { get; set; }
        public string Address { get; set; }

        public DateTime OrderDate { get; set; }

        public string Comments { get; set; }
    }
}


  • Create a OrderingContext class in Ordering.API/Db folder

OrderingContext.cs


using Microsoft.EntityFrameworkCore;
using Ordering.API.Models;

namespace Ordering.API.Db
{
    public class OrderingContext : DbContext
    {
        public OrderingContext(DbContextOptions<OrderingContext> options) : base(options)
        {

        }
        public DbSet<Ordering.API.Models.Order> Order { get; set; }
    }
}


  • Modify Program.cs file as follows

using Microsoft.EntityFrameworkCore;
using Ordering.API.Db;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddDbContext<OrderingContext>(opt => opt.UseInMemoryDatabase("CatalogDB"));

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

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

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

  • Create a conroller class name OrdersController in Ordering.API/Controllers folder

OrdersController.cs

#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Ordering.API.Db;
using Ordering.API.Models;

namespace Ordering.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly OrderingContext _context;

        public OrdersController(OrderingContext context)
        {
            _context = context;
        }

        // GET: api/Orders
        [HttpGet("GetAll")]
        public async Task<ActionResult<IEnumerable<Order>>> GetOrder()
        {
            return await _context.Order.ToListAsync();
        }

        // GET: api/Orders/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Order>> GetOrder(int id)
        {
            var order = await _context.Order.FindAsync(id);

            if (order == null)
            {
                return NotFound();
            }

            return order;
        }

        // PUT: api/Orders/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("Edit/{id}")]
        public async Task<IActionResult> PutOrder(int id, Order order)
        {
            if (id != order.Id)
            {
                return BadRequest();
            }

            _context.Entry(order).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!OrderExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Orders
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost("Add")]
        public async Task<ActionResult<Order>> PostOrder(Order order)
        {
            _context.Order.Add(order);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetOrder", new { id = order.Id }, order);
        }

        // DELETE: api/Orders/5
        [HttpDelete("Delete/{id}")]
        public async Task<IActionResult> DeleteOrder(int id)
        {
            var order = await _context.Order.FindAsync(id);
            if (order == null)
            {
                return NotFound();
            }

            _context.Order.Remove(order);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool OrderExists(int id)
        {
            return _context.Order.Any(e => e.Id == id);
        }
    }
}

Step 5: Organize Location.API Project

  • Create CountriesController in Location.API/Controllers folder

using Microsoft.AspNetCore.Mvc;

namespace Location.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class CountriesController : ControllerBase
    {
      [HttpGet("GetAll")]
      public IEnumerable<string> Get()
        {
            return new string[] {"America","Bangladesh", "Canada" };
        }
    }
}

Step 6: Organize BFF.Web (API Gateway) Project

  • Add a configuraton file for api gateway. I keep this file name - ocelot.json. Add this file in the root directory.

ocelot.json

{
  //---Location Service: Start ----------//
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/Countries/GetAll",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7003
        }
      ],

      // Configure caching
      // the cache will expire after 30 seconds.
      "FileCacheOptions": {
        "TtlSeconds": 30,
        "Region": "countriescaching"
      },

      "UpstreamPathTemplate": "/Countries/GetAll",
      "UpstreamHttpMethod": [ "Get" ],

      // Enable case sensative Routing/URL
      "RouteIsCaseSensitive": true
    },

    //---Location Service: End ----------//

    // Catalog Services
    //------------------//
    {
      "DownstreamPathTemplate": "/api/Products/GetAll",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7282
        }
      ],

      // Implement rate limiting
      // maximum admitted 1 per 5s.
      "RateLimitOptions": {
        "ClientWhitelist": [
          // This is an array used to specify the clients that should not be affected by the rate-limiting
        ],
        "EnableRateLimiting": true,
        "Period": "5s",
        "PeriodTimespan": 1,
        "Limit": 1
      },

      "UpstreamPathTemplate": "/Products/GetAll",
      "UpstreamHttpMethod": [ "Get" ]
    },
    {
      "DownstreamPathTemplate": "/api/Products/Add",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7282
        }
      ],

      "UpstreamPathTemplate": "/Products/Add",
      "UpstreamHttpMethod": [ "Post" ]
    },

    {
      "DownstreamPathTemplate": "/api/Products/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7282
        }
      ],

      "UpstreamPathTemplate": "/Products/{id}",
      "UpstreamHttpMethod": [ "Get" ]
    },

    {
      "DownstreamPathTemplate": "/api/Products/Edit/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7282
        }
      ],

      "UpstreamPathTemplate": "/Products/Edit/{id}",
      "UpstreamHttpMethod": [ "Put" ]
    },

    {
      "DownstreamPathTemplate": "/api/Products/Delete/{id}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7282
        }
      ],

      "UpstreamPathTemplate": "/Products/Delete/{id}",
      "UpstreamHttpMethod": [ "Delete" ]
    },

    //---Catalog service : End ------------//

    //---Ordering Service: Start ----------//
    // Catch All Routing
    {
      "DownstreamPathTemplate": "/{url}",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 7126
        }
      ],

      "UpstreamPathTemplate": "/{url}",
      "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ]
    }

  ],

  //---Ordering Service: End ----------//

  //https://localhost:7282/api/Products/GetAll

  "GlobalConfiguration": {
    // enable request correleation id to capture request information
    "RequestIdKey": "X-Correlation-Id",
    "BaseUrl": "https://localhost:7205/"
  }
}

  • Modify Program.cs file as follows

Program.cs

using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Cache.CacheManager;


var builder = WebApplication.CreateBuilder(args);


var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true)
    .AddEnvironmentVariables();


// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

// Swagger for ocelot
//builder.Services.AddSwaggerForOcelot(builder.Configuration);
//builder.Services.AddSwaggerForOcelot();
builder.Services.AddSwaggerGen();

//For ocelot
builder.Services.AddOcelot()
    
    // Added for caching
    .AddCacheManager(x => {
        x.WithDictionaryHandle();
    });

var app = builder.Build();

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

app.UseOcelot();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Step 7: Run and Test application

  • Now run multiple (all) projects and test application using postman.
  • You have to check application using api gateway.
  • For Location service, test using https://localhost:7205/Countries/GetAll
  • For Catalog service, test using https://localhost:7205/Products/{endpoints}
  • For Ordering service, test using https://localhost:7205/api/Orders/{endpoints}

Note:

Configure caching in api gateway

  • Added the following section in ocelot.json file.

"FileCacheOptions": {
"TtlSeconds": 30,
"Region": "countriescaching"
}

  • Added the following code block in the the Program.cs file as follows
//For ocelot
builder.Services.AddOcelot()
    
    // Added for caching
    .AddCacheManager(x => {
        x.WithDictionaryHandle();
    });

Enable case sensative URL

  • Add following blocks in ocelot.json for case sensative URL
      // Enable case sensative Routing/URL
      "RouteIsCaseSensitive": true

Implement rate limiting

  • Add following blocks in ocelot.json for rate limiting

// Implement rate limiting
// maximum admitted 1 per 5s.
"RateLimitOptions": {
"ClientWhitelist": [
    // This is an array used to specify the clients that should not be affected by the rate-limiting
],
"EnableRateLimiting": true,
"Period": "5s",
"PeriodTimespan": 1,
"Limit": 1
}

Catch all routing

  • Add following blocks in ocelot.json for catch all routing

{
    "DownstreamPathTemplate": "/{url}",
    "DownstreamScheme": "https",
    "DownstreamHostAndPorts": [
    {
        "Host": "localhost",
        "Port": 7126
    }
    ],

    "UpstreamPathTemplate": "/{url}",
    "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ]
}

Source code