ASP.NET Core 8: Using the Conditional Middleware
In ASP.NET Core middleware provides facility to manage the logic in HTTP Runtime Pipeline that is to be executed in each HTTP request. The logic e.g. Exception Handler, Authentication, Authorization, etc. These middlewares helps to define and manage behavior of the ASP.NET Core application. But sometimes we need to dynamically define the behavior of the ASP.NET Core application e.g. if we are developing APIs in ASP.NET Core where we want to log only POST/PUT/DELETE requests then it is very important to detect the HTTP request type and then based on that we need to log such request. In this case, it is very important to manage the behavior of the Middleware based on the condition.
In this article, we will implement a custom logger middleware, and we will load it using the UseWhen() extension method of an IApplicationBuilder interface.
We will log POST/PUT/DELETE requests in SQL Server database. The request will be logged in the RequestLogger table. The table definition is shown in Listing 1
Create Table RequestLogger(
RequestUniqueId int Identity Primary Key,
RequestId varchar(300),
RequestDateTime DateTime Not Null,
RequestMethod varchar(10) Not Null,
RequestPath varchar(1000) Not Null,
RequestBody varchar(MAX) Not Null
)
Listing 1: The RequestLogger Table
We will create an API using ASP.NET Core for home expenses management. The expenses details will be captured from the End-User. The Listing 2 shows the script for the Expenses Table.
CREATE TABLE [dbo].[Expenses](
[ExpensesId] [float] NOT NULL,
[VendorName] [nvarchar](255) NULL,
[PaymentDate] [datetime] NULL,
[ExpensesType] [nvarchar](255) NULL,
[AmountPaid] [float] NULL,
[PaymentMethod] [nvarchar](255) NULL,
[PaidBy] [nvarchar](255) NULL,
CONSTRAINT [pk_expenses_id] PRIMARY KEY CLUSTERED
(
[ExpensesId] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
Listing 2: The Expenses Table
Step 1: Open Visual Studio and create a new ASP.NET Core API project targeted to .NET 8. Name the project as Core_API_ConditionalMiddleware. Since, we will be using Entity framework Core, we need to add following packages in the project.
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Relational
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.Tools
dotnet ef dbcontext scaffold "Server=.;Initial Catalog=wowdb;Persist Security Info=False;User ID=sa;Password=P@ssw0rd_;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" Microsoft.EntityFrameworkCore.SqlServer -o Models -t Expenses -t RequestLogger
Listing 3: The Database First CLI Command
This command will add Models folder in the project with Expenses and RequestLogger class files in the folder. We will also have the WowdbContext class file in its folder. Copy the connection string from the WowdbContext class to the appsettings.json file in its ConnectionStrings section so that we will use it for Dependency Injection.
Step 3: Since we will be creating a Custom Middleware for logging requests in the database, we need to add logic for the same. In the project, add a new folder named Middlewares. In this folder, add a new class file named RequestLoggerMiddleware.cs. In this class file we will add code for the custom middleware. In this class we will have a Primary Constructor (New feature in C#) and an InvokeAsync() method. This method accepts HttpContext and WowdbContext objects. Since we need to read the Http request body, we need to use the StreamReader object to read the body contents with its supported encoding (UTF-8), we need to use the leavOpen property of the stream reader to true to make sure that the stream object is kept open after the StreamReader is disposed so that for the HTTP POST and PUT requests the body is available to read again otherwise the exception will be thrown. Once the body is read its contents will be logged. We need to reset the position of the of the stream to 0 so that the next middleware or the actual endpoint can read it again.
Listing 4 shows the code for the custom middleware.
using Core_API_ConditionalMiddleware.Models; using System.Text; namespace Core_API_ConditionalMiddleware.Middlewares { /// <summary> /// This middleware will log the request details to SQL Server Database /// </summary> public class RequestLoggerMiddleware(RequestDelegate next) { private readonly RequestDelegate _next = next; private readonly WowdbContext _context; public async Task InvokeAsync(HttpContext context, WowdbContext dbContext) { context.Request.EnableBuffering(); // Enable buffering to allow multiple reads using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true)) { // The body var body = await reader.ReadToEndAsync(); // If the body id empty (In case of the Delete request) if(String.IsNullOrEmpty(body)) body = "No Request Body, the record is requested for delete"; // Process the body if (context.Request.Path.StartsWithSegments("/api")) { // Log the request details to SQL Server Database var request = new RequestLogger { RequestId = Guid.NewGuid().ToString(), RequestMethod = context.Request.Method, RequestPath = context.Request.Path, RequestDateTime = DateTime.UtcNow, RequestBody = body }; await dbContext.RequestLoggers.AddAsync(request); await dbContext.SaveChangesAsync(); } // Reset the request body stream position so the next middleware can read it context.Request.Body.Position = 0; } await _next(context); } } }
Listing 4: The custom Middleware
The coed in Listing 4 shows that if the body is empty then it is a HTTP DELETE operation. The request will be logged in the database using the WowdbContext class.
Step 4: Modify the Program.cs to register the RequestLoggerMiddleware conditionally so that it will be executed for HTTP POST, PUT and DELETE requests. We will use the UseWhen() extension method for the same. Listing 5 shows the code for the registration.
.....
// Use the RequestLoggerMiddleware Conditionally app.UseWhen(context => context.Request.Path.StartsWithSegments("/api") && ( context.Request.Method == "POST" || context.Request.Method == "PUT" || context.Request.Method == "DELETE" ), appBuilder => { appBuilder.UseMiddleware<RequestLoggerMiddleware>(); }); ......
Listing 5: The Conditional Middleware
Let's add minimal Endpoints to perform CRUD operations for Expenses as shown in Listing 6.
app.MapGet("/api/expenses", async (WowdbContext ctx) => { return await ctx.Expenses.ToListAsync(); }); app.MapPost("/api/expenses", async (HttpContext context, WowdbContext ctx, Expense expense) => { await ctx.Expenses.AddAsync(expense); await ctx.SaveChangesAsync(); return Results.Created($"/api/{expense.ExpensesId}", expense); }); app.MapPut("/api/expenses/{id}", async (WowdbContext ctx, int id, Expense expense) => { if (Convert.ToInt32(id) != expense.ExpensesId) { return Results.BadRequest(); } ctx.Entry(expense).State = EntityState.Modified; await ctx.SaveChangesAsync(); return Results.NoContent(); }); app.MapDelete("/api/expenses/{id}", async (WowdbContext ctx, int id) => { var expense = await ctx.Expenses.FindAsync(Convert.ToDouble(id)); if (expense == null) { return Results.NotFound(); } ctx.Expenses.Remove(expense); await ctx.SaveChangesAsync(); return Results.NoContent(); });
Listing 6: HTTP Endpoints for CRUD Operations
Run the application and make HTTP GET, POST, PUT, and DELETE requests. You will find the only POST, PUT, and DELETE requests are logged into the database as shown in Figure 1.
Figure 1: The Logger in Database
The code for this article can be downloaded from this link.
Conclusion: ASP.NET Core Middleware offers a great scale of dynamic approach to control the behavior of the application during the runtime.