I want to prevent users submitting forms multiple times in .NET MVC. I've tried several methods using Javascript but have had difficulties getting it to work in all browsers. So, how can I prevent this in my controller? It there some way that multiple submissions can be detected?
After Form submission, when the Refresh Button in the Browser is clicked or the F5 key is pressed, a warning popup comes up which warns against Form resubmission. The solution is very simple, either the page must be redirected to itself or to some other page in order to avoid this particular behavior.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With