STP.Audit 1.2.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:

  1. If response is valid JSON with a message property, that value is used
  2. If response is 500 and no message found, the full response body is captured
  3. 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

Version Downloads Last updated
1.3.0 113 1/1/2026
1.2.0 15 12/30/2025
1.1.6-rc 332 3/25/2025
1.1.5-rc 33 3/23/2025
1.1.4-rc 116 3/17/2025
1.1.1-rc 62 3/12/2025
1.1.0 668 9/22/2025
1.0.9-rc 39 3/12/2025
1.0.8 40 9/17/2025
1.0.8-rc 635 3/10/2025
1.0.0 156 7/28/2025