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(10Not Null,
  RequestPath varchar(1000Not Null,
  RequestBody varchar(MAXNot 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](255NULL,
    [PaymentDate] [datetime] NULL,
    [ExpensesType] [nvarchar](255NULL,
    [AmountPaid] [float] NULL,
    [PaymentMethod] [nvarchar](255NULL,
    [PaidBy] [nvarchar](255NULL,
 CONSTRAINT [pk_expenses_id] PRIMARY KEY CLUSTERED 
(
    [ExpensesId] ASC
)WITH (STATISTICS_NORECOMPUTE = OFFIGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFFON [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
Step 2: Use the .NET EF CLI to create Models for the API project using the Database First approach using the command shown in Listing 3

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.

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach