ELMAH - Using custom error pages to collecting user feedback - asp.net-mvc

I'm looking at using ELMAH for the first time but have a requirement that needs to be met that I'm not sure how to go about achieving...
Basically, I am going to configure ELMAH to work under asp.net MVC and get it to log errors to the database when they occur. On top of this I be using customErrors to direct the user to a friendly message page when an error occurs. Fairly standard stuff...
The requirement is that on this custom error page I have a form which enables to user to provide extra information if they wish. Now the problem arises due to the fact that at this point the error is already logged and I need to associate the loged error with the users feedback.
Normally, if I was using my own custom implementation, after I log the error I would pass through the ID of the error to the custom error page so that an association can be made. But because of the way that ELMAH works, I don't think the same is quite possible.
Hence I was wondering how people thought that one might go about doing this....
Cheers
UPDATE:
My solution to the problem is as follows:
public class UserCurrentConextUsingWebContext : IUserCurrentConext
{
private const string _StoredExceptionName = "System.StoredException.";
private const string _StoredExceptionIdName = "System.StoredExceptionId.";
public virtual string UniqueAddress
{
get { return HttpContext.Current.Request.UserHostAddress; }
}
public Exception StoredException
{
get { return HttpContext.Current.Application[_StoredExceptionName + this.UniqueAddress] as Exception; }
set { HttpContext.Current.Application[_StoredExceptionName + this.UniqueAddress] = value; }
}
public string StoredExceptionId
{
get { return HttpContext.Current.Application[_StoredExceptionIdName + this.UniqueAddress] as string; }
set { HttpContext.Current.Application[_StoredExceptionIdName + this.UniqueAddress] = value; }
}
}
Then when the error occurs, I have something like this in my Global.asax:
public void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
var item = new UserCurrentConextUsingWebContext();
item.StoredException = args.Entry.Error.Exception;
item.StoredExceptionId = args.Entry.Id;
}
Then where ever you are later you can pull out the details by
var item = new UserCurrentConextUsingWebContext();
var error = item.StoredException;
var errorId = item.StoredExceptionId;
item.StoredException = null;
item.StoredExceptionId = null;
Note this isn't 100% perfect as its possible for the same IP to have multiple requests to have errors at the same time. But the likely hood of that happening is remote. And this solution is independent of the session, which in our case is important, also some errors can cause sessions to be terminated, etc. Hence why this approach has worked nicely for us.

The ErrorLogModule in ELMAH (version 1.1 as of this writing) provides a Logged event that you can handle in Global.asax and which you can use to communicate details, say via HttpContext.Items collection, to your custom error page. If you registered the ErrorLogModule under the name ErrorLog in web.config then your event handler in Global.asax will look like this:
void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
var id = args.Entry.Id
// ...
}

Related

How to count number of hits to the website using MVC [duplicate]

what is the best way to capture page views by person without slowing down performance on the site. I see that stackoverflow show page views all over the place. Are they doing an insert into a db everytime i click on a page?
In asp.net-mvc, Is there any recommended way to track page view per user (my site has a login screen) so i can review which pages people are going to and how often
First off.. if what you really care about is how are customers using my site then you most likely want to look into Google Analytics or a similar service.
But if you want a quick and dirty page view record and you are using ASP.Net MVC 3 then as Chris Fulstow mentioned you're going to want to use a mix of global action filters and caching. Here is an example.
PageViewAttribute.cs
public class PageViewAttribute : ActionFilterAttribute
{
private static readonly TimeSpan pageViewDumpToDatabaseTimeSpan = new TimeSpan(0, 0, 10);
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var calledMethod = string.Format("{0} -> {1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
var cacheKey = string.Format("PV-{0}", calledMethod);
var cachedResult = HttpRuntime.Cache[cacheKey];
if(cachedResult == null)
{
HttpRuntime.Cache.Insert(cacheKey, new PageViewValue(), null, DateTime.Now.Add(pageViewDumpToDatabaseTimeSpan) , Cache.NoSlidingExpiration, CacheItemPriority.Default,
onRemove);
}
else
{
var currentValue = (PageViewValue) cachedResult;
currentValue.Value++;
}
}
private static void onRemove(string key, object value, CacheItemRemovedReason reason)
{
if (!key.StartsWith("PV-"))
{
return;
}
// write out the value to the database
}
// Used to get around weird cache behavior with value types
public class PageViewValue
{
public PageViewValue()
{
Value = 1;
}
public int Value { get; set; }
}
}
And in your Global.asax.cs
public class MvcApplication : HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new PageViewAttribute());
}
}
For pre-ASP.Net MVC 3 ONLY you are going to have to apply the same attribute manually to all of your actions.
[PageView]
public ActionResult CallOne()
{
}
[PageView]
public ActionResult CallTwo()
{
}
The best way would probably be a global action filter that intercepts requests to all actions on all controllers, then increments a counter in the database for the current user and page. To save hitting the database too hard, you could cache these values and invalidate them every few minutes, depending on how much traffic you're dealing with.
We use the open source Piwik: http://piwik.org/, which is setup on it's own server. One line of Javascript in the _Layout page makes a call to Piwik after the page has loaded (put the JS at the end) and does not affect page load performance at all.
In addition to just counts, you'll get a ton of info about where your users are coming from, browser, screen resolutions, installed plugins. Plus you can track conversions and use the same tool to track marketing campaigns, etc.
<soapbox>
I cannot think of a situation where you'd be better off implementing this in MVC or in your web app in general. This stuff simply does not belong in your web app and is a meta-concern that should be separated out. This approach has enabled us to track analytics for all of our apps (32 of them: mvc 2/3, webforms, php...) in a unified manner.
If you really don't want to use another tool for this purpose, I would recommend tapping into your IIS log and getting your stats from there. Again, to get any real decision making power out of it, you'll need to put a good analyzer on it. I recommend Splunk: http://www.splunk.com/
</soapbox>
I wanted to post an updated version of Shane's answer for those who are interested. Some things to consider:
You have to set the action attribute up as a service when decorating your
methods using syntax like the following :
[ServiceFilter(typeof(PageViewAttribute))]
As far as I can tell, HttpRuntime.Cache.Insert isn't a thing in .NET Core, so I used a simple implementation of IMemoryCache (You may need to add this line to your startup.cs in order to use the interface):
services.AddMemoryCache();
Because we are injecting IMemoryCache into a class that is not a controller, we need to register our attribute as a service in startup.cs, like so:
services.AddScoped<[PageViewAttribute]>(); - without brackets!
Whatever object you return when creating a cacheKey will be assigned to the 'value' parameter of the OnRemove method.
Below is the code.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controllerActionDescriptor = filterContext.ActionDescriptor as ControllerActionDescriptor;
var arguments = filterContext.ActionArguments;
ActionId = arguments["id"].ToString();
var calledMethod = string.Format("{0} -> {1}",
controllerActionDescriptor.ControllerName,
controllerActionDescriptor.ActionName);
var cacheKey = string.Format("PV-{0}", calledMethod);
var cachedResult = _memoryCache.Get(cacheKey);
if (cachedResult == null)
{
//Get cacheKey if found, if not create cache key with following settings
_memoryCache.GetOrCreate(cacheKey, cacheKey =>
{
cacheKey.AbsoluteExpirationRelativeToNow
= pageViewDumpToDatabaseTimeSpan;
cacheKey.SetValue(1);
cacheKey.RegisterPostEvictionCallback(onRemove);
return cacheKey.Value;
});
}
else
{
_memoryCache.Get(cacheKey);
}
}
//Called when Memory entry is removed
private void onRemove(object key, object value, EvictionReason reason, object state)
{
if (!key.ToString().StartsWith("PV-"))
{
return;
}
// write out the value to the database
SaveToDataBase(key.ToString(), (int)value);
}
As a point of reference, this was done for a .NET Core 5 MVC App.
Regards.

Implementing ASP.NET MVC error handling using Castle Windsor's Dynamic Proxies

I spent a long time trying to get the ASP.NET MVC [HandleError] attribute to work in my websites. It seemed like a good idea to go with the solution offered by the framework, but I just couldn't get it to do anything useful. Then I tried writing my own attribute (mainly so that I could step in to the code with the debugger), but although my code seemed to be doing all the right things, after it executed the framework took over and did mysterious things. Finally I tried the MVC Contrib's [Rescue] attribute, which was better but I still couldn't get it to do what I wanted.
One problem is that exceptions thrown in code embedded in aspx / ascx pages get wrapped in HttpException's and WebHttpException's.
Another problem for me was that the system is very opaque. I was essentially poking inputs in to a black box with some desired outputs in mind, but with no idea (other than the documentation, which doesn't seem very accurate / thorough) what the relationship was between them.
So, what to do?
I went for Dynamic Proxies in Castle Windsor, using the code below, which tries to handle Database errors, for which I have a specific Exception (AccessDBException).
The _alreadyAttemptedToShowErrorPage is to stop infinite recursion in the case where the error page throws an Exception.
The GetAccessDBException(...) method finds the relevant exception anywhere in the Exception stack, for the case when there are problems in aspx / ascx code.
The code requires that there is a BaseController class that all controllers derive from. This class is used to add a CreateErrorView(...) method (being as the standard View(...) method is protected)
public class AccessDBExceptionHandlingDynamicProxy : IInterceptor
{
private bool _alreadyAttemptedToShowErrorPage;
public AccessDBExceptionHandlingDynamicProxy()
{
_alreadyAttemptedToShowErrorPage = false;
}
public void Intercept(IInvocation invocation)
{
Contract.Requires(invocation.Proxy is BaseController);
try
{
invocation.Proceed();
}
catch (HttpException e)
{
if (_alreadyAttemptedToShowErrorPage == true) throw e;
_alreadyAttemptedToShowErrorPage = true;
var dbException = GetAccessDBException(e);
if (dbException != null)
{
var baseController = (invocation.Proxy as BaseController);
var view = baseController.CreateErrorView("AccessDBException", new AccessDBExceptionViewModel(dbException));
baseController.Response.Clear();
baseController.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
view.ExecuteResult(baseController.ControllerContext);
baseController.Response.End();
}
else
{
throw e;
}
}
}
private static AccessDBException GetAccessDBException(HttpException e)
{
AccessDBException dbException = null;
Exception current = e;
while (dbException == null && current != null)
{
if (current is AccessDBException) dbException = (current as AccessDBException);
current = current.InnerException;
}
return dbException;
}
}

How to log custom exception with additional Data property to Elmah?

I have a custom exception, where I have overriden the Data property using reflection like the following...
public class MyCustomException : Exception
{
private readonly SomeModel _log;
public MyCustomException(SomeModel log)
: base(string.Format("Could not insert to some table"))
{
_log = log;
}
public override System.Collections.IDictionary Data
{
get
{
var data = new Dictionary<string, object>();
foreach (PropertyInfo pinfo in _log.GetType().GetProperties())
{
data.Add(pinfo.Name, pinfo.GetType().GetProperty(pinfo.Name));
}
return data;
}
}
}
When the above exception is thrown, it gets logged to elmah but the Data is not logged.
What changes do I have to make so that the Data is also logged to elmah ? Please advice.
The Detail property of the Elmah.Error object - which is then processed by an ErrorLog class - is built from the ToString() method of the exception.
// Elmah.Error
public Error(Exception e, HttpContext context)
{
// snip
this._detail = e.ToString(); // here
// snip
Add your data to an override of the ToString method in the MyCustomException to see it in Elmah.
Your question is currently the issue with most stars on the ELMAH issue tracker:
https://code.google.com/p/elmah/issues/detail?id=162
#samy may be more correct, but I have also found another possible option that works for my situation. I am using elmah in a webapi2 project where users are anonymous and in one particular controller I want to record some context of the request from the viewmodel (in my case, an email address but I could potentially record more data) and I want to be able to associate errors to email so I can determine, after an error, if the same user was able to submit an order successfully.
In my controller, I perform a number of database calls in a transaction and then submit an order to paypal, all within a try/catch block. In the catch, I create a new exception instance with a message containing the email and set the innerException property to the thrown exception and throw the new exception.
I know it is possible to lose some stack trace information, I tested this in my context and the stack trace seems to be maintained but exceptions occur inside the controller because there are not many layers to this particular controller and application. If anyone has a similar situation, this method might be the quickest and easiest.
catch (Exception ex)
{
Exception newException = new Exception(viewModel.ContactEmail, ex);
throw newException;
}
This assumes you have a exception filter, such as below (for webapi), and the filter is registered as global in global.asax.
public class LogExceptionAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if (HttpContext.Current != null)
{
ErrorSignal.FromCurrentContext().Raise(actionExecutedContext.Exception);
}
}
}

How to handle errors in my CustomAutorize attribute in asp.net 3.0 Application

I am working on an asp.net MVC 3.0 Application. I am using using my own CustomRoleProvider
and CustomErrorHandler by overriding default attributes.
Every thing is working fine. But ,the problem is with the exception handling.
While testing the application , tester has given invalid DB connection to test.
The result is , Custom Error Handler is not rendering Error View , instead it is routing the original path
For ex:
I am running my application as
Home/Index
It is first hitting Custom Role Provider to fetch the roles for the application
Since , the Db Connection is not correct , it is raising exception that "Not able to Connect"
Now , Instead of routing to Error View along with this error message. It is routing to Home Controller and Index action.
**The code for my Custom Error Handler is as Follows**
public class CustomHandleErrorAttribute : HandleErrorAttribute // Error handler
{
public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
if (new HttpException(null, filterContext.Exception).GetHttpCode() != 500)
{
return;
}
if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}
// if the request is AJAX return JSON else view.
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = AjaxError(filterContext.Exception.Message, filterContext);
}
else
{
filterContext.ExceptionHandled = true;
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
}
}
protected JsonResult AjaxError(string message, ExceptionContext filterContext)
{
if (String.IsNullOrEmpty(message))
message = "Something went wrong while processing your request. Please refresh the page and try again.";
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
return new JsonResult { Data = new { ErrorMessage = message }, ContentEncoding = System.Text.Encoding.UTF8, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}
}
In the above code , after setting up filterContext.Result . It is not rendering Error View as Expected.
Please correct/suggest me, where i am going wrong..
Updated:
public class CustomRoleProvider : RoleProvider // Custom role provider
{
public override string[] GetRolesForUser(string username)
{
// Fetching roles for user from database
}
// Some other Methods
}
This is method is generating exception , since it is trying to connect to wrong connection
Updated2:
1) I am using Custom Error Handler for the entire controller.
2) I need to catch all the exceptions including Ajax Errors
3) I have included my code for Custom Error Handler Above
4) I am also using CustomRole Provider for entire controller
5) Here, I am trying to generate exception , by giving wrong database connection
6) I am running the URL : Home/Index
7) Before going to thatr URL, it is hitting the methods in Role Provider class since i am using it as a attribute
8) Since, i have gave wrong DB Connection , It is generating exception
9) Then, it fires on exception method of Custom error handler
10) Building the Error Model for the error view
11) But, here is the problem. Instead of rendering Error View , it is going to index method of the Home Controller.
12) But, i need Error View to be rendered here, because it has failed to connect to database and getting roles . I want furthuer execution of URL Home/Index to be stopped here.
Hope this clarifies the problem..i am running in to. please feel free to ask me for furthuer details/Clarification
HandleError is designed to be able to register multiple filters (for example for different exceptions). One filter can handle only some specific exceptions or error cases and another unhandle cases can be handled by another HandleError. I suppose that currently both standard and your [CustomHandleError] filter are applied. You can set the Order property to an integer value that specifies a priority from -1 (highest priority) to any positive integer value. The greater the integer value is, the lower the priority of the filter is. You can use Order parameter for example (see here) to make your filter working before. More full description of the order you can find in the MSDN documentation.
The answer, this one and the article for example provide small examples of usage Order property of HandleError.

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

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?
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');
}
});
Strategy
The truth is that you need several lines of attack for this problem:
The Post/Redirect/Get (PRG) pattern is not enough by itself. Still, it should always be used to provide the user with good experiences when using back, refresh, etc.
Using JavaScript to prevent the user from clicking the submit button multiple times is a must because it provides a much less jarring user experience compared to server-side solutions.
Blocking duplicate posts solely on the client side doesn't protect against bad actors and does not help with transient connection problems. (What if your first request made it to the server but the response did not make it back to the client, causing your browser to automatically resend the request?)
I'm not going to cover PRG, but here are my answers for the other two topics. They build upon the other answers here. FYI I'm using .NET Core 3.1.
Client-Side
Assuming you are using jQuery validation, I believe this is the cleanest/most efficient way to prevent your form submit button from being double-clicked. Note that submitHandler is only called after validation has passed, so there is no need to re-validate.
$submitButton = $('#submitButton');
$('#mainForm').data('validator').settings.submitHandler = function (form) {
form.submit();
$submitButton.prop('disabled', true);
};
An alternative to disabling the submit button is to show an overlay in front of the form during submission to 1) block any further interaction with the form and 2) communicate that the page is "doing something." See this article for more detail.
Server-Side
I started off with Jim Yarbro's great answer above, but then I noticed Mark Butler's answer pointing out how Jim's method fails if someone submits forms via multiple browser tabs (because each tab has a different token and posts from different tabs can be interlaced). I confirmed that such a problem really does exist and then decided to upgrade from tracking just the last token to tracking the last x tokens.
To facilitate that, I made a couple of helper classes: one for storing the last x tokens and one for making it easy to store/retrieve objects to/from session storage. The main code now checks that the current token is not found in the token history. Other than that, the code is pretty much the same. I just made some little tweaks to suit my tastes. I included both the regular and asynchronous versions. The full code is below, but these are the critical lines:
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
}
Sadly, the fatal flaw of this approach is that the feedback from the first post (before any duplicates) gets lost. A better (but much more complex) solution would be to store the result of each unique request by GUID, and then handle duplicate requests by not only skipping doing the work again but also returning the same result from the first request, giving the user a seamless experience. This thorough article detailing Air BnB's methods of avoiding duplicate payments will give you an idea of the concepts.
PreventDuplicateFormSubmissionAttribute.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
// This class provides an attribute for controller actions that flags duplicate form submissions
// by adding a model error if the request's verification token has already been seen on a prior
// form submission.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateFormSubmissionAttribute: ActionFilterAttribute
{
const string TokenKey = "__RequestVerificationToken";
const string HistoryKey = "RequestVerificationTokenHistory";
const int HistoryCapacity = 5;
const string DuplicateSubmissionErrorMessage =
"Your request was received more than once (either due to a temporary problem with the network or a " +
"double button press). Any submissions after the first one have been rejected, but the status of the " +
"first one is unclear. It may or may not have succeeded. Please check elsewhere to verify that your " +
"request had the intended effect. You may need to resubmit it.";
public override void OnActionExecuting(ActionExecutingContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
{
string token = request.Form[TokenKey].ToString();
ISession session = context.HttpContext.Session;
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
session.Put(HistoryKey, history);
}
}
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
HttpRequest request = context.HttpContext.Request;
if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
{
string token = request.Form[TokenKey].ToString();
ISession session = context.HttpContext.Session;
await session.LoadAsync();
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
session.Put(HistoryKey, history);
await session.CommitAsync();
}
await next();
}
}
}
RotatingHistory.cs
using System.Linq;
// This class stores the last x items in an array. Adding a new item overwrites the oldest item
// if there is no more empty space. For the purpose of being JSON-serializable, its data is
// stored via public properties and it has a parameterless constructor.
public class RotatingHistory<T>
{
public T[] Items { get; set; }
public int Index { get; set; }
public RotatingHistory() {}
public RotatingHistory(int capacity)
{
Items = new T[capacity];
}
public void Add(T item)
{
Items[Index] = item;
Index = ++Index % Items.Length;
}
public bool Contains(T item)
{
return Items.Contains(item);
}
}
SessonExtensions.cs
using System.Text.Json;
using Microsoft.AspNetCore.Http;
// This class is for storing (serializable) objects in session storage and retrieving them from it.
public static class SessonExtensions
{
public static void Put<T>(this ISession session, string key, T value) where T : class
{
session.SetString(key, JsonSerializer.Serialize(value));
}
public static T Get<T>(this ISession session, string key) where T : class
{
string s = session.GetString(key);
return s == null ? null : JsonSerializer.Deserialize<T>(s);
}
}
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.
In its self, no, however depending on what the controller is actually doing, you should be able to work out a way.
Is a record being created in the database that you can check for to see if they've already submitted the form?
Just add this code at the end of your page. I am using "jquery-3.3.1.min.js" and "bootstrap 4.3.1"
<script type="text/javascript">
$('form').submit(function () {
if ($(this).valid()) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
</script>
Use the Post/Redirect/Get design pattern.
PS:
It looks to me that the answer by Jim Yarbro could have a fundamental flaw in that the __RequestVerificationToken stored in the HttpContext.Current.Session["LastProcessedToken"] will be replaced when a second form is submitted (from say another browser window). At this point, it is possible to re-submit the first form without it being recognized as a duplicate submission. For the proposed model to work, wouldn’t a history of __RequestVerificationToken be required? This doesn't seem feasible.
Dont reinvent the wheel :)
Use the Post/Redirect/Get design pattern.
Here you can find a question and an answer giving some suggestions on how to implement it in ASP.NET MVC.
You can also pass some sort of token in a hidden field and validate this in the controller.
Or you work with redirects after submitting values. But this get's difficult if you take heavily advantage of ajax.
This works on every browser
document.onkeydown = function () {
switch (event.keyCode) {
case 116: //F5 button
event.returnValue = false;
event.keyCode = 0;
return false;
case 82: //R button
if (event.ctrlKey) {
event.returnValue = false;
event.keyCode = 0;
return false;
}
}
}
You can do this by creating some sort of static entry flag that is user specific, or specific to whatever way you want to protect the resource. I use a ConcurrentDictionary to track entrance. The key is basically the name of the resource I'm protecting combined with the User ID. The trick is figuring out how to block the request when you know it's currently processing.
public async Task<ActionResult> SlowAction()
{
if(!CanEnterResource(nameof(SlowAction)) return new HttpStatusCodeResult(204);
try
{
// Do slow process
return new SlowProcessActionResult();
}
finally
{
ExitedResource(nameof(SlowAction));
}
}
Returning a 204 is a response to the double-click request that will do nothing on the browser side. When the slow process is done, the browser will receive the correct response for the original request and act accordingly.
Use this simple jquery input field and will work awesomely even if you have multiple submit buttons in a single form.
$('input[type=submit]').click(function () {
var clickedBtn = $(this)
setTimeout(function () {
clickedBtn.attr('disabled', 'disabled');
}, 1);
});

Resources