Introduction
Developed as open source by Microsoft, the .Net Core library provides many conveniences and advantages to developers. One of the most important innovations that entered our world with .Net Core is the concept of middleware.
In this article, we will examine the concept of middleware that we met with .Net core, then we will see how to easily control an application with JWT inclusion developed with Asp.Net Core 6 using middleware. Finally, we will use middleware as a global exception handler. So let’s get started.
Code on GitHub
Don’t want to wait? Get the complete source of the application I developed for this blog post on GitHub here: https://github.com/fillegar/dotnet-6-crud-api-jwt-sql-server
What is Middleware?
Before moving on to the question of what is middleware, it can be said that people who develop on .NET Framework before .NET Core are not very unfamiliar with this structure. Prior to .NET Core, developers could take customized actions by intervening before or after Requests using the Filters structure. The middleware structure actually aims at this and offers a more modular structure to the developers. In short, if we answer the question of what is middleware; They are modular and effective structures that make it possible to take customized actions on the Request/Response Pipeline and enable this Pipeline flow to be handled.
Where is Middleware Used?
Middleware structures are modular structures that allow Methods defined as Extensions to a Class derived from the IApplicationBuilder Interface to be run in the desired order. Along with these structures;
- It can be ensured that the requests are passed through the desired controls
- It can be ensured that the responses are derived from the cache
- We can create logging mechanisms on the application
- Solutions can be provided for exception handling
How to Write Custom Middleware?
In addition to these features, of course, if we want to perform certain operations on the incoming request, we can write custom middleware ourselves.
It is sufficient to create a class for the custom middleware and create a structure like the one below. Below is the function that will contain the business logic to be applied on the requests in the invoke method in the class and will be triggered automatically when the middleware is called. With the Invoke method of the RequestDelegate object injected in the constructor, we call the next middleware in the middleware pipeline and transfer our httpContext to it.
public class CustomSampleMiddleware
{
private readonly RequestDelegate _next;
public CustomSampleMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
//business logic
//calls the next middleware..
await _next.Invoke(httpContext);
}
}
To include the middleware in the pipeline, it will be sufficient to add the following line of code to the Program class.
app.UseMiddleware();
As it is understood, we can handle request and response by performing operations on request and response with middleware. There are 3 types of methods used to interact with request.
- Use
- Run
- Map
Use Method
This method gives us a delegate named next as seen below. With this we can access the next middleware member. It can be used to get between two middleware objects.
app.Use(async (context, next) =>
{
Debug.WriteLine("One");
await next.Invoke();
Debug.WriteLine("Two");
});
app.Run(async context =>
{
Debug.WriteLine("Three");
await context.Response.WriteAsync("Sample");
});
Output will be “One, Three, Two”.
Run Method
It is possible to short-circuit the pipeline with run. After the request falls into this method, other processes on the pipeline do not continue and do not pass through other middlewares.
app.Run(async context =>
{
await context.Response.WriteAsync("One");
});
app.Run(async context =>
{
await context.Response.WriteAsync("Two");
});
In the code block here, we can see that the text “One” is written to the screen and terminates without falling into the other run method.
Map Method
As the name suggests, we use the Map method when we want to capture a path. When we want to place a business logic for the map named branch as below, we can follow a path as follows.
app.Map(new PathString("/pathOne"), _configuration =>
{
_configuration.Run(async context =>
{
await context.Response.WriteAsync("Sample");
});
});
Code
In this section, we will cover how to add both token validation and global exception handling mechanism in a short way using middleware of an application developed with .Net Core 6 and manually validating JWT.
The application normally uses the Postgresql database in its development environment. We will use Sql Server for the production environment and we will perform our tests here. For the production environment, we will create a file named ‘appsettings.Production.json’ under the ‘appsettings.json’ file. In this file we will paste all the information in the ‘appsettings.Development.json’ file, then replace the database connection connectionString with the Sql Server connection information
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DB_UserApplication": "Server=localhost;Database=master;User Id=sa;Password=JeffFill12egar.;Integrated Security=false"
},
"Jwt": {
"Issuer": "https://blog.fillegar.com/",
"Audience": "https://blog.fillegar.com/",
"Key": "a72813uhasdkln.sakldSDQDO2181821JXMÇSDHG..123SAKJDa"
}
}
Now we will add the Sql Server library for Entity Framework Core to the ‘User.Repository’ layer. We will download and install the Microsoft.EntityFrameworkCore.SqlServer library from NuGet on the ‘User.Repository’ layer.
Then we will paste the following codes into Program.cs for database adjustments according to the environment.
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDbContext(o => o.UseNpgsql(builder.Configuration.GetConnectionString("DB_UserApplication")), ServiceLifetime.Transient);
}
else
{
builder.Services.AddDbContext(o => o.UseSqlServer(builder.Configuration.GetConnectionString("DB_UserApplication")), ServiceLifetime.Transient);
}
At this stage, we need to migrate the Sql Server ‘UserInfo’ entity to the Sql Server database. First, let’s update the ‘ASPNETCORE_ENVIRONMENT’ values in the ‘launchSetting.json’ file to ‘Production’. Then, let’s select the default project ‘User.Repository’ from the package manager console and run the following commands respectively and perform the migration of the Sql Server database.
Add-Migration MigrateSqlDb
Update-Database
Then we will add the ‘ValidateToken’ method from the ‘TokenUtilsHandler.cs’ class that generates the token, where we will perform token validation.
public string ValidateToken(string token)
{
if (token == null)
return string.Empty;
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidIssuer = configuration["Jwt:Issuer"],
ValidAudience = configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,
ValidateIssuerSigningKey = true
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var email = jwtToken.Claims.First(x => x.Type == "Email").Value;
return email;
}
catch
{
return string.Empty;
}
}
Let’s move on to creating custom middleware. We will create custom middleware for JWT validation. In the ‘User.API’ project we will create a folder named ‘Middlewares’ and create a class called ‘JwtMiddleware’ in it. Let’s paste the following codes into the custom middleware class we created.
public class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, UserInfoService userService, TokenUtilsHandler tokenUtilsHandler)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
var email = tokenUtilsHandler.ValidateToken(token);
if (!string.IsNullOrEmpty(email))
{
context.Items["User"] = userService.getUserInfoByEmail(email);
}
await _next(context);
}
}
After creating the custom middleware class, we will expand and use the [Authorize] attribute, which we will use in endpoints that only authorized users can access.
We will open a folder named ‘Attribute’ to the ‘User.Core’ project and create a class named ‘AuthorizeAttribute’ in it. Let’s paste the following code into the class we created.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.Items["User"];
if (user == null)
context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
}
}
To use the extended [Authorize] attribute, we will create a Web API named ‘CategoryController’ in the ‘User.API’ project and put the following codes in it.
using User.Core.Attributes;
namespace User.API.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class CategoryController : ControllerBase
{
[HttpGet]
public List getCategories()
{
return new List()
{
new string("Category 1"),
new string("Category 2"),
new string("Category 3"),
};
}
}
}
Before adding the Jwt Middleware we created to ‘Program.cs’, let’s create a middleware for global exception handling.
Let’s create a class named ‘GlobalExceptionHandlerMiddleware’ in the folder named ‘Middlewares’ that we created before and write the following codes.
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
public GlobalExceptionHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception error)
{
var response = context.Response;
response.ContentType = "application/json";
switch (error)
{
case UserInfoNotFoundException e:
response.StatusCode = (int)HttpStatusCode.BadRequest;
break;
default:
response.StatusCode = (int)HttpStatusCode.InternalServerError;
break;
}
var result = JsonSerializer.Serialize(new { message = error.Message });
await response.WriteAsync(result);
}
}
}
Let’s add the ‘UserInfoNotFoundException’ class, which is our custom exception, to the login endpoint.
[AllowAnonymous]
[HttpPost]
public async Task Login([FromForm] UserLogin userLogin)
{
UserInfo user = await userInfoService.getUserInfoByEmail(userLogin.Email);
if (user != null)
{
var token = tokenHandler.createAccessToken(user);
user.RefreshToken = token.RefreshToken;
user.RefreshTokenEndDate = token.Expiration.AddMinutes(3);
await userInfoService.updateUserInfo(user);
return token;
}
throw new UserInfoNotFoundException("User not found with email: " + userLogin.Email);
}
Now, as the last step, we will add the custom middlewares we created to ‘Program.cs’. The final version of the ‘Program.cs’ class will be as follows.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using User.API.Handler;
using User.API.Middlewares;
using User.Repository;
using User.Service;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
if (builder.Environment.IsDevelopment())
{
builder.Services.AddDbContext(o => o.UseNpgsql(builder.Configuration.GetConnectionString("DB_UserApplication")), ServiceLifetime.Transient);
}
else
{
builder.Services.AddDbContext(o => o.UseSqlServer(builder.Configuration.GetConnectionString("DB_UserApplication")), ServiceLifetime.Transient);
}
builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddTransient();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseMiddleware();
app.UseMiddleware();
app.MapControllers();
app.Run();
Test
Now we will test the application with the Production environment. After running the application, we will first create a user. Let’s create a user via Postman as follows.
We will login with the user we created and we will obtain a access token.
Now we will send a request to the /api/category endpoint with the token information we have obtained.
We have successfully obtained the category information. Now we will try to send a request from Postman without authorizing information.
With the custom JWT middleware we created and the authorize attribute that we expanded, we successfully performed token validation in an easy way.
Finally, to test the custom middleware, the global exception handling middleware, we will try to log in with an email that is not in the database.
We saw that our global exception handler is running successfully.
Conclusion
In this article, we touched on the concept of middleware that we met with .Net Core and we handled the JWT flow in an existing .Net core application in an easy way with middleware. In addition, we have included our middleware and global exception handler in our application in two steps. You can find more information about .net middleware here.
Code on Github
Get the complete source of the application we developed in this article, on GitHub here:
https://github.com/fillegar/dotnet-6-crud-api-jwt-sql-server