UserActivityLogger 1.0.9
dotnet add package UserActivityLogger --version 1.0.9
NuGet\Install-Package UserActivityLogger -Version 1.0.9
<PackageReference Include="UserActivityLogger" Version="1.0.9" />
<PackageVersion Include="UserActivityLogger" Version="1.0.9" />
<PackageReference Include="UserActivityLogger" />
paket add UserActivityLogger --version 1.0.9
#r "nuget: UserActivityLogger, 1.0.9"
#:package UserActivityLogger@1.0.9
#addin nuget:?package=UserActivityLogger&version=1.0.9
#tool nuget:?package=UserActivityLogger&version=1.0.9
📊 User Activity Logger
A flexible and comprehensive logging system for tracking user activities across the application with automatic context capture and security-first design.
🎯 Overview
The User Activity Logger provides seamless tracking of user interactions throughout your ASP.NET Core application. Using declarative attributes and action filters, it automatically captures detailed audit trails without cluttering your business logic.
✨ Key Features
- 🔐 Automatic Security - Redacts sensitive fields (passwords, secrets)
- 🎯 Zero Boilerplate - Simple attribute decoration on controller actions
- 📝 Rich Context - Captures HTTP details, user info, IP addresses, and custom data
- ⚡ Performance Optimized - Selective logging with configurable verbosity
- 🔄 Conditional Actions - Dynamic log descriptions based on operation results
- 🗃️ Database Backed - Persistent storage with Entity Framework Core
🏗️ Architecture
Controller Action
↓
[LogUserActivity] Attribute
↓
UserActivityLoggingFilter (IAsyncResultFilter)
↓
UserActivityLogger Service
↓
Database (UserLog Entity)
📦 Components
| Component | Purpose |
|---|---|
| UserLog | Entity model for storing log entries |
| IUserActivityLogger | Service interface for logging operations |
| UserActivityLogger | Implementation with automatic context capture |
| LogUserActivityAttribute | Declarative attribute for marking loggable actions |
| UserActivityLoggingFilter | Action filter that intercepts and processes requests |
🚀 Quick Start
1️⃣ Database Setup
The UserLog entity captures:
| Field | Description |
|---|---|
Id |
Primary key |
Event |
Action description |
UserId |
Foreign key to Employee |
ResponseStatusCode |
HTTP status code |
IPAddress |
Client IP address |
Path |
Full request URL |
Method |
HTTP method (GET, POST, etc.) |
AdditionalData |
JSON serialized custom data |
DateEvent |
Timestamp (defaults to DateTime.Now) |
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureUserActivityLogger();
}
Run your migrations to create the table:
dotnet ef migrations add AddUserActivityLogging
dotnet ef database update
2️⃣ Service Registration
Add to Program.cs:
// Add UserActivityLogger services
builder.Services.AddUserActivityLogger(options =>
{
options.EnableRequestBodyLogging = false;
options.LogAnonymousUsers = true;
options.RedactedFields = new[] { "password", "pwd", "secret" };
options.SkipPaths = new[] { "/health", "/api/metrics" };
// Custom IP resolver for load balancer/proxy scenarios
options.IpAddressResolver = (httpContext) =>
{
var forwardedFor = httpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault();
return !string.IsNullOrEmpty(forwardedFor)
? forwardedFor.Split(',')[0].Trim()
: httpContext.Connection.RemoteIpAddress?.ToString();
};
});
// Register the filter with MVC
builder.Services.AddControllersWithViews(options =>
{
options.AddUserActivityLogging(); // Add this line
});
3️⃣ Start Logging
Decorate your controller actions:
[LogUserActivity(ConstLogUser.ViewAssignments)]
public async Task<IActionResult> Index()
{
return View(await _assignmentsServicesBL.PrepareListAsync());
}
✅ That's it! The logger will automatically capture user activity.
📖 Usage Guide
🔹 Basic Logging
Perfect for simple GET requests or actions without sensitive data:
[HttpGet]
[LogUserActivity("View Dashboard")]
public async Task<IActionResult> Dashboard()
{
return View();
}
What gets logged:
- User ID
- Event: "View Dashboard"
- Request path and method
- IP address
- Response status code
- Timestamp
🔹 Logging with Request Body
Capture form data for POST/PUT operations (sensitive fields are automatically redacted):
[HttpPost]
[LogUserActivity(ConstLogUser.CreateAssignment, LogRequestBody = true)]
public async Task<IActionResult> CreateAssignment(AssignmentVM model)
{
// Your business logic
return Ok();
}
Security Features:
- ✅ Automatically redacts:
password,pwd,secretfields - ✅ Skips anti-forgery tokens (
__RequestVerificationToken) - ✅ Filters out DataTables parameters (
columns[],order[], etc.) - ✅ JSON serializes form data with UTF-8 encoding
Example logged data:
{
"assignmentTitle": "Q4 Project Review",
"assignedTo": "EMP001",
"password": "***REDACTED***"
}
🔹 Conditional Action Descriptions
For operations with multiple outcomes (activate/deactivate, approve/reject):
[HttpPost]
[LogUserActivity(ConstLogUser.DeactivateSites, LogActionArguments = true)]
public async Task<IActionResult> ChangeStatus(int id)
{
var result = await _sitesServicesBL.ChangeStatusAsync(id, User.GetUserId());
if (result == eChangeStatusResult.Activate)
{
// Override the log description dynamically
HttpContext.Items["ConditionalLogAction"] = ConstLogUser.ActivateSites;
return Ok(new { body = "Site activated successfully" });
}
if (result == eChangeStatusResult.Deactivate)
{
HttpContext.Items["ConditionalLogAction"] = ConstLogUser.DeactivateSites;
return Ok(new { body = "Site deactivated successfully" });
}
return BadRequest(new { body = "Operation failed" });
}
Result: The log will show the actual operation performed (Activate vs Deactivate) instead of the default description.
🔹 Manual Logging
For scenarios outside controller actions (background jobs, SignalR hubs, etc.):
public class ReportService
{
private readonly IUserActivityLogger _userLogger;
public ReportService(IUserActivityLogger userLogger)
{
_userLogger = userLogger;
}
public async Task GenerateReport(string reportType)
{
// Your logic here...
// Log the activity
await _userLogger.LogActionAsync(
"Generate Report",
new { ReportType = reportType, GeneratedAt = DateTime.Now }
);
}
}
For complete control:
await _userLogger.LogAsync(new UserLog
{
Event = "Background Job Completed",
UserId = systemUserId,
Path = "/jobs/process",
Method = "BACKGROUND",
AdditionalData = JsonSerializer.Serialize(jobDetails)
});
⚙️ Configuration Reference
LogUserActivityAttribute Properties
| Property | Type | Default | Description |
|---|---|---|---|
ActionDescription |
string? |
null |
Human-readable description of the action |
LogRequestBody |
bool |
false |
Captures form data (auto-redacts sensitive fields) |
LogActionArguments |
bool |
false |
Enables dynamic action description via HttpContext.Items |
Usage Examples
// Minimal - uses action name as description
[LogUserActivity]
public IActionResult Index() => View();
// With description
[LogUserActivity("View Employee List")]
public IActionResult Employees() => View();
// With request body logging
[HttpPost]
[LogUserActivity("Update Profile", LogRequestBody = true)]
public IActionResult UpdateProfile(ProfileVM model) => Ok();
// With conditional logging
[HttpPost]
[LogUserActivity("Change Status", LogActionArguments = true)]
public IActionResult Toggle(int id)
{
// Set HttpContext.Items["ConditionalLogAction"] based on outcome
return Ok();
}
🛡️ Security & Best Practices
✅ DO
Use constants for action descriptions to ensure consistency:
public static class ConstLogUser { public const string ViewAssignments = "View Assignments"; public const string CreateAssignment = "Create Assignment"; public const string ActivateSites = "Activate Site"; }Be selective with
LogRequestBody = true- only use for critical operationsReview logs regularly for security auditing and compliance
Implement retention policies to manage database size
❌ DON'T
- ❌ Log every single action (causes performance overhead and noise)
- ❌ Manually handle sensitive data redaction (it's automatic)
- ❌ Store logs indefinitely without archiving strategy
- ❌ Log to production without testing first
🔍 Querying Logs
Recent User Activity
var recentActivity = await _context.UserLogs
.Include(l => l.User)
.Where(l => l.UserId == currentUserId)
.OrderByDescending(l => l.DateEvent)
.Take(50)
.ToListAsync();
Search by Event Type
var auditTrail = await _context.UserLogs
.Where(l => l.Event.Contains("Delete") || l.Event.Contains("Deactivate"))
.OrderByDescending(l => l.DateEvent)
.ToListAsync();
Failed Operations (4xx/5xx status codes)
var failures = await _context.UserLogs
.Where(l => l.ResponseStatusCode >= 400)
.Include(l => l.User)
.OrderByDescending(l => l.DateEvent)
.Take(100)
.ToListAsync();
Activity by IP Address
var suspiciousActivity = await _context.UserLogs
.Where(l => l.IPAddress == "192.168.1.100")
.OrderByDescending(l => l.DateEvent)
.ToListAsync();
🐛 Troubleshooting
❓ Logs Not Being Created
Check:
- ✅ Filter is registered globally in
Program.cs - ✅ Attribute is applied to controller action
- ✅
IHttpContextAccessoris registered - ✅ Database connection is valid
- ✅ Migrations have been applied
Debug tip:
// Add logging in UserActivityLogger constructor
public UserActivityLogger(...)
{
_logger.LogInformation("UserActivityLogger initialized");
}
❓ Request Body Not Captured
Possible causes:
LogRequestBody = false(check attribute settings)- Request is not form-encoded (must be
application/x-www-form-urlencodedormultipart/form-data) - Fields are being filtered (DataTables params, anti-forgery tokens)
Test with:
[HttpPost]
[LogUserActivity("Test", LogRequestBody = true)]
public IActionResult Test([FromForm] string testField)
{
return Ok(new { received = testField });
}
❓ User ID Is NULL
Check:
- User is authenticated (
User.Identity.IsAuthenticated) ClaimTypes.NameIdentifierexists in claims- Authentication middleware is configured properly
Verify claims:
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation($"Current User ID: {userId ?? "NULL"}");
🎨 Example Dashboard Query
Build an activity dashboard with this query:
public async Task<ActivityDashboardVM> GetDashboardData(DateTime fromDate)
{
var logs = await _context.UserLogs
.Include(l => l.User)
.Where(l => l.DateEvent >= fromDate)
.ToListAsync();
return new ActivityDashboardVM
{
TotalActions = logs.Count,
UniqueUsers = logs.Select(l => l.UserId).Distinct().Count(),
MostActiveUser = logs.GroupBy(l => l.UserId)
.OrderByDescending(g => g.Count())
.Select(g => g.First().User.Name)
.FirstOrDefault(),
TopActions = logs.GroupBy(l => l.Event)
.OrderByDescending(g => g.Count())
.Take(5)
.Select(g => new { Action = g.Key, Count = g.Count() })
.ToList(),
FailureRate = logs.Count(l => l.ResponseStatusCode >= 400) * 100.0 / logs.Count
};
}
🚀 Future Enhancements
Consider implementing:
- 📨 Background Logging - Queue-based logging for high-traffic scenarios (using Hangfire/MassTransit)
- 📊 Analytics Dashboard - Visual reporting of user activities and trends
- 🗄️ Auto-Archiving - Move old logs to cold storage (e.g., Azure Blob Storage)
- 📤 Export Functionality - CSV/Excel export for compliance audits
- 🔌 External Integration - Send logs to Serilog, ELK Stack, or Application Insights
- 🔔 Alerting - Real-time notifications for suspicious activities
📞 Support
For questions or issues:
- Check this documentation
- Review existing logs for patterns
- Contact the development team
Version: 1.0.9
Last Updated: 2025
Maintained by: Asma El-Hadiedy
Built with ❤️ for better application monitoring and security
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.11)
- Microsoft.EntityFrameworkCore.Abstractions (>= 9.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.