We will attempt to grasp the concept of Asp.Net Core Middleware. How to use it in the application, as well as various methods for adding custom pipelines to the request life cycle.

If you have used an earlier version of Asp.Net then you might be aware of HttpHandler and Httpmodule.

Before we move on to the concept of asp.net core middleware, let’s talk briefly about HttpHandler and HttpModules, which you may have used in an earlier version of Asp. Net frameworks.

What is HttpHandler?

HTTP handlers are used by IIS to handle specific requests based on extensions. HTTPHandlers run as processes in response to a request made to the ASP.NET application.

As you can see in the above image. There are multiple handlers registered in the IIS.

  • StaticFile: To handle all kinds of requests made for static assets like HTML .css,.Js, and images.
  • PHP53_FastGate: To handle all .php request.

What is an Httpmodule in asp.net?

HTTP modules are filters that are capable of pre-and post-process requests as they enter the pipeline. Indeed many of ASP.NET’s resources are implemented as HTTP modules.

What is an Httpmodule in asp.net
Httpmodule in IIS

HttpModules runs for every request, it does not matter what the file extension. However, you can always add conditional logic to validate the request and perform the operation based on that.

In the Asp.Net application, we no more need to host the website on IIS. It can be hosted as a console application as well. So, you are never going to execute any of the modules and handlers. This is where your middleware concepts came into the picture.

What is .Net Core Custom Middleware?

.Net Core Custom middleware can either: Handle an incoming HTTP request by generating an HTTP response. Process an incoming HTTP request, modify it and pass it on to another piece of middleware. Or just return the response based on the conditional logic & skip the other pipelines.

What is a pipeline in Asp.Net core?

pipeline in asp.net core
Pipeline Chain in Asp.Net Core- using Middleware

As you can see in the above diagram. Middleware is chained to each other to form a pipeline. Incoming requests are passed through the pipeline, where each middleware has a chance to do something with them before passing them to the next middleware. Outgoing responses are also passed through the pipeline, in reverse order. 

Every Asp.Net core application must have at least one middleware to handle the incoming request. In order to work with MVC, you have to use the MVC pipeline in your “startup.cs” file

  app.UseMvc();

.Net Core middleware vs Filter

Middleware operates on the level of ASP.NET Core and can act on every single request that comes into the application.

MVC filters on the other hand only run for requests that come to MVC.

So for example, if I wanted to enforce that all requests must be done over HTTPS, I would have to use middleware for that. If I made an MVC filter that did that, users could still request e.g. static files over HTTP.

But then on the other hand something that logs request durations in MVC controllers could absolutely be an action filter.

.Net Core Custom Middleware real-time examples

Let’s try to understand the middleware concept by example code. Also, add custom middleware based on the specific request.

I will be using the Asp.net core 3.0 version to show the example of middleware. Example code will be available on Github.

understand the middleware concept by example code.
Visual Studio Solution for Asp.Net Core Middleware

If you have already worked on the Asp.Net core applications. You will be aware of the Startup.cs file. This is the file where we can define the configuration for the entire application.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MiddlewareDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

As seen in the file startup.cs above. Many middlewares have already been added to the Request pipeline.

  • UseDeveloperExceptionPage
  • UseExceptionHandler
  • UseStaticFiles
  • UseRouting
  • UseAuthorization

There are many more pipelines available in the Asp.net framework.

.Net Core Middleware Order of Execution

The Configure method defines the order in which the middleware components are invoked on requests and the reverse order for the response. Order is critical to security, performance, and functionality.

Let’s run the application and see what we get in the browser.

asp.net core middleware order demo application

Got the welcome page. The application is up and running without any issue.

What If you mess with the Order of Asp.Net Core Middlewares

Navigate to the Pages/index.cshtml page and add this line of code to throw the exception.

throw new Exception();
@page
@model IndexModel
@{
    throw new Exception();
    ViewData["Title"] = "Home page";
}
<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

As you can see, we are throwing an exception on top of the page which will cause 500 errors on the index page.

Run the application again, you should see something like this.

app.UseDeveloperExceptionPage()

We can see this detailed message because of the app.UseDeveloperExceptionPage() middleware is added before any other middleware in the pipeline.

Now lets change the order of the app.UseDeveloperExceptionPage() & add this middleware at last.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MiddlewareDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
        }
    }
}

Run the application and open the same page.

500 Error ASP.Net Core Application

Now you’re not seeing the detailed exception message as earlier. Since there is no pipeline to manage the exception before the MVC pipeline and display the detailed message in the UI.

So, it’s very important to register the middleware in a specific order.

Lets move on to the implementation of middleware. How we can add the custom middleware to the Asp.Net core application.

.Net Core Custom Middleware

There are multiple ways you can add middleware to the request pipeline.

  1. Run
  2. Map
  3. MapWhen .Net Core Function
  4. Use
  5. UseMiddleware
  6. UseMiddleware<>
  7. Using custom extension methods.

Let’s understand the .net core middleware with a real-time example

  • My client has a very basic requirement that can be fulfilled by simply adding the middleware to my solution.
  • When someone visits mywebsite.com/ip they should see their IP address as a response.

We will try to implement this requirement by adding middleware to the solution in different-2 ways.

Run Method in Asp.Net Core Middleware

Add custom middleware in ASP.NET Core using the Run method

It takes RequestDelegate as an argument according to the description of the method. So, we can pass the method to run the extension method as a reference, or we can write the anonymous method.

Note: Run method Terminates chain. No other middleware method will run after this. Should be placed at the end of any pipeline.

Middleware with a Run method using an anonymous block

 app.Run(async context =>
            {
                if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
                {
                    await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
                }
                await Task.CompletedTask;
            });

Make sure to add this piece of code after every pipeline in the configure method. Because no pipeline will be executed after calling a Run method. Run adds a terminal middleware, i.e. a middleware that is the last in the pipeline.

Your startup.cs file should look like this.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MiddlewareDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
            //Middleware using block
            app.Run(async context =>
            {
                if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
                {
                    await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
                }
                await Task.CompletedTask;
            });
        }
    }
}

Let’s run the application and hit the URL http://localhost:5000/ip

Middleware with a Run method using an anonymous block

Don’t worry about the Ip address you see in the response, this is because you are running locally.

Middleware with a Run method using a method reference

The run method takes a method for which the signature matches the RequestDelegate.

 A function that can process an HTTP request
RequestDelegate Methods
  private async Task GetIpInfo(HttpContext context)
        {
            if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
            {
                await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            }
            await Task.CompletedTask;
        }
 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MiddlewareDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
            //Middleware using block
            //app.Run(async context =>
            //{
            //    if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
            //    {
            //        await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            //    }
            //    await Task.CompletedTask;
            //});
            //Middleware using RequestDelegate method
            app.Run(GetIpInfo);
        }
        private async Task GetIpInfo(HttpContext context)
        {
            if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
            {
                await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            }
            await Task.CompletedTask;
        }
    }
}

Run the application and navigate to the URL. you should see the same response as earlier.

Map vs MapWhen .net Core

As we saw in the earlier piece of code. In order to match the URL, we had to put if condition. Then perform our logic for the pipeline. We can use a map function to accomplish the same mission.

Map

The Map Enables branching pipeline. Runs specified middleware if the condition is met. Lets achieve our requirement using this function. It can be a cleaner way to fulfill the requirement.

using System;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Builder
{
    //
    // Summary:
    //     Extension methods for the Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.
    public static class MapExtensions
    {
        //
        // Summary:
        //     Branches the request pipeline based on matches of the given request path. If
        //     the request path starts with the given path, the branch is executed.
        //
        // Parameters:
        //   app:
        //     The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
        //
        //   pathMatch:
        //     The request path to match.
        //
        //   configuration:
        //     The branch to take for positive path matches.
        //
        // Returns:
        //     The Microsoft.AspNetCore.Builder.IApplicationBuilder instance.
        public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration);
    }
}

As per the method description, Branches the request pipeline based on matches of the given request path. If the request path starts with the given path, the branch is executed.

Execute the Run block using the Map function.

 app.Map(new PathString("/ip"), a => a.Run(async context =>
               {
                   await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
               }));

As you can see, this piece of code will be executed when the incoming request matches the path provider in the map function.

You can also use the delegate function instead of a code block. As we have already seen in the earlier example.

.net Core Mapwhen

MapWhen also fulfills the same purpose with better control on mapping conditional logic using the predicates.

Middleware using the MapWhen function

Let’s write the predicate that matches the incoming request endpoint.

   app.MapWhen(
                 ctx => ctx.Request.Path.StartsWithSegments(new PathString("/ip")), a => a.Run(async context =>
            {
                await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            }));

The next requirement from the client is the calculate the total time taken by each request.

Lets see how we can fulfill the requirement using the custom middleware in the request pipeline.

Use and UseWhen in .Net Core Middleware

To configure multiple middlewares, use Use() the extension method. It is similar to Run() the method except that it includes the next parameter to invoke the next middleware in the sequence.

Add middleware using use() and UseWhen() extension methods

Middleware using Use() extension method

Let’s add the middleware using the Use() method. In this case, you don’t have to add at the end. Because it will always call the next() pipeline.

However, in order to meet our client requirements, we have to keep this middleware in the first place. So, it can calculate the total time taken by the entire request.

app.Use(async (context, next) =>
            {
                var watch = new Stopwatch();
                watch.Start();
                await next();
                watch.Stop();
                Console.WriteLine("Total Request Time in Milliseconds:" + watch.Elapsed.TotalMilliseconds);
            });

This line of code will calculate the total number of milliseconds to execute each request.

This is what your startup.cs should look like. After adding this line of code.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MiddlewareDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.Use(async (context, next) =>
            {
                var watch = new Stopwatch();
                watch.Start();
                await next();
                watch.Stop();
                Console.WriteLine("Total Request Time in Milliseconds:" + watch.Elapsed.TotalMilliseconds);
            });
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            //app.Map(new PathString("/ip"), a => a.Run(async context =>
            //{
            //    await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            //}));
            //app.MapWhen(
            //     ctx => ctx.Request.Path.StartsWithSegments(new PathString("/ip")), a => a.Run(async context =>
            //{
            //    await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            //}));
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
            //Middleware using block
            //app.Run(async context =>
            //{
            //    if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
            //    {
            //        await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            //    }
            //    await Task.CompletedTask;
            //});
            //Middleware using RequestDelegate method
            //app.Run(GetIpInfo);
        }
        private async Task GetIpInfo(HttpContext context)
        {
            if (context.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase))
            {
                await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
            }
            await Task.CompletedTask;
        }
    }
}

Lets run the application and see the console output to get the total time of each request.

middleware using the Use() method

Middleware using UseWhen() extension method

UseWhen also branches the request pipeline based on the result of the given predicate.

The only difference between MapWhen and useWhen is that MapWhen will not move to the next pipeline. However, UseWhen will continue with the other pipelines.

Let’s see the method definition of UseWhen()

Middleware using UseWhen() extension method

Read carefully the highlighted section of the above image. It does not matter how you add the pipeline either using Run() / use(), will rejoin the pipelines again.

Let’s see the example.

Run() inside the UseWhen method

  app.UseWhen(
                ctx =>
            ctx.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase), app =>
             {
                 app.Run(async context =>
                 {
                     await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
                     await Task.CompletedTask;
                 });
             });

Lets see our startup.cs file. how does it look? We have used the Run() method inside the UseWhen extension. When the condition matches the request it will execute the code inside the run and will rejoin the pipeline. However, it will not execute the remaining pipelines registered after this.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace MiddlewareDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.Use(async (context, next) =>
            {
                var watch = new Stopwatch();
                watch.Start();
                await next();
                watch.Stop();
                Console.WriteLine("Total Request Time in Milliseconds:" + watch.Elapsed.TotalMilliseconds);
            });
            app.UseWhen(
                ctx =>
            ctx.Request.Path.Value.Contains("/ip", StringComparison.OrdinalIgnoreCase), app =>
             {
                 app.Run(async context =>
                 {
                     await context.Response.WriteAsync("My IP  Is :" + context.Request.HttpContext.Connection.RemoteIpAddress);
                     await Task.CompletedTask;
                 });
             });
              app.Use(async (context, next) =>
            {
                Console.WriteLine("Executing the dummy pipeline");
                await next();
            });
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

For example, I have registered one more pipeline after the usewhen() method. Which will not execute if we navigate to the http://localhost:5000/ip.

Using Run() inside the UseWhen method

As you can see in the console output. there is no message which says “Executing the dummy pipeline“.

Use() inside the UseWhen method

Lets say I want to calculate request time only for specific requests. Then I can just place my middleware logic inside the UseWhen() method. However, this is not going to skip any pipeline until there is an exception in the pipeline.

This is one more approach to implementing the middleware and adding to the pipeline. This is when you want to keep the middleware logic in a separate class altogether.

Usemiddleware .net Core

Let’s write the middleware logic for calculating the request execution time, in a separate class.

using Microsoft.AspNetCore.Http;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
public class RequestExecutionTime
{
    public RequestExecutionTime(RequestDelegate requestDelegate)
    {
        _next = requestDelegate;
    }
    public RequestDelegate _next { get; }
    public async Task Invoke(HttpContext httpContext)
    {
        var watch = new Stopwatch();
        watch.Start();
        await _next.Invoke(httpContext);
        watch.Stop();
        Console.WriteLine("Total Request Time in Milliseconds:" + watch.Elapsed.TotalMilliseconds);
    }
}

In order to use this class as middleware, you have to register this class in the startup.cs file. Since we want to calculate the total execution time of the request, we need to add this as the first pipeline in the configure method.

app.UseMiddleware<RequestExecutionTime>();

Passing parameters to middleware In ASP.NET Core 

In case you want to pass some additional parameters to the middleware perform any additional operation. you just need to just add additional parameters in the constructor.

Let’s update our existing RequestExecutionTime class to take the ILogger parameter. Which can be used to log the Total request time along with the console.

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
public class RequestExecutionTime
{
    private readonly ILogger _logger;
    public RequestExecutionTime(RequestDelegate requestDelegate, ILogger logger)
    {
        _next = requestDelegate;
        this._logger = logger;
    }
    public RequestDelegate _next { get; }
    public async Task Invoke(HttpContext httpContext)
    {
        var watch = new Stopwatch();
        watch.Start();
        await _next.Invoke(httpContext);
        watch.Stop();
        var logmsg = "Total Request Time in Milliseconds:" + watch.Elapsed.TotalMilliseconds;
        this._logger.LogInformation(logmsg);
        Console.WriteLine(logmsg);
    }
}

Also, we need to pass this parameter while registering the middleware.

app.UseMiddleware<RequestExecutionTime>(logger);

Or, you can also register like this.

app.UseMiddleware(typeof(RequestExecutionTime),logger);
Source Code AVailable at Github

Conclusion

So, in this article we understood, what is middleware. when we should use the middleware.How to add custom middleware using extension methods like Run, Use, and UseMiddleware.

In the comment section below, let us know your thoughts or questions if this article has been helpful to you or if there is anything that needs to be changed or corrected.

What are the Asp.Net Core Middleware Functions?

Run
Map
MapWhen .Net Core Function
Use
UseMiddleware
UseMiddleware<>
Using custom extension methods.

83 / 100

2 Comments

Comments are closed