STP.Audit 1.3.0
AuditTrail
A lightweight, RabbitMQ-based audit trail solution for distributed .NET applications.
Overview
The AuditTrail package provides a simple way to capture and store audit events across your application. It uses RabbitMQ as a message broker to decouple the generation of audit events from their processing and storage.
Setup
Add the following to your Program.cs:
builder.Services.AddAuditServices(
uri: "amqp://guest:guest@localhost:5672/",
exchange: "audit_exchange"
);
app.UseAuditTrail();
Usage
Basic Auditing
[Audited("User.Login")]
public async Task<IResult> Login(LoginRequest request)
{
// ...
}
Severity and Category
// Using predefined category
[Audited("User.Delete", Severity = AuditSeverity.High, Category = AuditCategories.Security)]
public async Task<IResult> DeleteUser(int userId)
{
// ...
}
// Using custom category (any string is valid)
[Audited("Order.Ship", Category = "Shipping")]
public async Task<IResult> ShipOrder(int orderId)
{
// ...
}
Controller-Level Category
Apply a default category to all audited actions in a controller:
[AuditCategory("Inventory")]
public class InventoryController : ControllerBase
{
[Audited("Item.Create")]
public async Task<IResult> CreateItem() { }
[Audited("Item.Delete", Category = "Security")] // Overrides controller category
public async Task<IResult> DeleteItem(int id) { }
}
Audit Only On Success
[Audited("Report.Generate", AuditOnlyOnSuccess = true)]
public async Task<IResult> GenerateReport()
{
// Only audited if response is 200 OK
}
Runtime Overrides with AuditContext
Override audit values at runtime via IAuditTrailService.AuditContext:
public class OrderService
{
private readonly IAuditTrailService _auditService;
public async Task ProcessOrder(Order order)
{
_auditService.AuditContext.Details = $"OrderId: {order.Id}";
_auditService.AuditContext.Severity = AuditSeverity.High;
_auditService.AuditContext.Category = "Orders";
// ...
}
}
Capturing State Changes with Snapshots
Use Before() and After() to capture state changes for audit:
[Audited("User.Update", Severity = AuditSeverity.Medium)]
public async Task<IResult> UpdateUser(int id, UpdateUserRequest request)
{
// Capture before state
var existingUser = await _userRepo.GetById(id);
_auditService.AuditContext.Before(existingUser);
// Perform update
var updatedUser = await _userRepo.Update(id, request);
// Capture after state
_auditService.AuditContext.After(updatedUser);
return Results.Ok(new { message = "User updated" });
}
[Audited("User.Delete", Severity = AuditSeverity.High)]
public async Task<IResult> DeleteUser(int id)
{
var user = await _userRepo.GetById(id);
_auditService.AuditContext.Before(user); // Only before for deletes
await _userRepo.Delete(id);
return Results.Ok(new { message = "User deleted" });
}
Behavior
Source Header
The Source header must be included in requests to identify the originating application:
Source: MyApplication
This value is captured in the audit trail's Source field.
Status Code Handling
| Status Code | Behavior |
|---|---|
| 403 Forbidden | Audit is skipped entirely |
| 200 OK | Audited (respects AuditOnlyOnSuccess) |
| Other 2xx | Audited as "Success" |
| 4xx/5xx | Audited as "Failed" (skipped if AuditOnlyOnSuccess = true) |
Message Extraction
The audit description is extracted from the response body:
- If response is valid JSON with a
messageproperty, that value is used - If response is 500 and no
messagefound, the full response body is captured - Otherwise, defaults to "Success" or "Failed" based on status code
API Reference
AuditedAttribute
| Property | Type | Default | Description |
|---|---|---|---|
| Action | string | (required) | The action identifier |
| Severity | AuditSeverity | None | Severity level |
| Category | string | "Uncategorized" | Category for grouping |
| AuditOnlyOnSuccess | bool | false | Only audit on 200 OK |
AuditCategoryAttribute
Class-level attribute to set default category for all audited actions in a controller.
[AuditCategory("CategoryName")]
AuditSeverity
public enum AuditSeverity
{
None = 0,
Low = 1,
Medium = 2,
High = 3
}
AuditCategories
Predefined categories for convenience. Any string value can be used as a category.
public static class AuditCategories
{
public const string Security = "Security";
public const string Inventory = "Inventory";
public const string Users = "Users";
}
AuditContext
Runtime override context available via IAuditTrailService.AuditContext:
| Property/Method | Type | Description |
|---|---|---|
| UserName | string? | Override user name |
| UserId | string? | Override user ID |
| Roles | string? | Override roles |
| FacilityId | string? | Override facility ID |
| FacilityTypeId | string? | Override facility type ID |
| Details | string? | Additional details |
| Severity | AuditSeverity? | Override severity |
| Category | string? | Override category |
| Before(object) | method | Capture state before operation |
| After(object) | method | Capture state after operation |
AuditTrail Record
The structure published to RabbitMQ:
public record AuditTrail(
DateTime Date,
string UserId,
string UserName,
string UserRole,
string Action,
string Description,
string Status, // "Success" or "Failed"
string Source,
string FacilityId,
string FacilityTypeId,
string? Details,
AuditSeverity Severity,
string Category,
string? Snapshot // JSON: { "Before": ..., "After": ... }
);
User information (UserId, UserName, UserRole, FacilityId, FacilityTypeId) is extracted from JWT claims unless overridden via AuditContext.
Priority Resolution
Severity: AuditContext > [Audited(Severity=)] > None
Category: AuditContext > [Audited(Category=)] > [AuditCategory] > "Uncategorized"
Requirements
- .NET 6.0 or later
- RabbitMQ 6.8.1
License
MIT
No packages depend on STP.Audit.
.NET 8.0
- Identity.Common (>= 4.1.7)
- RabbitMQ.Client (>= 7.1.2)