developers

Implementing an API Gateway in ASP.NET Core with Ocelot

Learn what an API Gateway is and how to build yours in ASP.NET Core by using Ocelot.

The urge towards adopting microservices architecture has been a welcome trend in the software development industry. Microservices architecture has been one of the most talked-about technologies in recent times – it has been embraced by many leading organizations worldwide. Originally developed to solve the limitations of monolithic systems, microservices architecture has seen a significant increase in popularity over the last several years, mainly due to increased scalability, flexibility, and performance.

Since microservices-based applications comprise several different services, you often need a common interface or gateway to call these services so that you define and manage all concerns in one place rather than replicate them across all downstream services. This is precisely where an API Gateway comes in. This article briefly discusses the concepts around microservices architecture and how you can work with API Gateways to have a consistent way to connect to the microservices. Let's get started!

What Is Microservices Architecture?

Microservices architecture is a service-oriented architecture (SOA) variant in which an application comprises a collection of lightweight, loosely coupled, modular services. These services can be built to execute on various platforms and independently developed, tested, deployed, and managed.

Microservices architecture can replace long-running, complicated monolithic systems requiring considerable resource and management overhead. Microservice is a word that refers to a service having a limited set of functionalities rather than referring to the length of the code used to create it.

What Is an API Gateway?

When building microservices-based applications, an API Gateway is needed to have a central place where authentication, throttling, orchestration, etc., is implemented. Without an API Gateway in place, you might typically implement each of these in each service, and hence maintaining them for each service would be a daunting task at hand. An API Gateway decouples the service producer from its consumer, providing a security layer since you need not expose your microservices directly.

As soon as it receives a request, it breaks it into multiple requests (if needed) and then routes them to the appropriate downstream microservice. You can take advantage of an API Gateway to centralize, manage, and monitor the non-functional requirements of an application, orchestrate the cross-functional microservices, and reduce roundtrips. By managing requests consistently, an API Gateway can help reduce the latency and improve security.

The figure below illustrates an API Gateway used to connect to two downstream microservices named Customer and Product.

API Gateway architecture diagram

Usually, the service consumers or clients of a microservice don't communicate with it directly. Instead, an API Gateway provides a single entry point for directing traffic to various microservices, as shown in the figure above. Hence the clients don't have any direct access to the services and cannot exploit the services. If your API Gateway is behind the firewall, you can add an extra layer of protection around the attack surface.

An API Gateway pattern corresponds to two famous Gang of Four design patterns: Facade and Adapter. Like the Facade design pattern, an API Gateway provides an API to the consumers while encapsulating the internal architecture. An API Gateway enables communication and collaboration like in the Adapter design pattern, even if the interfaces are incompatible.

Why Do We Need an API Gateway?

A microservices-based application comprises many different microservices built using homogenous or heterogeneous technologies. An API Gateway provides a centralized point of entry for external consumers, regardless of the number or composition of the downstream microservices. An API Gateway can often contain an additional layer of rate-limiting and security.

Here are the main benefits of an API Gateway:

  • Better isolation: An API Gateway provides isolation by preventing direct access to internal concerns. As a result, it can encapsulate service discovery, versioning, and other internal details from the service consumers or clients. Additionally, an API Gateway enables you to add more microservices or change boundaries without impacting the external consumers.
  • Improved security: An API Gateway provides a security layer for your microservices that can help prevent attack vectors such as SQL Injection, Denial-of-Service (DoS), etc. You can leverage an API Gateway to authenticate users. If a particular service consumer needs data from multiple services, you need to authenticate the user just once hence reducing latency and making your authentication mechanism consistent across the application.
  • Performance metrics: Since an API Gateway is a single component through which all requests and responses flow, it is a great place to collect metrics. For example, you can measure the count and execution times of the requests forwarded to the downstream microservices.
  • Reduced complexity: Microservices have specific common concerns that include logging, rate-limiting, security, etc. You'll need more time to develop these concerns in each of the microservices your application is using. An API Gateway can eliminate this code duplication hence reducing the effort required to create these components.

API Gateways and reverse proxies

There is a lot of confusion around a reverse proxy and an API Gateway. While there are similarities between them, there are subtle differences between the two as well.

Reverse proxy servers typically sit behind a firewall and route requests from the client to the appropriate back-end server. A reverse proxy is a lightweight API Gateway that comprises a few basic security and monitoring capabilities. So, if you need an API Gateway with basic features, a reverse proxy server should suffice. Note that a reverse proxy is incapable of performing transformation or orchestration.

An API gateway sits between the client and a set of back-end services and provides much more extensive security and monitoring capabilities than a reverse proxy server. An API Gateway provides support for comprehensive service orchestration, transformation, and mediation. It also offers extensive support for transport security - much more than a simple proxy can provide.

Introducing Ocelot

In this article, we are going to use Ocelot API Gateway. It is a lightweight, open-source, scalable, and fast API Gateway based on .NET Core and specially designed for microservices architecture. Basically, it is a set of middleware designed to work with ASP.NET Core. It has several features such as routing, caching, security, rate limiting, etc.

The Order Processing Microservices-Based Application

Let's now put the concepts we've learned thus far into practice by implementing a concrete example. We'll build an order processing application that illustrates how an API Gateway can be used to invoke each service to retrieve customer and product data using the Customer and Product microservice, respectively.

Typically, an order processing microservices-based application comprises microservices such as Product, Customer, Order, OrderDetails, etc. In this example, we'll consider a minimalistic microservices-based application. This application will contain an API Gateway and two microservices - the Product and Customer microservice. The application would be simple so that we can focus more on building the API Gateway.

Prerequisites

To execute the code examples shown in this article, here are the minimum requirements you should have installed in your system:

  • .NET 5 SDK
  • Visual Studio 2019

The solution structure

The application you are going to build will comprise the following projects as part of a single Visual Studio solution:

  • OrderProcessing
    project - This project represents the API Gateway and is responsible for getting requests from the clients and invoking the microservices.
  • OrderProcessing.Customer
    project - This project defines the classes and interfaces used to represent the customer microservice.
  • OrderProcessing.Product
    project - This project defines the types used to represent the product microservice.

The Customer microservice project will comprise the following classes and interfaces:

  • Customer
    class – This represents the customer entity class.
  • ICustomerRepository
    interface – This represents the interface for the customer repository.
  • CustomerRepository
    class – This represents the customer repository class that implements the
    ICustomerRepository
    interface.
  • CustomerController
    class – This class represents the API controller for the Customer microservice.

The Product microservice project will contain the following types:

  • Product
    class – This class represents the product entity.
  • IProductRepository
    interface – This represents the interface for the product repository.
  • ProductRepository
    class – This is the product repository class that implements the
    IProductRepository
    interface.
  • ProductController
    class – This represents the API controller class for the Product microservice.

The following picture shows how the solution structure of the completed application will look like:

Solution Structure of the OrderProcessing Application

Create the projects for the Order Processing application

Open a command shell and enter the following commands to create the three ASP.NET projects we need:

dotnet new web --framework "net5.0" -o OrderProcessing
dotnet new webapi --framework "net5.0" -o OrderProcessing.Customer
dotnet new webapi --framework "net5.0" -o OrderProcessing.Product

While the

OrderProcessing
project is an empty ASP.NET project, the other two projects are WebAPI projects. Ensure that you delete the default controller and entity classes from these two projects as we don’t need them.

Create the Customer microservice

Create a new file named

Customer.cs
at the root of the
OrderProcessing.Customer
project with the following code in there:

// OrderProcessing.Customer/Customer.cs

using System;

namespace OrderProcessing.Customer
{
    public class Customer
    {
        public Guid Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string EmailAddress { get; set; }
    }
}

Create the CustomerRepository class

Create an interface named

ICustomerRepository
in a file named
ICustomerRepository.cs
at the root of the
OrderProcessing.Customer
project with the following code in there:

// OrderProcessing.Customer/ICustomerRepository.cs

using System.Collections.Generic;
using System.Threading.Tasks;

namespace OrderProcessing.Customer
{
    public interface ICustomerRepository
    {
        public Task<List<Customer>> GetAllCustomers();
    }
}

Create the

CustomerRepository
class that implements the
ICustomerRepository
interface at the root of the
OrderProcessing.Customer
project as shown in the following code snippet:

// OrderProcessing.Customer/CustomerRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace OrderProcessing.Customer
{
    public class CustomerRepository : ICustomerRepository
    {
        private readonly List<Customer> customers = new List<Customer>();
        public CustomerRepository()
        {
            customers.Add(new Customer()
            {
                Id = Guid.NewGuid(),
                FirstName = "Joydip",
                LastName = "Kanjilal",
                EmailAddress = "joydipkanjilal@yahoo.com"
            });

            customers.Add(new Customer()
            {
                Id = Guid.NewGuid(),
                FirstName = "Steve",
                LastName = "Smith",
                EmailAddress = "stevesmith@yahoo.com"
            });
        }
        public Task<List<Customer>> GetAllCustomers()
        {
            return Task.FromResult(customers);
        }
    }
}

Create the CustomerController class

In the

Controllers
folder of the
OrderProcessing.Customer
project, create an API controller named
CustomerController
and replace the default code with the following:

// OrderProcessing.Customer/Controllers/CustomerController.cs

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace OrderProcessing.Customer.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomerController : ControllerBase
    {
        private readonly ICustomerRepository _customerRepository;
        public CustomerController(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }

        [HttpGet]
        public async Task<ActionResult<List<Customer>>> GetAllCustomers()
        {
            return await _customerRepository.GetAllCustomers();
        }
    }
}

Create the Product microservice

Create a new file named

Product.cs
at the root of the
OrderProcessing.Product
project with the following code in there:

// OrderProcessing.Product/Product.cs

using System;

namespace OrderProcessing.Product
{
    public class Product
    {
        public Guid Id { get; set; }
        public string Code { get; set; }
        public string Name { get; set; }
        public int Quantity_In_Stock { get; set; }
        public decimal Unit_Price { get; set; }
    }
}

Create the ProductRepository class

Next, you should create a new file called

IProductRepository.cs
in the
OrderProcessing.Product
project and write the following code to create the
IProductRepository
interface.

// OrderProcessing.Product/IProductRepository.cs

using System.Collections.Generic;
using System.Threading.Tasks;

namespace OrderProcessing.Product
{
    public interface IProductRepository
    {
        public Task<List<Product>> GetAllProducts();
    }
}

Create the

ProductRepository
class that implements the
IProductRepository
interface at the root of the
OrderProcessing.Product
project as shown in the following code snippet:

// OrderProcessing.Product/ProductRepository.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace OrderProcessing.Product
{
    public class ProductRepository : IProductRepository
    {
        private readonly List<Product> products = new List<Product>();
        public ProductRepository()
        {
            products.Add(new Product
            {
                Id = Guid.NewGuid(),
                Code = "P0001",
                Name = "Lenovo Laptop",
                Quantity_In_Stock = 15,
                Unit_Price = 125000
            });

            products.Add(new Product
            {
                Id = Guid.NewGuid(),
                Code = "P0002",
                Name = "DELL Laptop",
                Quantity_In_Stock = 25,
                Unit_Price = 135000
            });

            products.Add(new Product
            {
                Id = Guid.NewGuid(),
                Code = "P0003",
                Name = "HP Laptop",
                Quantity_In_Stock = 20,
                Unit_Price = 115000
            });
        }
        public Task<List<Product>> GetAllProducts()
        {
            return Task.FromResult(products);
        }
    }
}

Create the ProductController class

In the

Controllers
folder of the
OrderProcessing.Product
project, create an API controller named
ProductController
and replace the default code with the following:

// OrderProcessing.Product/ProductController.cs

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace OrderProcessing.Product.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductController : ControllerBase
    {
        private readonly IProductRepository _productRepository;
        public ProductController(IProductRepository customerRepository)
        {
            _productRepository = customerRepository;
        }

        [HttpGet]
        public async Task<ActionResult<List<Product>>> GetAllCustomers()
        {
            return await _productRepository.GetAllProducts();
        }
    }
}

Implement the API Gateway Using Ocelot

Now that the projects have been created with the necessary files in them, let’s implement the API Gateway using Ocelot.

Before going any further, you should be aware of the terms upstream and downstream. While upstream refers to the request sent by the client to the API Gateway, downstream is related to the request that the API Gateway sends to a particular microservice.

Install the required package

To work with Ocelot, you must install it in your ASP.NET Core project. In our case, you will install Ocelot in the

OrderProcessing
project. You can do it by using the NuGet Package Manager inside Visual Studio IDE. Alternatively, you can execute the following command at the Package Manager Console window:

Install-Package Ocelot

Implement routing

An Ocelot API Gateway accepts an incoming HTTP request and forwards it to a downstream service. Ocelot makes use of routes to define how a request is routed from one place to another. Add a new file named

ocelot.json
to this project with the following content in there:

// OrderProcessing/Ocelot.json
{
   "Routes":[
      //Customer API{
         "DownstreamPathTemplate":"/api/Customer",
         "DownstreamScheme":"http",
         "DownstreamHostAndPorts":[
            {
               "Host":"localhost",
               "Port":"20057"
            }
         ],
         "UpstreamPathTemplate":"/Customer",
         "UpstreamHttpMethod":[
            "GET"
         ]
      },
      //Product API{
         "DownstreamPathTemplate":"/api/Product",
         "DownstreamScheme":"http",
         "DownstreamHostAndPorts":[
            {
               "Host":"localhost",
               "Port":"32345"
            }
         ],
         "UpstreamPathTemplate":"/Product",
         "UpstreamHttpMethod":[
            "GET"
         ]
      }
   ]
}

The above configuration specifies the downstream and upstream metadata (scheme, path, ports) for the customer and product microservices. So, while use the upstream metadata to call the endpoints specified here, the request is routed to the appropriate downstream service as specified in the downstream metadata. In other words, the downstream metadata is used to specify the internal service URL to redirect a request to when the API Gateway receives a new request.

You should add Ocelot to the service container by calling the

AddOcelot
method in the
ConfigureServices
method of the
Startup
class as shown below:

// OrderProcessing/Startup.cs

// ... existing code

public void ConfigureServices(IServiceCollection services) 
{
  services.AddOcelot(Configuration);
}

// ... existing code

Next, you should enable Ocelot in the

Configure
method of the
Startup
class by calling the
UseOcelot
extension method as shown here:

// OrderProcessing/Startup.cs

// ... existing code

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
{
  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }

  app.UseRouting();
  app.UseOcelot();

  app.UseEndpoints(endpoints => {
    endpoints.MapGet("/", async context => {
      await context.Response.WriteAsync("Hello World!");
    });
  });
}

// ... existing code

Run the projects

Now make sure that you've made all three projects as startup projects. To do this, follow these steps:

  1. In the Solution Explorer window, right-click on the solution file.
  2. Click "Properties".
  3. In the "Property Pages" window, select the "Multiple startup projects" radio button:

Multiple startup projects option in Visual Studio

  1. Click Ok.

Press the F5 key to run the application. Now send an HTTP Get request to the following URL from Postman or any other HTTP client of your choice:

http://localhost:39469/Customer

The HTTP Get method of the Customer controller will be executed and the output will look like this:

The Customer Microservice in Execution!

Send an HTTP Get request from Postman to the following URL:

http://localhost:39469/Product

The request first goes to the API Gateway. Next, the API Gateway routes the request to the correct downstream microservice as specified in

ocelot.json
. The HTTP Get method named
GetAllProducts
of the
ProductController
will be called, and the output will look like this:

The Product Microservice in Execution!

Implement rate limiting

Rate limiting is a technique for controlling network traffic. It sets a limit on how many times you can perform a specific activity within a given period - for example, accessing a particular resource, logging into an account, etc. Typically, rate-limiting keeps track of the IP addresses and the time elapsed between requests. The IP address helps determine the source of a particular request.

A rate-limiting solution is adept at tracking the time elapsed between each request and the total number of requests in a particular period. If a single IP address makes excessive requests within a specific timeframe, the rate-limiting solution will reject the requests for a specified period.

In order to prevent your downstream services from being overburdened, Ocelot enables rate-limiting of upstream requests. The following configuration illustrates how you specify rate-limiting in Ocelot:

// OrderProcessing/Ocelot.json

"RateLimitOptions":{
   "ClientWhitelist":[      
   ],
   "EnableRateLimiting":true,
   "Period":"5s",
   "PeriodTimespan":1,
   "Limit":1,
   "HttpStatusCode":429
}

Let us now examine each of these options briefly:

  • ClientWhitelist
    setting - This is an array used to specify the clients that should not be affected by the rate-limiting.
  • EnableRateLimiting
    setting - This is a boolean value,
    true
    if you want to enable rate-limiting,
    false
    otherwise.
  • HttpStatusCode
    setting - This is used to specify the HTTP status code that is returned when rate limiting occurs.
  • Period
    setting - This specifies the duration that the rate limit is applicable, which in turn implies that if you make more requests within this duration than what is allowed, you'll need to wait for the duration specified in the
    PeriodTimespan
    .
  • PeriodTimespan
    setting - This is used to specify the duration after which you can retry to connect to a service.
  • Limit
    setting - This specifies the maximum number of requests that are allowed within the duration specified in
    Period
    .

Let us assume that rate limiting is applied to the Product microservice only. The updated

ocelot.json
file will now look like this:

// OrderProcessing/Ocelot.json

{
   "Routes":[
      //Customer API
     {
         "DownstreamPathTemplate":"/api/Customer",
         "DownstreamScheme":"http",
         "DownstreamHostAndPorts":[
            {
               "Host":"localhost",
               "Port":"20057"
            }
         ],
         "UpstreamPathTemplate":"/Customer",
         "UpstreamHttpMethod":[
            "GET"
         ]
      },
      //Product API
      {
         "DownstreamPathTemplate":"/api/Product",
         "DownstreamScheme":"http",
         "DownstreamHostAndPorts":[
            {
               "Host":"localhost",
               "Port":"32345"
            }
         ],
         "RateLimitOptions":{
            "ClientWhitelist":[
               
            ],
            "EnableRateLimiting":true,
            "Period":"5s",
            "PeriodTimespan":1,
            "Limit":1
         },
         "UpstreamPathTemplate":"/Product",
         "UpstreamHttpMethod":[
            "GET"
         ]
      }
   ]
}

Now, run the application and send frequent requests (more than 1 per 5sec) and you’ll see the following error:

Rate Limiting at work!

Implement caching

Caching is a widely popular technique used in web applications to keep data in memory so that the same data may be quickly accessed when required by the application. Ocelot provides support for basic caching. To take advantage of it, you should install the

Ocelot.Cache.CacheManager
NuGet package as shown below:

Install-Package Ocelot.Cache.CacheManager

Next, you should configure caching using the following code in the

ConfigureServices
method:

// OrderProcessing/Startup.cs

// ... existing code ...

public void ConfigureServices(IServiceCollection services)
{
  services.AddOcelot(Configuration)
    .AddCacheManager(x =>
      {
        x.WithDictionaryHandle();
      });
}

// ... existing code ...

Lastly, you should specify caching on a particular route in the route configuration using the following settings:

// OrderProcessing/Ocelot.json

// ... existing settings ...

"Routes":[
   //Customer API{
      "DownstreamPathTemplate":"/api/Customer",
      "DownstreamScheme":"http",
      "DownstreamHostAndPorts":[
         {
            "Host":"localhost",
            "Port":"20057"
         }
      ],
      "FileCacheOptions":{
         "TtlSeconds":30,
         "Region":"customercaching"
      },
      "UpstreamPathTemplate":"/Customer",
      "UpstreamHttpMethod":[
         "GET"
      ]
   }
]

// ... existing settings ...

Here, we've set

TtlSeconds
to 30 seconds which implies that the cache will expire after this time has elapsed. Note that you should specify your cache configuration in the
FileCacheOptions
section. The
Region
setting identifies the area within the cache that will contain the data. This way you can clear that area by using the Ocelot's administration API.

To test this, you can set a breakpoint on the HTTP Get method named

GetAllCustomers
in the
CustomerController
class. When you execute the application and send an HTTP Get request to the endpoint, the breakpoint will be hit as usual. However, all subsequent calls to the same endpoint within 30 seconds (this is the duration we've specified) will fetch data, but the breakpoint will not be hit anymore.

Implement correlation ID

Ocelot enables a client to send a request Id in the header to the server. Once this request Id is available in the middleware pipeline, you can log it along with other information. Ocelot can also forward this request Id to the downstream services if required. A correlation ID is a unique identifier attached to every request and response and used to track requests and responses in a distributed application. You can use either a request Id or a correlation ID when working with Ocelot to track requests.

The primary difference between a request Id and a correlation ID is that while the former uniquely identifies every HTTP request, the latter is a unique identifier attached to a particular request-response chain. While you can use

Request-Id
for every HTTP request, you can use
X-Correlation-Id
for an event chain of requests and responses.
X-Correlation-Id
is the name of the HTTP header attached to the downstream requests used to track HTTP requests that flow through multiple back-end services.

Ocelot must know the URL that it is running on in order to perform certain administration configurations. This is the

BaseUrl
specified in the
ocelot.json
file. Note that this URL should be the URL that your clients will see the API Gateway running on.

Here's the complete source code of the

ocelot.json
file for your reference:

// OrderProcessing/Ocelot.json

{
   "Routes":[
      //Customer API
     {
         "DownstreamPathTemplate":"/api/Customer",
         "DownstreamScheme":"http",
         "DownstreamHostAndPorts":[
            {
               "Host":"localhost",
               "Port":"20057"
            }
         ],
         "FileCacheOptions":{
            "TtlSeconds":30,
            "Region":"customercaching"
         },
         "UpstreamPathTemplate":"/Customer",
         "UpstreamHttpMethod":[
            "GET"
         ]
      },
      //Product API
     {
         "DownstreamPathTemplate":"/api/Product",
         "DownstreamScheme":"http",
         "DownstreamHostAndPorts":[
            {
               "Host":"localhost",
               "Port":"32345"
            }
         ],
         "RateLimitOptions":{
            "ClientWhitelist":[
               
            ],
            "EnableRateLimiting":true,
            "Period":"5s",
            "PeriodTimespan":1,
            "Limit":1
         },
         "UpstreamPathTemplate":"/Product",
         "UpstreamHttpMethod":[
            "GET"
         ]
      }
   ],
   "GlobalConfiguration":{
      "RequestIdKey":"X-Correlation-Id",
      "BaseUrl":"http://localhost:39469"
   }
}

Conclusion

Choosing an exemplary architecture for the needs of your business is the first and foremost step in building applications that are flexible, scalable, and high performant. One of the most significant advantages of microservices architecture is its support for heterogeneous platforms and technologies.

Your API Gateway can manage concerns such as security, rate limiting, performance, and scalability. However, you should be aware of handling the complexity it brings in and the risk of a single point of failure. Besides, there is a learning curve when you're building microservices-based applications using an API Gateway. Possible performance degradation is yet another concern that you must handle.

The complete source code of the OrderProcessing application built throughout this article is available here.

Aside: Securing ASP.NET Core with Auth0

Securing ASP.NET Core applications with Auth0 is easy and brings a lot of great features to the table. With Auth0, you only have to write a few lines of code to get a solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).

On ASP.NET Core, you need to create an API in your Auth0 Management Dashboard and change a few things on your code. To create an API, you need to sign up for a free Auth0 account. After that, you need to go to the API section of the dashboard and click on "Create API". On the dialog shown, you can set the Name of your API as "Books", the Identifier as "http://books.mycompany.com", and leave the Signing Algorithm as "RS256".

Creating API on Auth0

After that, you have to add the call to

services.AddAuthentication()
in the
ConfigureServices()
method of the
Startup
class as follows:

string authority = $"https://{Configuration["Auth0:Domain"]}/";
string audience = Configuration["Auth0:Audience"];

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
  options.Authority = authority;
  options.Audience = audience;
});

In the body of the

Configure()
method of the
Startup
class, you also need to add an invocation to
app.UseAuthentication()
and
app.UseAuthorization()
as shown below:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

Make sure you invoke these methods in the order shown above. It is essential so that everything works properly.

Finally, add the following element to the

appsettings.json
configuration file:

{
  "Logging": {
    // ...
  },
  "Auth0": {
    "Domain": "YOUR_DOMAIN",
    "Audience": "YOUR_AUDIENCE"
  }
}

Note: Replace the placeholders

YOUR_DOMAIN
and
YOUR_AUDIENCE
with the actual values for the domain that you specified when creating your Auth0 account and the Identifier you assigned to your API.