Error Handling & Logging
Exception Handling and Structured Logging with Serilog
Error Handling Overview
The Shirinzad E-Commerce Platform implements comprehensive error handling and logging using ABP Framework's exception handling middleware and Serilog for structured logging.
Error Handling Goals
- Catch and handle all exceptions gracefully
- Provide user-friendly error messages
- Log detailed technical information for debugging
- Maintain correlation IDs for request tracing
- Prevent sensitive data leakage
- Enable quick issue resolution
Exception Handling Flow
Automatic exception handling by ABP Framework middleware
ABP Exception Handling Middleware
ABP Framework provides automatic exception handling that converts exceptions to appropriate HTTP responses.
Middleware Configuration
// In ShopWebModule.cs
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// Use ABP exception handling in production
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// ABP automatically adds exception handling middleware
app.UseAbpExceptionHandling();
// ... other middleware
}
Exception to HTTP Status Code Mapping
| Exception Type | HTTP Status | Usage |
|---|---|---|
BusinessException |
400 Bad Request | Business rule violations |
AbpValidationException |
400 Bad Request | Input validation failures |
EntityNotFoundException |
404 Not Found | Entity not found in database |
AbpAuthorizationException |
403 Forbidden | Insufficient permissions |
UnauthorizedAccessException |
401 Unauthorized | Not authenticated |
DbUpdateException |
500 Internal Server Error | Database operation failures |
Exception (unhandled) |
500 Internal Server Error | Unexpected errors |
Custom Exception Types
1. BusinessException
Used for business rule violations that should be communicated to users.
// In ProductAppService.cs
public async Task<ProductDto> CreateAsync(CreateProductDto input)
{
// Business rule validation
if (await _productRepository.AnyAsync(p => p.Slug == input.Slug))
{
throw new BusinessException(ShopDomainErrorCodes.ProductSlugAlreadyExists)
.WithData("Slug", input.Slug);
}
// Another example
if (input.Price <= 0)
{
throw new BusinessException("Product price must be greater than zero");
}
// Continue with creation...
}
// In domain entity
public class Product : FullAuditedAggregateRoot<Guid>
{
public void DecreaseStock(int quantity)
{
if (quantity <= 0)
{
throw new BusinessException("Quantity must be positive");
}
if (StockQuantity < quantity)
{
throw new BusinessException(ShopDomainErrorCodes.InsufficientStock)
.WithData("Available", StockQuantity)
.WithData("Requested", quantity);
}
StockQuantity -= quantity;
}
}
Error Response Example
{
"error": {
"code": "Shop:InsufficientStock",
"message": "Insufficient stock available",
"details": null,
"data": {
"Available": 5,
"Requested": 10
},
"validationErrors": null
}
}
2. UserFriendlyException
Similar to BusinessException, provides localized user-friendly messages.
// Example usage
public async Task ProcessPaymentAsync(Guid orderId)
{
var order = await _orderRepository.GetAsync(orderId);
if (order.Status == OrderStatus.Cancelled)
{
throw new UserFriendlyException(
"Cannot process payment for cancelled order",
"PAYMENT_CANCELLED_ORDER"
);
}
if (order.TotalAmount <= 0)
{
throw new UserFriendlyException(
L["InvalidOrderAmount", order.TotalAmount]
);
}
// Process payment...
}
3. Domain-Specific Error Codes
Define error codes as constants for consistency and localization.
// ShopDomainErrorCodes.cs
namespace Shirinzad.Shop
{
public static class ShopDomainErrorCodes
{
// Product errors
public const string ProductSlugAlreadyExists = "Shop:ProductSlugAlreadyExists";
public const string InsufficientStock = "Shop:InsufficientStock";
public const string ProductNotActive = "Shop:ProductNotActive";
// Order errors
public const string OrderNotFound = "Shop:OrderNotFound";
public const string OrderAlreadyCancelled = "Shop:OrderAlreadyCancelled";
public const string OrderCannotBeCancelled = "Shop:OrderCannotBeCancelled";
// Payment errors
public const string PaymentFailed = "Shop:PaymentFailed";
public const string PaymentAlreadyProcessed = "Shop:PaymentAlreadyProcessed";
public const string InvalidPaymentMethod = "Shop:InvalidPaymentMethod";
// Cart errors
public const string CartExpired = "Shop:CartExpired";
public const string CartItemNotFound = "Shop:CartItemNotFound";
public const string InvalidQuantity = "Shop:InvalidQuantity";
// Discount errors
public const string DiscountExpired = "Shop:DiscountExpired";
public const string DiscountNotApplicable = "Shop:DiscountNotApplicable";
public const string UsageLimitReached = "Shop:UsageLimitReached";
}
}
// Localization file (en.json)
{
"Shop:ProductSlugAlreadyExists": "A product with slug '{Slug}' already exists",
"Shop:InsufficientStock": "Insufficient stock. Available: {Available}, Requested: {Requested}",
"Shop:OrderCannotBeCancelled": "Order in '{Status}' status cannot be cancelled",
"Shop:PaymentFailed": "Payment processing failed: {Reason}"
}
Serilog Configuration
Serilog provides structured logging with rich context and multiple output sinks.
Serilog Setup
// Program.cs
public class Program
{
public static async Task<int> Main(string[] args)
{
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", "Shirinzad.Shop")
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"
)
.WriteTo.File(
path: "Logs/log-.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
retainedFileCountLimit: 30
)
.WriteTo.Seq(
serverUrl: "http://localhost:5341",
apiKey: Environment.GetEnvironmentVariable("SEQ_API_KEY")
)
.CreateLogger();
try
{
Log.Information("Starting Shirinzad.Shop web host");
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
await builder.AddApplicationAsync<ShopWebModule>();
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}
Configuration via appsettings.json
// appsettings.json
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.AspNetCore": "Warning",
"Volo.Abp": "Information"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "Logs/log-.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 30,
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341"
}
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
]
}
}
// appsettings.Production.json
{
"Serilog": {
"MinimumLevel": {
"Default": "Warning"
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "/var/log/shirinzad/log-.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 90
}
},
{
"Name": "ApplicationInsights",
"Args": {
"instrumentationKey": "your-app-insights-key",
"telemetryConverter": "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
}
}
]
}
}
Logging Levels
| Level | When to Use | Example |
|---|---|---|
| Verbose | Extremely detailed diagnostic information | Loop iterations, variable values |
| Debug | Detailed diagnostic information for debugging | Method entry/exit, parameter values |
| Information | General application flow tracking | User logged in, order created |
| Warning | Unexpected but recoverable situations | Deprecated API usage, business rule violations |
| Error | Errors and exceptions that affect operation | Database errors, external API failures |
| Fatal | Critical failures causing application shutdown | Cannot connect to database on startup |
Logging Level Usage Examples
public class ProductAppService : ShopAppService, IProductAppService
{
private readonly ILogger<ProductAppService> _logger;
public ProductAppService(ILogger<ProductAppService> logger)
{
_logger = logger;
}
public async Task<ProductDto> GetAsync(Guid id)
{
// Debug - detailed diagnostic
_logger.LogDebug("Getting product with ID: {ProductId}", id);
var product = await _productRepository.GetAsync(id);
// Information - normal flow
_logger.LogInformation(
"Retrieved product {ProductName} (ID: {ProductId})",
product.Name,
product.Id
);
return ObjectMapper.Map<Product, ProductDto>(product);
}
public async Task<ProductDto> CreateAsync(CreateProductDto input)
{
try
{
// Information - business operation
_logger.LogInformation(
"Creating new product: {ProductName}",
input.Name
);
// Check business rule
if (await _productRepository.AnyAsync(p => p.Slug == input.Slug))
{
// Warning - business rule violation
_logger.LogWarning(
"Attempt to create product with duplicate slug: {Slug}",
input.Slug
);
throw new BusinessException(ShopDomainErrorCodes.ProductSlugAlreadyExists)
.WithData("Slug", input.Slug);
}
var product = new Product(
GuidGenerator.Create(),
input.Name,
input.Slug,
input.Price
);
await _productRepository.InsertAsync(product);
// Information - successful operation
_logger.LogInformation(
"Product created successfully: {ProductName} (ID: {ProductId})",
product.Name,
product.Id
);
return ObjectMapper.Map<Product, ProductDto>(product);
}
catch (DbUpdateException ex)
{
// Error - database operation failed
_logger.LogError(
ex,
"Database error while creating product: {ProductName}",
input.Name
);
throw;
}
catch (Exception ex)
{
// Error - unexpected exception
_logger.LogError(
ex,
"Unexpected error while creating product: {ProductName}",
input.Name
);
throw;
}
}
}
Structured Logging with Properties
Structured logging captures rich context as properties, enabling powerful querying and analysis.
Best Practices
// ✅ GOOD: Structured logging with properties
_logger.LogInformation(
"Order {OrderId} placed by user {UserId} for amount {TotalAmount:C}",
order.Id,
order.UserId,
order.TotalAmount
);
// ❌ BAD: String interpolation (loses structure)
_logger.LogInformation($"Order {order.Id} placed by {order.UserId} for {order.TotalAmount}");
// ✅ GOOD: Rich context with multiple properties
_logger.LogInformation(
"Payment processed: Order={OrderId}, Method={PaymentMethod}, Amount={Amount:C}, Status={Status}",
payment.OrderId,
payment.PaymentMethod,
payment.Amount,
payment.Status
);
// ✅ GOOD: Using structured data objects
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId,
["UserId"] = userId,
["SessionId"] = sessionId
}))
{
_logger.LogInformation("Processing order items");
// All logs in this scope will include OrderId, UserId, SessionId
foreach (var item in items)
{
_logger.LogDebug("Processing item {ProductId}", item.ProductId);
}
}
Correlation IDs for Request Tracing
// ABP automatically adds correlation ID to all logs
// Access via ICorrelationIdProvider
public class OrderAppService : ShopAppService
{
private readonly ICorrelationIdProvider _correlationIdProvider;
private readonly ILogger<OrderAppService> _logger;
public async Task<OrderDto> CreateAsync(CreateOrderDto input)
{
var correlationId = _correlationIdProvider.Get();
_logger.LogInformation(
"Creating order with correlation ID: {CorrelationId}",
correlationId
);
// All logs for this request will have the same correlation ID
// Enables tracking the full request flow across services
}
}
// Example log output with correlation ID:
// [12:34:56 INF] Creating order with correlation ID: 8a7b6c5d-4e3f-2a1b-9c8d-7e6f5a4b3c2d
Log Sinks and Destinations
1. Console Sink
Output logs to console for development and debugging.
- Use Case: Development, Docker containers
- Performance: Fast
- Searchable: No (unless piped)
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
)
2. File Sink
Write logs to rolling log files.
- Use Case: All environments
- Performance: Fast
- Searchable: Manual (grep, etc.)
.WriteTo.File(
path: "Logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
fileSizeLimitBytes: 10_485_760 // 10MB
)
3. Seq Sink
Centralized structured logging and search.
- Use Case: Development, staging
- Performance: Good
- Searchable: Excellent (SQL-like queries)
.WriteTo.Seq(
serverUrl: "http://localhost:5341",
apiKey: "your-api-key"
)
Query Example:
UserId = '123' AND Level = 'Error'
4. Application Insights Sink
Azure Application Insights integration for production monitoring.
- Use Case: Production (Azure)
- Performance: Good (async)
- Searchable: Excellent (KQL queries)
.WriteTo.ApplicationInsights(
instrumentationKey: "your-key",
TelemetryConverter.Traces
)
Comparison Table
| Sink | Setup | Performance | Search | Alerts | Cost |
|---|---|---|---|---|---|
| Console | Easy | Excellent | None | No | Free |
| File | Easy | Excellent | Manual | No | Free |
| Seq | Medium | Good | Excellent | Yes | Free/Paid |
| App Insights | Medium | Good | Excellent | Yes | Paid |
| Sentry | Medium | Good | Good | Yes | Free/Paid |
Error Response Format
ABP Framework standardizes error responses across all API endpoints.
Standard Error Response
{
"error": {
"code": "string", // Error code (e.g., "Shop:InsufficientStock")
"message": "string", // User-friendly error message
"details": "string", // Additional details (optional)
"data": { // Additional context data (optional)
"key1": "value1",
"key2": "value2"
},
"validationErrors": [ // Validation errors (if applicable)
{
"message": "string",
"members": ["propertyName"]
}
]
}
}
Example Responses
Business Rule Violation
// Request: POST /api/app/cart-item
// Body: { "productId": "...", "quantity": 10 }
// Response: 400 Bad Request
{
"error": {
"code": "Shop:InsufficientStock",
"message": "Insufficient stock. Available: 5, Requested: 10",
"details": "The product does not have enough stock to fulfill this request",
"data": {
"ProductId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"Available": 5,
"Requested": 10
},
"validationErrors": null
}
}
Validation Error
// Request: POST /api/app/product
// Body: { "name": "", "price": -10 }
// Response: 400 Bad Request
{
"error": {
"code": "Volo.Abp.Validation.AbpValidationException",
"message": "Validation failed",
"details": "One or more validation errors occurred",
"data": {},
"validationErrors": [
{
"message": "The Name field is required.",
"members": ["Name"]
},
{
"message": "The field Price must be between 0 and 1000000.",
"members": ["Price"]
}
]
}
}
Authorization Error
// Request: DELETE /api/app/product/{id}
// Response: 403 Forbidden
{
"error": {
"code": "Volo.Abp.Authorization.AbpAuthorizationException",
"message": "Authorization failed",
"details": "You do not have permission to delete products",
"data": {},
"validationErrors": null
}
}
Not Found Error
// Request: GET /api/app/product/{id}
// Response: 404 Not Found
{
"error": {
"code": "Volo.Abp.Domain.Entities.EntityNotFoundException",
"message": "Entity not found",
"details": "There is no such entity with id = 3fa85f64-5717-4562-b3fc-2c963f66afa6 of Product",
"data": {
"EntityType": "Shirinzad.Shop.Products.Product",
"Id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
},
"validationErrors": null
}
}
Performance Logging
Track slow operations and identify performance bottlenecks.
Slow Query Logging
// Configure EF Core to log slow queries
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<ShopDbContext>(options =>
{
options.AddDefaultRepositories(includeAllEntities: true);
});
// Log slow queries (> 1000ms)
Configure<AbpDbContextOptions>(options =>
{
options.Configure<ShopDbContext>(ctx =>
{
ctx.DbContextOptions.LogTo(
Console.WriteLine,
new[] { DbLoggerCategory.Database.Command.Name },
LogLevel.Warning,
DbContextLoggerOptions.DefaultWithLocalTime
);
});
});
}
// In appsettings.json
{
"Logging": {
"LogLevel": {
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
Operation Timing
using System.Diagnostics;
public async Task<PagedResultDto<ProductDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var stopwatch = Stopwatch.StartNew();
try
{
var result = await base.GetListAsync(input);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 1000)
{
_logger.LogWarning(
"Slow product list query: {ElapsedMs}ms, PageSize={PageSize}, Sorting={Sorting}",
stopwatch.ElapsedMilliseconds,
input.MaxResultCount,
input.Sorting
);
}
else
{
_logger.LogDebug(
"Product list query completed in {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds
);
}
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"Product list query failed after {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds
);
throw;
}
}
Audit Logging
ABP Framework automatically tracks all user actions through the audit logging system.
Automatic Audit Logging
// Audit logging is automatic for all application services
// Tracks: User, Action, Parameters, Execution time, Result
public class ProductAppService : ShopAppService
{
// This action is automatically audited
public async Task<ProductDto> CreateAsync(CreateProductDto input)
{
// ABP logs:
// - UserId
// - Action: CreateAsync
// - Parameters: { name: "...", price: 100 }
// - Execution time
// - Success/Failure
// - Exception (if any)
}
// Disable audit logging for specific action
[DisableAuditing]
public async Task<List<string>> GetCategoriesAsync()
{
// This action won't be audited
}
}
// Query audit logs
public class AuditLogAppService : ShopAppService
{
private readonly IRepository<AuditLog, Guid> _auditLogRepository;
public async Task<List<AuditLogDto>> GetUserActionsAsync(Guid userId, DateTime from, DateTime to)
{
var logs = await _auditLogRepository.GetListAsync(
x => x.UserId == userId &&
x.ExecutionTime >= from &&
x.ExecutionTime <= to
);
return ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(logs);
}
}
Audit Log Data
- User Information: UserId, Username, IP Address, Browser
- Action Details: Service name, Method name, Parameters
- Execution: Start time, Duration, HTTP status code
- Result: Success/Failure, Exception details
- Context: Correlation ID, Tenant ID
Error Monitoring Tools
Sentry Integration
Real-time error tracking and monitoring.
// Install: Sentry.AspNetCore
// In Program.cs
builder.WebHost.UseSentry(options =>
{
options.Dsn = "your-sentry-dsn";
options.Environment = builder.Environment.EnvironmentName;
options.TracesSampleRate = 1.0;
options.AttachStacktrace = true;
options.SendDefaultPii = false;
});
// Sentry automatically captures:
// - Unhandled exceptions
// - HTTP errors
// - Performance data
// - User context
Application Insights
Azure's application performance management service.
// Install: Microsoft.ApplicationInsights.AspNetCore
// In Program.cs
builder.Services.AddApplicationInsightsTelemetry(
builder.Configuration["ApplicationInsights:InstrumentationKey"]
);
// Features:
// - Exception tracking
// - Request tracking
// - Dependency tracking
// - Custom metrics
// - Live metrics stream
Best Practices
Exception Handling
- Use specific exception types
- Provide user-friendly error messages
- Include relevant context data
- Don't catch exceptions you can't handle
- Log before re-throwing
- Avoid exposing sensitive data in errors
Logging
- Use structured logging with properties
- Choose appropriate log levels
- Include correlation IDs
- Log at service boundaries
- Avoid logging sensitive data (passwords, tokens)
- Use log scopes for related operations
Performance
- Use async logging sinks
- Set appropriate minimum log levels
- Implement log sampling for high-volume operations
- Monitor log volume and adjust as needed
- Use buffering for file writes
- Archive old logs regularly
Security
- Never log passwords or API keys
- Sanitize user input before logging
- Protect log files with proper permissions
- Encrypt logs containing sensitive data
- Implement log retention policies
- Monitor for log injection attacks
Log Retention Policies
| Environment | Log Type | Retention Period | Storage |
|---|---|---|---|
| Development | All Logs | 7 days | Local files |
| Staging | Application Logs | 30 days | Azure Blob / Seq |
| Staging | Audit Logs | 90 days | Database |
| Production | Application Logs | 90 days | Azure Blob / App Insights |
| Production | Audit Logs | 365 days | Database (archived) |
| Production | Error Logs | 180 days | Sentry / App Insights |