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.

Key Principle: Fail gracefully, log comprehensively, and provide user-friendly error messages while maintaining detailed technical logs for debugging.

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

sequenceDiagram participant C as Client participant API as API Controller participant M as Exception Middleware participant S as Application Service participant L as Logger participant DB as Database C->>API: HTTP Request API->>S: Call Service Method alt Business Exception S->>S: Throw BusinessException S->>M: Exception bubbles up M->>L: Log Warning with details M->>C: 400 Bad Request (user-friendly) else Validation Exception S->>S: Throw AbpValidationException S->>M: Exception bubbles up M->>L: Log Information M->>C: 400 Bad Request (validation errors) else Authorization Exception S->>S: Throw AbpAuthorizationException S->>M: Exception bubbles up M->>L: Log Warning M->>C: 403 Forbidden else Database Exception S->>DB: Database operation DB-->>S: DbUpdateException S->>M: Exception bubbles up M->>L: Log Error with stack trace M->>C: 500 Internal Server Error else Unhandled Exception S->>S: Unexpected exception S->>M: Exception bubbles up M->>L: Log Fatal with full details M->>C: 500 Internal Server Error end

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
Compliance Note: Adjust retention periods based on legal and compliance requirements for your region and industry.

Related Documentation