Search code examples
c#.net-corecontrollertimeoutaction-filter

Controller timeout on dotnet core


I have an web api on dotnet core 3.1 and I want to set different timeout specific controller action.I try to create an actionfilter something like below

public class TimeOutAttribute : ActionFilterAttribute
{
    private readonly int _timeout;

    public TimeOutAttribute(int timeout)
    {
        _timeout = timeout;
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        try
        {
            var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(_timeout));
            await Task.Run(async () => await next(), cts.Token);
        }
        catch (TaskCanceledException)
        {
            var request = context.HttpContext.Request;
            var message = $"Action exceeded the set timeout limit {_timeout} milisecond for {request.PathBase}{request.Path}";
            throw new ActionTimeOutException(message);
        }
    }
}

and I use it on controller method

[TimeOut(100)]
public async Task<IActionResult> Get()
{
}

Although Get method takes more than 100 ms I can not get exception.Could you see any problem on code or If you have a another options for controller timeout Im ready to try it


Solution

  • Could you see any problem on code

    Yes; passing a cancellation token to Task.Run isn't going to work. The token for that method only cancels the scheduling of the task to the thread pool, not the delegate itself.

    The only way to cancel your delegate code is to have your delegate take a CancellationToken and observe that (usually by passing it to other methods). I have a blog post series on the subject of cancellation.

    If you have a another options for controller timeout Im ready to try it

    So, that's a harder problem.

    There is built-in support for CancellationToken in ASP.NET Core; you can add a CancellationToken argument to any controller action method. However, this token doesn't have anything to do with timeouts; it cancels if the user request is aborted (e.g., the user closes their browser).

    One approach is to add a CancellationToken parameter to your action method and have your filter modify the model binding results, replacing the provided CancellationToken. Something like this should work:

    public sealed class TimeoutAttribute : ActionFilterAttribute
    {
        private readonly TimeSpan _timeout;
        public TimeoutAttribute(int timeoutMilliseconds) => _timeout = TimeSpan.FromMilliseconds(timeoutMilliseconds);
    
        public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            // Find the CancellationToken argument passed to the action
            var cancellationTokenArgument = context.ActionArguments.FirstOrDefault(x => x.Value is CancellationToken);
            if (cancellationTokenArgument.Key == null || cancellationTokenArgument.Value == null)
                throw new InvalidOperationException("TimeoutAttribute must be used on an action with a CancellationToken");
    
            // Create a new CancellationToken that will be cancelled if *either* the user disconnects *or* a timeout
            using var cts = CancellationTokenSource.CreateLinkedTokenSource((CancellationToken)cancellationTokenArgument.Value);
            cts.CancelAfter(_timeout);
    
            // Replace the action's CancellationToken argument with our own
            context.ActionArguments[cancellationTokenArgument.Key] = cts.Token;
    
            await next();
        }
    }
    

    This will work - to an extent. Your host (i.e., IIS) likely has its own timeout, and this timeout is completely separate from that one.