覆盖默认的 asp.net 核心取消令牌或更改请求的默认超时 方法 1:操作过滤器方法 2:中间件

问题描述

在我的 asp.net core 5.0 应用程序中,我调用一个可能需要一些时间来处理的异步方法

 await someObject.LongRunningProcess(cancelationToken);

但是,我希望该方法在 5 秒后超时。我知道,我可以使用“CancellationTokenSource”,而不是通过 asp.net 核心操作传递的“cancelationToken”:

var s_cts = new CancellationTokenSource();
s_cts.CancelAfter(TimeSpan.FromSeconds(5);
    
await someObject.LongRunningProcess(s_cts );

是否可以将“CancellationTokenSource”用作所有 asp.net 核心请求的认“取消令牌”策略?我的意思是覆盖作为动作参数传递的那个?

或者是否可以更改 asp.net core 5.0 中所有请求的认超时时间?

[更新]

enter image description here

解决方法

解决这个问题的一种方法是将该逻辑包装在一个类中。编写一个运行具有可配置超时的任务的类。

然后在 DI 中注册它,然后在任何你想重用配置的地方使用它。

public class TimeoutRunner
{
    private TimeoutRunnerOptions _options;

    public TimeoutRunner(IOptions<TimeoutRunnerOptions> options)
    {
        _options = options.Value;
    }

    public async Task<T> RunAsync<T>(Func<CancellationToken,Task<T>> runnable,CancellationToken cancellationToken = default)
    {
        // cancel the task as soon as one of the tokens is set
        var timeoutCts = new CancellationTokenSource();
        var token = timeoutCts.Token;
        if (cancellationToken != default)
        {
            timeoutCts.CancelAfter(_options.Timeout);
            var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token,cancellationToken);
            token = combinedCts.Token;
        }

        return await runnable(token);
    }
}

internal static class ServiceCollectionExtensions
{
    public static IServiceCollection AddTimeoutRunner(this IServiceCollection services,Action<TimeoutRunnerOptions> configure = null)
    {
        if (configure != null)
        {
            services.Configure<TimeoutRunnerOptions>(configure);
        }

        return services.AddTransient<TimeoutRunner>();
    }
}

public class TimeoutRunnerOptions
{
    public int TimeoutSeconds { get; set; } = 10;
    public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}

然后你会在 Startup 类中注册它,

public void ConfigureServices(IServiceCollection services)
{
    services.AddTimeoutRunner(options =>
    {
        options.TimeoutSeconds = 10;
    });
}

然后在需要全局选项的任何地方使用它:

public class MyController : ControllerBase
{
    private TimeoutRunner _timeoutRunner;

    public MyController(TimeoutRunner timeoutRunner)
    {
        _timeoutRunner = timeoutRunner;
    }

    public async Task<IActionResult> DoSomething(CancellationToken cancellationToken)
    {
        await _timeoutRunner.RunAsync(
            async (CancellationToken token) => {
                await Task.Delay(TimeSpan.FromSeconds(20),token);
            },cancellationToken
        );
        return Ok();
    }
}

在每个动作分派前运行一个任务

方法 1:操作过滤器

我们可以使用动作过滤器在每个请求之前/之后运行任务。

public class ApiCallWithTimeeotActionFilter : IAsyncActionFilter
{
    private TimeoutRunner _runner;

    public ApiCallWithTimeeotActionFilter(TimeoutRunner runner)
    {
        _runner = runner;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context,ActionExecutionDelegate next)
    {
        var result = await _runner.RunAsync(
            async (CancellationToken token) =>
            {
                await Task.Delay(TimeSpan.FromSeconds(20),token);
                return 42;
            },default
        );
        await next();
    }
}

然后使用它用 [TypeFilter(typeof(MyAction))] 注释一个类:

[TypeFilter(typeof(ApiCallWithTimeeotActionFilter))]
public class MyController : ControllerBase { /* ... */ }

方法 2:中间件

另一种选择是使用中间件

class ApiCallTimeoutMiddleware
{
    private TimeoutRunner _runner;

    public ApiCallTimeoutMiddleware(TimeoutRunner runner)
    {
        _runner = runner;
    }

    public async Task InvokeAsync(HttpContext context,RequestDelegate next)
    {
        // run a task before every request
        var result = await _runner.RunAsync(
            async (CancellationToken token) =>
            {
                await Task.Delay(TimeSpan.FromSeconds(20),default
        );
        await next(context);
    }
}

然后在 Startup.Configure 方法中附加中间件:

public void Configure(IApplicationBuilder app)
{
    app.UseMiddleware<ApiCallTimeoutMiddleware>();
    app.UseRouting();
    app.UseEndpoints(e => e.MapControllers());
}
,

自定义传递给操作的 CancellationToken

您需要替换将 CancellationTokenModelBinderProvider 令牌绑定到操作的 HttpContext.RequestAborted 参数的默认 CancellationToken

这涉及创建自定义 IModelBinderProvider。然后我们可以用我们自己的替换默认绑定结果。

public class TimeoutCancellationTokenModelBinderProvider : IModelBinderProvider
{
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context?.Metadata.ModelType != typeof(CancellationToken))
        {
            return null;
        }

        var config = context.Services.GetRequiredService<IOptions<TimeoutOptions>>().Value;
        return new TimeoutCancellationTokenModelBinder(config);
    }

    private class TimeoutCancellationTokenModelBinder : CancellationTokenModelBinder,IModelBinder
    {
        private readonly TimeoutOptions _options;

        public TimeoutCancellationTokenModelBinder(TimeoutOptions options)
        {
            _options = options;
        }

        public new async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            await base.BindModelAsync(bindingContext);
            if (bindingContext.Result.Model is CancellationToken cancellationToken)
            {
                // combine the default token with a timeout
                var timeoutCts = new CancellationTokenSource();
                timeoutCts.CancelAfter(_options.Timeout);
                var combinedCts =
                    CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token,cancellationToken);
                
                // We need to force boxing now,so we can insert the same reference to the boxed CancellationToken
                // in both the ValidationState and ModelBindingResult.
                //
                // DO NOT simplify this code by removing the cast.
                var model = (object)combinedCts.Token;
                bindingContext.ValidationState.Clear();
                bindingContext.ValidationState.Add(model,new ValidationStateEntry() { SuppressValidation = true });
                bindingContext.Result = ModelBindingResult.Success(model);
            }
        }
    }
}

class TimeoutOptions
{
    public int TimeoutSeconds { get; set; } = 30; // seconds
    public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutSeconds);
}

然后将此提供程序添加到 Mvc 的默认绑定程序提供程序列表中。它需要在所有其他人之前运行,所以我们在开头插入它。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.Configure<MvcOptions>(options =>
    {
        options.ModelBinderProviders.RemoveType<CancellationTokenModelBinderProvider>();
        options.ModelBinderProviders.Insert(0,new TimeoutCancellationTokenModelBinderProvider());
    });
    // remember to set the default timeout
    services.Configure<TimeoutOptions>(configuration => { configuration.TimeoutSeconds = 2; });
}

现在 ASP.NET Core 将在看到 CancellationToken 类型的参数时运行您的绑定器,该参数将 HttpContext.RequestAborted 令牌与我们的超时令牌结合在一起。每当其组件之一被取消(由于超时或请求中止,以先取消者为准)时,就会触发组合令牌

[HttpGet("")]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
    await Task.Delay(TimeSpan.FromSeconds(5),cancellationToken); // throws TaskCanceledException after 2 seconds
    return Ok("hey");
}

参考文献:

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...