A performance monitoring package for Entity Framework Core that tracks slow queries, execution times, and provides detailed analytics for query optimization.
$ dotnet add package EFCore.QueryAnalyzerA zero-impact, production-ready NuGet package for monitoring Entity Framework Core query performance with automatic slow query detection, execution plan analysis, and flexible reporting capabilities. Features built-in asynchronous queue processing that eliminates performance overhead on your main application.
ConcurrentDictionaryInstall the package via NuGet Package Manager:
dotnet add package EFCore.QueryAnalyzerOr via Package Manager Console:
Install-Package EFCore.QueryAnalyzerusing EFCore.QueryAnalyzer.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Add the query analyzer with configuration
builder.Services.AddEFCoreQueryAnalyzer(builder.Configuration);
// Configure your DbContext with the analyzer
builder.Services.AddDbContext<MyDbContext>((serviceProvider, options) =>
{
options.UseSqlServer(connectionString)
.AddQueryAnalyzer(serviceProvider);
});
var app = builder.Build();✨ Performance Boost: The analyzer now processes all slow queries in the background, ensuring zero impact on your application performance! Your HTTP endpoints will maintain full speed while comprehensive query monitoring happens asynchronously.
{
"QueryAnalyzer": {
"IsEnabled": true,
"ThresholdMilliseconds": 1000,
"ApiEndpoint": "https://your-monitoring-api.com/slow-queries",
"ApiKey": "your-secret-api-key",
"ProjectId": "my-application",
"CaptureStackTrace": true,
"CaptureExecutionPlan": true,
}
}That's it! The analyzer will now monitor your queries and report slow ones automatically in the background with zero performance impact on your application.
| Option | Type | Default | Description |
|---|---|---|---|
ThresholdMilliseconds | double | 1000 | Threshold in milliseconds for slow query detection |
IsEnabled | bool | false | Whether the analyzer is enabled |
CaptureStackTrace | bool | true | Capture stack traces for slow queries |
CaptureExecutionPlan | bool | false | Capture database execution plans |
MaxStackTraceLines | int | 20 | Maximum lines in captured stack traces |
MaxQueryLength | int | 10000 | Maximum query text length to store |
ApiEndpoint | string? | null | HTTP endpoint for reporting slow queries |
ApiKey | string? | null | API key for authentication |
ProjectId | string? | null | Project identifier sent as X-PROJECT-ID header |
ApiTimeoutMs | int | 5000 | API request timeout in milliseconds |
DatabaseProvider | DatabaseProvider | Auto | Database provider for execution plans |
ExecutionPlanTimeoutSeconds | int | 30 | Timeout for execution plan capture |
ConnectionString | string? | null | Connection string for execution plan capture |
Note: All query analysis and reporting happens asynchronously in background threads, ensuring zero impact on your application's response times.
builder.Services.AddEFCoreQueryAnalyzerWithHttp(
options =>
{
options.IsEnabled = true
options.ThresholdMilliseconds = 500;
options.ApiEndpoint = "https://monitoring.company.com/api/queries";
options.ApiKey = builder.Configuration["MonitoringApiKey"];
},
httpClient =>
{
httpClient.Timeout = TimeSpan.FromSeconds(10);
httpClient.DefaultRequestHeaders.Add("X-App-Version", "1.0.0");
});Configure project identification, custom headers, and enhanced HTTP settings:
builder.Services.AddEFCoreQueryAnalyzerWithHttp(
options =>
{
// Basic configuration
options.IsEnabled = true,
options.ApiEndpoint = "https://monitoring.company.com/api/slow-queries";
options.ApiKey = builder.Configuration["MonitoringApiKey"];
// Project identification - sent as X-PROJECT-ID header
options.ProjectId = "my-microservice-api";
// Performance tuning
options.ApiTimeoutMs = 10000; // 10 second timeout
options.ThresholdMilliseconds = 500;
// Production settings
options.CaptureExecutionPlan = true;
},
httpClient =>
{
// HTTP client configuration
httpClient.Timeout = TimeSpan.FromSeconds(15);
// Custom headers
httpClient.DefaultRequestHeaders.Add("X-Service-Name", "UserService");
httpClient.DefaultRequestHeaders.Add("X-Environment", builder.Environment.EnvironmentName);
httpClient.DefaultRequestHeaders.Add("X-Version", Assembly.GetExecutingAssembly().GetName().Version?.ToString());
// Optional: Configure proxy, certificates, etc.
// httpClient.DefaultRequestHeaders.Add("X-Correlation-ID", correlationId);
});The analyzer automatically sets:
EFCore.QueryAnalyzer/1.0.0application/jsonBearer {ApiKey} (when ApiKey is provided){ProjectId} (when ProjectId is provided)builder.Services.AddEFCoreQueryAnalyzerWithInMemory(options =>
{
options.ThresholdMilliseconds = 100;
options.CaptureStackTrace = true;
options.CaptureExecutionPlan = true;
});
// In tests, access the reports
var reportingService = serviceProvider.GetService<IQueryReportingService>()
as InMemoryQueryReportingService;
var reports = reportingService?.GetReports();Create and register custom reporting services with flexible service lifetime options:
public class DatabaseReportingService : IQueryReportingService
{
private readonly IDbContextFactory<LoggingDbContext> _dbContextFactory;
private readonly ILogger<DatabaseReportingService> _logger;
public DatabaseReportingService(
IDbContextFactory<LoggingDbContext> dbContextFactory,
ILogger<DatabaseReportingService> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task ReportSlowQueryAsync(QueryTrackingContext context,
CancellationToken cancellationToken = default)
{
try
{
using var dbContext = _dbContextFactory.CreateDbContext();
var logEntry = new SlowQueryLog
{
QueryId = context.QueryId,
Query = context.CommandText,
ExecutionTimeMs = context.ExecutionTime.TotalMilliseconds,
Timestamp = context.StartTime,
ContextType = context.ContextType
};
dbContext.SlowQueryLogs.Add(logEntry);
await dbContext.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Logged slow query {QueryId} to database", context.QueryId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to log slow query {QueryId} to database", context.QueryId);
}
}
}
// Register custom service with default (Transient) lifetime
builder.Services.AddEFCoreQueryAnalyzerWithCustomReporting<DatabaseReportingService>(
options =>
{
options.ThresholdMilliseconds = 750;
options.CaptureStackTrace = true;
options.CaptureExecutionPlan = true;
});
// Register with specific service lifetime (Singleton for performance)
builder.Services.AddEFCoreQueryAnalyzerWithCustomReporting<DatabaseReportingService>(
options => options.ThresholdMilliseconds = 500,
ServiceLifetime.Singleton);
// Register with Scoped lifetime for database operations
builder.Services.AddEFCoreQueryAnalyzerWithCustomReporting<DatabaseReportingService>(
options => options.ThresholdMilliseconds = 1000,
ServiceLifetime.Scoped);var options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlServer(connectionString)
.AddQueryAnalyzer(analyzerOptions =>
{
analyzerOptions.ThresholdMilliseconds = 500;
analyzerOptions.CaptureExecutionPlan = true;
})
.Options;
using var context = new MyDbContext(options);EFCore.QueryAnalyzer v1.1+ features a revolutionary built-in queue processing system that eliminates performance overhead:
graph LR
A[Query Execution] --> B[Interceptor Detection]
B --> C["Queue Item (μs)"]
C --> D[Background Service]
D --> E[Execution Plan Capture]
D --> F[HTTP Reporting]
D --> G[Analysis Complete]Existing users get automatic benefits - no code changes required! Simply update your package version and enjoy zero-impact monitoring.
When a slow query is detected, a comprehensive JSON report is generated:
{
"queryId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"rawQuery": "SELECT u.Id, u.Email FROM Users u WHERE u.Email LIKE @p0",
"parameters": {
"@p0": "%john%"
},
"executionTimeMs": 1250.5,
"stackTrace": [
"at MyApp.Controllers.UserController.GetUsers() in UserController.cs:line 45",
"at MyApp.Services.UserService.FindByEmail() in UserService.cs:line 23"
],
"timestamp": "2024-01-15T10:30:00Z",
"contextType": "MyApp.Data.ApplicationDbContext",
"environment": "Development",
"applicationName": "MyApplication",
"version": "1.0.0",
"executionPlan": {
"databaseProvider": "SqlServer",
"planFormat": {
"contentType": "application/xml",
"fileExtension": ".sqlplan",
"description": "SQL Server XML Execution Plan"
},
"content": "<ShowPlanXML xmlns=\"...\">...</ShowPlanXML>"
}
}The analyzer can capture and analyze database execution plans for deep performance insights:
builder.Services.AddEFCoreQueryAnalyzer(options =>
{
options.DatabaseProvider = DatabaseProvider.SqlServer;
options.CaptureExecutionPlan = true;
options.ExecutionPlanTimeoutSeconds = 30;
});// Auto-detect database provider
options.DatabaseProvider = DatabaseProvider.Auto;
// Or specify explicitly
options.DatabaseProvider = DatabaseProvider.PostgreSQL; // MySQL, Oracle, SQLiteEFCore.QueryAnalyzer now includes advanced parameterized query handling for more accurate execution plans:
SQL Server's SET SHOWPLAN_XML ON doesn't work well with parameterized queries because the query optimizer needs actual values to make cost estimations and generate accurate execution plans.
The analyzer automatically converts parameterized queries to use literal values when capturing execution plans:
// Original parameterized query
SELECT * FROM Users WHERE Age > @p0 AND City = @p1
// Automatically converted to literal values for execution plan
SELECT * FROM Users WHERE Age > 25 AND City = 'New York'The literal value conversion handles all common data types safely:
// String parameters: Properly escaped and quoted
@name = 'John O''Connor' → 'John O''Connor'
// Numeric parameters: Direct conversion
@age = 25 → 25
@price = 99.99 → 99.99
// Boolean parameters: Converted to bits
@isActive = true → 1
// Date parameters: SQL Server format
@created = DateTime.Now → '2024-01-15 10:30:00.123'
// GUID parameters: String format
@userId = Guid.NewGuid() → 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
// NULL parameters: SQL NULL
@optional = null → NULLThis enhancement is automatically enabled when CaptureExecutionPlan = true and works with both existing and new connection scenarios.
The analyzer automatically detects environments and applies appropriate settings:
builder.Services.AddEFCoreQueryAnalyzer(options =>
{
if (builder.Environment.IsDevelopment())
{
options.ThresholdMilliseconds = 100;
options.CaptureStackTrace = true;
options.CaptureExecutionPlan = true;
}
else
{
options.ThresholdMilliseconds = 2000;
options.CaptureStackTrace = false;
}
});EFCore.QueryAnalyzer features a sophisticated zero-impact architecture:
ConcurrentDictionary<Guid, QueryTrackingContext> for active query trackingSend reports to multiple destinations simultaneously:
builder.Services.AddEFCoreQueryAnalyzer(options => { });
builder.Services.AddHttpClient<HttpQueryReportingService>();
builder.Services.AddTransient<IQueryReportingService, HttpQueryReportingService>();
builder.Services.AddTransient<IQueryReportingService, InMemoryQueryReportingService>();public class SmartReportingService : IQueryReportingService
{
public async Task ReportSlowQueryAsync(QueryTrackingContext context,
CancellationToken cancellationToken)
{
// Only report critical context queries
if (context.ContextType.Contains("CriticalDbContext"))
{
await _urgentReporter.ReportAsync(context);
}
// Different handling for different query types
if (context.CommandText.Contains("SELECT") &&
context.ExecutionTime.TotalSeconds > 5)
{
await _selectQueryReporter.ReportAsync(context);
}
}
}public class PerformanceAnalysisService : IQueryReportingService
{
public async Task ReportSlowQueryAsync(QueryTrackingContext context,
CancellationToken cancellationToken)
{
// Analyze execution plan if available
if (context.ExecutionPlan?.Content != null)
{
var analysis = await AnalyzeExecutionPlan(context.ExecutionPlan);
await RecommendOptimizations(context, analysis);
}
// Pattern-based analysis
if (context.CommandText.Contains("SELECT *"))
{
await AlertSelectStarUsage(context);
}
}
}With built-in queue processing, you can now confidently enable the analyzer in production:
// Production-safe configuration with zero performance impact
builder.Services.AddEFCoreQueryAnalyzer(options =>
{
options.ThresholdMilliseconds = 1000;
options.CaptureExecutionPlan = true; // Even heavy operations are safe
options.ApiEndpoint = "https://monitoring.company.com/api/queries";
});// Configure limits for memory optimization
options.MaxQueryLength = 5000; // Limit memory usage
options.MaxStackTraceLines = 5; // Reduce overhead
options.ExecutionPlanTimeoutSeconds = 15; // Prevent hanging in backgroundThe built-in queue processing means you can:
EFCore.QueryAnalyzer has been optimized for production environments with minimal logging overhead:
The package now operates with minimal log noise:
For production environments:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"EFCore.QueryAnalyzer": "Warning",
"EFCore.QueryAnalyzer.Services.HttpQueryReportingService": "Information"
}
}
}For development environments:
{
"Logging": {
"LogLevel": {
"EFCore.QueryAnalyzer": "Information",
"EFCore.QueryAnalyzer.Core.QueryPerformanceInterceptor": "Warning"
}
}
}✅ Information Level:
⚠️ Warning Level:
❌ Error Level:
🔇 Removed (No longer logged):
This ensures your production logs stay clean while maintaining operational visibility.
// Test setup - background processing works in tests too
services.AddEFCoreQueryAnalyzerWithInMemory(options =>
{
options.ThresholdMilliseconds = 1; // Capture all queries in tests
});
// Test assertions
var reports = inMemoryReporter.GetReports();
Assert.Contains(reports, r => r.RawQuery.Contains("Users"));| Issue | Cause | Solution |
|---|---|---|
| API authentication errors | Invalid API key | Verify ApiKey configuration |
| Missing stack traces | Stack trace capture disabled | Set CaptureStackTrace = true |
| High memory usage | Large query texts/stack traces | Reduce MaxQueryLength and MaxStackTraceLines |
| Execution plan timeouts | Database connection issues | Increase ExecutionPlanTimeoutSeconds |
| Background processing not working | Service not registered | Ensure you're using the provided extension methods |
Monitor the background service with logging:
{
"Logging": {
"LogLevel": {
"EFCore.QueryAnalyzer.Services.QueryAnalysisBackgroundService": "Information"
}
}
}Enable detailed logging to diagnose issues:
{
"Logging": {
"LogLevel": {
"EFCore.QueryAnalyzer": "Debug",
"EFCore.QueryAnalyzer.Core.QueryPerformanceInterceptor": "Trace"
}
}
}{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyApp;Trusted_Connection=true;"
},
"QueryAnalyzer": {
"ThresholdMilliseconds": 100,
"CaptureStackTrace": true,
"CaptureExecutionPlan": true,
"EnableInDevelopment": true,
"DatabaseProvider": "SqlServer",
"ConnectionString": "Server=localhost;Database=MyApp;Trusted_Connection=true;",
"MaxStackTraceLines": 15,
"MaxQueryLength": 20000,
"ExecutionPlanTimeoutSeconds": 45
},
"Logging": {
"LogLevel": {
"Default": "Information",
"EFCore.QueryAnalyzer": "Information",
"EFCore.QueryAnalyzer.Core.QueryPerformanceInterceptor": "Warning"
}
}
}{
"QueryAnalyzer": {
"IsEnabled": true,
"ThresholdMilliseconds": 1000,
"ApiEndpoint": "https://monitoring.company.com/api/slow-queries",
"ApiKey": "prod-api-key-here",
"ProjectId": "my-production-app",
"CaptureStackTrace": true,
"CaptureExecutionPlan": true,
"ApiTimeoutMs": 10000,
"DatabaseProvider": "SqlServer",
"ConnectionString": "Server=prod-server;Database=MyApp;Integrated Security=true;",
"MaxStackTraceLines": 10,
"MaxQueryLength": 15000,
"ExecutionPlanTimeoutSeconds": 30
},
"Logging": {
"LogLevel": {
"Default": "Information",
"EFCore.QueryAnalyzer": "Warning",
"EFCore.QueryAnalyzer.Services.HttpQueryReportingService": "Information"
}
}
}Contributions are welcome! Please read our Contributing Guidelines and submit pull requests to our GitHub repository.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ for the .NET community