At times you want to handle information before the request reaches the logic in a controller and/or action. That is why we use middleware. A small piece of code that can handle simple logic before it reaches the actual code. It lives in APIs and it can serve different goals. In this article, I will show you how you can create and use middleware in .NET Core and MVC API.
Table Of Contents
Goals
After this article you
- Understand the idea behind middleware in an API
- Can create a middleware using a new class and extension
- Can activate the middleware in the Program.cs by adding it to the IApplicationBuilder
- Know how to check the path of a request to make a decision
- Understand the RequestDelegate and set a status code when needed
- Can create a middleware by not using a new class and extension, but the app.Use()
Middleware In A Nutshell
You know the term “let’s cut out the middleman”? Well, middleware in an API actually adds it. It can help you perform actions before and after a request. There are so many reasons why you could and/or should use it.
Middleware handles logic inside the pipeline of a request. There is a whole sheet of the steps a request takes to receive and handle a request. I could create an image all by myself, but Microsoft has a really nice sheet that represents the process:

Middleware is already added to your API as soon as you create it. You can recognize a middleware by the “use” in the name used in the app variable. For example app.UseHttpsRedirection.
And there are way more middleware for you to add if you want. UseCors for example, or UseAuthentication and UseAuthorization. But you can also create your own if needed.
Creating middleware is pretty simple. The only important thing you need to consider is: Is it already created? And with that I mean you should research if .NET doesn’t already have what you want to achieve.
Before We Start Coding
In this article, I will be using .NET 7 and a minimal API. And of course C#. I will show you how to create your own middleware, but since it is really hard for me to know what you want to achieve, I will be creating a middleware that can see if a certain IP address is blocked or not. You can download the starting code here:
I have created a simple API that I will expand with the middleware. To make it a little bit more interesting, I have created a business layer that has a service, IpAddresses, and an interface, IIpAddresses. It’s all static data, but the interface is set up for dependency injection in the APIs Program.cs. I want to use that injection in the middleware. Let’s assume the IIpAddresses implementation holds all the logic for saving and retrieving information about saved IP addresses.
In the end, I want the API to block requests to the SayHello endpoint if the IP address is blocked. The endpoint SayGoodbye should be accessible to everyone, blocked or not. This gives us a good opportunity to dig further into the middleware and how we can retrieve information about the request.
Create A Middleware Class
Everything starts with a class… Well, most things. But a middleware is a class with methods. But where to place this new class? It is only used in an API. It has no point in placing the logic in a business or a domain layer. I like to create a folder in the API with the name Middleware. All my middleware classes – yes, you can have more than one – are placed here.
Let’s add the folder and the class. I call the class IpHandlerMiddleware.
RequestDelegate
We now have an empty class. The idea behind middleware is that it can handle some logic and move on to the next stage, or member in the pipeline. The next stage could be the endpoint or the next middleware. To make it move on we need a delegate. This delegate is injected into the class and can be called when we are done with the logic.
This is the RequestDelegate and we consistently call it next. Let’s add the constructor, inject the RequestDelegate, and store the information in the private property _next.
public class IpHandlerMiddleware
{
private readonly RequestDelegate _next;
public IpHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
}
We will be coming back to the RequestDelegate later on.
InvokeAsync
The next part that is needed to make the class a middleware is the InvokeAsync. This one is called as soon as it is its turn to be executed. The method holds the real logic we want to execute. It has one required parameter: HttpContext. This one holds all the information about the request and can also manipulate the response.
Besides the HttpContext we can add more parameters. Although it feels strange, here we can inject the services. Normally, we inject these in the constructor. But not with middleware classes. In the demo application, I will inject the IIpAddresses into the InvokeAsync.
The initial method looks like this:
public async Task InvokeAsync(HttpContext context, IIpAddresses ipAddresses)
{
}
Please note it’s async.
The Logic
Now it’s time to add our own logic to the InvokeAsync. In short what I want to do:
- Get the IP address of the person that sends the request
- Retrieve the IP address from the IpAddresses service
- Send a 401 status back when the IP address is blocked
- Move on to the next step when the IP address is not blocked
- Add the IP address to the saved IP addresses and move on
Since this is an article about middleware and not how-to-store-and-handle-ip-addresses, I will keep this chapter short.
The code of the InvokeAsync is below.
public async Task InvokeAsync(HttpContext context, IIpAddresses ipAddresses)
{
string clientAddress = context.Connection.RemoteIpAddress?.ToString() ?? throw new Exception("Address not found");
try
{
IpAddress ipAddress = ipAddresses.Get(clientAddress);
if (ipAddress.IsBlocked)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
else
{
await _next(context);
}
}
catch (Exception ex) when (ex.Message == "Sequence contains no matching element")
{
ipAddresses.Add(new() { Address = clientAddress, IsBlocked = false });
await _next(context);
}
catch (Exception)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
}
First I get the client’s IP address. Then I check if it exists in my “database”. If it exists I check if the address is blocked. If not, I await the _next and add the context to it. If it is blocked I set the status code of the response to 401 (unauthorized). Note that I don’t call the RequestDelegate _next.
If it does not exist in the database an exception is thrown (because of the single). I use an exception filter to grab that exception and add the IP address. Then I await the _next again.
If another exception is thrown, I just set the status code to 500 and not awaiting the _next.
To _Next Or Not To _Next
In two cases, I don’t await the RequestDelegate. The reason is that I don’t want to go to the next part of the pipeline. It’s over and done; the IP address is not authorized to go to the next stage. We call this short-circuiting the pipeline.
It would be best if you remembered that it’s all in a chain and the RequestDelegate can go to the next member. If someone is not authorized (by IP address or other) there is no point in going to the next member.
Some middleware is just there to prepare some data. They won’t short-circuit the pipeline. For example, if a certain custom header is present in the request (HttpContext) we could grab it and add it to caching. If it’s not present there is no problem. Just don’t send it to the cache and move on to the next step/member.
You might have noticed that I didn’t await the _next after I set the status codes. If I would do that, the next member in the chain would be invoked, losing the information of the status codes. If you don’t call the next in line, the pipeline is short-circuited again and shows the information you have set, like a status code.
Using The Middleware
We now have a simple middleware class, ready to use. But how do we use it? We need to use the Use convention for the app in the Program.cs. It should be an extension.
Correct, we need to make an extension to use the middleware. I don’t like using multiple classes in one file, but I’ll make an exception for middleware.
Add the following class under the IpHandlerMiddleware class:
public static class IpHandlerMiddlewareExtensions
{
public static IApplicationBuilder UseIpHandler(this IApplicationBuilder builder) => builder.UseMiddleware<IpHandlerMiddleware>();
}
This will create an extension on the IApplicationBuilder, the app in Program.cs, telling to use the IpHandlerMiddleware class.
Back at the Program.cs, use the following line of code to activate and use the middleware:
app.UseIpHandler();
Place this line somewhere after app is created. I placed it under app.UseHttpsRedirection().
Running The API
You can start the API and test it. The requests will be handled correctly. Which is a bit strange, when you think about it. The IpAddress class has a list of IP addresses. One of them is 127.0.0.1, which is your localhost. Well, your IP is different.
Put a breakpoint in the middleware we’ve created. Place is on the line that says IpAddress ipAddress = ipAddresses.Get(clientAddress). Execute a request again. Check out your IP address by hovering over clientAddress. It’s ::1, not 127.0.0.1.
This also means the code will enter the first exception, add the IP address, and move on.

Add this IP address to the list of IP addresses in the IPAddresses class, and set the IsBlocked to true.
readonly List<IpAddress> addresses = new()
{
new() { Address = "127.0.0.1", IsBlocked = false },
new() { Address = "192.168.1.1", IsBlocked = true },
new() { Address = "80.74.0.1", IsBlocked = true },
new() { Address = "::1", IsBlocked = true }
};
Run the API again and execute a request.

Nice! A 401 status code is returned to the client.
Excluding Endpoint
At the beginning of this article, I said I only want to allow requests to the SayGoodbye endpoint at all times. The SayHello should handle the IP addresses. This means I need to check which endpoint (or URL) is called and act accordingly.
The HttpContext, used in the InvokeAsync, holds that information. The context variable has a request path, which we can use to detect the path to the endpoint.
So I will add the following 2 lines of code at the beginning of the InvokeAsync method:
if (context.Request.Path.ToString().Contains("saygoodbye", StringComparison.OrdinalIgnoreCase))
await _next(context);
If the endpoint contains the word saygoobye, it will execute the _next. Otherwise, it will continue with the logic.
Sure, there are better ways to handle this. But I want to give you some ideas about what you can do with middleware.
Middleware Is Invoked Twice
In some cases, you might notice that the middleware is called twice. This usually happens when you call it from a different application, like an Angular app. The reason for this is that the method OPTIONS is called. This doesn’t do much, but it does mess up your middleware.
What you can do is make sure the OPTIONS method is not sent, but you can also tell the middleware to avoid it. Simple check the request method and handle it accordingly:
if (context.Request.Method == "OPTIONS")
{
await _next(context);
}
App.Use
Since .NET 6, with the introduction of the minimal API, you can write the middleware shorter. This is helpful when you don’t have reusable classes. It removes the need to create extra classes.
With the App.Use you can create and use a middleware without creating the class and the extension. It’s also used in the Program.cs, so no need to use different files.
If I would rewrite the code I just created for the App.Use way it looks like this:
app.Use(async (context, next) =>
{
if (context.Request.Path.ToString().Contains("saygoodbye", StringComparison.OrdinalIgnoreCase))
await next(context);
IIpAddresses ipAddresses = context.RequestServices?.GetService(typeof(IIpAddresses)) as IIpAddresses ?? throw new Exception("Service not found");
string clientAddress = context.Connection.RemoteIpAddress?.ToString() ?? throw new Exception("Address not found");
try
{
IpAddress ipAddress = ipAddresses.Get(clientAddress);
if (ipAddress.IsBlocked)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
}
else
{
await next(context);
}
}
catch (Exception ex) when (ex.Message == "Sequence contains no matching element")
{
ipAddresses.Add(new() { Address = clientAddress, IsBlocked = false });
await next(context);
}
catch (Exception)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
});
Two downsides:
- You can’t inject services, so you need to get them from the RequestServices, which is in the context.
Big deal? Not really, just annoying. - If you have multiple middleware and mappings in one file, the Program.cs, it can become a lot of code.
I like to keep my files, classes, and methods small and clean. I am afraid you might get lost in the code.
But if you have a small middleware this could be better than creating the class and extension.
Conclusion On Middleware In .NET Core And MVC API
Middleware is a great tool to handle information before the request reaches the controller and actions. You can prepare data, authorize a request, or do some logging before entering the action.
Before you are going to create your own, awesome middleware in C# and MVC API, check if there isn’t a library or built-in logic that you can use. Microsoft has listed the built-in middleware you can use.
Creating your own isn’t hard, but you need to keep in mind when to call the next member in the pipeline or set the correct status code to let your client know what is happening. Besides that, just follow the structure and you’ll be fine.

