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
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
"ConnectionStrings": { "DefaultConnection": "Data Source=.;Initial Catalog=Company;Integrated Security=SSPI;TrustServerCertificate=True" }
builder.Services.AddDbContext<CompanyContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); });
using Net9_KeyedServiceInMiddleware.Models; namespace Net9_KeyedServiceInMiddleware.Contract { public interface IRequestLogger { Task RequestLogger(Logger logger); } }
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); } } }
builder.Services.AddKeyedScoped<IRequestLogger, DbLogger>("dbLogger"); builder.Services.AddKeyedScoped<IRequestLogger, FileLogger>("fileLogger");
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>(); } } }
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>(); } } }
...... /* 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); }); .......
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.