Skip to content

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>
}

@Body

Razor 组件性能监控 ​

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 其他工具 ​

特性MiniProfilerApplication InsightsPrometheus
设置难度⭐ 简单⭐⭐⭐ 中等⭐⭐⭐⭐ 复杂
实时性✅ 实时⚠️ 延迟✅ 实时
UI 友好度✅ 优秀✅ 良好⚠️ 一般
生产适用⚠️ 需配置✅ 原生支持✅ 原生支持
成本✅ 免费💰 付费✅ 开源
学习曲线⭐ 低⭐⭐⭐ 中⭐⭐⭐⭐ 高

核心要点 ​

  1. 开发必备: 开发环境始终启用,即时发现性能问题
  2. 生产谨慎: 低采样率 + 严格授权 + 隐藏敏感信息
  3. 主动标记: 使用 Step 标记关键业务步骤
  4. 定期审查: 建立性能监控和告警机制
  5. 结合使用: MiniProfiler + APM 工具互补

基于 MIT 许可发布