MVC multi page form losing session
- by Bryan
I have a multi-page form that's used to collect leads. There are multiple versions of the same form that we call campaigns. Some campaigns are 3 page forms, others are 2 pages, some are 1 page. They all share the same lead model and campaign controller, etc. There is 1 action for controlling the flow of the campaigns, and a separate action for submitting all the lead information into the database.
I cannot reproduce this locally, and there are checks in place to ensure users can't skip pages. Session mode is InProc.
This runs after every POST action which stores the values in session:
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
if (this.Request.RequestType == System.Net.WebRequestMethods.Http.Post && this._Lead != null)
ParentStore.Lead = this._Lead;
}
This is the Lead property within the controller:
private Lead _Lead;
/// <summary>
/// Gets the session stored Lead model.
/// </summary>
/// <value>The Lead model stored in session.</value>
protected Lead Lead
{
get
{
if (this._Lead == null)
this._Lead = ParentStore.Lead;
return this._Lead;
}
}
ParentStore class:
public static class ParentStore
{
internal static Lead Lead
{
get { return SessionStore.Get<Lead>(Constants.Session.Lead, new Lead()); }
set { SessionStore.Set(Constants.Session.Lead, value); }
}
Campaign POST action:
[HttpPost]
public virtual ActionResult Campaign(Lead lead, string campaign, int page)
{
if (this.Session.IsNewSession)
return RedirectToAction("Campaign", new { campaign = campaign, page = 0 });
if (ModelState.IsValid == false)
return View(GetCampaignView(campaign, page), this.Lead);
TrackLead(this.Lead, campaign, page, LeadType.Shared);
return RedirectToAction("Campaign", new { campaign = campaign, page = ++page });
}
The problem is occuring between the above action, and before the following Submit action executes:
[HttpPost]
public virtual ActionResult Submit(Lead lead, string campaign, int page)
{
if (this.Session.IsNewSession || this.Lead.Submitted || !this.LeadExists)
return RedirectToAction("Campaign", new { campaign = campaign, page = 0 });
lead.AddCustomQuestions();
MergeLead(campaign, lead, this.AdditionalQuestionsType, false);
if (ModelState.IsValid == false)
return View(GetCampaignView(campaign, page), this.Lead);
var sharedLead = this.Lead.ToSharedLead(Request.Form.ToQueryString(false)); //Error occurs here and sends me an email with whatever values are in the form collection.
EAUtility.ProcessLeadProxy.SubmitSharedLead(sharedLead);
this.Lead.Submitted = true;
VisitorTracker.DisplayConfirmationPixel = true;
TrackLead(this.Lead, campaign, page, LeadType.Shared);
return RedirectToAction(this.ConfirmationView);
}
Every visitor to our site gets a unique GUID visitorID. But when these error occurs there is a different visitorID between the Campaign POST and the Submit POST. Because we track each form submission via the TrackLead() method during campaign and submit actions I can see session is being lost between calls, despite the OnActionExecuted firing after every POST and storing the form in session.
So when there are errors, we get half the form under one visitorID and the remainder of the form under a different visitorID. Luckily we use a third party service which sends an API call every time a form value changes which uses it's own ID. These IDs are consistent between the first half of the form, and the remainder of the form, and the only way I can save the leads from the lost session issues.
I should also note that this works fine 99% of the time.
EDIT:
I've modified my code to explicitly store my lead object in TempData and used the TempData.Keep() method to persist the object between subsequent requests. I've only deployed this behavior to 1 of my 3 sites but so far so good.
I had also tried storing my lead objects in Session directly in the controller action i.e.,
Session.Add("lead", this._Lead);
which uses HTTPSessionStateBase, attempting to circumvent the wrapper class, instead of HttpContext.Current.Session which uses HTTPSessionState. This modification made no difference on the issue, as expected.