Sponsored By Aspose - File Format APIs for .NET

Aspose are the market leader of .NET APIs for file business formats – natively work with DOCX, XLSX, PPT, PDF, MSG, MPP, images formats and many more!

Give your website more context

A pattern I've used over the years, when applicable, is to create an XXXContext
class within my web project. The scope of the class is for a request; the
purpose is to make available data and methods relevant to most requests the site
will deal with.

Let's pretend we're building some type of reporting website. Almost every page
and control is going to need to know three very important things: StartDate,
EndDate and ReportType. For simplicity, let's pretend those parameters are
passed as querystring values, i.e.:
index.aspx?s=20061001&e=20061005&t=pie.  A bad solution would be to
have every page retrieve the information and validate it – since we are dealing
with a querystring, we'll also need to convert the data. Rather, we'll create a
ReportContext class and let it take care of it.  It'll look something like: 

internal class ReportContext

{

   #region Fields and Properties
   private DateTime _startDate;
   private DateTime _endDate;
 
   public DateTime StartDate
   {
      get { return _startDate; }
   }
   public DateTime EndDate
   {
      get { return _endDate; }
   }
   #endregion
 
   #region Constructors
   public ReportContext(HttpContext context)
   {      
      if (context == null)
      {
         throw new ArgumentNullException("context");
      }
      bool hasStartDate = GetStartTime(request.QueryString["s"], out _startDate);
      _endDate = GetEndTime(request.QueryString["e"], hasStartDate);
   }
   #endregion
 
   #region Singleton
   //this is a pseudo singleton, but since it's per request, there should never be a problem
   public static ReportContext Current
   {
      get
      {
         HttpContext context = HttpContext.Current;
         if (context == null)
         {
            throw new InvalidOperationException("ReportContext can only be used from within a http context");
         }
         ReportContext reportContext = (ReportContext)HttpContext.Current.Items["ReportContext"];
         if (reportContext == null)
         {
            reportContext = new ReportContext(context);
            HttpContext.Current.Items["ReportContext"] = reportContext;
         }
         return reportContext;
      }
   }
   #endregion
 
   #region Private Methods
   private bool GetStartTime(string startDateString, out DateTime startDate)
   {
      if (string.IsNullOrEmpty(startDateString) || !DateTime.TryParseExact(startDateString, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out startDate))
      {
         startDate = DateTime.Now.AddDays(-5);
         return false;
      }
      return true;
   }
 
   private DateTime? GetEndTime(string endDateString, bool hasStartDate)
   {
      DateTime endDate;
      if (string.IsNullOrEmpty(endDateString) || !DateTime.TryParseExact(endDateString, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out endDate))
      {
         if (hasStartDate)
         {
            return null;
         }
         return DateTime.Now;
      }
      return endDate;
   }
   #endregion
}



 

It can then be consumed from anywhere in our web project via:
ReportContext.Current.StartDate

This greatly helps reduce code duplication. It's also possibly to cleanly encapsulate input validation and custom rules. If you followed the code above, you'll notice that if the "s" and "e" parameters are invalid, then it'll automatically set a StartDate of 5 days ago and an EndDate of today. But, if there's a valid "s" without a valid "e", well, we'll assume we want a report for a single day and leave EndDate null. Granted, that might business logic which should be located in your business layer, but in this case it's a presentation specific rule ('cuz we said so). Also worth noting is the use of the HttpContext.Items collection, which provides us exactly with the scope we want – 1 request. We're using a lazy-load, but you could just as easily place the code in BeginRequest.

It'd be possible to achieve the same thing using a base class for your pages. Either way works, but I do prefer this method.

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

7 Responses to Give your website more context

  1. With .NET 2.0 I tend to make the Context class static.

  2. karl says:

    Chris:
    Ya…ur right. I tend to stay away from Private in some cases because it’s so much harder to test. Internal works well, especially with the InternalsVisibleTo attribute in 2.0

  3. Chris Taylor says:

    This is a nice idea, I guess the constructor should be made private since the Current property is acting as a factory type method which could be bypassed by the public constructor?

  4. DavidHogue says:

    Brilliant!

    We’ve just been using the HttpContext, but that involves casting all over the place. Things break if the context hasn’t been setup. And of course it’s all difficult to test.

  5. Haacked says:

    I was just going to say that. That’s one problem I’ve been having in Subtext is some code is not testable because it accesses the HttpContext directly. ReportContext.Current could suffer from the same problem, since it’s hard to inject a mock in there.

    You could use my technique for simulating HTTP Context http://haacked.com/archive/2005/06/11/Simulating_HttpContext.aspx to test this code, but that technique has limitations (for example, Server.MapPath won’t work).

  6. jmiller says:

    One of the nice things in RoR is that they built in mechanisms to stub out the equivalent of HttpContext so you can write automated tests easier without all the extra interface abstractions.

    For testability, one easy way to do it is simply set a rule that your presenter/controller’s are never allowed to reference any class from the System.Web.* namespaces.

  7. jmiller says:

    I’ve done the same thing for the exact same reasons. For the sake of TDD, you might want to extract an interface out of the ReportContext class and have it pushed into other classes via dependency injection for the sake of testability. That’s one of the quickest ways to decouple controller code from the ASP.Net runtime.