Appearance
MiniProfiler - 性能分析工具
概述
MiniProfiler 是一个轻量级的性能分析工具,可以实时监控 EF Core 查询性能,包括 SQL 执行时间、查询次数、N+1 问题检测等。它提供了直观的 Web UI,帮助开发者快速定位性能瓶颈。
核心功能
| 功能 | 说明 |
|---|---|
| SQL 监控 | 记录每个 SQL 查询的执行时间和参数 |
| N+1 检测 | 自动识别 N+1 查询问题 |
| 调用堆栈 | 显示查询的 C# 代码位置 |
| 自定义步骤 | 标记业务逻辑步骤的性能 |
| 历史对比 | 比较不同请求的性能差异 |
| 生产环境支持 | 可选择性在生产环境启用 |
安装与配置
安装 NuGet 包
bash
# ASP.NET Core MVC / Minimal API
dotnet add package MiniProfiler.AspNetCore.Mvc
# EF Core 集成
dotnet add package MiniProfiler.EntityFrameworkCore
# Blazor Server(可选)
dotnet add package MiniProfiler.Blazor基础配置
csharp
// Program.cs
using StackExchange.Profiling;
var builder = WebApplication.CreateBuilder(args);
// 添加 MiniProfiler 服务
builder.Services.AddMiniProfiler(options =>
{
// 路由前缀(访问 /profiler 查看结果)
options.RouteBasePath = "/profiler";
// UI 配置
options.ColorScheme = ColorScheme.Dark; // 深色主题
options.PopupRenderPosition = RenderPosition.Right; // 浮窗位置
options.ShowControls = true; // 显示控制按钮
// 授权配置(生产环境重要!)
options.ResultsAuthorize = request =>
request.HttpContext.User.IsInRole("Admin");
options.ResultsListAuthorize = options.ResultsAuthorize;
// 采样率(生产环境降低采样率)
options.SampleRate = 100; // 100% 采样(开发环境)
// options.SampleRate = 1; // 1% 采样(生产环境)
})
.AddEntityFramework(); // ← 集成 EF Core
var app = builder.Build();
// 使用中间件
if (app.Environment.IsDevelopment())
{
app.UseMiniProfiler(); // 仅开发环境启用
}
app.Run();基本使用
自动监控 EF Core 查询
csharp
// 配置完成后,所有 EF Core 查询自动被监控
// 无需额外代码!
app.MapGet("/api/products", async (AppDbContext db) =>
{
var products = await db.Products.ToListAsync();
return Results.Ok(products);
});
// 访问 /profiler 可以看到:
// - SQL 查询列表
// - 每个查询的执行时间
// - 参数值
// - 调用堆栈自定义监控步骤
csharp
app.MapGet("/api/orders/{id}/details", async (int id, AppDbContext db) =>
{
// 自定义步骤 1: 加载订单
Order? order;
using (MiniProfiler.Current.Step("Load Order"))
{
order = await db.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
}
if (order == null)
return Results.NotFound();
// 自定义步骤 2: 加载客户信息
Customer? customer;
using (MiniProfiler.Current.Step("Load Customer"))
{
customer = await db.Customers
.FirstOrDefaultAsync(c => c.Id == order.CustomerId);
}
// 自定义步骤 3: 计算统计数据
OrderStatistics stats;
using (MiniProfiler.Current.Step("Calculate Statistics"))
{
stats = await CalculateOrderStatistics(db, order.Id);
}
return Results.Ok(new
{
Order = order,
Customer = customer,
Statistics = stats
});
});
// MiniProfiler UI 会显示每个步骤的耗时标记关键代码块
csharp
public class ProductService
{
private readonly AppDbContext _context;
public async Task<List<Product>> SearchProductsAsync(string searchTerm)
{
// 标记整个方法
using var profiler = MiniProfiler.Current.Step(nameof(SearchProductsAsync));
// 子步骤 1: 验证输入
using (MiniProfiler.Current.Step("Validate Input"))
{
if (string.IsNullOrWhiteSpace(searchTerm))
throw new ArgumentException("Search term is required");
}
// 子步骤 2: 执行搜索
List<Product> products;
using (MiniProfiler.Current.Step("Execute Search Query"))
{
products = await _context.Products
.Where(p => p.Name.Contains(searchTerm) ||
p.Description.Contains(searchTerm))
.ToListAsync();
}
// 子步骤 3: 后处理
using (MiniProfiler.Current.Step("Post-process Results"))
{
// 格式化、过滤等
products = products.Take(100).ToList();
}
return products;
}
}EF Core 深度集成
查看 SQL 详情
csharp
// MiniProfiler 自动记录:
/*
EF Core Queries (3 queries, 45ms total)
1. SELECT [p].[Id], [p].[Name], [p].[Price]
FROM [Products] AS [p]
WHERE [p].[Price] > @__minPrice_0
Duration: 12ms
Parameters: @__minPrice_0 = 100
Stack Trace:
at ProductService.GetExpensiveProducts() in ProductService.cs:line 45
at ProductController.GetProducts() in ProductController.cs:line 23
2. SELECT [c].[Id], [c].[Name]
FROM [Categories] AS [c]
Duration: 8ms
3. UPDATE [Products] SET [ViewCount] = [ViewCount] + 1
WHERE [Id] = @__id_0
Duration: 25ms
*/检测 N+1 查询
csharp
// ❌ N+1 问题会被 MiniProfiler 高亮显示
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
// ⚠️ MiniProfiler 会标记这是潜在问题
order.Customer = await context.Customers
.FirstOrDefaultAsync(c => c.Id == order.CustomerId);
}
// MiniProfiler 警告:
// ⚠️ Detected 100 similar queries executed in a loop
// 💡 Suggestion: Use Include() to eager load related data
// ✅ 修复后
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();查询计划缓存监控
csharp
// MiniProfiler 显示查询计划缓存命中率
/*
Query Plan Cache:
- Total Queries: 150
- Cache Hits: 142 (94.7%)
- Cache Misses: 8 (5.3%)
- Cached Plans: 45
💡 Tip: High cache hit rate indicates good query parameterization
*/Blazor Server 集成
配置 Blazor
csharp
// Program.cs
builder.Services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
})
.AddEntityFramework();
var app = builder.Build();
app.UseMiniProfiler();
// Blazor Hub 配置
app.MapBlazorHub();razor
@* _Imports.razor *@
@using StackExchange.Profiling
@inject IMiniProfilerAccessor ProfilerAccessor
@* MainLayout.razor *@
@if (ProfilerAccessor.TryGetCurrent(out var profiler))
{
<div class="profiler-info">
Request Duration: @profiler.DurationMilliseconds ms
SQL Queries: @profiler.GetSqlTimings().Count
</div>
}
@BodyRazor 组件性能监控
razor
@page "/products"
@inject IDbContextFactory<AppDbContext> DbFactory
<h3>Products</h3>
@code {
private List<Product> _products = new();
protected override async Task OnInitializedAsync()
{
// 自动监控 DbContext 操作
using var context = DbFactory.CreateDbContext();
using (MiniProfiler.Current.Step("Load Products"))
{
_products = await context.Products.ToListAsync();
}
}
}高级配置
自定义存储
csharp
// 默认使用内存存储,重启后丢失
// 可以自定义存储到数据库
public class DatabaseStorage : IMemoryCacheStorage
{
private readonly string _connectionString;
public DatabaseStorage(string connectionString)
{
_connectionString = connectionString;
}
public void Save(Profile profile)
{
// 保存到数据库
using var connection = new SqlConnection(_connectionString);
connection.Execute(@"
INSERT INTO MiniProfilers (Id, Name, Started, DurationMs, HasSqlTimings)
VALUES (@Id, @Name, @Started, @DurationMs, @HasSqlTimings)",
new
{
profile.Id,
profile.Name,
profile.Started,
profile.DurationMilliseconds,
profile.HasSqlTimings
});
}
public Profile? Load(Guid id)
{
// 从数据库加载
// ...
}
public IEnumerable<Guid> List(int maxResults)
{
// 列出最近的 profiling 会话
// ...
}
}
// 注册
builder.Services.AddSingleton<IMemoryCacheStorage>(
new DatabaseStorage(builder.Configuration.GetConnectionString("ProfilingDb")));忽略特定路径
csharp
builder.Services.AddMiniProfiler(options =>
{
// 忽略健康检查端点
options.IgnoredPaths.Add("/health");
options.IgnoredPaths.Add("/healthz");
// 忽略静态文件
options.IgnoredPaths.Add("/css/");
options.IgnoredPaths.Add("/js/");
options.IgnoredPaths.Add("/images/");
// 忽略 Swagger
options.IgnoredPaths.Add("/swagger");
});条件启用
csharp
builder.Services.AddMiniProfiler(options =>
{
// 仅在特定条件下启用
options.ShouldProfile = request =>
{
// 仅分析管理员用户
if (!request.HttpContext.User.IsInRole("Admin"))
return false;
// 仅分析特定路径
if (!request.Path.StartsWithSegments("/api"))
return false;
// 采样率控制
return Random.Shared.Next(100) < 10; // 10% 采样
};
});实际案例分析
案例 1: 识别慢查询
csharp
// 问题: API 响应缓慢
app.MapGet("/api/reports/sales", async (AppDbContext db) =>
{
// MiniProfiler 显示此查询耗时 2.5 秒!
var sales = await db.Orders
.Where(o => o.OrderDate >= DateTime.UtcNow.AddMonths(-12))
.Select(o => new
{
o.OrderDate,
o.TotalAmount,
Customer = o.Customer.Name, // ⚠️ 隐式 JOIN
Items = o.Items.Select(i => new
{
i.ProductName,
i.Quantity,
i.UnitPrice
}) // ⚠️ N+1 问题
})
.ToListAsync();
return Results.Ok(sales);
});
// MiniProfiler 输出:
/*
⚠️ Slow Query Detected (2,543ms)
SELECT [o].[OrderDate], [o].[TotalAmount], [c].[Name], ...
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [o].[OrderDate] >= @__date_0
💡 Optimization Suggestions:
1. Add index on Orders.OrderDate
2. Consider pagination (currently loading all 12 months)
3. Project only needed columns
*/
// ✅ 优化后
app.MapGet("/api/reports/sales", async (
AppDbContext db,
int page = 1,
int pageSize = 50) =>
{
var sales = await db.Orders
.Where(o => o.OrderDate >= DateTime.UtcNow.AddMonths(-3)) // 仅 3 个月
.OrderByDescending(o => o.OrderDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(o => new
{
o.OrderDate,
o.TotalAmount,
CustomerName = o.Customer.Name
})
.ToListAsync();
return Results.Ok(sales);
});
// 优化后: 45ms (提升 98%)案例 2: 检测 N+1 问题
csharp
// 问题: 页面加载缓慢
app.MapGet("/api/blog/posts", async (AppDbContext db) =>
{
var posts = await db.Posts.ToListAsync();
foreach (var post in posts)
{
// ⚠️ N+1: 每个帖子都查询作者
post.Author = await db.Users.FindAsync(post.AuthorId);
// ⚠️ N+1: 每个帖子都查询评论
post.Comments = await db.Comments
.Where(c => c.PostId == post.Id)
.ToListAsync();
}
return Results.Ok(posts);
});
// MiniProfiler 警告:
/*
⚠️ Performance Issues Detected
N+1 Query Pattern:
- Query 1: SELECT * FROM Posts (1 query, 5ms)
- Query 2: SELECT * FROM Users WHERE Id = @id (100 queries, 250ms) ⚠️
- Query 3: SELECT * FROM Comments WHERE PostId = @id (100 queries, 180ms) ⚠️
Total: 201 queries, 435ms
💡 Fix: Use Include() to eager load related data
*/
// ✅ 修复后
app.MapGet("/api/blog/posts", async (AppDbContext db) =>
{
var posts = await db.Posts
.Include(p => p.Author)
.Include(p => p.Comments)
.ToListAsync();
return Results.Ok(posts);
});
// 优化后: 3 queries, 25ms (提升 94%)生产环境部署
安全配置
csharp
if (!app.Environment.IsDevelopment())
{
// 生产环境配置
builder.Services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
// ⚠️ 严格授权: 仅管理员可访问
options.ResultsAuthorize = request =>
{
var user = request.HttpContext.User;
return user.Identity?.IsAuthenticated == true &&
user.IsInRole("Administrator") &&
IsInternalIp(request.HttpContext.Connection.RemoteIpAddress);
};
// 低采样率
options.SampleRate = 1; // 1% 采样
// 限制存储数量
options.MaxUnviewedProfiles = 10;
options.StorageDuration = TimeSpan.FromHours(1);
// 隐藏敏感信息
options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter();
})
.AddEntityFramework();
app.UseMiniProfiler();
}
bool IsInternalIp(IPAddress? ip)
{
if (ip == null) return false;
// 检查是否为内网 IP
return ip.IsIPv6SiteLocal ||
ip.ToString().StartsWith("10.") ||
ip.ToString().StartsWith("192.168.") ||
ip.ToString().StartsWith("172.16.");
}性能考虑
csharp
// 生产环境优化
builder.Services.AddMiniProfiler(options =>
{
// 禁用堆栈跟踪(减少开销)
options.TrackConnectionOpenClose = false;
// 仅监控慢查询
options.EnableServerTimingHeader = false;
// 异步存储
options.Storage = new AsyncStorageDecorator(
new MemoryCacheStorage(TimeSpan.FromMinutes(5)));
});
public class AsyncStorageDecorator : IStorage
{
private readonly IStorage _inner;
public AsyncStorageDecorator(IStorage inner)
{
_inner = inner;
}
public Task SaveAsync(Profile profile)
{
// 异步保存,不阻塞请求
return Task.Run(() => _inner.Save(profile));
}
// 其他方法...
}最佳实践
✅ 推荐做法
1. 开发环境始终启用
csharp
if (env.IsDevelopment())
{
app.UseMiniProfiler(); // 始终启用
}2. 生产环境谨慎使用
csharp
// ✅ 推荐: 低采样率 + 严格授权
options.SampleRate = 1; // 1%
options.ResultsAuthorize = request => IsAdmin(request);3. 标记关键业务步骤
csharp
using (MiniProfiler.Current.Step("Process Payment"))
{
await paymentGateway.Charge(order.TotalAmount);
}4. 定期审查性能数据
csharp
// 每周生成性能报告
public class WeeklyPerformanceReport : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromDays(7), ct);
var report = GeneratePerformanceReport();
await EmailTeam(report);
}
}
}❌ 避免的错误
1. 生产环境无限制启用
csharp
// ❌ 危险: 性能开销大,安全风险
app.UseMiniProfiler(); // 所有环境都启用,无授权
// ✅ 正确
if (env.IsDevelopment())
{
app.UseMiniProfiler();
}2. 忽略采样率
csharp
// ❌ 错误: 100% 采样影响性能
options.SampleRate = 100; // 生产环境!
// ✅ 正确
options.SampleRate = env.IsDevelopment() ? 100 : 1;3. 暴露敏感 SQL
csharp
// ❌ 危险: SQL 中可能包含敏感数据
options.SqlFormatter = new InlineFormatter(); // 显示完整 SQL
// ✅ 正确: 生产环境隐藏参数值
if (!env.IsDevelopment())
{
options.SqlFormatter = new VerboseFormatter(false); // 隐藏参数
}总结
MiniProfiler vs 其他工具
| 特性 | MiniProfiler | Application Insights | Prometheus |
|---|---|---|---|
| 设置难度 | ⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 复杂 |
| 实时性 | ✅ 实时 | ⚠️ 延迟 | ✅ 实时 |
| UI 友好度 | ✅ 优秀 | ✅ 良好 | ⚠️ 一般 |
| 生产适用 | ⚠️ 需配置 | ✅ 原生支持 | ✅ 原生支持 |
| 成本 | ✅ 免费 | 💰 付费 | ✅ 开源 |
| 学习曲线 | ⭐ 低 | ⭐⭐⭐ 中 | ⭐⭐⭐⭐ 高 |
核心要点
- 开发必备: 开发环境始终启用,即时发现性能问题
- 生产谨慎: 低采样率 + 严格授权 + 隐藏敏感信息
- 主动标记: 使用 Step 标记关键业务步骤
- 定期审查: 建立性能监控和告警机制
- 结合使用: MiniProfiler + APM 工具互补