The logging package includes built-in support for GDPR compliance including data deletion (right to erasure) and data export (right to data portability).
Right to Erasure (Data Deletion)
Delete all logging data for a specific user:
[HttpDelete("users/{userId}/data")]
[Authorize]
public async Task<IActionResult> DeleteUserData(string userId)
{
// Verify the requester has permission to delete this user's data
var currentUserId = User.FindFirst("sub")?.Value;
if (currentUserId != userId && !User.IsInRole("Admin"))
{
return Forbid();
}
var tenantId = User.FindFirst("tenantId")?.Value ?? "default";
await _loggingService.DeleteUserDataAsync(userId, tenantId);
_logger.LogInformation(
"Deleted logging data for user {UserId} in tenant {TenantId}",
userId,
tenantId);
return NoContent();
}What Gets Deleted
- All
LoginSessionrecords for the user - All
ApiActionrecords for the user - Deletion is scoped by tenant ID for multi-tenant apps
Right to Data Portability (Export)
Export all user data in a streaming fashion for large datasets:
[HttpGet("users/{userId}/export")]
[Authorize]
public async Task<IActionResult> ExportUserData(
string userId,
CancellationToken cancellationToken)
{
// Verify permission
var currentUserId = User.FindFirst("sub")?.Value;
if (currentUserId != userId && !User.IsInRole("Admin"))
{
return Forbid();
}
// Stream the response as JSON
Response.ContentType = "application/json";
Response.Headers.Append("Content-Disposition",
$"attachment; filename=user-data-{userId}.json");
await using var writer = new Utf8JsonWriter(Response.Body);
writer.WriteStartObject();
writer.WritePropertyName("pages");
writer.WriteStartArray();
await foreach (var page in _loggingService.ExportUserDataStreamAsync(
userId,
pageSize: 1000,
cancellationToken))
{
if (!page.Success)
{
_logger.LogError("Export error: {Error}", page.ErrorMessage);
break;
}
JsonSerializer.Serialize(writer, page);
}
writer.WriteEndArray();
writer.WriteEndObject();
await writer.FlushAsync(cancellationToken);
return new EmptyResult();
}UserDataExportPage Structure
public class UserDataExportPage
{
// Status
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
// Pagination
public int PageNumber { get; set; }
public bool HasMore { get; set; }
// Data - Sessions only on first page
public List<LoginSession>? Sessions { get; set; }
// Data - Actions on every page
public List<ApiAction> Actions { get; set; }
}Configuration
builder.Services.AddApiLogging(options =>
{
// Page size for exports (default: 1000)
options.ExportPageSize = 1000;
});Sensitive Data Filtering
Automatically sanitize sensitive data before logging:
builder.Services.AddApiLogging(options =>
{
options.EnableDataSanitization = true;
// Default sensitive fields
options.SensitiveFieldNames = new List<string>
{
"password",
"token",
"authorization",
"apiKey",
"api_key",
"secret",
"creditCard",
"credit_card",
"ssn",
"pin",
"cvv",
"bearer"
};
// Add your own
options.SensitiveFieldNames.Add("socialSecurityNumber");
options.SensitiveFieldNames.Add("bankAccountNumber");
});Sanitization Behavior
Sensitive field values are replaced with [REDACTED] in both request and response bodies.
// Original request body:
{
"username": "john@example.com",
"password": "secret123",
"creditCard": "4111111111111111"
}
// Logged as:
{
"username": "john@example.com",
"password": "[REDACTED]",
"creditCard": "[REDACTED]"
}Stack Trace Sanitization
Optionally redact stack traces in production to prevent information disclosure:
options.SanitizeStackTracesInProduction = true; options.EnvironmentName = "Production"; // Stack traces logged as "[REDACTED]" in production
Data Retention
The package creates TTL indexes for automatic data cleanup:
// Default TTL (configured in MongoDB indexes): // - Sessions: 30 days // - Actions: 90 days // These are created automatically by MongoDbIndexSetupService
Consent Management
Implement consent checking before logging. Use the [ApiLogging]attribute selectively or check consent in middleware:
public class ConsentMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
var hasConsent = await _consentService.HasLoggingConsent(
context.User.FindFirst("sub")?.Value);
// Skip logging for users without consent
if (!hasConsent)
{
context.Items["ApiLogging_SkipLogging"] = true;
}
await _next(context);
}
}Audit Trail
Log GDPR operations for compliance audit trails:
[HttpDelete("users/{userId}/data")]
public async Task<IActionResult> DeleteUserData(string userId)
{
var requesterId = User.FindFirst("sub")?.Value;
var tenantId = User.FindFirst("tenantId")?.Value;
// Log the GDPR request before deletion
_logger.LogInformation(
"GDPR deletion request: Requester={Requester}, Subject={Subject}, Tenant={Tenant}",
requesterId,
userId,
tenantId);
await _loggingService.DeleteUserDataAsync(userId, tenantId);
// Log completion
_logger.LogInformation(
"GDPR deletion completed: Subject={Subject}, Tenant={Tenant}",
userId,
tenantId);
return NoContent();
}Complete GDPR Controller Example
[ApiController]
[Route("api/gdpr")]
[Authorize]
public class GdprController : ControllerBase
{
private readonly IApiLoggingService _loggingService;
private readonly ILogger<GdprController> _logger;
public GdprController(
IApiLoggingService loggingService,
ILogger<GdprController> logger)
{
_loggingService = loggingService;
_logger = logger;
}
[HttpGet("export")]
public async IAsyncEnumerable<object> ExportMyData(
[EnumeratorCancellation] CancellationToken ct)
{
var userId = User.FindFirst("sub")?.Value;
await foreach (var page in _loggingService
.ExportUserDataStreamAsync(userId, ct: ct))
{
if (!page.Success)
{
yield return new { error = page.ErrorMessage };
yield break;
}
yield return page;
}
}
[HttpDelete("delete")]
public async Task<IActionResult> DeleteMyData()
{
var userId = User.FindFirst("sub")?.Value;
var tenantId = User.FindFirst("tenantId")?.Value ?? "default";
_logger.LogInformation(
"User {UserId} requested data deletion", userId);
await _loggingService.DeleteUserDataAsync(userId, tenantId);
return NoContent();
}
[HttpDelete("users/{userId}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteUserData(string userId)
{
var adminId = User.FindFirst("sub")?.Value;
var tenantId = User.FindFirst("tenantId")?.Value ?? "default";
_logger.LogInformation(
"Admin {AdminId} requested deletion for user {UserId}",
adminId, userId);
await _loggingService.DeleteUserDataAsync(userId, tenantId);
return NoContent();
}
}