当新请求到达ASP.NET MVC 5时,如何中止旧请求处理?

时间:2017-06-07 14:31:13

标签: asp.net asp.net-mvc asp.net-mvc-5

我有一个包含数百个复选框和下拉菜单的表单(其中许多值的耦合在一起)。在操作中有更新机制来更新Session中的对象。此对象执行所有值的验证和耦合,例如,如果用户在一个输入字段中键入%50,我们可能会在下拉列表中添加3个新的SelectListItem。

一切正常,但如果使用开始非常快速地点击复选框(这是我们场景中的正常情况),控制器会在处理之前的帖子时获得多个帖子。幸运的是,我们只对最后一个POST感兴趣,因此当来自同一表单的更新请求到来时,我们需要一种方法来中止取消请求。

我尝试了什么: 1-阻止客户端在服务器仍在上一个服务器上工作时发出多个帖子。这是不可取的,因为它会在浏览器端出现明显的暂停。

2-有几种解决方案可以使用HASH代码或AntiForgeryToken阻止多个帖子后备。但是他们并不是我需要的东西,我需要中止正在进行的线程,而不是阻止传入的请求。

3-我尝试通过添加两个消息处理程序(一个在操作之前,另一个在执行操作之后)来扩展管道以保持哈希代码(或AntiForgeryToken)但问题仍然存在,即使我可以检测到正在进行的线程在同一个请求上工作,我无法中止该线程或将旧请求设置为Complete。

有什么想法吗?

3 个答案:

答案 0 :(得分:0)

基本上,您需要在单击复选框时设置超时。您可以让初始请求通过,但随后任何其他请求都会排队(或者在您的方案中第一个排队请求之后实际丢弃)并且不会运行该请求直到超时清除。

没有办法中止请求服务器端。每个请求都是幂等的。对于之前或之后发生的任何事情都没有固有的知识。服务器有多个线程来处理请求,并且会尽可能快地处理这些请求。无法处理请求的处理方式或响应的发送方式。第一个请求可能是接收响应的第三个请求,这仅仅取决于每个请求的处理方式。

答案 1 :(得分:0)

您正在尝试通过异步技术实现事务功能(即仅计算最后一个请求)。这是一个设计缺陷。

由于您拒绝在客户端阻止,因此您无法控制哪些请求首先处理,或者在客户端再次正确处理结果。

您可能会遇到这种情况:

  1. 客户端发送请求A
  2. 服务器开始处理请求B
  3. 客户端发送请求B
  4. 服务器开始处理请求B
  5. 服务器返回请求B的结果,客户端也相应地更改
  6. 服务器返回请求A的结果,并且客户端相应地更改(并撤消由请求B产生的先前更改)
  7. 阻止是确保订单正确的唯一方法。

答案 2 :(得分:0)

Thanks for your help @xavier-j. After playing around this, I wrote this. Hope it be useful for someone who needs same thing:

First you need add this ActionFilter

public class KeepLastRequestAttribute : ActionFilterAttribute
{
    public string HashCode { get; set; }
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
        Dictionary<string, CancellationTokenSource> clt;
        if (filterContext.HttpContext.Application["CancellationTokensDictionary"] != null)
        {
            clt = (Dictionary<string, CancellationTokenSource>)filterContext.HttpContext.Application["CancellationTokensDictionary"];
        }
        else
        {
            clt = new Dictionary<string, CancellationTokenSource>();
        }
        if (filterContext.HttpContext.Request.Form["__RequestVerificationToken"] != null)
        {
            HashCode = filterContext.HttpContext.Request.Form["__RequestVerificationToken"];
        }

        CancellationTokenSource oldCt = null;
        clt.TryGetValue(HashCode, out oldCt);
        CancellationTokenSource ct = new CancellationTokenSource();
        if (oldCt != null)
        {
            oldCt.Cancel();
            clt[HashCode] = ct;
        }
        else
        {
            clt.Add(HashCode, ct);
        }

        filterContext.HttpContext.Application["CancellationTokensDictionary"] = clt;
        filterContext.Controller.ViewBag.CancellationToken = ct;
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        base.OnResultExecuted(filterContext);
        if (filterContext.Controller.ViewBag.ThreadHasBeenCanceld == null && filterContext.HttpContext.Application["CancellationTokensDictionary"] != null) {
            lock (filterContext.HttpContext.Application["CancellationTokensDictionary"])
            {
                Dictionary<string, CancellationTokenSource>  clt = (Dictionary<string, CancellationTokenSource>)filterContext.HttpContext.Application["CancellationTokensDictionary"];
                clt.Remove(HashCode);
                filterContext.HttpContext.Application["CancellationTokensDictionary"] = clt;
            }
        }
    }
}

I am using AntiForgeryToken here as key token, you can add your own custom hash code to have more control.

In the controller you will have something like this

        [HttpPost]
    [KeepLastRequest]
    public async Task<ActionResult> DoSlowJob(CancellationToken ct)
    {
        CancellationTokenSource ctv = ViewBag.CancellationToken;
        CancellationTokenSource nct = CancellationTokenSource.CreateLinkedTokenSource(ct, ctv.Token, Response.ClientDisconnectedToken);

        var mt = Task.Run(() =>
        {
            SlowJob(nct.Token);
        }, nct.Token);
        await mt;

        return null;
    }

    private void SlowJob(CancellationToken ct)
    {
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(200);
            if (ct.IsCancellationRequested)
            {
                this.ViewBag.ThreadHasBeenCanceld = true;
                System.Diagnostics.Debug.WriteLine("cancelled!!!");
                break;
            }
            System.Diagnostics.Debug.WriteLine("doing job " + (i + 1));
        }
        System.Diagnostics.Debug.WriteLine("job done");

        return;
    }

And finally in your JavaScript you need to abort ongoing requests, otherwise browser blocks new requests.

        var onSomethingChanged = function () {

        if (currentRequest != null) {
            currentRequest.abort();
        }

        var fullData = $('#my-heavy-form :input').serializeArray();

        currentRequest = $.post('/MyController/DoSlowJob', fullData).done(function (data) {
            // Do whatever you want with returned data
        }).fail(function (f) {
            console.log(f);
        });
        currentRequest.always(function () {
            currentRequest = null;
        })
    }