环境

  • .NET SDK 2.2

参考

步骤

  1. 在项目内使用NuGet添加AspNetCoreRateLimit包
    • 注意.NET Core版本必须为2.2及以上,2.1版本安装会报错
  2. 编辑Startup.cs,DI组件,并启用中间件
    //using ...
    using AspNetCoreRateLimit;
    
    public void ConfigureServices(IServiceCollection services) {
        // ...
        services.AddOptions();
        services.AddMemoryCache();
    
        services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
        services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
    
        services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
        services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
    
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        // ...   
        app.UseIpRateLimiting(); //注意在UseMvc()前注册
    
        app.UseMvc();
    }
  3. 编辑appsettings.json,设置具体的IP限制
    {
        ...
        "IpRateLimiting": {
            "EnableEndpointRateLimiting": true, //此字段设定为false则全局有效,会限制所有*的规则。例如:如果频率限制到每秒5次,则会纪录向任何端点的任何HTTP请求
                                                //这里设置为true可以对每个端点分别设置请求频率
            "StackBlockedRequests": false,
            "RealIpHeader": "X-Real-IP",
            "ClientIdHeader": "X-ClientId",
            "HttpStatusCode": 429,
            "GeneralRules": [
                { //设置hello路径的请求频率为每30秒3次请求,"*:"代表任何HTTP VERB,"/*"代表其后的参数
                    "Endpoint": "*:/hello/*",
                    "Period": "30s",
                    "Limit": 3
                }
            ]
        }
    }
    • 可以配置更多功能,例如白名单,更改计数方式,自定义返回内容等等,详细信息可以查看GIthub WIki
  4. 配置Action的路由
    namespace IP_restrict_test.Controllers {
        public class HomeController: Controller {
            //...
    
            [Route("/hello/")]
            public IActionResult Index() {
                return View();
            }
            
            //...
        }
    }
  5. 生成并运行,测试频率限制功能
    • 可以看到,连续打开或者刷新页面3次后,第4次打开页面会提示访问频率过快。

目标是让网站在后台每月进行一次用户统计,考虑过直接在数据库操作,但是最后还是选择直接在程序内实现。

虽然功能是实现了,但是看过文档和其他解释后还没能理解,所以这里只写How不写Why。

环境

  • dotnet core 2.1

参考

步骤

官方一共写了三种后台任务用例,分别为Timed background tasks(定时任务)、scoped service background tasks(有作用域服务) 和 Queued background tasks(队列任务)。

对于定时向数据库写入内容,我的实现方式是 timed bg task + scoped service 结合,即在定时任务内执行有作用域的服务(数据库操作)。一开始尝试只用定时方式,但是数据库的上下文依赖注入一直失败,之后在StackOverflow上找到了需要实现一样功能的人,发现他的写法结合了两种方式,最后添加了scoped service方式后成功写入数据库。

  1. 首先实现scoped service,在类中添加接口,以及实现接口的类,在类中写入数据库相关代码
    public class DbOperation {
        internal interface IScopedProcessingService {
            void DoWork();
        }
    
        internal class ScopedProcessingService : IScopedProcessingService {
            private readonly TestDbContext db;
    
            public ScopedProcessingService(TestDbContext _db) {
                db = _db;
            }
    
            ... ...
    
            public void DoWork() {
                var user = new User() {
                    name = GetRandomName(),
                    age = GetRandomAge()
                };
    
                db.Users.Add(user);
                db.SaveChanges();
            }
        }
    }

    DbOperation中定义了接口IScopedProcessingService和接口实现ScopedProcessingServiceScopedProcessingService中实现了接口中声明的,向数据库写入新条目的方法DoWork()

  2. 新建定时任务类,并在定时执行的方法中调用数据库操作服务
    internal class WriteToDb : IHostedService, IDisposable {
        private readonly ILogger logger;
        private Timer timer;
    
        public WriteToDb(ILogger<WriteToDb> _logger, IServiceProvider services) {
            Services = services;
            logger = _logger;
        }
    
        public IServiceProvider Services { get; }
    
        public Task StartAsync(CancellationToken cancellationToken) {
            logger.LogInformation("Starting");
    
            timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromHours(1)); //新建定时任务
    
            return Task.CompletedTask;
        }
    
        public void DoWork(object state) { //数据库服务
            using (var scope = Services.CreateScope()) {
                var scopedProcessingService =
                    scope.ServiceProvider.GetRequiredService<DbOperation.IScopedProcessingService>();
    
                scopedProcessingService.DoWork(); //调用写入新条目的方法
            }
            logger.LogInformation("Insert New User");
        }
    
        public Task StopAsync(CancellationToken cancellationToken) {
            logger.LogInformation("Stopping");
    
            timer?.Change(Timeout.Infinite, 0);
    
            return Task.CompletedTask;
        }
    
        public void Dispose() {
            timer?.Dispose();
        }
    }

    IHostedService中,StartAsync()StopAsync() 是必须实现的方法。应用程序准备好时会触发 StartAsync(),在应用程序“优雅地”关闭(原文:graceful shutdown)时,会触发 StopAsync()

    通过修改新建定时器的最后一个参数(代码中为TimeSpan.FromHours(1)) 来改变任务执行的周期,修改第三个参数(代码中为TimeSpan.Zero)来更改程序启动后执行任务的延时——指定为TimeSpan.Zero则在程序启动后立刻执行一次任务。

  3. 在Startup.cs中依赖注入
    public void ConfigureServices(IServiceCollection services) {
        ... ...
    
        services.AddHostedService<WriteToDb>();
        services.AddScoped<DbOperation.IScopedProcessingService, DbOperation.ScopedProcessingService>();
    
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

Continue reading “ASP.NET Core 通过 IHostedService 实现后台任务运行”

有一视图模型——控制器先获取数据库中的信息,再将信息通过构造函数参数传递给这个视图模型,视图模型拿到数据后在类内生成对应的List<SelectItem>列表实例,这一顿操作的目的是在视图中生成对应本模型某属性的DropDownList

模型的构造函数一开始是这么写的:

但是在页面将信息post回控制器时,会报“无法将模型实例化”的错误:

InvalidOperationException: Could not create an instance of type ‘[我的模型]’. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the ‘[我的模型实例]’ parameter a non-null default value.

Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.CreateModel(ModelBindingContext bindingContext)

之后尝试过将列表直接给ViewBag,但是不能实现模型的绑定(假如数据库中用户的性别为男,在页面生成后DropDownList无法直接显示“男”),或许有其他方法(比如在Controller先对List进行IsSelected处理,但是比较繁琐),没有进行更多尝试(其实就是想一个模型就完事)。

之后在StackOverflow找到解决方法:Passing data into an Asp.Net Core bound model

解决方法很简单,将带参数的构造函数写为重载函数:

回答者的解释:

在模型绑定过程中,引擎不能识别带参数的构造函数

原文:during model binding engine can’t identify with parameter constructor.