Allow Users to Selectively Override your Website's Default Date
Low impact method for using a Session variable + custom Action filter to allow end users to select a test date of their choosing, thus overriding the default date used during web requests.
Introduction
An ASP.NET MVC website that I've developed needed preparation for demonstration purposes, of the views available sometime in the past. Furthermore, in the future, it'd be good to have an easy means for historical date user testing. I've created a side project (called VRSandbox
), which is a proof of concept for achieving this. It relies on a Session
variable, assigned by the end user in the client browser, for the desired test or demonstration date. This allows for easy change of the test date, as well as simultaneous specification of different dates by different users. Introducing this functionality basically comes down to adding a customer filter to controllers and/or controller actions, and making a minor modification to the View Models that need to be tested.
Background
This project was coded to support another, much bigger project of mine, called Voter's Revenge (henceforth, VR). See votersrevenge.org, and votersrevenge.info. Briefly, Voter's Revenge is tasked with enabling citizens to grow populist political power, by facilitating the formation of punitive voting blocs. Disruptive, punitive voting blocs (which have the political muscle to "fire" incumbents, even if too small/weak to "hire" their replacement of choice) will be particularly important in countries where reform forces of all sorts are weak due to apathy and demoralization, lack of organization, etc. Such is the case in the United States, where reformists of all political stripes who are opposed to the plutocratic status quo, expressed through the Duopoly of Democrats + Republicans, find themselves continually marginalized, disempowered, and/or co-opted.
I paused active development of VR back in the summer of 2016. Sometime after that point, all US campaigns whose data was in the program's database entered their final phases. To be useful in the future, fresh data needs to be loaded, and programming changes need to be added to seamlessly handle new campaigns. (Plus, decent interfaces for CRUD operations need to be introduced.) All in all, a fair amount of work.
I am tentatively planning on open sourcing VR. In order to both make the program display an interesting view, with its existing data and (virtually unchanged) coding, I needed a way to trick the program into showing views with respect to a historical date, taken well before most primary contests were over. This will also help future testers of the program.
VRSandbox (the name of this Visual Studio solution) was thus an ASP.NET MVC side project to VR, proper. I believe the code I developed for VRSandbox to be useful for other developers who may want to enable historical date testing, with only a smallish disruption to their existing code base.
Using the Code
To use the code, data models have to be modified similarly to how I extended my CampaignModel
class to AssignableDateCampaignModel
class. The key steps are:
- Installing NodaTime (via nuget) and adding namespace references, where needed.
- Adding a
private
member to hold a NodaTimeLocalDate
variable (in my case, "assignedLocalDate
"). - Modifying the getter of your pre-existing
DateTime
field to return thisLocalDate
variable (converted toDateTime
), when it has been assigned. - Creating a public means of setting the
private
LocalDate
variable. In my case, I added both a constructor that included aLocalDate
value, as well as anAssignLocalDate
method.
Here are my View Models:
public class CampaignModel
{
public string CandidateName { get; set; }
public DateTime QueryDateTime
{
get
{
// return midnight UTC
return DateTime.UtcNow.Date.ToUniversalTime();
}
}
}
public class AssignableDateCampaignModel : CampaignModel
{
private LocalDate assignedLocalDate = new LocalDate();
public AssignableDateCampaignModel(string candidateName, LocalDate assignedLocalDate)
{
this.assignedLocalDate = assignedLocalDate;
this.CandidateName = candidateName;
}
public DateTime QueryDateTime
{
get
{
// if we didn't assign a query date (stored as assignedLocalDate)
if (assignedLocalDate == new LocalDate())
// return midnight UTCNow
return DateTime.UtcNow.Date.ToUniversalTime();
else
{
// return midnight of LocalDate
return assignedLocalDate.ToDateTime();
}
}
}
public void AssignLocalDate(LocalDate assignedLocalDate)
{
this.assignedLocalDate = assignedLocalDate;
}
}
Additionally, coders must:
- Add relevant files such as /Utils/*.*.
- Add the
SessionQueryDate
custom filter class, which I have added to /App_Start/FilterConfig.cs. - Add the custom filter attribute [
SessionQueryDate
] to controllers or actions for which you want to use the assigned date. - Provide some means for end users to set the "
sessionLocalDate
" Session variable to a date of their choice. In VRSandbox, I have hacked the code from the sample project. How to create and access session variables in ASP.NET MVC
Code for SessionQueryDate
action filter:
public class SessionQueryDate : ActionFilterAttribute, IActionFilter
{
// set ViewBag.TestQueryLocalDate if parse successful
// otherwise, set it to null
public void OnActionExecuting(ActionExecutingContext context)
{
LocalDate validLocalDate;
var sessionLocalDate = context.HttpContext.Session.Contents["sessionLocalDate"];
if (sessionLocalDate == null)
{
context.Controller.ViewBag.TestQueryLocalDate = null;
}
else
{
bool tryParsing = ((string)sessionLocalDate).IsValidLocalDate(out validLocalDate);
if (tryParsing)
{
context.Controller.ViewBag.TestQueryLocalDate = validLocalDate;
}
else
{
context.Controller.ViewBag.TestQueryLocalDate = null;
}
}
}
}
Here is the code for two actions which use [SessionQueryDate
]. The first of these is GetResults()
, which uses AssignableDateCampaignModel
. Since this model has the method AssignLocalDate()
, calling this with the LocalDate
value set in SessionQueryDate
results in the test date selected by the user being used in queries involving AssignableDateCampaignModel.QueryDateTime
.
[SessionQueryDate]
public ViewResult GetEvents()
{
ViewBag.Title = "GetEvents() - view model has an assignable date";
ViewBag.Assignable = true;
// 2 example models
// ============================
// CampaignModel does not have an assignable date
// CampaignModel campaignModel = new CampaignModel { CandidateName = "Donald J. Trump" };
// AssignableDateCampaignModel, used here in GetEvents() does have an assignable date
AssignableDateCampaignModel campaignModel =
new AssignableDateCampaignModel("Hillary Clinton", new LocalDate());
LocalDatePattern pattern0 = LocalDatePattern.CreateWithInvariantCulture("MM-dd-yyyy");
// ViewBag.TestQueryLocalDate is set in the SessionQueryDate Action Filter
if (ViewBag.TestQueryLocalDate != null)
{
// assign the LocalDate to the model
LocalDate localDate = ViewBag.TestQueryLocalDate;
// add to model
campaignModel.AssignLocalDate(localDate);
// return the same value in the session variable
ViewData["sessionLocalDate"] = pattern0.Format(localDate);
// update status
ViewData["sessionLocalDateStatus"] = "sessonLocalDate session variable set";
}
else
{
// return the same (bad format or empty) value
ViewData["sessionLocalDate"] = null; // as String;
// update status
ViewData["sessionLocalDateStatus"] = "sessonLocalDate session variable not set";
}
// construct the view model using .QueryDateTime
List<MyEvent> events = MyEvents.Where
(x => x.EventDate >= campaignModel.QueryDateTime).ToList();
return View(events);
}
This second sample action also calls campaignModel.AssignLocalDate()
. Even though the campaignModel
is of type CampaignModel
, and has no assignable date function, there is an extension method of that name defined in class NodaTimeUtils
, so the code still compiles. In this case, AssignLocalDate
essentially does nothing.
[SessionQueryDate]
public ViewResult GetEventsNoAssignableDate()
{
ViewBag.Title = "NodaSessionStateController.GetEventsNoAssignableDate()
- view model does not have an assignable date";
// 2 example models
// ============================
// CampaignModel does not have an assignable date
CampaignModel campaignModel =
new CampaignModel { CandidateName = "Donald J. Trump" };
//// AssignableDateCampaignModel does have an assignable date
//AssignableDateCampaignModel campaignModel =
//new AssignableDateCampaignModel("Hillary Clinton", new LocalDate());
LocalDatePattern pattern0 =
LocalDatePattern.CreateWithInvariantCulture("MM-dd-yyyy");
// ViewBag.TestQueryLocalDate is set in the SessionQueryDate Action Filter
if (ViewBag.TestQueryLocalDate != null)
{
// assign the LocalDate to the model
LocalDate localDate = ViewBag.TestQueryLocalDate;
// add to model, where appropriate
// since the current model has no assignable date,
// AssignLocalDate() will not do anything to the model
campaignModel.AssignLocalDate(localDate);
// return the same value in the session variable
ViewData["sessionLocalDate"] = pattern0.Format(localDate);
// update status
ViewData["sessionLocalDateStatus"] = "sessonLocalDate session variable set";
}
else
{
// return the same (bad format or empty) value
ViewData["sessionLocalDate"] =
System.Web.HttpContext.Current.Session["sessionLocalDate"] as String;
// update status
ViewData["sessionLocalDateStatus"] =
"sessonLocalDate session variable not set";
}
// construct the view model using .QueryDateTime
List<MyEvent> events = MyEvents.Where
(x => x.EventDate >= campaignModel.QueryDateTime).ToList();
return View("GetEvents", events);
}
Screen Shots
Home Page with All Data
Assign a Date, Before Submitting
After Submitting an Invalid Date
After Submitting a Valid Date
Query Results Using Assigned Date
Query Results Ignoring Assigned Date (executed 3/9/2017)
Notes
- NodaTime is used, but not really necessary for my purposes. NodaTime shines when dealing with time zones, but in my application, a granularity of
Date
is sufficient. If you don't anticipate needing to deal with time zones, you should be able to easily refactor the code to just use the .NETDateTime
library. - I have hard-coded the date format as "
MM-dd-yyyy
". - It's just fine to decorate an entire controller with [
SessionQueryDate
], as actions that use models that do not have assignable dates will not break.
My Development Environment
I developed in Visual Studio 2013 Ultimate, using .NET framework 4.5.2 (thus, C# 6). The 'database' is just a static collection.
History
- 15th March, 2017: Initial version