web-dev-qa-db-fra.com

Délai d'expiration d'une demande Web Api?

Comme MVC WebApi s'exécute sur le pipeline asynchrone ASP.NET, ce qui signifie le délai d'exécution n'est pas pris en charge .

Dans MVC, j'utilise le [AsyncTimeout] filtre, WebApi ne l'a pas. Alors, comment puis-je expirer une demande dans WebApi?

15
DalSoft

En s'appuyant sur la suggestion de Mendhak, il est possible de faire ce que vous voulez, mais pas exactement de la façon dont vous le souhaitez sans sauter à travers quelques cerceaux. Le faire sans un filtre pourrait ressembler à ceci:

public class ValuesController : ApiController
{
    public async Task<HttpResponseMessage> Get( )
    {
        var work    = this.ActualWork( 5000 );
        var timeout = this.Timeout( 2000 );

        var finishedTask = await Task.WhenAny( timeout, work );
        if( finishedTask == timeout )
        {
            return this.Request.CreateResponse( HttpStatusCode.RequestTimeout );
        }
        else
        {
            return this.Request.CreateResponse( HttpStatusCode.OK, work.Result );
        }
    }

    private async Task<string> ActualWork( int sleepTime )
    {
        await Task.Delay( sleepTime );
        return "work results";
    }

    private async Task Timeout( int timeoutValue )
    {
        await Task.Delay( timeoutValue );
    }
}

Ici, vous recevrez un délai d'attente car le "travail" que nous effectuons prendra plus de temps que le délai d'attente.

Pour faire ce que vous voulez avec un attribut est possible, mais pas idéal. C'est la même idée de base qu'auparavant, mais le filtre pourrait en fait être utilisé pour exécuter l'action via la réflexion. Je ne pense pas que je recommanderais cette route, mais dans cet exemple artificiel, vous pouvez voir comment cela pourrait être fait:

public class TimeoutFilter : ActionFilterAttribute
{
    public int Timeout { get; set; }

    public TimeoutFilter( )
    {
        this.Timeout = int.MaxValue;
    }
    public TimeoutFilter( int timeout )
    {
        this.Timeout = timeout;
    }


    public override async Task OnActionExecutingAsync( HttpActionContext actionContext, CancellationToken cancellationToken )
    {

        var     controller     = actionContext.ControllerContext.Controller;
        var     controllerType = controller.GetType( );
        var     action         = controllerType.GetMethod( actionContext.ActionDescriptor.ActionName );
        var     tokenSource    = new CancellationTokenSource( );
        var     timeout        = this.TimeoutTask( this.Timeout );
        object result          = null;

        var work = Task.Run( ( ) =>
                             {
                                 result = action.Invoke( controller, actionContext.ActionArguments.Values.ToArray( ) );
                             }, tokenSource.Token );

        var finishedTask = await Task.WhenAny( timeout, work );

        if( finishedTask == timeout )
        {
            tokenSource.Cancel( );
            actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.RequestTimeout );
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse( HttpStatusCode.OK, result );
        }
    }

    private async Task TimeoutTask( int timeoutValue )
    {
        await Task.Delay( timeoutValue );
    }
}

Cela pourrait ensuite être utilisé comme ceci:

[TimeoutFilter( 10000 )]
public string Get( )
{
    Thread.Sleep( 5000 );
    return "Results";
}

Cela fonctionne pour les types simples (par exemple chaîne), nous donnant: <z:anyType i:type="d1p1:string">Results</z:anyType> dans Firefox, mais comme vous pouvez le voir, la sérialisation n'est pas idéale. L'utilisation de types personnalisés avec ce code exact sera un peu problématique en ce qui concerne la sérialisation, mais avec un peu de travail, cela pourrait probablement être utile dans certains scénarios spécifiques. Le fait que les paramètres d'action se présentent sous la forme d'un dictionnaire au lieu d'un tableau peut également poser des problèmes en termes de classement des paramètres. De toute évidence, il serait préférable de bénéficier d'un véritable soutien.

En ce qui concerne les éléments vNext, ils envisagent peut-être d'ajouter la possibilité de faire des délais d'attente côté serveur pour l'API Web, car les contrôleurs MVC et API sont en cours d'unification. S'ils le font, ce ne sera probablement pas par le biais de System.Web.Mvc.AsyncTimeoutAttribute classe, car ils suppriment explicitement les dépendances sur System.Web.

À ce jour, il ne semble pas que l'ajout d'un System.Web.Mvc entrée dans le project.json le fichier fonctionne, mais cela pourrait bien changer. Si tel est le cas, bien que vous ne puissiez pas utiliser le nouveau framework optimisé pour le cloud avec un tel code, vous pourrez peut-être utiliser l'attribut AsyncTimeout sur du code qui est uniquement destiné à fonctionner avec le .NET complet cadre.

Pour ce que ça vaut, c'est ce que j'ai essayé d'ajouter à project.json. Peut-être qu'une version spécifique l'aurait rendu plus heureux?

"frameworks": {
    "net451": {
        "dependencies": { 
            "System.Web.Mvc": ""
        }
    }
}

Une référence à celui-ci apparaît dans la liste des références de l'Explorateur de solutions, mais il le fait avec un point d'exclamation jaune indiquant un problème. L'application elle-même renvoie 500 erreurs tant que cette référence reste.

17
Adam

Avec WebAPI, vous gérez généralement les délais d'attente du côté client , plutôt que du côté serveur. En effet, et je cite :

La façon d'annuler les requêtes HTTP consiste à les annuler directement sur HttpClient. La raison étant que plusieurs requêtes peuvent réutiliser les connexions TCP au sein d'un même HttpClient) et que vous ne pouvez donc pas annuler en toute sécurité une seule requête sans affecter également d'autres requêtes.

Vous pouvez contrôler le délai d'expiration des demandes - je pense que c'est sur le HttpClientHandler si je me souviens bien.

Si vous avez vraiment besoin d'implémenter un délai d'expiration du côté de l'API lui-même, je recommanderais de créer un thread pour faire votre travail, puis de l'annuler après une certaine période. Vous pouvez par exemple le mettre dans un Task, créer votre tâche 'timeout' en utilisant Task.Wait et utilise Task.WaitAny pour que le premier revienne. Cela peut simuler un délai d'attente.

De même, si vous effectuez une opération spécifique, vérifiez si elle prend déjà en charge les délais d'expiration. Très souvent, je vais effectuer un HttpWebRequest à partir de mon WebAPI, et spécifier sa propriété Timeout .

10
Mendhak

Simplifiez-vous la vie, ajoutez dans votre contrôleur de base la méthode suivante:

    protected async Task<T> RunTask<T>(Task<T> action, int timeout) {
        var timeoutTask = Task.Delay(timeout);
        var firstTaskFinished = await Task.WhenAny(timeoutTask, action);

        if (firstTaskFinished == timeoutTask) {
            throw new Exception("Timeout");
        }

        return action.Result;
    }

Désormais, chaque contrôleur héritant de votre contrôleur de base peut accéder à la méthode RunTask. Maintenant, dans votre API, appelez la méthode RunTask comme ça:

    [HttpPost]
    public async Task<ResponseModel> MyAPI(RequestModel request) {
        try {
            return await RunTask(Action(), Timeout);
        } catch (Exception e) {
            return null;
        }
    }

    private async Task<ResponseModel> Action() {
        return new ResponseModel();
    }
0
Daniel Dantas