Configure Swagger on api gateway using ocelot in asp.net core application

6 minute read

Introduction

Swagger configuration on API gateway is not as simple as you are configure normal application. You have to configure it in different way. In this article I will create an API gateway using ocelot and asp.net core application and show you how to configure swagger on API gateway.

Tools and technologies used

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

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.Provider.Polly
PM> Install-Package Ocelot.Cache.CacheManager
PM> Install-Package MMLib.SwaggerForOcelot

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

CatalogController.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

  • Create a folder name Routes and add the following files in that folder

ocelot.catalog.api.json


{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "https",
      "SwaggerKey": "catalog",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": "7282"
        }
      ],
      "UpstreamPathTemplate": "/catalog/{everything}",
      "UpstreamHttpMethod": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
      ]
    }
  ]
}

ocelot.location.api.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "https",
      "SwaggerKey": "location",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": "7003"
        }
      ],
      "UpstreamPathTemplate": "/location/{everything}",
      "UpstreamHttpMethod": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
      ]
    }
  ]
}

ocelot.ordering.api.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/{everything}",
      "DownstreamScheme": "https",
      "SwaggerKey": "ordering",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": "7126"
        }
      ],
      "UpstreamPathTemplate": "/ordering/{everything}",
      "UpstreamHttpMethod": [
        "GET",
        "POST",
        "PUT",
        "DELETE"
      ]
    }
  ]
}

ocelot.global.json

{
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5205"
  }
}

ocelot.SwaggerEndPoints.json

{
  "SwaggerEndPoints": [
    {
      "Key": "bffweb",
      "TransformByOcelotConfig": false,
      "Config": [
        {
          "Name": "BFF.Web",
          "Version": "1.0",
          "Url": "http://localhost:5205/swagger/v1/swagger.json"
        }
      ]
    },
    {
      "Key": "location",
      "TransformByOcelotConfig": true,
      "Config": [
        {
          "Name": "Location.API",
          "Version": "1.0",
          "Url": "http://localhost:5205/location/swagger/v1/swagger.json"
        }
      ]
    },
    {
      "Key": "catalog",
      "TransformByOcelotConfig": true,
      "Config": [
        {
          "Name": "Catalog.API",
          "Version": "1.0",
          "Url": "http://localhost:5205/catalog/swagger/v1/swagger.json"
        }
      ]
    },
    {
      "Key": "ordering",
      "TransformByOcelotConfig": true,
      "Config": [
        {
          "Name": "Ordering.API",
          "Version": "1.0",
          "Url": "http://localhost:5205/catalog/swagger/v1/swagger.json"
        }
      ]
    }
  ]
}
  • Add AlterUpstream Class in Config Folder

AlterUpstream.cs

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace BFF.Web.Config
{
    public class AlterUpstream
    {
        public static string AlterUpstreamSwaggerJson(HttpContext context, string swaggerJson)
        {
            var swagger = JObject.Parse(swaggerJson);
            // ... alter upstream json
            return swagger.ToString(Formatting.Indented);
        }
    }
}
  • Modify Program.cs file as follows

Program.cs

using BFF.Web.Config;
using MMLib.SwaggerForOcelot.DependencyInjection;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Polly;

var builder = WebApplication.CreateBuilder(args);

var routes = "Routes";

builder.Configuration.AddOcelotWithSwaggerSupport(options =>
{
    options.Folder = routes;
});

builder.Services.AddOcelot(builder.Configuration).AddPolly();
builder.Services.AddSwaggerForOcelot(builder.Configuration);

var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
    .AddOcelot(routes, builder.Environment)
    .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.AddSwaggerGen();

var app = builder.Build();

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


app.UseHttpsRedirection();

app.UseAuthorization();

app.UseSwaggerForOcelotUI(options =>
{
    options.PathToSwaggerGenerator = "/swagger/docs";
    options.ReConfigureUpstreamSwaggerJson = AlterUpstream.AlterUpstreamSwaggerJson;

}).UseOcelot().Wait();

app.MapControllers();

app.Run();

Step 7: Run and Test application

  • Now run multiple (all) projects and test application using postman.
  • Check all end point using api gateway and swagger using the following URL

https://localhost:7205/swagger/index.html

  • Select Swagger definition from top right corner of BFF

Source code