Skip to main content

8 ASP.NET Core

缓存

客户端响应缓存

使用 cache-control 响应报文头, 实现客户端缓存这一请求的结果一段时间, cache-control: max-age=20 表示在浏览器中缓存 20 秒

// 设置缓存 20 秒
[ResponseCache(Duration = 20)]
[HttpGet}
public int GetNum()
{
    return 10;
}

服务器端响应缓存

app.UseCors();

// 启用服务器端响应缓存
app.UseResponseCaching();

app.MapControllers();

当使用"禁用缓存"时, 浏览器端请求时带有请求头 cache-control: no-cache, 即使启用了服务器端响应缓存, 服务器也不会从缓存中获取数据

服务器端响应缓存的问题

  • 无法解决恶意请求给服务器带来的压力
  • 响应状态码为 200 的 GET 或 HEAD 响应才可能被缓存, 报文头中不能含有 Authorization, Set-Cookie 等

内存缓存

// Program.cs
builder.Services.AddMemoryCache();

// 服务类
private readonly IMemoryCache _memoryCache;

public WeatherForecastController(IMemoryCache memoryCache)
{
    _memoryCache = memoryCache;
}

public async Task<ActionResult<Book>> GetBook(int id)
{
    // 从缓存取数据, 如果没有缓存, 则调用方法, 并返回数据(同时保存到缓存)
    var book = await _memoryCache.GetOrCreateAsync("Book_" + id, async entry =>
    {
        // 没有缓存, 查询数据
        Console.WriteLine($"没有缓存, 查询数据");
        var bookData = id switch
        {
            1 => new Book(1, "1"),
            2 => new Book(2, "2"),
            3 => new Book(3, "3"),
            _ => new Book(4, "4")
        };

        return bookData;
    });
  
    if (book == null)
    {
        return NotFound("查询不到数据");
    }

    return book;
}

缓存过期时间策略

绝对过期策略

var book = await _memoryCache.GetOrCreateAsync("Book_" + id, async entry =>
{
    var bookData = id switch
    {
        1 => new Book(1, "1"),
        2 => new Book(2, "2"),
        3 => new Book(3, "3"),
        _ => null
    };
    
    // 缓存时间为 1 分钟
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);

    return bookData;
});

滑动过期策略

// 从缓存取数据, 如果没有缓存, 则调用方法, 并返回数据(同时保存到缓存)
var book = await _memoryCache.GetOrCreateAsync("Book_" + id, async entry =>
{
    var bookData = id switch
    {
        1 => new Book(1, "1"),
        2 => new Book(2, "2"),
        3 => new Book(3, "3"),
        _ => null
    };

    // 在缓存没过期的时间, 请求一次的时候, 缓存有效期会自动延长
    entry.SlidingExpiration = TimeSpan.FromSeconds(10);

    return bookData;
});

同时设置两种策略

使用滑动过期时间策略, 如果一个缓存项一直被频繁访问, 那么这个缓存项就会一直被续期而不过期. 可以对一个缓存项同时设定滑动过期时间和绝对过期时间, 并且把绝对过期时间设定的比滑动过期时间长, 这样缓存项的内容会在绝对过期时间内随着访问被滑动续期, 但是一旦超过了绝对过期时间, 缓存项就会被删除

缓存问题

缓存穿透

缓存穿透是由于"查询不到的数据用 null 表示" 导致的, 因此解决方法是, 将"查不到"也当成数据放入缓存, 使用 GetOrCreateAsync() 方法可以解决这种问题, 因为这个方法会把 null 也当成合法的缓存值

缓存雪崩

缓存项集中过期引起的缓存雪崩, 解决方法是在基础过期时间之上, 加入一个随机的过期时间

// 不使用 new Random(), 高频访问时可能造成随机数是一样的
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(10, 20));


配置系统

默认添加的配置提供者

ASP.NET Core 默认添加了常用的提供者, 按以下顺序:

  • 加载现有的 IConfiguration
  • 加载项目根目录下的 appsettings.json
  • 加载项目根目录下的 appsettings.{Environment}.json
    • 环境变量 ASPNETCORE_ENVIRONMENT 的值
      • Development(开发环境)
      • Staging(测试环境)
      • Production(生产环境)
    • 代码中读取当前环境
      • 在 Program.cs 中
        • app.Environment.EnvironmentName
        • app.Environment.IsDevelopment()
      • 在其它代码文件中
        • 注入 IWebHostEnvironment 即可
  • 当程序运行在开发环境下, 程序会加载"用户机密"配置
  • 加载环境变量中的配置
  • 加载命令行中的配置


EF Core

分层项目中 EF Core 迁移的问题

可能会出现"No DbContext was found in assembly", "Unable to create an object of type 'MyDbContext'"等错误提示, 可以采用 IDesignTimeDbContextFactory 接口来解决问题

当项目中存在一个 IDesignTimeDbContextFactory 接口的实现类时, 数据库迁移工具会调用这个实现类的 CreateDbContext() 方法来获取上下文对象, 使用这个上下文对象来连接数据库

// 数据库项目安装 Nuget 包: Microsoft.EntityFrameworkCore.Tools, Microsoft.EntityFrameworkCore.SqlServer
public class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    // 只在做数据库迁移时会运行, 在正式运行程序时不会使用到这个类
    public MyDbContext CreateDbContext(string[] args])
    {
        DbContextOptionsBuilder<MyDbContext> builder = new();
        // 连接字符串可以从环境变更中读取, 也可以直接写死
        string connStr = Environment.GetEnvironmentVariable("ConnectionStrings:BooksEFCore");
        builder.UseSqlServer(connStr);
        return new MyDbContext(builder.Options);
    }
}


上下文池

初始化时的 AddDbContext() 是 Scope, 也可以使用 AddDbContextPool() 实现池化, 但也会有问题(可看视频或书197页)

Filter

ExceptionFilter(异常筛选器)

接口: IAsyncExceptionFilter

程序出现未处理异常时, 会执行 Filter

public class MyExceptionFilter : IAsyncExceptionFilter
{
    private readonly IWebHostEnvironment _webHostEnvironment;

    public MyExceptionFilter(IWebHostEnvironment webHostEnvironment)
    {
        _webHostEnvironment = webHostEnvironment;
    }

    public Task OnExceptionAsync(ExceptionContext context)
    {
        /*
         * context.Exception: 异常信息对象
         * context.ExceptionHandled: 赋值为 true, 则之后的 ExceptionFilter 不会再执行
         * context.Result: 返回给客户端的值
         */

        string msg;
        if (!_webHostEnvironment.IsDevelopment())
        {
            msg = context.Exception.Message;
        }
        else
        {
            msg = "服务器出现未知异常";
        }

        context.ExceptionHandled = true;
        context.Result = new ObjectResult(new
        {
            code = 500,
            message = msg,
        });

        return Task.CompletedTask;
    }
}

public class LogExceptionFilter : IAsyncExceptionFilter
{
    public LogExceptionFilter(IWebHostEnvironment webHostEnvironment)
    {
    }

    public Task OnExceptionAsync(ExceptionContext context)
    {
        Console.WriteLine(context.Exception.ToString());
        return Task.CompletedTask;
    }
}
// 需要注意添加顺序, 后添加的先执行
builder.Services.Configure<MvcOptions>(p =>
{
    p.Filters.Add<MyExceptionFilter>();
    p.Filters.Add<LogExceptionFilter>();
});

ActionFilter

接口: IAsyncActionFilter

在每个 Action 执行之前之后会执行 Filter

public class MyActionFilter1 : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        Console.WriteLine("MyActionFilter1: 开始执行");

        ActionExecutedContext result = await next();

        if (result.Exception != null)
        {
            Console.WriteLine("MyActionFilter1: 出现异常");
        }
        else
        {
            Console.WriteLine("MyActionFilter1: 执行成功");
        }
    }
}

public class MyActionFilter2 : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        Console.WriteLine("MyActionFilter2: 开始执行");

        ActionExecutedContext result = await next();

        if (result.Exception != null)
        {
            Console.WriteLine("MyActionFilter2: 出现异常");
        }
        else
        {
            Console.WriteLine("MyActionFilter2: 执行成功");
        }
    }
}

// 输出
MyActionFilter1: 开始执行
MyActionFilter2: 开始执行
MyActionFilter2: 执行成功
MyActionFilter1: 执行成功
// 执行 Filter 的顺序是按添加的顺序来执行的
builder.Services.Configure<MvcOptions>(p =>
{
    p.Filters.Add<MyActionFilter1>();
    p.Filters.Add<MyActionFilter2>();
});

 

自动启用事务的 ActionFilter

 

 

中间件