Advanced Audit Logging in .NET Core Using Middleware
Supporting Classes
public class AuditLog
{
public Guid Id { get; set; }
public bool IsDeleted { get; set; }
public DateTime CreatedDate { get; set; }
public string? CreatorId { get; set; }
public string? CorrelationId { get; set; }
public AuditLogEventTypes AuditLogEventType { get; set; }
public string? EntityId { get; set; }
public string? EntityName { get; set; }
public string? UserName { get; set; }
public string? UserRole { get; set; }
public string? SessionId { get; set; }
public string? ClientIpAddress { get; set; }
public string? BrowserInfo { get; set; }
public string? ApplicationName { get; set; }
public string? MachineName { get; set; }
public string? MachineVersion { get; set; }
public string? MachineOsVersion { get; set; }
public string? ServiceName { get; set; }
public string? MethodName { get; set; }
public string? HttpMethod { get; set; }
public string? RequestUrl { get; set; }
public string? ResponseStatus { get; set; }
public DateTime? ExecutionTime { get; set; }
public string? ExecutionDuration { get; set; }
public string? RequestHeaders { get; set; }
public string? QueryParameters { get; set; }
public string? BodyParameters { get; set; }
public string? Exception { get; set; }
public ICollection<EntityPropertyChange> EntityPropertyChanges { get; set; } = new List<EntityPropertyChange>();
}
public class EntityPropertyChange
{
public Guid Id { get; set; }
public string? PropertyName { get; set; }
public string? OriginalValue { get; set; }
public string? NewValue { get; set; }
public string? PropertyTypeFullName { get; set; }
public Guid AuditLogId { get; set; }
}
public class AuditScope
{
public List<AuditLog> Logs { get; set; } = new List<AuditLog>();
}
Audit Logging with Entity Tracking in DbContext
public class AppDbContext : DbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AuditScope _auditScope;
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public AppDbContext(DbContextOptions<AppDbContext> options, IHttpContextAccessor httpContextAccessor, AuditScope auditScope)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
_auditScope = auditScope;
}
public override int SaveChanges()
{
var auditEntries = OnBeforeSaveChanges();
var result = base.SaveChanges();
OnAfterSaveChanges(auditEntries);
return result;
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var auditEntries = OnBeforeSaveChanges();
var result = await base.SaveChangesAsync(cancellationToken);
OnAfterSaveChanges(auditEntries);
return result;
}
private List<EntityEntry> OnBeforeSaveChanges()
{
return ChangeTracker.Entries().ToList();
}
private void OnAfterSaveChanges(List<EntityEntry> auditEntries)
{
foreach (var entry in auditEntries)
{
if (entry.Entity is AuditLog) continue;
var tableName = entry.Metadata.GetTableName();
var auditLog = new AuditLog();
auditLog.Id = Guid.NewGuid();
auditLog.AuditLogEventType = entry.State switch
{
EntityState.Added => AuditLogEventTypes.Added,
EntityState.Modified => AuditLogEventTypes.Modified,
EntityState.Deleted => AuditLogEventTypes.Deleted,
EntityState.Unchanged => AuditLogEventTypes.Unchanged,
EntityState.Detached => AuditLogEventTypes.Detached,
_ => AuditLogEventTypes.Unknown
};
auditLog.EntityId = entry.OriginalValues[entry.Metadata.FindPrimaryKey()!.Properties.First().Name]!.ToString();
auditLog.EntityName = tableName;
auditLog.UserName = _httpContextAccessor.HttpContext?.User?.GetUserName();
auditLog.UserRole = _httpContextAccessor.HttpContext?.User?.GetUserRole() != null
? string.Join(",", _httpContextAccessor.HttpContext.User.GetUserRole()!)
: null;
foreach (var property in entry.Properties)
{
var columnName = property.Metadata.Name;
var oldValue = property.OriginalValue?.ToString();
var newValue = property.CurrentValue?.ToString();
if (!Equals(oldValue, newValue))
{
var entityPropertyChange = new EntityPropertyChange
{
Id = Guid.NewGuid(),
NewValue = newValue,
OriginalValue = oldValue,
PropertyName = columnName,
PropertyTypeFullName = property.Metadata.ClrType.FullName,
AuditLogId = auditLog.Id
};
auditLog.EntityPropertyChanges.Add(entityPropertyChange);
}
}
_auditScope.Logs.Add(auditLog);
}
}
}
The AuditLogMiddleware Class
This middleware captures a wide range of information, including HTTP request/response details, execution times, and exception messages.
public class AuditLogMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public AuditLogMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context, AuditScope auditScope)
{
var stopwatch = Stopwatch.StartNew();
var executionTime = DateTime.UtcNow;
var applicationName = _configuration.GetApplicationName();
var correlationId = context.TraceIdentifier;
var originalBodyStream = context.Response.Body;
using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
stopwatch.Stop();
if (auditScope.Logs.Any())
{
foreach (var auditLog in auditScope.Logs)
{
var forwardedHeader = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
var ipAddress = string.IsNullOrEmpty(forwardedHeader)
? context.Connection.RemoteIpAddress?.ToString()
: forwardedHeader.Split(',')[0];
auditLog.CorrelationId = correlationId;
auditLog.SessionId = context.User.GetUserCustomProperty("SessionId");
auditLog.ClientIpAddress = ipAddress;
auditLog.BrowserInfo = context.Request.Headers["User-Agent"];
auditLog.ApplicationName = applicationName;
auditLog.MachineName = Environment.MachineName;
auditLog.MachineVersion = Environment.Version.ToString();
auditLog.MachineOsVersion = Environment.OSVersion.ToString();
var routeData = context.GetRouteData();
auditLog.ServiceName = routeData?.Values["controller"]?.ToString();
auditLog.MethodName = routeData?.Values["action"]?.ToString();
auditLog.HttpMethod = context.Request.Method;
auditLog.RequestUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
auditLog.ResponseStatus = context.Response.StatusCode.ToString();
auditLog.ExecutionTime = executionTime;
auditLog.ExecutionDuration = stopwatch.ElapsedMilliseconds.ToString();
auditLog.RequestHeaders = JsonConvert.SerializeObject(
context.Request.Headers.ToDictionary(
header => header.Key, header => header.Value.ToString()
)
);
auditLog.QueryParameters = JsonConvert.SerializeObject(
context.Request.Query.ToDictionary(
query => query.Key, query => query.Value.ToString()
)
);
if (context.Request.Body.CanSeek == true)
{
context.Request.Body.Position = 0;
}
else
{
context.Request.EnableBuffering();
}
string originalContent;
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true))
{
originalContent = await reader.ReadToEndAsync();
context.Request.Body.Position = 0;
}
var bodyParameters = JsonConvert.DeserializeObject<Dictionary<string, object>>(originalContent);
if (bodyParameters != null && bodyParameters.Any())
{
var bodyContent = JsonConvert.SerializeObject(bodyParameters);
auditLog.BodyParameters = bodyContent;
}
if (context.Response.StatusCode >= 400)
{
responseBody.Seek(0, SeekOrigin.Begin);
var responseText = await new StreamReader(responseBody).ReadToEndAsync();
auditLog.Exception = responseText;
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
}
}
}
}
Program.cs
app.UseMiddleware<AuditLogMiddleware>(); app.UseRouting(); app.UseAuthorization(); builder.Services.AddScoped<AuditScope>();