C# Web开发教程(七)切面编程
切面编程: 在项目中的某一处,执行我们自定义的代码!
- 比如说
自定义异常,先简单写一个接口,触发系统自带异常
using Microsoft.AspNetCore.Mvc;
namespace WebApplicationAboutCustomCode.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class Test1Controller : ControllerBase
{
[HttpGet]
public string Test1()
{
// 访问一个并不存在的文件
string s = System.IO.File.ReadAllText("d:/123.txt");
return s;
}
}
}
- 结果,触发系统自带异常: System.IO.FileNotFoundException:“Could not find file 'd:\123.txt'.”
- 现在,自定义异常,比如统一返回
{"code":500,"message":"xxx"}这种格式
// MyExceptionFilter.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApplicationAboutCustomCode
{
public class MyExceptionFilter:IAsyncExceptionFilter
{
private readonly IWebHostEnvironment hostEnv;
public MyExceptionFilter(IWebHostEnvironment hostEnv)
{
this.hostEnv = hostEnv;
}
public Task OnExceptionAsync(ExceptionContext context)
{
string msg;
if (hostEnv.IsDevelopment())
{
msg = context.Exception.ToString();
Console.WriteLine("触发开发环境异常");
}
else
{
msg = "服务端发生未知处理异常";
}
ObjectResult objResult = new ObjectResult(new { code = 500, message = msg });
context.Result = objResult;
context.ExceptionHandled = true;
return Task.CompletedTask;
}
}
}
// LogExceptionFilter.cs
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApplicationAboutCustomCode
{
public class LogExceptionFilter: IAsyncExceptionFilter
{
public Task OnExceptionAsync(ExceptionContext context)
{
return File.AppendAllTextAsync("d:/error.log", context.Exception.ToString());
}
}
}
// Program.cs
......
builder.Services.AddSwaggerGen();
builder.Services.Configure<MvcOptions>(opt =>
{
// 这里注意顺序
opt.Filters.Add<MyExceptionFilter>();
opt.Filters.Add<LogExceptionFilter>();
});
// 接口程序
using Microsoft.AspNetCore.Mvc;
namespace WebApplicationAboutCustomCode.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class Test1Controller : ControllerBase
{
[HttpGet]
public string Test1()
{
// 主动触发异常
string s = System.IO.File.ReadAllText("d:/123.txt");
return s;
}
}
}
切面编程(AOP)在 ASP.NET Core 中的实现
这段代码演示了如何使用过滤器(Filters) 在 ASP.NET Core 中实现切面编程(AOP)的概念。
核心概念解释
什么是切面编程(AOP)?
切面编程是一种编程范式,允许开发者在不修改原有业务代码的情况下,在程序的特定执行点插入自定义逻辑。就像"切一刀"进去执行额外代码。
代码详细解析
1. 原始问题代码
[HttpGet]
public string Test1()
{
// 访问一个并不存在的文件,会抛出异常
string s = System.IO.File.ReadAllText("d:/123.txt");
return s;
}
问题:直接抛出系统异常,用户体验差,没有统一的错误处理。
2. 自定义异常过滤器
MyExceptionFilter - 主要异常处理器
public class MyExceptionFilter : IAsyncExceptionFilter
{
private readonly IWebHostEnvironment hostEnv;
public MyExceptionFilter(IWebHostEnvironment hostEnv)
{
this.hostEnv = hostEnv;
}
public Task OnExceptionAsync(ExceptionContext context)
{
string msg;
// 根据环境决定错误信息详细程度
if (hostEnv.IsDevelopment())
{
msg = context.Exception.ToString(); // 开发环境:详细错误
Console.WriteLine("触发开发环境异常");
}
else
{
msg = "服务端发生未知处理异常"; // 生产环境:友好提示
}
// 创建统一格式的响应
ObjectResult objResult = new ObjectResult(new { code = 500, message = msg });
context.Result = objResult; // 设置返回结果
context.ExceptionHandled = true; // 标记异常已处理,阻止继续传播
return Task.CompletedTask;
}
}
作用:
- 捕获控制器中未处理的异常
- 根据运行环境返回不同的错误信息
- 统一响应格式:
{ code: 500, message: "..." }
LogExceptionFilter - 日志记录器
public class LogExceptionFilter : IAsyncExceptionFilter
{
public Task OnExceptionAsync(ExceptionContext context)
{
// 将异常信息写入日志文件
return File.AppendAllTextAsync("d:/error.log", context.Exception.ToString());
}
}
作用:
- 专门负责记录异常日志
- 将异常详细信息写入文件
3. 注册过滤器
builder.Services.Configure<MvcOptions>(opt =>
{
opt.Filters.Add<MyExceptionFilter>(); // 第一个执行
opt.Filters.Add<LogExceptionFilter>(); // 第二个执行
});
执行顺序很重要:
MyExceptionFilter先执行,处理异常并设置响应LogExceptionFilter后执行,记录日志
执行流程
切面编程的优势
1. 关注点分离
- 业务代码:只关注核心逻辑(读取文件)
- 横切关注点:异常处理、日志记录单独处理
2. 代码复用
// 这个过滤器会自动应用到所有Controller的所有Action
// 不需要在每个方法中写try-catch
3. 维护性
修改异常处理逻辑只需修改过滤器,不影响业务代码。
4. 一致性
所有异常都按照统一格式返回,前端处理更简单。
实际效果对比
修改前:
HTTP/1.1 500 Internal Server Error
System.IO.FileNotFoundException: Could not find file 'd:\123.txt'.
修改后:
{
"code": 500,
"message": "System.IO.FileNotFoundException: Could not find file 'd:\\123.txt'."
}
扩展应用场景
这种切面编程思想还可以用于:
- 身份认证:在进入业务方法前验证用户权限
- 性能监控:记录每个方法的执行时间
- 缓存:在方法执行前检查缓存
- 数据验证:统一验证请求参数
- 事务管理:自动管理数据库事务
总结
这个例子完美展示了切面编程的核心价值:
- ✅ 非侵入式:不改动原有业务代码
- ✅ 集中管理:异常处理逻辑集中在一处
- ✅ 灵活配置:根据不同环境返回不同信息
- ✅ 职责分离:业务逻辑与横切关注点分离
通过过滤器机制,ASP.NET Core 提供了强大的 AOP 支持,让开发者能够优雅地处理这类"横切关注点"问题。
Action过滤器,在Action方法之前或者之后,都可以执行
1. MyActionFilter1 过滤器
// MyActionFilter1.cs
public class MyActionFilter1 : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter1 执行前代码");
ActionExecutedContext res = await next();
if(res.Exception != null)
{
Console.WriteLine("MyActionFilter1 发生异常了");
}
else
{
Console.WriteLine("MyActionFilter1 执行成功");
}
}
}
执行流程:
Console.WriteLine("MyActionFilter1 执行前代码");- 在Action方法执行前运行await next()- 调用下一个过滤器或实际的Action方法- 根据执行结果判断是否发生异常,输出相应信息
2. 过滤器注册()
// Program.cs
......
builder.Services.Configure<MvcOptions>(opt =>
{
opt.Filters.Add<MyActionFilter1>();
});
这里将过滤器注册为全局过滤器,意味着它会应用到所有的Controller和Action方法。
3. 测试接口
[HttpGet]
public string Test1()
{
Console.WriteLine("我在Api中的Test1方法里");
return "测试请求接口";
}
执行顺序
当调用 GET /api/Test1/Test1 时,控制台输出顺序为:
MyActionFilter1 执行前代码
我在Api中的Test1方法里
MyActionFilter1 执行成功
异常处理场景
如果Action方法中抛出异常:
public string Test1()
{
Console.WriteLine("我在Api中的Test1方法里");
throw new Exception("测试异常");
return "测试请求接口";
}
输出顺序为:
MyActionFilter1 执行前代码
我在Api中的Test1方法里
MyActionFilter1 发生异常了
总结
这个过滤器实现了:
- 前置处理 - 在Action执行前运行
- 后置处理 - 在Action执行后运行
- 异常监控 - 可以捕获并处理Action中的异常
这是一个典型的AOP(面向切面编程)应用,用于横切关注点如日志记录、异常处理、性能监控等。
此时如果再写一个MyActionFilter2.cs,要注意执行的顺序
// MyActionFilter2.cs
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApplicationAboutCustomCode
{
public class MyActionFilter2:IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context,ActionExecutionDelegate next)
{
Console.WriteLine("MyActionFilter2 执行前代码");
ActionExecutedContext res = await next();
if(res.Exception != null)
{
Console.WriteLine("MyActionFilter2 发生异常了");
}
else
{
Console.WriteLine("MyActionFilter2 执行成功");
}
}
}
}
// Program.cs
......
builder.Services.Configure<MvcOptions>(opt =>
{
opt.Filters.Add<MyActionFilter1>();
opt.Filters.Add<MyActionFilter2>(); // 新注册
});
执行流程(正常情况)
当调用 GET /api/Test1/Test1 时:
MyActionFilter1 执行前代码
↓
MyActionFilter2 执行前代码
↓
我在Api中的Test1方法里
↓
MyActionFilter2 执行成功
↓
MyActionFilter1 执行成功
代码执行顺序:
MyActionFilter1.OnActionExecutionAsync开始执行- 输出 "MyActionFilter1 执行前代码"
- 调用
await next()→ 进入MyActionFilter2 MyActionFilter2.OnActionExecutionAsync开始执行- 输出 "MyActionFilter2 执行前代码"
- 调用
await next()→ 进入实际的 Action 方法 - 执行
Test1()方法,输出 "我在Api中的Test1方法里" - 返回
MyActionFilter2的await next()之后 - 输出 "MyActionFilter2 执行成功"
- 返回
MyActionFilter1的await next()之后 - 输出 "MyActionFilter1 执行成功"
异常处理场景
如果 Action 方法中抛出异常:
public string Test1()
{
Console.WriteLine("我在Api中的Test1方法里");
throw new Exception("测试异常");
}
执行顺序为:
MyActionFilter1 执行前代码
MyActionFilter2 执行前代码
我在Api中的Test1方法里
MyActionFilter2 发生异常了
MyActionFilter1 发生异常了
洋葱模型(Onion Model)
这种执行模式被称为"洋葱模型":
- 进入时:从外到内(Filter1 → Filter2 → Action)
- 离开时:从内到外(Action → Filter2 → Filter1)
Filter1 前 → Filter2 前 → Action → Filter2 后 → Filter1 后
重要特点
- 执行顺序:按照注册顺序执行前置代码,逆序执行后置代码
- 异常传播:异常会从内层向外层传播,每个过滤器都能捕获到
- 全局影响:两个过滤器都会应用到所有的 Controller 和 Action
- 嵌套执行:
await next()会调用管道中的下一个组件

事务
- 先写一个演示示例
// Book.cs
namespace WebApplicationAboutCustomCode
{
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public int Price { get; set; }
}
}
// Person.cs
namespace WebApplicationAboutCustomCode
{
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
namespace WebApplicationAboutCustomCode
{
public class MyDbContext:DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Person> Persons { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> opt) : base(opt)
{
}
}
}
// Program.cs
......
builder.Services.AddDbContext<MyDbContext>(opt =>
{
opt.UseSqlServer("Server=.;Database=demo10;Trusted_Connection=True;");
});
- 最后,作迁移并更新数据库,测试接口如下,正常响应,数据库查看数据没有问题
using Microsoft.AspNetCore.Mvc;
namespace WebApplicationAboutCustomCode.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class Test1Controller : ControllerBase
{
private MyDbContext ctx;
public Test1Controller(MyDbContext ctx)
{
this.ctx = ctx;
}
[HttpPost]
public string Test2()
{
ctx.Books.Add(new Book { Title="我的XXX",Price=666 });
ctx.SaveChanges();
ctx.Persons.Add(new Person { Name="Jim Green",Age=20 });
ctx.SaveChanges();
return "OK";
}
}
}
- 现在,故意把数据库中的
Books表的Title字段的数据类型,由nvarchar(MAX)变成nvarchar(3),再次执行上面的接口,就触发异常了,因为超出字段范围了,接口中的两个SaveChanges()均不生效- 注意事项: 若想修改成功,必须先删除原来的数据,否则会报错!
默认事务行为
- 每个
SaveChanges()都是一个独立的事务 - 第一个
SaveChanges()失败时,它自己的操作会回滚 - 但由于异常抛出,第二个
SaveChanges()根本不会执行
注意事项: 若此时先执行Person,再执行Book,就会造成Person事务成功,而Book事务失败的情况
public string Test2()
{
// 成功
ctx.Persons.Add(new Person { Name = "Jim Green", Age = 20 });
ctx.SaveChanges();
// 失败并触发异常
ctx.Books.Add(new Book { Title = "我的XXX", Price = 666 });
ctx.SaveChanges();
return "OK";
}
- 解决办法: 使用
TransactionScope实现批量事务
public string Test2()
{
// 使用TransactionScope包裹业务逻辑
using (TransactionScope tx = new())
{
ctx.Persons.Add(new Person { Name = "Jim Green", Age = 20 });
ctx.SaveChanges();
ctx.Books.Add(new Book { Title = "我的XXX", Price = 666 });
ctx.SaveChanges();
return "OK";
}
}
- 再次测试,当Books表写入失败时,即使Person写入成功,也会被回滚~~~
// 正确写法
public string Test2()
{
using (TransactionScope tx = new TransactionScope())
{
try
{
// 操作1
ctx.Persons.Add(new Person { Name = "Jim Green", Age = 20 });
ctx.SaveChanges();
// 操作2
ctx.Books.Add(new Book { Title = "我的XXX", Price = 666 });
ctx.SaveChanges();
// 必须调用 Complete() 来提交事务
tx.Complete();
return "OK";
}
catch (Exception ex)
{
// 发生异常时,TransactionScope 会自动回滚
// 不需要显式调用 Rollback()
throw;
}
}
}
TransactionScope 的工作原理
提交流程:
开始 TransactionScope
↓
执行数据库操作
↓
调用 tx.Complete() ← 这是提交的标记
↓
离开 using 块 → 事务提交
回滚流程:
开始 TransactionScope
↓
执行数据库操作
↓
发生异常 或 没有调用 Complete()
↓
离开 using 块 → 事务回滚
总结
原代码的问题:
- 缺少
tx.Complete()调用 - 导致所有操作都被回滚
- 看起来执行成功,但实际上没有数据保存
正确的理解:
TransactionScope默认在离开using块时回滚- 只有调用
Complete()才会提交事务 - 这是一种"隐式提交,显式标记"的设计模式
这是一个很好的例子,说明了为什么理解框架的默认行为非常重要!
把上述示例修改成异步版本
[HttpPost]
public async Task<string> Test3()
{
// 启用事务在异步操作中的流动(在异步方法中必须使用此选项,否则事务上下文不会在 await 之间保持)
using (TransactionScope tx = new(TransactionScopeAsyncFlowOption.Enabled))
{
ctx.Persons.Add(new Person { Name = "Jim Green", Age = 20 });
await ctx.SaveChangesAsync();
ctx.Books.Add(new Book { Title = "我的XXX", Price = 666 });
await ctx.SaveChangesAsync();
tx.Complete();
return "OK";
}
}
- 新需求如下: 通过简单的
特性标记来控制事务行为
1、对于强制不进行事务控制的Action方法,请标注
NotTransactionalAttribute.
2、开发筛选器TransactionScopeFilter;把
TransactionScopeFilter注册到Program.cs中。
3、编写两个插入数据的代码,测试。
1. 创建 NotTransactionalAttribute
首先创建标记不进行事务控制的特性:
// NotTransactionalAttribute.cs
using Microsoft.AspNetCore.Mvc.Filters;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class NotTransactionalAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 直接执行下一个过滤器或Action,不进行事务控制
await next();
}
}
2. 创建 TransactionScopeFilter
创建事务范围过滤器:
// TransactionScopeFilter.cs
using Microsoft.AspNetCore.Mvc.Filters;
using System.Transactions;
public class TransactionScopeFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 检查是否标记了 NotTransactional 特性
var hasNotTransactional = context.ActionDescriptor.EndpointMetadata
.Any(attr => attr is NotTransactionalAttribute);
if (hasNotTransactional)
{
// 如果标记了不进行事务控制,直接执行
await next();
return;
}
// 创建事务范围(启用异步流支持)
using var transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
try
{
// 执行Action方法
var executedContext = await next();
// 如果没有异常,提交事务
if (executedContext.Exception == null)
{
transactionScope.Complete();
Console.WriteLine("事务已提交");
}
else
{
Console.WriteLine("发生异常,事务已回滚");
}
}
catch (Exception ex)
{
Console.WriteLine($"捕获到异常: {ex.Message},事务已回滚");
throw;
}
}
}
3. 在 Program.cs 中注册过滤器
// Program.cs
......
// 注册事务过滤器
builder.Services.Configure<MvcOptions>(opt =>
{
opt.Filters.Add<TransactionScopeFilter>();
});
......
4. 编写测试控制器
创建测试控制器,包含使用事务和不使用事务的测试方法:
// TestTransactionController.cs
using Microsoft.AspNetCore.Mvc;
namespace WebApplicationAboutCustomCode.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class TestTransactionController : ControllerBase
{
private readonly MyDbContext ctx;
public TestTransactionController(MyDbContext ctx)
{
this.ctx = ctx;
}
// 这个Action会使用事务(默认行为)
[HttpPost]
public async Task<string> WithTransaction()
{
Console.WriteLine("=== 测试事务控制 ===");
// 第一步:添加Person(成功)
ctx.Persons.Add(new Person { Name = "Jim Green", Age = 20 });
await ctx.SaveChangesAsync();
Console.WriteLine("Person 添加成功");
// 第二步:添加Book(会失败,因为Title超过3个字符)
ctx.Books.Add(new Book { Title = "我的XXX", Price = 666 }); // 5个字符 > 3个字符限制
await ctx.SaveChangesAsync();
Console.WriteLine("Book 添加成功");
return "两个操作都成功";
}
// 这个Action标记为不使用事务
[HttpPost]
[NotTransactional]
public async Task<string> WithoutTransaction()
{
Console.WriteLine("=== 测试无事务控制 ===");
// 第一步:添加Person(成功)
ctx.Persons.Add(new Person { Name = "Alice", Age = 25 });
await ctx.SaveChangesAsync();
Console.WriteLine("Person 添加成功");
// 第二步:添加Book(会失败)
ctx.Books.Add(new Book { Title = "我的XXX", Price = 999 });
await ctx.SaveChangesAsync();
Console.WriteLine("Book 添加成功");
return "两个操作都成功";
}
// 这个Action会使用事务,但数据都合法
[HttpPost]
public async Task<string> SuccessTransaction()
{
Console.WriteLine("=== 测试成功事务 ===");
// 第一步:添加Person
ctx.Persons.Add(new Person { Name = "Tom", Age = 30 });
await ctx.SaveChangesAsync();
Console.WriteLine("Person 添加成功");
// 第二步:添加Book(使用3个字符的Title)
ctx.Books.Add(new Book { Title = "ABC", Price = 100 }); // 3个字符,符合限制
await ctx.SaveChangesAsync();
Console.WriteLine("Book 添加成功");
return "事务成功完成";
}
}
}
5. 完整的实体类和DbContext
确保你的实体类和DbContext如下:
// Book.cs
namespace WebApplicationAboutCustomCode
{
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public int Price { get; set; }
}
}
// Person.cs
namespace WebApplicationAboutCustomCode
{
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
namespace WebApplicationAboutCustomCode
{
public class MyDbContext : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Person> Persons { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> opt) : base(opt)
{
}
}
}
测试预期结果
测试1:/api/TestTransaction/WithTransaction
-
行为:使用事务控制
-
结果:由于第二个操作(Book)会失败,整个事务回滚
-
数据库:没有数据被插入
-
控制台输出:
=== 测试事务控制 === Person 添加成功 捕获到异常: ...,事务已回滚
测试2:/api/TestTransaction/WithoutTransaction
-
行为:不使用事务控制(标记了
[NotTransactional]) -
结果:第一个操作成功提交,第二个操作失败
-
数据库:Person 记录被插入,Book 记录没有插入
-
控制台输出:
=== 测试无事务控制 === Person 添加成功 捕获到异常: ...
测试3:/api/TestTransaction/SuccessTransaction
-
行为:使用事务控制,所有数据都合法
-
结果:两个操作都成功,事务提交
-
数据库:Person 和 Book 记录都被插入
-
控制台输出:
=== 测试成功事务 === Person 添加成功 Book 添加成功 事务已提交
总结
这个解决方案实现了:
- 灵活的事务控制:通过
[NotTransactional]特性可以灵活控制哪些方法不需要事务 - 全局过滤器:通过注册全局过滤器自动为所有Action添加事务控制
- 异常处理:自动处理异常并回滚事务
- 异步支持:使用
TransactionScopeAsyncFlowOption.Enabled支持异步操作 - 清晰的测试案例:提供了三种不同场景的测试方法
这样你就可以通过简单的特性标记来控制事务行为了!
简单限流器实现
1. 创建简单限流过滤器
// SimpleRateLimitFilter.cs
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using System.Net;
public class SimpleRateLimitFilter : IAsyncActionFilter
{
private static readonly Dictionary<string, DateTime> _requestHistory = new();
private static readonly object _lock = new object();
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 获取客户端IP作为标识
var clientIp = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// 检查请求间隔
bool isAllowed = CheckRequestInterval(clientIp);
if (!isAllowed)
{
// 触发限流,返回429状态码
context.Result = new ObjectResult(new
{
Code = 429,
Message = "请求过于频繁,请至少间隔2秒再请求",
Timestamp = DateTime.Now
})
{
StatusCode = (int)HttpStatusCode.TooManyRequests
};
return;
}
// 记录本次请求时间
RecordRequest(clientIp);
// 执行Action
await next();
}
private bool CheckRequestInterval(string clientIp)
{
lock (_lock)
{
if (_requestHistory.TryGetValue(clientIp, out DateTime lastRequestTime))
{
// 计算距离上次请求的时间间隔
var timeSpan = DateTime.Now - lastRequestTime;
// 如果间隔小于2秒,拒绝请求
if (timeSpan.TotalSeconds < 2)
{
return false;
}
}
return true;
}
}
private void RecordRequest(string clientIp)
{
lock (_lock)
{
_requestHistory[clientIp] = DateTime.Now;
// 清理过期的记录(可选,防止内存泄漏)
CleanupOldRecords();
}
}
private void CleanupOldRecords()
{
// 清理1分钟前的记录,避免内存无限增长
var cutoffTime = DateTime.Now.AddMinutes(-1);
var oldKeys = _requestHistory.Where(x => x.Value < cutoffTime).Select(x => x.Key).ToList();
foreach (var key in oldKeys)
{
_requestHistory.Remove(key);
}
}
}
2. 在 Program.cs 中注册
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
// 配置DbContext
builder.Services.AddDbContext<MyDbContext>(opt =>
{
opt.UseSqlServer("Server=.;Database=demo10;Trusted_Connection=True;");
});
// 注册限流过滤器为全局过滤器
builder.Services.Configure<MvcOptions>(opt =>
{
opt.Filters.Add<SimpleRateLimitFilter>();
});
var app = builder.Build();
// 配置中间件
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
3. 创建测试控制器
// TestRateLimitController.cs
using Microsoft.AspNetCore.Mvc;
namespace WebApplicationAboutCustomCode.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class TestRateLimitController : ControllerBase
{
[HttpGet]
public string QuickTest()
{
return $"请求成功 - 当前时间: {DateTime.Now:HH:mm:ss.fff}";
}
[HttpPost]
public async Task<string> AddData()
{
// 模拟一些处理时间
await Task.Delay(100);
return $"数据添加成功 - 时间: {DateTime.Now:HH:mm:ss.fff}";
}
}
}
4. 测试方法
快速连续请求测试
你可以使用以下方式测试:
方法1:使用浏览器开发者工具
// 在浏览器控制台中快速执行多次
for(let i = 0; i < 5; i++) {
fetch('/api/TestRateLimit/QuickTest').then(r => r.text()).then(console.log);
}
方法2:使用 curl 命令
# 第一次请求
curl -X GET "https://localhost:7000/api/TestRateLimit/QuickTest"
# 立即再次请求(会被限流)
curl -X GET "https://localhost:7000/api/TestRateLimit/QuickTest"
方法3:使用 Postman
- 快速连续发送多个 GET 请求到
/api/TestRateLimit/QuickTest
5. 预期效果
正常响应(间隔2秒以上):
"请求成功 - 当前时间: 14:30:25.123"
限流响应(间隔小于2秒):
{
"code": 429,
"message": "请求过于频繁,请至少间隔2秒再请求",
"timestamp": "2024-01-01T14:30:25.456"
}
6. 验证测试
- 第一次请求:立即返回成功
- 立即第二次请求:返回429限流错误
- 等待2秒后再次请求:返回成功
- 快速连续请求:只有第一个成功,后续都被限流
特点说明
这个简单限流器的特点:
- 基于时间间隔:要求请求间隔至少2秒
- IP级别限流:基于客户端IP进行识别
- 内存存储:使用静态字典存储请求历史
- 自动清理:定期清理过期记录防止内存泄漏
- 全局生效:所有接口都会应用这个限流规则
如果你想要更细粒度的控制(比如只对特定接口限流),可以创建一个特性标记,然后在过滤器中检查这个特性。但这个简单版本已经能满足你的基本需求了!
lock(_lock){....} 是 C# 中的锁语句,用于实现线程同步。让我详细解释这个语法:
基本语法
lock (lockObject)
{
// 需要同步执行的代码块
// 同一时间只有一个线程可以进入这个代码块
}
在限流器中的具体应用
private static readonly object _lock = new object();
lock (_lock)
{
if (_requestHistory.TryGetValue(clientIp, out DateTime lastRequestTime))
{
// 检查时间间隔的代码
}
// 其他需要同步的操作
}
各部分解释
1. lockObject (_lock)
private static readonly object _lock = new object();
- 作用:作为同步的标识对象
- 要求:
- 必须是引用类型(不能是值类型)
- 通常是
private和readonly的 - 静态字段用于跨实例同步
2. lock 语句块
lock (_lock)
{
// 临界区代码
}
- 作用:确保同一时间只有一个线程可以执行大括号内的代码
- 机制:基于监视器(Monitor)实现
执行原理
没有锁的情况(线程不安全):
线程A: 读取 _requestHistory
线程B: 同时读取 _requestHistory
线程A: 修改 _requestHistory
线程B: 同时修改 _requestHistory ← 数据竞争!
有锁的情况(线程安全):
线程A: 获取锁 → 执行代码 → 释放锁
线程B: 等待锁 → 获取锁 → 执行代码 → 释放锁
等价代码
lock 语句实际上是以下代码的语法糖:
// lock 语句的等价实现
object lockObj = _lock;
bool lockTaken = false;
try
{
Monitor.Enter(lockObj, ref lockTaken);
// 临界区代码
if (_requestHistory.TryGetValue(clientIp, out DateTime lastRequestTime))
{
// ...
}
}
finally
{
if (lockTaken)
Monitor.Exit(lockObj);
}
在限流器中的必要性
为什么需要锁?
private static readonly Dictionary<string, DateTime> _requestHistory = new();
_requestHistory是静态共享资源- 多个请求可能同时访问同一个字典
- 没有锁会导致:
- 数据竞争:多个线程同时修改字典
- 状态不一致:计数不准确
- 异常:集合被修改时枚举会抛出异常
具体风险场景
假设两个请求几乎同时到达:
请求A: 检查 clientIp1 的最后请求时间
请求B: 同时检查 clientIp1 的最后请求时间
请求A: 更新为当前时间
请求B: 也更新为当前时间 ← 时间戳被覆盖,限流失效!
最佳实践
1. 选择合适的锁对象
// 好的做法
private static readonly object _lock = new object();
// 不好的做法(可能导致意外锁定)
lock (this) { ... } // 锁定当前实例
lock (typeof(MyClass)) { ... } // 锁定类型
2. 保持锁范围最小化
// 好的做法 - 只保护必要的代码
lock (_lock)
{
// 只包含需要同步的代码
var exists = _requestHistory.ContainsKey(key);
if (exists) _requestHistory[key] = value;
}
// 不好的做法 - 包含不需要同步的代码
lock (_lock)
{
// 包含大量计算或IO操作 ← 会阻塞其他线程
var result = ExpensiveCalculation();
_requestHistory[key] = result;
}
3. 避免死锁
// 危险:可能死锁
lock (lockA)
{
lock (lockB) // 如果另一个线程先锁B再锁A,就会死锁
{
// ...
}
}
替代方案
对于字典操作,也可以使用线程安全的集合:
// 使用 ConcurrentDictionary(不需要锁)
private static readonly ConcurrentDictionary<string, DateTime> _requestHistory = new();
// 不需要 lock 语句
bool isAllowed = _requestHistory.AddOrUpdate(clientIp,
DateTime.Now,
(key, oldValue) => DateTime.Now) == DateTime.Now;
总结
lock(_lock){....} 的作用:
- 线程安全:确保同一时间只有一个线程执行临界区代码
- 数据一致性:防止多线程同时修改共享数据
- 避免竞态条件:确保操作的原子性
在限流器场景中,锁确保了:
- 请求计数的准确性
- 时间戳更新的正确性
- 字典操作的线程安全
这是一个在多线程环境下保护共享资源的经典用法!
out关键字的作用
在C#中,out关键字用于指示参数通过引用来传递,并且在方法返回前必须被赋值。它通常用于方法需要返回多个值的情况。
在_requestHistory.TryGetValue(clientIp, out DateTime lastRequestTime)这行代码中:
TryGetValue是Dictionary类的一个方法,它尝试获取与指定键关联的值。- 如果字典中存在该键,则方法返回
true,并通过out参数返回对应的值。 - 如果不存在,则返回
false,并且out参数将被设置为类型的默认值(对于DateTime,默认值是DateTime.MinValue)。
使用out参数的好处是,方法可以通过一个布尔返回值来指示操作是否成功,同时通过out参数返回获取到的值。
在这个限流器的上下文中,我们通过TryGetValue来检查之前是否记录过该客户端的IP。如果存在,我们就得到上一次请求的时间(lastRequestTime),然后计算时间间隔。如果不存在,则说明这是该客户端的第一次请求,或者之前的记录已经被清理了。
下面是一个简单的例子来说明out的用法:
csharp
复制下载
Dictionary<string, int> myDictionary = new Dictionary<string, int>();
myDictionary.Add("apple", 1);
if (myDictionary.TryGetValue("apple", out int value))
{
Console.WriteLine(value); // 输出 1
}
else
{
Console.WriteLine("Key not found");
}
在上面的代码中,因为键"apple"存在,所以TryGetValue返回true,并且通过out参数将值1赋给变量value。
在限流器的代码中,我们使用out DateTime lastRequestTime来声明一个变量,这个变量将在TryGetValue方法内部被赋值。然后我们可以在条件分支中使用这个变量。
注意:从C# 7.0开始,我们可以在方法调用中直接声明out变量,而不需要预先声明。所以代码中直接写out DateTime lastRequestTime是合法的。
总结:out关键字在这里用于从TryGetValue方法中获取与键clientIp对应的值(上一次请求的时间),同时通过方法的返回值判断是否成功获取到值。
过滤器(Filter) vs 中间件(Middleware)的区别
你说得很对!SimpleRateLimitFilter 的功能确实和中间件很像,但它们在使用场景、执行位置和功能上有重要区别。让我详细解释:
1. 执行位置和管道的区别
ASP.NET Core 请求管道:
请求 → 中间件1 → 中间件2 → ... → 路由中间件 → MVC框架 → 过滤器 → Action方法
具体对比:
| 组件 | 执行位置 | 访问权限 |
|---|---|---|
| 中间件 | MVC框架之前 | 只能访问 HttpContext |
| 过滤器 | MVC框架内部 | 可以访问 HttpContext + MVC特定对象 |
2. 功能对比表格
| 特性 | 过滤器(Filter) | 中间件(Middleware) |
|---|---|---|
| 执行时机 | MVC管道内部 | MVC管道之前 |
| MVC上下文 | ✅ 可访问 ActionContext、Controller 等 |
❌ 无法访问 |
| Action信息 | ✅ 知道具体哪个Controller/Action | ❌ 不知道具体Action |
| 模型绑定 | ✅ 可以访问绑定后的模型数据 | ❌ 无法访问 |
| 依赖注入 | ✅ 支持构造函数注入 | ✅ 支持构造函数注入 |
| 应用范围 | MVC Action级别 | 全局所有请求 |
| 执行顺序 | 过滤器有不同类型和顺序 | 按注册顺序执行 |
3. 代码示例对比
过滤器方式(我们刚才写的):
public class SimpleRateLimitFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// 可以访问MVC特定对象
var actionName = context.ActionDescriptor.DisplayName;
var controllerName = context.Controller.GetType().Name;
var modelState = context.ModelState;
// 限流逻辑...
await next();
}
}
中间件方式:
public class SimpleRateLimitMiddleware
{
private readonly RequestDelegate _next;
public SimpleRateLimitMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 只能访问HttpContext,不知道具体的MVC信息
var path = context.Request.Path;
var method = context.Request.Method;
// 限流逻辑...
await _next(context);
}
}
4. 注册方式对比
过滤器注册(Program.cs):
// 方式1:全局注册
builder.Services.Configure<MvcOptions>(opt =>
{
opt.Filters.Add<SimpleRateLimitFilter>();
});
// 方式2:特性标记(选择性应用)
[ServiceFilter(typeof(SimpleRateLimitFilter))]
public class MyController : ControllerBase
{
// ...
}
中间件注册(Program.cs):
var app = builder.Build();
// 在管道中按顺序注册
app.UseMiddleware<SimpleRateLimitMiddleware>();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
5. 在限流场景中的选择考虑
适合使用过滤器的情况:
// 需要对特定Action进行不同限流策略
[RateLimit("api_v1", 10, 60)] // 自定义特性
public IActionResult ApiV1() { ... }
[RateLimit("api_v2", 100, 60)] // 不同限流规则
public IActionResult ApiV2() { ... }
// 需要基于Action信息做限流
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var actionName = context.ActionDescriptor.RouteValues["action"];
// 对不同的Action应用不同的限流规则
if (actionName == "Login")
ApplyStrictLimit();
else
ApplyNormalLimit();
await next();
}
适合使用中间件的情况:
// 全局统一的限流规则
public async Task InvokeAsync(HttpContext context)
{
// 对所有请求应用相同的限流规则
var clientIp = context.Connection.RemoteIpAddress?.ToString();
if (!IsAllowed(clientIp))
{
context.Response.StatusCode = 429;
await context.Response.WriteAsync("Too Many Requests");
return;
}
await _next(context);
}
6. 实际项目中的使用建议
推荐使用过滤器的场景:
- API限流:需要基于不同的API端点设置不同限流规则
- 业务逻辑限流:需要访问Action参数、模型数据等
- 细粒度控制:某些Action需要限流,某些不需要
- MVC特定功能:需要与模型验证、授权等MVC功能配合
推荐使用中间件的场景:
- 全局限流:对所有请求应用相同规则
- 基础设施限流:在进入业务逻辑前就进行限制
- 性能优化:尽早拒绝请求,减少后续处理开销
- 非MVC请求:需要对静态文件、SignalR等也进行限流
7. 我们的 SimpleRateLimitFilter 分析
在我们刚才的限流器例子中:
为什么选择过滤器?
- 我们可能希望将来对不同API应用不同限流规则
- 可能需要在某些特殊情况下跳过限流(如管理员)
- 可能想基于Action参数做更复杂的限流逻辑
如果用中间件会怎样?
- 实现更简单,性能可能更好(更早拦截)
- 但无法针对不同API做差异化限流
- 无法访问MVC的特定信息
总结
虽然过滤器和中间件在功能上有重叠,但它们的设计目的不同:
- 中间件:处理HTTP层面的横切关注点
- 过滤器:处理MVC层面的横切关注点
选择哪个取决于你的具体需求:
- 如果需要MVC上下文信息 → 选择过滤器
- 如果只需要HTTP信息 → 两者都可以,中间件可能更轻量
- 如果需要全局统一处理 → 选择中间件
- 如果需要细粒度控制 → 选择过滤器
在我们的限流器场景中,使用过滤器是合理的选择,因为它为将来的功能扩展留下了更多可能性!

浙公网安备 33010602011771号