How do I prevent multiple form submission in .NET MVC without using Javascript?

Updated answer for ASP.NET Core MVC (.NET Core & .NET 5.0)

Update note: Remember ASP.NET Core is still called "Core" in .NET 5.0.

I'm going to stick to the least-impact use case like before, where you're only adorning those controller actions that you specifically want to prevent duplicate requests on. If you want to have this filter run on every request, or want to use async, there are other options. See this article for more details.

The new form tag helper now automatically includes the AntiForgeryToken so you no longer need to manually add that to your view.

Create a new ActionFilterAttribute like this example. You can do many additional things with this, for example including a time delay check to make sure that even if the user presents two different tokens, they aren't submitting multiple times per minute.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext context) {
        if (context.HttpContext.Request.HasFormContentType && context.HttpContext.Request.Form.ContainsKey("__RequestVerificationToken")) {
            var currentToken = context.HttpContext.Request.Form["__RequestVerificationToken"].ToString();
            var lastToken = context.HttpContext.Session.GetString("LastProcessedToken");

            if (lastToken == currentToken) {
                context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
            }
            else {
                context.HttpContext.Session.SetString("LastProcessedToken", currentToken);
            }
        }
    }
}

By request, I also wrote an asynchronous version which can be found here.

Here's a contrived usage example of the custom PreventDuplicateRequest attribute.

[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public IActionResult Create(InputModel input) {
    if (ModelState.IsValid) {
        // ... do something with input

        return RedirectToAction(nameof(SomeAction));
    }

    // ... repopulate bad input model data into a fresh viewmodel

    return View(viewModel);
}

A note on testing: simply hitting back in a browser does not use the same AntiForgeryToken. On faster computers where you can't physically double click the button twice, you'll need to use a tool like Fiddler to replay your request with the same token multiple times.

A note on setup: Core MVC does not have sessions enabled by default. You'll need to add the Microsoft.AspNet.Session package to your project, and configure your Startup.cs properly. Please read this article for more details.

Short version of Session setup is: In Startup.ConfigureServices() you need to add:

services.AddDistributedMemoryCache();
services.AddSession();

In Startup.Configure() you need to add (before app.UseMvc() !!):

app.UseSession();

Original answer for ASP.NET MVC (.NET Framework 4.x)

First, make sure you're using the AntiForgeryToken on your form.

Then you can make a custom ActionFilter:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        if (HttpContext.Current.Request["__RequestVerificationToken"] == null)
            return;

        var currentToken = HttpContext.Current.Request["__RequestVerificationToken"].ToString();

        if (HttpContext.Current.Session["LastProcessedToken"] == null) {
            HttpContext.Current.Session["LastProcessedToken"] = currentToken;
            return;
        }

        lock (HttpContext.Current.Session["LastProcessedToken"]) {
            var lastToken = HttpContext.Current.Session["LastProcessedToken"].ToString();

            if (lastToken == currentToken) {
                filterContext.Controller.ViewData.ModelState.AddModelError("", "Looks like you accidentally tried to double post.");
                return;
            }

            HttpContext.Current.Session["LastProcessedToken"] = currentToken;
        }
    }
}

And on your controller action you just...

[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public ActionResult CreatePost(InputModel input) {
   ...
}

You'll notice this doesn't prevent the request altogether. Instead it returns an error in the modelstate, so when your action checks if ModelState.IsValid then it will see that it is not, and will return with your normal error handling.


I've tried several methods using Javascript but have had difficulties getting it to work in all browsers

Have you tried using jquery?

$('#myform').submit(function() {
    $(this).find(':submit').attr('disabled', 'disabled');
});

This should take care of the browser differences.


Just to complete the answer of @Darin, if you want to handle the client validation (if the form has required fields), you can check if there's input validation error before disabling the submit button :

$('#myform').submit(function () {
    if ($(this).find('.input-validation-error').length == 0) {
        $(this).find(':submit').attr('disabled', 'disabled');
    }
});

What if we use $(this).valid()?

$('form').submit(function () {
    if ($(this).valid()) {
        $(this).find(':submit').attr('disabled', 'disabled');
    }
});

You could include a hidden (random or counter) value in the form post, a controller could track these values in an 'open' list or something similar; every time your controller hands out a form it embeds a value, which it tracks allowing one post use of it.