🚀 自动过滤:用 AutoFilterer 实现高性能动态查询
📚 目录
🧩 项目场景
在中大型 .NET Web API 项目中,数据查询过滤是高频且重复度极高的任务。传统方式下:
- 每个字段都需写
Where
条件; - 多字段组合查询冗长、易错;
- Swagger 接口测试缺乏一致性与自动生成文档;
因此,引入 AutoFilterer —— 一个属性驱动的 LINQ 表达式生成器,支持动态查询和 OpenAPI 自动展示,全面提升代码质量与开发效率。
🌟 AutoFilterer 核心优势
🧩 特性 | ✨ 描述 |
---|---|
属性驱动 | DTO 上声明过滤规则,自动生成表达式 |
无需手写 | 摒弃手工 LINQ 代码 |
多条件支持 | 字符串、区间、布尔、枚举、日期等 |
OpenAPI 3.0 | 与 Swagger UI 无缝集成 |
高性能 | 表达式缓存、延迟执行、支持 EF Core |
支持分页 | 内置 PaginationFilterBase |
🎯 项目结构流程图
🛠 快速集成
1️⃣ 安装依赖
dotnet add package AutoFilterer.Extensions.Microsoft.DependencyInjection --version 3.1.0
dotnet add package AutoFilterer.Swagger --version 3.1.0
dotnet add package System.Linq.Dynamic.Core --version 1.6.5
2️⃣ 注册服务(Program.cs)
using AutoFilterer.Swagger;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
// 1. AutoFilterer + 表达式缓存
builder.Services.AddAutoFilterer(options =>
{
options.EnableExpressionCache = true;
});
// 2. Swagger 集成 AutoFilterer 参数
builder.Services.AddSwaggerGen(c =>
{
c.UseAutoFiltererParameters();
c.SwaggerDoc("v1", new() { Title = "Book API", Version = "v1" });
});
// 3. EF Core:演示用 InMemory,生产可切换 SQL Server
builder.Services.AddDbContext<AppDbContext>(opt =>
{
opt.UseInMemoryDatabase("BooksDb");
// 生产示例:
// opt.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
});
// 4. MVC + 全局 camelCase JSON 命名
builder.Services.AddControllers()
.AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
// 5. 内存缓存,用于结果缓存
builder.Services.AddMemoryCache();
var app = builder.Build();
// Seed 数据
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
DbSeeder.Seed(db);
}
// 中间件
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
app.Run();
🌱 初始化种子数据流程图
3️⃣ 实体与过滤 DTO
public class Book
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public DateTime PublishedDate { get; set; }
public int PageCount { get; set; }
}
using AutoFilterer.Attributes;
using AutoFilterer.Types;
using System.ComponentModel.DataAnnotations;
public class BookFilter : PaginationFilterBase
{
[ToLowerContainsComparison]
public string? Title { get; set; }
[ToLowerContainsComparison]
public string? Author { get; set; }
[CompareTo(nameof(Book.PublishedDate), FilterType.GreaterThanOrEqual)]
public DateTime? FromDate { get; set; }
[CompareTo(nameof(Book.PublishedDate), FilterType.LessThanOrEqual)]
public DateTime? ToDate { get; set; }
[CompareTo(nameof(Book.PageCount), FilterType.GreaterThan)]
public int? MinPages { get; set; }
/// <summary>
/// 支持动态排序,格式示例:"PublishedDate desc"
/// </summary>
public string? SortBy { get; set; }
[Range(1, 100)]
public override int PageSize { get; set; } = 10;
}
4️⃣ Controller 实现:请求 & 缓存流程图
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly AppDbContext _db;
private readonly ILogger<BooksController> _logger;
private readonly IMemoryCache _cache;
public BooksController(
AppDbContext db,
ILogger<BooksController> logger,
IMemoryCache cache)
{
_db = db;
_logger = logger;
_cache = cache;
}
[HttpGet]
public async Task<ActionResult<PagedResult<Book>>> Get([FromQuery] BookFilter filter)
{
string cacheKey = $"books_{filter.GetCacheKey()}_{filter.SortBy}";
try
{
if (!_cache.TryGetValue(cacheKey, out PagedResult<Book> result))
{
// 1. 应用过滤器
var query = _db.Books.ApplyFilter(filter);
// 2. 动态排序(依赖 System.Linq.Dynamic.Core)
if (!string.IsNullOrWhiteSpace(filter.SortBy))
query = query.OrderBy(filter.SortBy);
else
query = query.OrderBy("PublishedDate desc");
// 3. 分页 + 总数
var total = await query.CountAsync();
var items = await query
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();
result = new PagedResult<Book>(total, items);
// 缓存 5 分钟
_cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
}
return Ok(result);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid filter parameter");
return BadRequest("请求参数错误");
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Database update failure");
return StatusCode(503, "服务暂不可用");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error");
return StatusCode(500, "查询失败");
}
}
}
// 分页结果模型
public record PagedResult<T>(int Total, List<T> Items);
🌐 示例请求与响应
GET /api/books?Author=alice&MinPages=110&SortBy=PageCount%20desc&PageSize=5&PageNumber=1
{
"total": 25,
"items": [
{ "id": "guid", "title": "Book 48", "author": "Alice", "publishedDate": "2025-05-02T00:00:00", "pageCount": 148 },
...
]
}
🧪 单元测试示例(xUnit)
public class BookFilterTests
{
[Fact]
public void ApplyFilter_ShouldFilterByTitle()
{
// Arrange
var data = new[]
{
new Book { Title = "alice" },
new Book { Title = "bob" }
}.AsQueryable();
var filter = new BookFilter { Title = "ali" };
// Act
var result = data.ApplyFilter(filter).ToList();
// Assert
Assert.Single(result);
Assert.Equal("alice", result[0].Title);
}
}
📊 最佳实践小结
维度 | 建议 |
---|---|
性能 | 开启表达式缓存;合理设置缓存过期 |
安全性 | DTO 参数加 [Range] 限制;细粒度异常返回 |
可维护性 | 提取 DbSeeder 、PagedResult<T> 、IBookRepository 等;分层结构 |
文档体验 | Mermaid 图导出为图片;示例请求/响应;代码高亮 |
扩展性 | 动态排序(System.Linq.Dynamic.Core);分布式/共享缓存方案 |
可测试性 | 单元测试覆盖过滤逻辑;集成测试验证 API 行为 |