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>(); // 第二个执行
});

执行顺序很重要

  1. MyExceptionFilter 先执行,处理异常并设置响应
  2. LogExceptionFilter 后执行,记录日志

执行流程

sequenceDiagram participant Client as 客户端 participant Controller as Test1Controller participant Filter1 as MyExceptionFilter participant Filter2 as LogExceptionFilter participant Response as 响应 Client->>Controller: GET /api/Test1/Test1 Controller->>Controller: 执行Test1()方法 Controller->>Controller: 抛出FileNotFoundException Note over Controller: 异常发生,开始过滤器链 Controller->>Filter1: 触发OnExceptionAsync Note over Filter1: 处理异常<br/>设置统一响应格式<br/>标记异常已处理 Filter1->>Filter2: 继续执行下一个过滤器 Note over Filter2: 记录异常到日志文件 Filter2->>Response: 返回处理后的响应 Response->>Client: { "code": 500, "message": "..." }

切面编程的优势

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'."
}

扩展应用场景

这种切面编程思想还可以用于:

  1. 身份认证:在进入业务方法前验证用户权限
  2. 性能监控:记录每个方法的执行时间
  3. 缓存:在方法执行前检查缓存
  4. 数据验证:统一验证请求参数
  5. 事务管理:自动管理数据库事务

总结

这个例子完美展示了切面编程的核心价值

  • 非侵入式:不改动原有业务代码
  • 集中管理:异常处理逻辑集中在一处
  • 灵活配置:根据不同环境返回不同信息
  • 职责分离:业务逻辑与横切关注点分离

通过过滤器机制,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 发生异常了

总结

这个过滤器实现了:

  1. 前置处理 - 在Action执行前运行
  2. 后置处理 - 在Action执行后运行
  3. 异常监控 - 可以捕获并处理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 执行成功

代码执行顺序:

  1. MyActionFilter1.OnActionExecutionAsync 开始执行
  2. 输出 "MyActionFilter1 执行前代码"
  3. 调用 await next() → 进入 MyActionFilter2
  4. MyActionFilter2.OnActionExecutionAsync 开始执行
  5. 输出 "MyActionFilter2 执行前代码"
  6. 调用 await next() → 进入实际的 Action 方法
  7. 执行 Test1() 方法,输出 "我在Api中的Test1方法里"
  8. 返回 MyActionFilter2await next() 之后
  9. 输出 "MyActionFilter2 执行成功"
  10. 返回 MyActionFilter1await next() 之后
  11. 输出 "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 后

重要特点

  1. 执行顺序:按照注册顺序执行前置代码,逆序执行后置代码
  2. 异常传播:异常会从内层向外层传播,每个过滤器都能捕获到
  3. 全局影响:两个过滤器都会应用到所有的 Controller 和 Action
  4. 嵌套执行await next() 会调用管道中的下一个组件

未命名绘图-第 1 页

事务

  • 先写一个演示示例
// 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 添加成功
    事务已提交
    

总结

这个解决方案实现了:

  1. 灵活的事务控制:通过 [NotTransactional] 特性可以灵活控制哪些方法不需要事务
  2. 全局过滤器:通过注册全局过滤器自动为所有Action添加事务控制
  3. 异常处理:自动处理异常并回滚事务
  4. 异步支持:使用 TransactionScopeAsyncFlowOption.Enabled 支持异步操作
  5. 清晰的测试案例:提供了三种不同场景的测试方法

这样你就可以通过简单的特性标记来控制事务行为了!

简单限流器实现

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. 验证测试

  1. 第一次请求:立即返回成功
  2. 立即第二次请求:返回429限流错误
  3. 等待2秒后再次请求:返回成功
  4. 快速连续请求:只有第一个成功,后续都被限流

特点说明

这个简单限流器的特点:

  • 基于时间间隔:要求请求间隔至少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();
  • 作用:作为同步的标识对象
  • 要求
    • 必须是引用类型(不能是值类型)
    • 通常是 privatereadonly
    • 静态字段用于跨实例同步

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){....} 的作用:

  1. 线程安全:确保同一时间只有一个线程执行临界区代码
  2. 数据一致性:防止多线程同时修改共享数据
  3. 避免竞态条件:确保操作的原子性

在限流器场景中,锁确保了:

  • 请求计数的准确性
  • 时间戳更新的正确性
  • 字典操作的线程安全

这是一个在多线程环境下保护共享资源的经典用法!

out关键字的作用

在C#中,out关键字用于指示参数通过引用来传递,并且在方法返回前必须被赋值。它通常用于方法需要返回多个值的情况。

_requestHistory.TryGetValue(clientIp, out DateTime lastRequestTime)这行代码中:

  • TryGetValueDictionary类的一个方法,它尝试获取与指定键关联的值。
  • 如果字典中存在该键,则方法返回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上下文 ✅ 可访问 ActionContextController ❌ 无法访问
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信息 → 两者都可以,中间件可能更轻量
  • 如果需要全局统一处理 → 选择中间件
  • 如果需要细粒度控制 → 选择过滤器

在我们的限流器场景中,使用过滤器是合理的选择,因为它为将来的功能扩展留下了更多可能性!

posted @ 2025-10-21 15:07  清安宁  阅读(2)  评论(0)    收藏  举报