Add request logging to a database in an ASP.NET Core Web API

Published: Monday 27 October 2025

You can add request logging to a database in an ASP.NET Core Web API. And the way you can do that is by adding middleware.

Adding a middleware class

You create a new middleware class. When we add the request log to the database, we are going to calculate how long the request took to get a response. For that, we are going to create a new Stopwatch instance and start the timer.

To work out how long the request took to complete and add it to the database, we are going to add a delegate after the response has been completed. We can do that by calling httpContext.Response.OnCompleted in the middleware.

// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
	private const string ResponseTimer = "ResponseTimer";

	private readonly RequestDelegate _next;

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

	public async Task Invoke(HttpContext httpContext)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();
		httpContext.Items.TryAdd(ResponseTimer, stopwatch);
			
		httpContext.Response.OnCompleted(async () =>
		{
			Stopwatch? responseTimer = null;
			if (httpContext.Items.TryGetValue(ResponseTimer, out var timer))
			{
				responseTimer = timer as Stopwatch;

				if (responseTimer != null)
				{
					// Timer found so stop it.
					responseTimer.Stop();
				}
			}
		});	
		
		// Next middleware
		await _next(httpContext);
	}
}

We have to remember to register the middleware in the Web API. To do that, go into Program.cs and call the AddMiddleware extension method:

// Program.cs
var app = builder.Build();

app.UseMiddleware<RequestLoggingMiddleware>();

It's recommended to add this after builder.Build has been called so it's high up the middleware priority chain.

Setting up the database

To save the request data to the database, you'll need to set up the database first. And we'll use SQL Server and Entity Framework Core to do that.

You'll have to add the following NuGet packages to your application:

You'll also need to have SQL Server and SQL Server Management Studio installed so you can create and view the database.

Database configuration in the app

This is the entity that we'll use to map against the RequestLogging table in the database.

// RequestLoggingEntity.cs
public class RequestLoggingEntity
{
	public int Id { get; set; }

	public DateTime Date { get; set; }

	public string Method { get; set; } = string.Empty;

	public string EncodedPathAndQuery { get; set; } = string.Empty;

	public string? IpAddress { get; set; }

	public string? UserAgent { get; set; }

	public int ResponseCode { get; set; }

	public TimeSpan? LoadTime { get; set; }
}

And that entity will need to be configured so string properties have maximum lengths set.

// RequestLoggingConfiguration.cs
public class RequestLoggingConfiguration
	: IEntityTypeConfiguration<RequestLoggingEntity>
{
	public void Configure(EntityTypeBuilder<RequestLoggingEntity> builder)
	{
		builder.HasKey(s => s.Id);

		builder.Property(s => s.Date)
			.HasColumnName("DateUtc")
			.HasColumnType("datetime")
			.HasConversion(new DateTimeUtcConverter());

		builder.Property(s => s.Method)
			.HasMaxLength(7);

		builder.Property(s => s.EncodedPathAndQuery)
			.HasMaxLength(300);

		builder.Property(s => s.IpAddress)
			.HasMaxLength(30);

		builder.Property(s => s.UserAgent)
			.HasMaxLength(300);

		builder.Property(s => s.ResponseCode);

		builder.Property(s => s.LoadTime)
			.HasColumnType("time(3)");

		builder.ToTable("RequestLogging");
	}
}

In addition to that, we've created a DateTimeUtcConverter class. This is a value converter that will save the request time as UTC in the database and then convert it back to the local timezone in the application.

// DateTimeUtcConverter.cs
public class DateTimeUtcConverter : ValueConverter<DateTime, DateTime>
{
	public DateTimeUtcConverter() : 
		base(d => d.ToUniversalTime(), d => DateTime.SpecifyKind(d, DateTimeKind.Utc).ToLocalTime()) {}
}

We've set up a repository class that will call the DbContext and save the request log to the database.

// IRequestLoggingRepository.cs
public interface IRequestLoggingRepository
{
	Task Create(CreateRequestLogDto createRequestLog);
}
// RequestLoggingRepository.cs
public class RequestLoggingRepository : IRequestLoggingRepository
{
	private readonly RequestLoggingDbContext _dbContext;
	public RequestLoggingRepository(RequestLoggingDbContext dbContext)
	{
		_dbContext = dbContext;
	}

	public async Task Create(CreateRequestLogDto createRequestLog)
	{
		await _dbContext.RequestLogging.AddAsync(new RequestLoggingEntity
		{
			Date = DateTime.Now,
			Method = TruncateString(createRequestLog.Method, 7),
			EncodedPathAndQuery = TruncateString(createRequestLog.EncodedPathAndQuery, 300),
			IpAddress = TruncateNullableString(createRequestLog.IpAddress, 30),
			UserAgent = TruncateNullableString(createRequestLog.UserAgent, 300),
			ResponseCode = (int)createRequestLog.ResponseCode,
			LoadTime = createRequestLog.LoadTime
		});
		await _dbContext.SaveChangesAsync();
	}

	private string? TruncateNullableString(string? value, int maxCharacters)
	{
		return (value?.Length ?? 0) > maxCharacters ? value?[..maxCharacters] : value;
	}

	private string TruncateString(string value, int maxCharacters)
	{
		return TruncateNullableString(value, maxCharacters) ?? string.Empty;
	}
}

We have added a couple of private methods to truncate a string. This is to ensure that they don't go over the max length limit when being saved to the database otherwise an exception will be thrown.

This will need to be registered as a scoped service in dependency injection.

// Program.cs
builder.Services.AddScoped<IRequestLoggingRepository, 
	RequestLoggingRepository>();

In the Create method, we expect a CreateRequestLogDto type in the parameter. This will store details about the request that we get from the request, such as the method, the request path and the IP address.

// CreateRequestLogDto.cs
public class CreateRequestLogDto
{
	public required string Method { get; init; } = string.Empty;

	public required string EncodedPathAndQuery { get; init; } = string.Empty;

	public required string? IpAddress { get; init; }

	public required string? UserAgent { get; init; }

	public required HttpStatusCode ResponseCode { get; init; }

	public required TimeSpan? LoadTime { get; init; }
}

The RequestLoggingDbContext class is the DbContext that will be used to save the request log to the database.

// RequestLoggingDbContext.cs
public class RequestLoggingDbContext : DbContext
{
	public required DbSet<RequestLoggingEntity> RequestLogging { get; set; }

	public RequestLoggingDbContext()
	{

	}

	public RequestLoggingDbContext(
		DbContextOptions<RequestLoggingDbContext> options
		) : base(options)
	{

	}

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.ApplyConfigurationsFromAssembly(typeof(RequestLoggingDbContext).Assembly);

		base.OnModelCreating(modelBuilder);
	}
}

We've overridden the OnModelCreating method so we can apply configurations from any class that implements the IEntityTypeConfiguration interface. This means it will applies the configuration that's in RequestLoggingConfiguration to the RequestLogging table in the database.

You'll need to add the connection string in appsettings.json so the application is able to connect to the database. The connection string below assumes that you are using Local DB, the database is called RequestLogging and you are using integrated security. If your database is on a different host, it's called something different, or you are not using integrated security, you''ll need to modify the connection string accordingly.

{
  "ConnectionStrings": {
    "RequestLoggingDbContext": "Server=(localdb)\\MSSQLLocalDB; Database=RequestLogging; Trusted_Connection=true; Trust Server Certificate=true; MultipleActiveResultSets=true; Integrated Security=true;"
  }
}

You'll then need to configure that in Program.cs by calling the AddDbContext extension method:

// Program.cs
builder.Services.AddDbContext<RequestLoggingDbContext>(options =>
{
	options.UseSqlServer(builder.Configuration.GetConnectionString(nameof(RequestLoggingDbContext)));
});

You'll need to add the RequestLogging table to the database. You can use Entity Framework Core migrations if you are comfortable with that. Otherwise, you can run this database script.

CREATE TABLE [dbo].[RequestLogging](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[DateUtc] [datetime] NOT NULL,
	[Method] [nvarchar](7) NOT NULL,
	[EncodedPathAndQuery] [nvarchar](300) NOT NULL,
	[IpAddress] [nvarchar](30) NULL,
	[UserAgent] [nvarchar](300) NULL,
	[ResponseCode] [int] NOT NULL,
	[LoadTime] [time](3) NULL,
CONSTRAINT [PK_RequestLogging] 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

Finally, we've set up a service that the middleware will call to create the request log in the database. This will call the RequestLoggingRepository class that will communicate with the DbContext and add the request log to the database.

// IRequestLoggingService.cs
public interface IRequestLoggingService
{
	Task Create(CreateRequestLogDto createRequestLog);
}
// RequestLoggingService.cs
public class RequestLoggingService : IRequestLoggingService
{
	private readonly IRequestLoggingRepository _requestLoggingRepository;

	public RequestLoggingService(IRequestLoggingRepository requestLoggingRepository)
	{
		_requestLoggingRepository = requestLoggingRepository;
	}

	public async Task Create(CreateRequestLogDto createRequestLog)
	{
		await _requestLoggingRepository.Create(createRequestLog);
	}
}

This will need to be registered in dependency injection as a scoped service.

// Program.cs
builder.Services.AddScoped<IRequestLoggingService, 
	RequestLoggingService>();

Create the log to the database

With the database set up and configured, we can now use the middleware to create the log in the database. To do that, we need to inject the IServiceScope instance into the RequestLogging middleware.

When we add a delegate to the completed request, we will create a new scope so we can get instances of the services that we have set up. The scope in the httpContext.RequestServices instance returns unpredicatable results once the request has been completed hence the reason for creating a new scope.

Then it's a case of setting up the properties needed for the CreateRequestLogDto instance and calling the Create method in the RequestLoggingService.

// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
	private const string ResponseTimer = "ResponseTimer";

	private readonly IServiceScopeFactory _serviceScopeFactory;
	private readonly RequestDelegate _next;

	public RequestLoggingMiddleware(
		IServiceScopeFactory serviceScopeFactory,
		RequestDelegate next
		)
	{
		_serviceScopeFactory = serviceScopeFactory;
		_next = next;
	}

	public async Task Invoke(HttpContext httpContext)
	{
		var stopwatch = new Stopwatch();
		stopwatch.Start();
		httpContext.Items.TryAdd(ResponseTimer, stopwatch);

		httpContext.Response.OnCompleted(async () =>
		{
			using var scope = _serviceScopeFactory.CreateScope();
			var requestLoggingService = scope.ServiceProvider.GetRequiredService<IRequestLoggingService>();

			Stopwatch? responseTimer = null;
			if (httpContext.Items.TryGetValue(ResponseTimer, out var timer))
			{
				responseTimer = timer as Stopwatch;

				if (responseTimer != null)
				{
					// Timer found so stop it.
					responseTimer.Stop();
				}
			}

			await requestLoggingService.Create(new CreateRequestLogDto
			{
				Method = httpContext.Request.Method,
				EncodedPathAndQuery = httpContext.Request.GetEncodedPathAndQuery(),
				IpAddress = httpContext.Connection.RemoteIpAddress?.ToString(),
				UserAgent = httpContext.Request.Headers.UserAgent,
				ResponseCode = (HttpStatusCode)httpContext.Response.StatusCode,
				LoadTime = responseTimer?.Elapsed
			});
		});

		// Next middleware
		await _next(httpContext);
	}
}

Forwarded headers

If you're running your application in Kubernetes, on a load balancer or using a reverse proxy like Cloudflare, you will need to turn on forwarded headers to get the IP address of the original request.

You do that by configuring the ForwardedHeadersOptions in Program.cs and specifying which forwarded headers you want to configure. You use the ForwardedHeaders enum and you have the following options:

  • ForwardedHeaders.XForwardedFor - The originating IP address of the client using the X-Forwarded-For request header.
  • ForwardedHeaders.XForwardedHost - The originating host of the client using the X-Forwarded-Host request header.
  • ForwardedHeaders.XForwardedProto - Identifies whether the client connected is using http:// or https://. This comes from the X-Forwarded-Proto request header.
  • ForwardedHeaders.XForwardedPrefix - Identifies the original path base of the client using the X-Forwarded-Prefix request header.
  • ForwardedHeaders.All - Adds the For, Host, Proto and Prefix forwarded headers.

Here's an example of how you can configure the application to read the X-Forwarded-For request header to get the IP address.

// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
});

If you want to add multiple forwarded headers but not all of them, you can separate each one with the | symbol.

// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
	options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | 
		ForwardedHeaders.XForwardedHost;
});

You'll also need to call the UseForwardedHeaders  in Program.cs after the application has been built to use forwarded headers.

// Program.cs
app.UseForwardedHeaders();

Overriding the request header

Some reverse proxy software specify their own request headers for forwarded properties. If you are using Cloudflare for example, they add the originating IP address of the client to the CF-Connecting-IP request header. Therefore, you can set it in the ForwardedForHeaderName property of the ForwardedHeadersOptions class like this.

// Program.cs
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
    options.ForwardedForHeaderName = "CF-Connecting-IP";
});

When not to enable forwarded headers

If users connect directly to the server of where your application is hosted and there is no load balancer configured, do not turn on forwarded headers.

Otherwise users will be able to change the IP address of the request by overriding the request header. This would be a major security concern as users could pretend they are someone else.

Watch the video

When you watch the video, will be able to see how we added request logging to an ASP.NET Core Web API and how it works.

And when you download the code example, you'll be able to try out request logging for yourself and see the request logs being created in the database.

Removing logs

As is the case with all types of logging, it can easily build up over time. Therefore, it's recommended to setup a scheduler to delete old request logs at periodic intervals. Otherwise the RequestLogging table will get very big.