Post

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

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
    1
    2
    3
    
    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
    1
    2
    3
    4
    
    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
    1
    2
    3
    4
    
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    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

1
2
3
4
5
6
7
8
9
10
11
12
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    {
      "Routes": [
        {
          "DownstreamPathTemplate": "/{everything}",
          "DownstreamScheme": "https",
          "SwaggerKey": "catalog",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": "7282"
            }
          ],
          "UpstreamPathTemplate": "/catalog/{everything}",
          "UpstreamHttpMethod": [
            "GET",
            "POST",
            "PUT",
            "DELETE"
          ]
        }
      ]
    }

ocelot.location.api.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    {
      "Routes": [
        {
          "DownstreamPathTemplate": "/{everything}",
          "DownstreamScheme": "https",
          "SwaggerKey": "location",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": "7003"
            }
          ],
          "UpstreamPathTemplate": "/location/{everything}",
          "UpstreamHttpMethod": [
            "GET",
            "POST",
            "PUT",
            "DELETE"
          ]
        }
      ]
    }

ocelot.ordering.api.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    {
      "Routes": [
        {
          "DownstreamPathTemplate": "/{everything}",
          "DownstreamScheme": "https",
          "SwaggerKey": "ordering",
          "DownstreamHostAndPorts": [
            {
              "Host": "localhost",
              "Port": "7126"
            }
          ],
          "UpstreamPathTemplate": "/ordering/{everything}",
          "UpstreamHttpMethod": [
            "GET",
            "POST",
            "PUT",
            "DELETE"
          ]
        }
      ]
    }

ocelot.global.json

1
2
3
4
5
    {
      "GlobalConfiguration": {
        "BaseUrl": "http://localhost:5205"
      }
    }

ocelot.SwaggerEndPoints.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    {
      "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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    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

This post is licensed under CC BY 4.0 by the author.