C# MAUI Audit Trail Logging คืออะไร
.NET MAUI (Multi-platform App UI) เป็น framework สำหรับสร้าง cross-platform applications ด้วย C# รองรับ Android, iOS, macOS และ Windows ด้วย codebase เดียว Audit Trail Logging คือการบันทึกทุกการกระทำของผู้ใช้และระบบอย่างละเอียด เพื่อ compliance, security forensics และ debugging ครอบคลุม user actions, data changes, authentication events และ error logs การ implement audit trail ใน MAUI app ช่วยให้ track ได้ว่าใครทำอะไร เมื่อไหร่ จากอุปกรณ์ไหน สำคัญมากสำหรับ enterprise apps ที่ต้อง comply กับ GDPR, HIPAA หรือ SOC 2
MAUI Audit Trail Architecture
// AuditArchitecture.cs — Audit trail architecture for MAUI
using System;
using System.Collections.Generic;
namespace MauiAuditTrail
{
public class AuditArchitecture
{
// Audit event types
public enum AuditEventType
{
UserLogin,
UserLogout,
DataCreate,
DataRead,
DataUpdate,
DataDelete,
SettingsChange,
PermissionChange,
ErrorOccurred,
AppStarted,
AppBackgrounded,
NavigationEvent,
}
// Audit log entry
public class AuditLogEntry
{
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public AuditEventType EventType { get; set; }
public string UserId { get; set; }
public string UserEmail { get; set; }
public string Action { get; set; }
public string EntityType { get; set; }
public string EntityId { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public string DeviceInfo { get; set; }
public string Platform { get; set; }
public string AppVersion { get; set; }
public string IpAddress { get; set; }
public Dictionary<string, string> Metadata { get; set; } = new();
public override string ToString()
{
return $"[{Timestamp:yyyy-MM-dd HH:mm:ss}] {EventType}: {Action} by {UserId}";
}
}
// Architecture layers
public static Dictionary<string, string> Layers = new()
{
["MAUI App"] = "UI layer — capture user actions, navigation events",
["Audit Service"] = "Business logic — create audit entries, validate, enrich",
["Local Storage"] = "SQLite — offline storage for audit logs",
["Sync Service"] = "Background sync — upload logs to server when online",
["Backend API"] = "REST API — receive and store audit logs permanently",
["Analytics"] = "Dashboard — visualize audit data, search, export",
};
}
}
Audit Logger Implementation
// AuditLogger.cs — Core audit logger for MAUI
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Maui.Devices;
namespace MauiAuditTrail
{
public interface IAuditLogger
{
Task LogAsync(AuditArchitecture.AuditEventType eventType, string action,
string entityType = null, string entityId = null,
object oldValue = null, object newValue = null,
Dictionary<string, string> metadata = null);
Task<List<AuditArchitecture.AuditLogEntry>> GetLogsAsync(
DateTime? from = null, DateTime? to = null, int limit = 100);
}
public class AuditLogger : IAuditLogger
{
private readonly IAuditStorage _storage;
private readonly IAuditSyncService _syncService;
private readonly IUserContext _userContext;
public AuditLogger(IAuditStorage storage, IAuditSyncService syncService,
IUserContext userContext)
{
_storage = storage;
_syncService = syncService;
_userContext = userContext;
}
public async Task LogAsync(
AuditArchitecture.AuditEventType eventType,
string action,
string entityType = null,
string entityId = null,
object oldValue = null,
object newValue = null,
Dictionary<string, string> metadata = null)
{
var entry = new AuditArchitecture.AuditLogEntry
{
EventType = eventType,
Action = action,
UserId = _userContext.UserId,
UserEmail = _userContext.Email,
EntityType = entityType,
EntityId = entityId,
OldValue = oldValue != null ? JsonSerializer.Serialize(oldValue) : null,
NewValue = newValue != null ? JsonSerializer.Serialize(newValue) : null,
DeviceInfo = GetDeviceInfo(),
Platform = DeviceInfo.Current.Platform.ToString(),
AppVersion = AppInfo.Current.VersionString,
Metadata = metadata ?? new(),
};
// Store locally first (offline-first)
await _storage.SaveAsync(entry);
// Try to sync if online
if (Connectivity.Current.NetworkAccess == NetworkAccess.Internet)
{
await _syncService.SyncPendingAsync();
}
}
public async Task<List<AuditArchitecture.AuditLogEntry>> GetLogsAsync(
DateTime? from = null, DateTime? to = null, int limit = 100)
{
return await _storage.QueryAsync(from, to, limit);
}
private string GetDeviceInfo()
{
return $"{DeviceInfo.Current.Manufacturer} {DeviceInfo.Current.Model} " +
$"({DeviceInfo.Current.Platform} {DeviceInfo.Current.VersionString})";
}
}
}
SQLite Local Storage
// AuditStorage.cs — SQLite storage for audit logs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SQLite;
namespace MauiAuditTrail
{
// SQLite table model
[Table("audit_logs")]
public class AuditLogRecord
{
[PrimaryKey]
public string Id { get; set; }
public DateTime Timestamp { get; set; }
public string EventType { get; set; }
public string UserId { get; set; }
public string UserEmail { get; set; }
public string Action { get; set; }
public string EntityType { get; set; }
public string EntityId { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public string DeviceInfo { get; set; }
public string Platform { get; set; }
public string AppVersion { get; set; }
public string MetadataJson { get; set; }
public bool IsSynced { get; set; } = false;
}
public interface IAuditStorage
{
Task SaveAsync(AuditArchitecture.AuditLogEntry entry);
Task<List<AuditArchitecture.AuditLogEntry>> QueryAsync(
DateTime? from, DateTime? to, int limit);
Task<List<AuditLogRecord>> GetUnsyncedAsync(int batchSize = 50);
Task MarkSyncedAsync(IEnumerable<string> ids);
}
public class SqliteAuditStorage : IAuditStorage
{
private readonly SQLiteAsyncConnection _db;
public SqliteAuditStorage()
{
var dbPath = Path.Combine(
FileSystem.AppDataDirectory, "audit_trail.db");
_db = new SQLiteAsyncConnection(dbPath);
_db.CreateTableAsync<AuditLogRecord>().Wait();
}
public async Task SaveAsync(AuditArchitecture.AuditLogEntry entry)
{
var record = new AuditLogRecord
{
Id = entry.Id.ToString(),
Timestamp = entry.Timestamp,
EventType = entry.EventType.ToString(),
UserId = entry.UserId,
UserEmail = entry.UserEmail,
Action = entry.Action,
EntityType = entry.EntityType,
EntityId = entry.EntityId,
OldValue = entry.OldValue,
NewValue = entry.NewValue,
DeviceInfo = entry.DeviceInfo,
Platform = entry.Platform,
AppVersion = entry.AppVersion,
MetadataJson = System.Text.Json.JsonSerializer.Serialize(entry.Metadata),
IsSynced = false,
};
await _db.InsertAsync(record);
// Cleanup old synced logs (keep 30 days)
var cutoff = DateTime.UtcNow.AddDays(-30);
await _db.ExecuteAsync(
"DELETE FROM audit_logs WHERE IsSynced = 1 AND Timestamp < ?", cutoff);
}
public async Task<List<AuditLogRecord>> GetUnsyncedAsync(int batchSize = 50)
{
return await _db.Table<AuditLogRecord>()
.Where(r => !r.IsSynced)
.OrderBy(r => r.Timestamp)
.Take(batchSize)
.ToListAsync();
}
public async Task MarkSyncedAsync(IEnumerable<string> ids)
{
foreach (var id in ids)
{
await _db.ExecuteAsync(
"UPDATE audit_logs SET IsSynced = 1 WHERE Id = ?", id);
}
}
public async Task<List<AuditArchitecture.AuditLogEntry>> QueryAsync(
DateTime? from, DateTime? to, int limit)
{
var query = _db.Table<AuditLogRecord>();
if (from.HasValue)
query = query.Where(r => r.Timestamp >= from.Value);
if (to.HasValue)
query = query.Where(r => r.Timestamp <= to.Value);
var records = await query
.OrderByDescending(r => r.Timestamp)
.Take(limit)
.ToListAsync();
// Map to AuditLogEntry...
return new List<AuditArchitecture.AuditLogEntry>();
}
}
}
Background Sync Service
// SyncService.cs — Background sync for audit logs
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace MauiAuditTrail
{
public interface IAuditSyncService
{
Task SyncPendingAsync();
}
public class AuditSyncService : IAuditSyncService
{
private readonly IAuditStorage _storage;
private readonly HttpClient _httpClient;
private readonly string _apiUrl;
private bool _isSyncing = false;
public AuditSyncService(IAuditStorage storage, HttpClient httpClient,
string apiUrl = "https://api.example.com/audit")
{
_storage = storage;
_httpClient = httpClient;
_apiUrl = apiUrl;
}
public async Task SyncPendingAsync()
{
if (_isSyncing) return;
_isSyncing = true;
try
{
while (true)
{
var batch = await _storage.GetUnsyncedAsync(50);
if (batch.Count == 0) break;
var response = await _httpClient.PostAsJsonAsync(
$"{_apiUrl}/logs/batch", batch);
if (response.IsSuccessStatusCode)
{
var ids = batch.Select(r => r.Id);
await _storage.MarkSyncedAsync(ids);
}
else
{
break; // Retry later
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Sync failed: {ex.Message}");
}
finally
{
_isSyncing = false;
}
}
}
// Register in MauiProgram.cs
// builder.Services.AddSingleton<IAuditStorage, SqliteAuditStorage>();
// builder.Services.AddSingleton<IAuditSyncService, AuditSyncService>();
// builder.Services.AddSingleton<IAuditLogger, AuditLogger>();
}
Usage in MAUI Pages
// Usage examples in MAUI pages and ViewModels
// LoginPageViewModel.cs
public class LoginPageViewModel : BaseViewModel
{
private readonly IAuditLogger _audit;
private readonly IAuthService _auth;
public LoginPageViewModel(IAuditLogger audit, IAuthService auth)
{
_audit = audit;
_auth = auth;
}
public async Task LoginAsync(string email, string password)
{
try
{
var result = await _auth.LoginAsync(email, password);
if (result.Success)
{
await _audit.LogAsync(
AuditArchitecture.AuditEventType.UserLogin,
$"User logged in: {email}",
metadata: new() { ["method"] = "email_password" }
);
}
else
{
await _audit.LogAsync(
AuditArchitecture.AuditEventType.ErrorOccurred,
$"Login failed: {email}",
metadata: new() { ["reason"] = result.Error }
);
}
}
catch (Exception ex)
{
await _audit.LogAsync(
AuditArchitecture.AuditEventType.ErrorOccurred,
$"Login error: {ex.Message}"
);
}
}
}
// CustomerDetailViewModel.cs — Audit data changes
public class CustomerDetailViewModel : BaseViewModel
{
private readonly IAuditLogger _audit;
public async Task UpdateCustomerAsync(Customer old, Customer updated)
{
await _customerService.UpdateAsync(updated);
await _audit.LogAsync(
AuditArchitecture.AuditEventType.DataUpdate,
"Customer updated",
entityType: "Customer",
entityId: updated.Id.ToString(),
oldValue: old,
newValue: updated,
metadata: new()
{
["changed_fields"] = GetChangedFields(old, updated),
}
);
}
private string GetChangedFields(Customer old, Customer updated)
{
var changes = new List<string>();
if (old.Name != updated.Name) changes.Add("Name");
if (old.Email != updated.Email) changes.Add("Email");
if (old.Phone != updated.Phone) changes.Add("Phone");
return string.Join(", ", changes);
}
}
// Python backend for receiving audit logs
# audit_api.py
# from fastapi import FastAPI
# app = FastAPI()
# @app.post("/audit/logs/batch")
# async def receive_logs(logs: list):
# for log in logs:
# await db.insert("audit_logs", log)
# return {"received": len(logs)}
FAQ - คำถามที่พบบ่อย
Q: Audit trail จำเป็นสำหรับ mobile app ไหม?
A: จำเป็นสำหรับ: Enterprise apps ที่จัดการข้อมูลสำคัญ (healthcare, finance, HR) Apps ที่ต้อง comply กับ GDPR, HIPAA, SOC 2, PCI DSS Apps ที่มี multi-user + role-based access ไม่จำเป็นสำหรับ: Simple consumer apps, games, utility apps แต่แนะนำ: logging พื้นฐาน (errors, crashes) ยังจำเป็นเสมอ
Q: Offline-first audit logging ทำยังไง?
A: เก็บ logs ลง SQLite ก่อน (offline storage) เมื่อมี internet: sync ไป server เป็น batch ถ้า sync ล้มเหลว: retry ภายหลัง (exponential backoff) Cleanup: ลบ synced logs ที่เก่ากว่า 30 วันจาก device สำคัญ: อย่า block user actions เพราะ audit logging — ทำ async เสมอ
Q: .NET MAUI กับ Flutter อันไหนดีสำหรับ enterprise?
A: .NET MAUI: ดีถ้าทีมถนัด C#/.NET, integrate กับ Azure/Microsoft ecosystem ง่าย, enterprise libraries เยอะ Flutter: ดีถ้าต้องการ UI สวย + performance สูง, community ใหญ่กว่า, cross-platform ดีกว่า สำหรับ audit trail: ทั้งสองทำได้ — architecture คล้ายกัน (offline-first + sync) เลือกตาม: skill ของทีม + ecosystem ที่ใช้อยู่
Q: เก็บ audit logs นานแค่ไหน?
A: ขึ้นกับ compliance requirements: GDPR: ตาม purpose (ไม่เก็บนานเกินจำเป็น) PCI DSS: อย่างน้อย 1 ปี HIPAA: 6 ปี SOC 2: ตาม policy (แนะนำ 1 ปี+) บน device: เก็บ 30 วัน แล้ว sync ไป server บน server: เก็บตาม compliance requirement + archive เก่าไป cold storage
