A powerful Aspect-Oriented Programming (AOP) library for .NET that provides transparent logging of method calls using dynamic proxies with distributed tracing. Supports async methods, parent/child span correlation, OpenTelemetry compatibility, custom formatting, configurable log levels, and HTTP middleware logging.
$ dotnet add package Mariodbx.AspectLoggingA powerful Aspect-Oriented Programming (AOP) library for .NET that provides transparent, non-invasive logging and distributed tracing of method calls using dynamic proxies. AspectLogging enables comprehensive observability without modifying your business logic.
AspectLogging implements Aspect-Oriented Programming principles to separate cross-cutting concerns (logging, tracing) from business logic. It uses .NET's DispatchProxy to create dynamic proxies that intercept method calls, providing comprehensive logging and distributed tracing without requiring changes to your existing codebase.
ILogger provider (Console, Serilog, NLog, etc.)| Feature | Description |
|---|---|
| Dynamic Proxy Interception | Automatically wraps service interfaces with logging |
| Async/Await Support | Full support for Task and Task<T> methods |
| Distributed Tracing | Parent/child span correlation across service calls |
| Timestamps | Configurable timestamps in log output |
| HTTP Middleware | Request/response logging with automatic span creation |
| Depth Visualization | Visual indentation showing call hierarchy |
| OpenTelemetry Compatible | Uses System.Diagnostics.Activity |
| Configurable Presets | Development, Balanced, Production, Minimal |
dotnet add package Mariodbx.AspectLogging
Install-Package Mariodbx.AspectLogging
<PackageReference Include="Mariodbx.AspectLogging" Version="3.0.1" />
using AspectLogging;
var builder = WebApplication.CreateBuilder(args);
// Configure your logging provider (any ILogger-compatible provider works)
builder.Logging.AddConsole();
// Register your services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IOrderService, OrderService>();
// Add AspectLogging
builder.Services.AddAspectLogging(options =>
{
options.Enabled = true;
options.IncludeNamespacePrefixes = new[] { "MyApp.Services" };
// Logging options
options.LogEntry = true;
options.LogExit = true;
options.LogExceptions = true;
// Tracing options
options.Tracing.Enabled = true;
options.Tracing.ServiceName = "MyApp";
options.Tracing.IncludeTimestamp = true;
options.Tracing.IncludeSpanDepth = true;
});
var app = builder.Build();
// Enable HTTP request logging
app.UseLoggingMiddleware();
app.Run();
// Simple preset-based configuration
builder.Services.AddAspectLogging(LoggingPreset.Development);
// Preset with namespace filtering
builder.Services.AddAspectLogging(
LoggingPreset.Development,
"MyApp.Services",
"MyApp.Repositories");
AspectLogging produces clear, ASCII-compatible log output:
14:32:15.123 [e0d6063f1e26:386805c03872] --> Enter IOrderService.PlaceOrderAsync (parent: N/A, depth: 0) with args []
14:32:15.125 [e0d6063f1e26:32319e266a10] --> Enter IPricingService.CalculatePrice (parent: 386805c03872, depth: 1) with args [productId: int]
14:32:15.130 [e0d6063f1e26:b4974870b49a] --> Enter IProductRepository.GetByIdAsync (parent: 32319e266a10, depth: 2) with args []
14:32:15.145 [e0d6063f1e26:b4974870b49a] <-- Exit IProductRepository.GetByIdAsync (parent: 32319e266a10, depth: 2, ms=15) with return Product
14:32:15.147 [e0d6063f1e26:32319e266a10] <-- Exit IPricingService.CalculatePrice (parent: 386805c03872, depth: 1, ms=22) with return Decimal
14:32:15.150 [e0d6063f1e26:386805c03872] <-- Exit IOrderService.PlaceOrderAsync (parent: N/A, depth: 0, ms=27) with return Order
| Component | Example | Description |
|---|---|---|
| Timestamp | 14:32:15.123 | Time of the log entry (HH:mm:ss.fff) |
| TraceId | e0d6063f1e26 | Unique ID for the entire request |
| SpanId | 386805c03872 | Unique ID for this specific operation |
| Arrow | --> / <-- | Entry (-->) or Exit (<--) |
| Method | IOrderService.PlaceOrderAsync | Full method name |
| Parent | parent: 386805c03872 | Parent span for correlation |
| Depth | depth: 1 | Call hierarchy level (with indentation) |
| Duration | ms=22 | Execution time in milliseconds |
| Return | with return Order | Return type or value |
14:32:15.200 [e0d6063f1e26:386805c03872] [X] Exception in IOrderService.PlaceOrderAsync (parent: N/A, depth: 0, ms=50)
System.InvalidOperationException: Order validation failed
at MyApp.Services.OrderService.PlaceOrderAsync()
builder.Services.AddAspectLogging(options =>
{
// === Core Options ===
options.Enabled = true; // Enable/disable globally
options.IncludeNamespacePrefixes = new[] { "MyApp" }; // Namespaces to intercept
options.ExcludedServiceTypeFullNames = new[] { "MyApp.IHealthCheck" };
options.IgnoredMethods = new[] { "ToString", "GetHashCode" };
// === Logging Behavior ===
options.LogEntry = true; // Log method entry
options.LogExit = true; // Log method exit
options.LogExceptions = true; // Log exceptions
options.LogDetailedArgs = false; // Log argument values
options.LogDetailedReturns = false; // Log return values
options.IncludeParameterNames = true; // Include param names
// === Log Levels ===
options.EntryLogLevel = LogLevel.Information;
options.ExitLogLevel = LogLevel.Information;
options.ExceptionLogLevel = LogLevel.Error;
// === Performance ===
options.MinimumDurationMs = 0; // Min duration to log
options.SlowExecutionThresholdMs = 1000; // Warn level threshold
options.MaxReturnValueLength = 500; // Truncate long values
// === Tracing Options ===
options.Tracing.Enabled = true; // Enable distributed tracing
options.Tracing.ServiceName = "MyApp"; // Service identifier
options.Tracing.IncludeTimestamp = true; // Add timestamp to logs
options.Tracing.IncludeSpanDepth = true; // Show depth with indent
options.Tracing.MaxSpanDepth = 10; // Max trace depth
options.Tracing.CreateMethodSpans = true; // Create spans for methods
options.Tracing.CreateHttpSpans = true; // Create spans for HTTP
options.Tracing.PushToLogContext = true; // Add to ILogger scope
// === Middleware Options ===
options.Middleware.Enabled = true;
options.Middleware.LogRequestStart = true;
options.Middleware.LogRequestEnd = true;
options.Middleware.LogHeaders = false;
options.Middleware.LogRequestBody = false;
options.Middleware.LogResponseBody = false;
options.Middleware.ExcludedPaths = new[] { "/health", "/swagger*", "*.css", "*.js" };
});
{
"AspectLogging": {
"Enabled": true,
"IncludeNamespacePrefixes": ["MyApp.Services", "MyApp.Repositories"],
"LogEntry": true,
"LogExit": true,
"LogExceptions": true,
"EntryLogLevel": "Information",
"ExitLogLevel": "Information",
"ExceptionLogLevel": "Error",
"Tracing": {
"Enabled": true,
"ServiceName": "MyApp",
"IncludeTimestamp": true,
"IncludeSpanDepth": true,
"MaxSpanDepth": 10,
"PushToLogContext": true
},
"Middleware": {
"Enabled": true,
"ExcludedPaths": ["/health", "/swagger*"]
}
}
}
builder.Services.AddAspectLogging(builder.Configuration);
| Preset | Entry | Exit | Args | Returns | Exceptions | Tracing | Min Duration |
|---|---|---|---|---|---|---|---|
| Development | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 0ms |
| Balanced | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | 0ms |
| Production | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | 100ms |
| Minimal | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | 1000ms |
AspectLogging tracks the complete call hierarchy:
HTTP POST /api/orders [Depth 0]
└── IOrderService.PlaceOrderAsync [Depth 1]
├── IOrderValidationService.ValidateAsync [Depth 2]
│ ├── IProductRepository.GetByIdAsync [Depth 3]
│ └── IStockService.CheckAvailabilityAsync [Depth 3]
├── IPricingService.CalculateTotalAsync [Depth 2]
│ ├── IDiscountService.ApplyDiscountsAsync [Depth 3]
│ └── ITaxService.CalculateTaxAsync [Depth 3]
└── IPaymentService.ProcessPaymentAsync [Depth 2]
| Property | Description |
|---|---|
TraceId | Unique ID shared across all spans in a request |
SpanId | Unique ID for this specific operation |
ParentSpanId | ID of the parent span (null for root) |
Depth | Nesting level (0 = HTTP request) |
OperationName | Method or operation name |
DurationMs | Execution time in milliseconds |
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("AspectLogging")
.AddJaegerExporter()
.AddZipkinExporter());
app.UseLoggingMiddleware();
14:32:15.100 [e0d6063f1e26:root] HTTP POST /api/orders - Request started
14:32:15.150 [e0d6063f1e26:root] HTTP POST /api/orders completed: 200 in 50ms
options.Middleware.Enabled = true;
options.Middleware.LogRequestStart = true;
options.Middleware.LogRequestEnd = true;
options.Middleware.LogHeaders = false; // Log request headers
options.Middleware.LogRequestBody = false; // Log request body (careful with sensitive data)
options.Middleware.LogResponseBody = false; // Log response body
options.Middleware.IncludeTraceId = true;
options.Middleware.ExcludedPaths = new[]
{
"/health",
"/swagger*",
"/favicon.ico",
"*.css",
"*.js"
};
public interface IUserService
{
Task<User> GetUserAsync(int id);
[NoLog] // This method won't be logged
Task<string> GetPasswordHashAsync(int userId);
}
// Exclude entire interface
[NoLog]
public interface ISensitiveDataService
{
Task<string> GetSsnAsync(int userId);
}
// In your service method
TracingContext.SetTag("customer.id", customerId);
TracingContext.SetTag("order.total", orderTotal);
using (var scope = TracingContext.StartMethodSpan("CustomOperation"))
{
// Your code here
// Span automatically timed and closed
}
var traceId = TracingContext.CurrentTraceId;
var spanId = TracingContext.CurrentSpanId;
var depth = TracingContext.CurrentDepth;
To see database commands between your service logs:
services.AddDbContext<YourDbContext>(options =>
{
options.UseSqlServer(connectionString)
.UseLoggerFactory(loggerFactory)
.EnableSensitiveDataLogging(); // Shows parameter values
});
This produces interleaved output:
14:32:15.130 [e0d6063f1e26:b497] --> Enter IProductRepository.GetByIdAsync (parent: 3231, depth: 2)
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (5ms) SELECT * FROM Products WHERE Id = @p0
14:32:15.145 [e0d6063f1e26:b497] <-- Exit IProductRepository.GetByIdAsync (parent: 3231, depth: 2, ms=15)
A comprehensive sample is included: samples/AspectLogging.Sample.WebApi
cd samples/AspectLogging.Sample.WebApi
dotnet run
Navigate to http://localhost:5000 for:
// Preset configuration
IServiceCollection AddAspectLogging(LoggingPreset preset)
IServiceCollection AddAspectLogging(LoggingPreset preset, params string[] namespaces)
// Lambda configuration
IServiceCollection AddAspectLogging(Action<LoggingProxyOptions> configure)
// Configuration binding
IServiceCollection AddAspectLogging(IConfiguration configuration)
static SpanInfo? Current { get; }
static string? CurrentTraceId { get; }
static string? CurrentSpanId { get; }
static int CurrentDepth { get; }
static ActivitySource ActivitySource { get; }
static TracingScope StartHttpSpan(HttpContext context, string? serviceName)
static TracingScope StartMethodSpan(string operationName, SpanKind kind)
static void SetTag(string key, object? value)
[NoLog] // Exclude method or interface from logging
Contributions are welcome! Please read CONTRIBUTING.md for guidelines.
# Clone the repository
git clone https://github.com/mariodbx/aspect-logging.git
cd aspect-logging
# Build
dotnet build
# Run tests (208 tests)
dotnet test
# Run sample
cd samples/AspectLogging.Sample.WebApi
dotnet run
This project is licensed under the MIT License - see the LICENSE file for details.
Version: 3.0.1
Author: Mario De Benedictis
Repository: https://github.com/mariodbx/aspect-logging