ASP.NET Core 9: Injecting Keyed Service In Middleware

 .NET 9 is finally released and hence ASp.NET Core 9. You can download it from this link. The ASp.NET Core 9 have several features and some of the features are as follows:

  • Performance gain to Minimal APIs
  • OpenAPI document generation
  • Keyed Services in Middleware
  • ... and many more
These new features are helpful for modern applications with respect to performance improvisation and optimization.

In this article, we will focus on accessing Keyed Service in ASP.NET Core Middleware. Keyed services are introduced in ASP.NET Core 8. This feature provides a robust way to manage multiple implementations of the same interface. These implementations can be registered and resolved using the AddKeyedService() extension method. The code shown in Listing 1 the Keyed service Registration in ASP.NET Core Service Containers.


services.AddKeyedService<IAppService, AppService1>("AppKey1");
services.AddKeyedService<IAppService, AppService2>("AppKey2");

Listing 1: Keyed Service Registration

As shown in Listing 1, the IAppSAervice interface is implemented by AppService1 and AppService2 classes, these classes are registered as service in ASP.NET Core Service container. In the container they are referred using AppKey1 and AppKey2. Hence, whenever they will be injected, these key names will be used. The Keyed Service, hence, offers great scale of flexibility. But in ASP.NET Core 8, these key services were not used in Middlewares, but in ASP.NET Core 9 we can use them.   

In this article, I have created a Logger service for logging HTTP requests in SQL Server Database and in JSON File. I have created ASP.NET Core 9 APIs project to perform HTTP GET and POST operations for reading and writing PersonInfo from SQL Server. The Requests Log will also be stored in SQL Server database in Logger table. The Listing 2 shows the scripts for PersonInfo and Logger Table. These tables as created in Company Database.

Create Database Company

Go

Use Company

Go

-- Create PersonInfo Table 

CREATE TABLE [dbo].[PersonInfo](

[Id] [int] IDENTITY(1,1) NOT NULL,

[Name] [varchar](200) NOT NULL,

[Address] [varchar](400) NOT NULL,

PRIMARY KEY CLUSTERED 

(

[Id] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]

) ON [PRIMARY]

GO

-- Create Logger Table

CREATE TABLE [dbo].[Logger](

[LogRecordId] [int] IDENTITY(1,1) NOT NULL,

[LoggerId] [int] NOT NULL,

[RequestDate] [datetime] NOT NULL,

[RequestBody] [varchar](1000) NOT NULL,

[Method] [varchar](20) NOT NULL,

[Path] [varchar](100) NOT NULL,

PRIMARY KEY CLUSTERED 

(

[LogRecordId] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]

) ON [PRIMARY]

GO

Listing 2: The PersonInfo and Logger Tables 

Step 1: Create ASP.NET Core 9 API Project and name it as Net9_KeyedServiceInMiddleware. In this project add the following Entity Framework Core packages.

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Relational
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.Tools
Step 2: Let's use Entity Framework Core Database First approach to create Models using the command as shown in Listing 3.

dotnet ef dbcontext scaffold "Data Source=.;Initial Catalog=Company;Integrated Security=SSPI;TrustServerCertificate=True" Microsoft.EntityFrameworkCore.SqlServer -o Models -t Logger -t PersonInfo 

Listing 3: The Database First Command 

This command will add a Models folder in the project with CompanyContext.cs, PersonInfo.cs and Logger.cs files. 

Step 3: Move the Connection string from the CompanyContext.cs file to appsettings.json file as shown in Listing 4.


 "ConnectionStrings": {
   "DefaultConnection": "Data Source=.;Initial Catalog=Company;Integrated Security=SSPI;TrustServerCertificate=True"
 }
Listing 4: The Connection String in appsettings.json file.

Let's register the CompanyContext class in the ASP.NET Core Service Container in Program.cs as shown in Listing 5

builder.Services.AddDbContext<CompanyContext>(options => 
{
   options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));    
});

Listing 5: Register the CompanyContext class.

Creating the Keyed Service  
 
Step 4: In the project, add the new folder named Contract. In this folder, add a new interface file named IRequestLogger.cs. In this file, we will add code for IRequestLogger interface as shown in Listing 6.

using Net9_KeyedServiceInMiddleware.Models;

namespace Net9_KeyedServiceInMiddleware.Contract
{
    public interface IRequestLogger
    {
        Task RequestLogger(Logger logger);  
    }
}


Listing 6: The IRequestLogger interface   

Step 5: In the project, add a new folder named LoggerInfra. In this folder we will add classes for logging HTTP requests in Database and JSON file. These classes will implement IRequestLogger interface. We will add DbLogger class and FileLogger classes in DbLogger.cs and FileLogger.cs class files. The code for these classes is shown in Listing 7.


using Net9_KeyedServiceInMiddleware.Contract;
using Net9_KeyedServiceInMiddleware.Models;

namespace Net9_KeyedServiceInMiddleware.LoggerInfra
{
    public class DbLogger : IRequestLogger
    {
        private readonly CompanyContext _context;

        public DbLogger(CompanyContext context)
        {
            _context = context;
        }
        async Task IRequestLogger.RequestLogger(Logger logger)
        {
            await _context.Loggers.AddAsync(logger);
            await _context.SaveChangesAsync();
        }
    }
}


using Microsoft.Extensions.Hosting;
using Net9_KeyedServiceInMiddleware.Contract;
using Net9_KeyedServiceInMiddleware.Models;
using System.Text.Json;

namespace Net9_KeyedServiceInMiddleware.LoggerInfra
{
    public class FileLogger : IRequestLogger
    {
        private readonly string _filePath;

        public FileLogger(IWebHostEnvironment env)
        {
            string dirPath = Path.Combine(env.ContentRootPath, "LogFiles");
            if (!Directory.Exists(dirPath))
            {
                Directory.CreateDirectory(dirPath);
            }
            _filePath = Path.Combine(dirPath, "RequestLog.json");
        }

        public async Task RequestLogger(Logger logger)
        {
            var logEntry = JsonSerializer.Serialize(logger);
            await File.AppendAllTextAsync(_filePath, logEntry + Environment.NewLine);
        }
    }
}

Listing 7: The DbLogger and FileLogger classes

The code in Listing 7, shows the FileLogger class that implements IRequestLogger interface. This code shows that the logger object is serialized and then this serialized JSON data in RequestLog.json file. Add a new folder named LogFiles folder in the project and add RequestLog.json file in it. The request data for each HTTP request will be written in the JSON file. 

The DbLogger class also implements IRequestLogger interface and its RequestLogger method. This method contains code to save the Logger data in the Logger table in SQL Server database.

Hence, we have created to Services those are implementing IRequestLogger interface. So now it is time for use to register these two services in the ASP.NET Core Services container in Program.cs using Keyed service as shown in Listing 8.

builder.Services.AddKeyedScoped<IRequestLogger, DbLogger>("dbLogger");
builder.Services.AddKeyedScoped<IRequestLogger, FileLogger>("fileLogger");


Listing 8: The Service registration         

As shown in Listing 8, we have registered DbLogger service class using the key dbLogger and FileLogger service class using the fileLogger key.

Step 6: In the project, add a new folder named Middlewares. In this folder we will add class files named DbLoggerMiddleware.cs and FileLoggerMiddleware.cs. In the DbLoggerMiddleware.cs file, add the code shown in Listing 9.


using Net9_KeyedServiceInMiddleware.Contract;
using Net9_KeyedServiceInMiddleware.Extensions;
using Net9_KeyedServiceInMiddleware.LoggerInfra;
using Net9_KeyedServiceInMiddleware.Models;
using Logger = Net9_KeyedServiceInMiddleware.Models.Logger;

namespace Net9_KeyedServiceInMiddleware.Middlewares
{
    public class DbLoggerMiddleware
    {
        private RequestDelegate _next;
        private CompanyContext _context;
        public DbLoggerMiddleware(RequestDelegate next)
        {
            _next = next;    
        }

        public async Task InvokeAsync(HttpContext context, [FromKeyedServices("dbLogger")] IRequestLogger logger)
        {

            // code to log request to database
            var request = context.Request;
            var loggerData = new Logger
            {
                LoggerId = new Random().Next(1, 1000),
                RequestDate = DateTime.UtcNow,
                RequestBody = await request.ReadRequestBodyAsync(),
                Method = request.Method,
                Path = request.Path
            };

            await logger.RequestLogger(loggerData);
            // Your DB Logger Logic Here
            await _next(context);
        }
    }


    public static class DBLoggerExtension
    { 
        public static void UseDbLogger(this IApplicationBuilder app)
        { 
           app.UseMiddleware<DbLoggerMiddleware>();
        }
    }
}

Listing 9: The DbLoggerMiddleware code          

As shown in Listing 9, the InvokeAsync() method is using the [FromKeysedService("dbLogger")] parameter. This parameter means that the instance of the service registered as dbLogger key in the container will be used in the InvokeAsync() method. This means that the InvokeAsync() method will use the DbLogger class instance for logging the HTTP request in the SQL Server database. Similarly, the FileLoggerMiddleware class shown in Listing 10, has the InvokeAsync() method that uses the [FromKeysedService("fileLogger")] parameter that indicates that the FileLogger service instance will be used by FileLoggerMiddleware class to log HTTP request in the JSON file. Listing 10 shows the FileLoggerMiddleware class.


using Net9_KeyedServiceInMiddleware.Contract;
using Net9_KeyedServiceInMiddleware.Extensions;
using Net9_KeyedServiceInMiddleware.Models;

namespace Net9_KeyedServiceInMiddleware.Middlewares
{
    public class FileLoggerMiddleware
    {
        RequestDelegate _next;

        public FileLoggerMiddleware(RequestDelegate next)
        {
            _next = next;   
        }

        public async Task InvokeAsync(HttpContext context, [FromKeyedServices("fileLogger")] IRequestLogger logger)
        {
            // Your File Logger Logic Here
            var request = context.Request;
            var loggerData = new Logger
            {
                LoggerId = new Random().Next(1, 1000), 
                RequestDate = DateTime.UtcNow,
                //RequestBody = await ReadRequestBodyAsync(request),
                RequestBody = await request.ReadRequestBodyAsync(),
                Method = request.Method,
                Path = request.Path
            };

            await logger.RequestLogger(loggerData);
            await _next(context);
        }

       
    }


    public static class FileLoggerMiddlewareExtensions
    {
        public static IApplicationBuilder UseFileLogger(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<FileLoggerMiddleware>();
        }
    }
}

Listing 10: The FileLoggerMiddleware class

This is how in ASP.NET Core 9, we can use the Keys services in Middlewares.

Step 7: Modify the Program.cs to use the Middlewares as well as the endpoints for reading and writing the person data as shown in Listing 11.


......

/* Usign the Middleware */
app.UseDbLogger();
app.UseFileLogger();

/*
   The Endpoints
 */
app.MapGet("/persons", async(CompanyContext ctx) => await ctx.PersonInfos.ToListAsync());

app.MapPost("/persons", async (CompanyContext ctx, PersonInfo person) =>
{
    await ctx.PersonInfos.AddAsync(person);
    await ctx.SaveChangesAsync();
    return Results.Created($"/persons/{person.Id}", person);
});

.......
Listing 11: Using the Middleware and adding Endpoints

Run the application and make HTTP GET and POST requests in Endpoints, The Log will be created in JSON file as shown in Figure 1



Figure 1: The Log in JSON file

The Log in database will be shown in Figure 2



Figure 2: The log in SQL Server Logger table       


The code for this article can be downloaded from this link.         


Conclusion: The ASP.NET 9 accessing the Keyed Service in Middleware provides a great scale of flexibility to access multiple services registered in Services Container implementing the same interface.  


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