Mark Everard

Hello, I'm Mark – a PhD physicist turned technologist / architect.

Archive for the ‘Code’ Category

Publish all the things!

with 5 comments

If you’ve seen some of my recent posts you’ll know I’ve been playing around with WebHooks. These are simple HTTP requests to endpoints allowing services to exchange data and events.

Integration Platforms as a Service

Those of you cool kids will have known about IFTTT (If This Then That) for some time. It’s a online tool that acts as a way to glue a huuuge number of online services together into simple workflow pairs called ‘recipes’. Each service can provide a number of triggers to the platform, which are then linked to actions provided by other services.

I’ve got a number of personal recipes set up, some useful some not. but all of them are good fun!

  • If somebody tags me in a picture on Facebook, then upload the picture to my OneDrive account
  • If one of my team creates a new GitHub repository then email me
  • If I arrive or leave my office then log the time in a Google Doc.
  • If an astronaut is launched into space then send me a notification

Whilst IFTTT targets individual users, other iPaaS platforms focus more towards business and enterprise. These will become increasingly useful and popular as a cheap means of system integration and data exchange. For example – if somebody submits a payment via the organisation’s payment gateway, log the entry directly into the cloud-based finance system. You won’t need development teams to set these up, just someone to configure the data exchange / webhook endpoints.

Publish your home!

I’ve been playing around with smart home devices (tado smart heating and WeMo lights and switches). I wanted to experiment a little with the API’s they offered.

One of the channels IFTTT offer is called the Maker channel. This is a user specific channel that will accept http triggers to an endpoint and can also make requests to a specified endpoint. It allows you pass up to 4 data points from a trigger or to an action.

publish-lights-on

Publish…… and there was light, and slippers.

As I’ve demonstrated before, adding webhooks to an existing ASP.NET app is straightforward. I decided I’d hook an Episerver solution up to an IFTTT maker channel, meaning when I clicked the publish button I could make all manner of amazing things happen, like turning my lights on :)

The code to do this is straightforward (see below). I’m using an Episerver initialisation module to hook a handler for the Content Published event. I’ve also built a simple helper method to call the Maker channel API. To use this you’d just have to add in your own API key that you get when you configure the Maker channel on IFTTT.


Written by mark

March 7th, 2016 at 11:00 am

Posted in ASP.NET,C#,Code,EPiServer

Adding GA tracking to Emails delivered using Adobe Campaign

with one comment

Adobe Campaign contains a formula that is executed in the delivery preparation phase which parses a delivery template and modifies all links, so that they are passed through a tracking server. This allows links to be individually tracked to a both an email recipient and a specific delivery.

Example
A link to the BBC news site, http://www.bbc.co.uk/news is changed to https://t.adobe-campaign.server/r/?id=hcc970,436a07,436a08. The latter url eventually resolves to BBC news, but first is passed through the tracking server, that issues a redirect and records that the link has been followed.

Google Analytics Tracking

Google Analytics can track inbound links to a site and attribute them to a source and an associated campaign. It does this in a number of ways, either via cookies, or for cross-domain links by checking querystring parameters. The following parameters are recognised

  • utm_content
  • utm_source
  • utm_medium

See the following link for further information about each parameter and the ‘meaning’ – https://support.google.com/analytics/answer/1033867?hl=en

Modifying the Click Tracking Formula

The click tracking formula in Adobe Campaign can be modified to include additional query string parameters within the forwarding record. This is achieved by modifying the NmsTracking_ClickFormula contained in Administration > Platform > Options

Warning
You should very carefully test the click tracking formula after making any modifications. Be very sure that the record you enter doesn’t include any spurious white space or line-breaks. Adobe Campaign is very ‘fussy’ about character encoding! As an example, when I first modified the below we discovered that modified outbound links were rendering incorrectly in Hotmail / Outlook.com email clients.

Links within the rendered email body had a line break before and after the URL which Hotmail interpreted as a link without a protocol and so added an additional protocol to the start of the url, so our links looked like https://https://t.adobe-campaign.server/r/?id=hcc970,436a07,436a08 . I’m sure this type of functionality is intentional from Hotmail / Outlook so they can turn www.bbc.co.uk entries in text into a fully-formed hyperlink. Regardless, it does show the complexity and why its vital to test your deliveries in a tool like Litmus – test email rendering.

Be very careful about cutting and pasting this as I’ve deliberately added line-breaks and white space to make it readable!

<% if( typeof strPurlTrackingServer!="undefined" && strPurlTrackingServer.toString() ) { %>
<%= strPurlTrackingServer %>
<% } else { %>

<%@ include option='NmsTracking_ServerUrl' %><% } %>/r/?id=<%= type.substr(0, 1) + (message.id<0 ? (message.id+4294967296) : message.id).toString(16).toLowerCase() %>,
<%@ value object="delivery" xpath="@idTracking" %>,<%= escapeUrl("$(urlId)") %>&utm_campaign=<%= variables.utm_content%>&utm_medium=email&utm_source=adobe_campaign
<%
if (document.mode == "forward")
{
 var d = message.getParameter("d")
 if( d )
 d = d.split(",")
%>
&ap_visitorId=<%=message.getParameter("visitorId") != '' ? message.getParameter("visitorId"):0 %>&ap_category=<%= d[0]?d[0]:'' %>&ap_deliveryId=<%=d[1]?parseInt(d[1],16):0%><%
} %>
<%
if( typeof proposition != "undefined" && proposition.length == undefined )
{ %>&ap_oid=<%= proposition.offer.id %>
<% } %>

The utm_medium and utm_source variables are hardcoded to be ’email’ and ‘adobecampaign’. These parameters are used by marketing analysts to understand the channel and source system that a visit can be attributed to.

Adding a variable to a delivery template

The above tracking formula depends on the delivery template having a variable named utm_content added into its context. If a template does not have this variable defined then the delivery will fail at the html compilation stage, and that’s always a pain to debug!

A good practise is to have the variable’s value taken from the internal delivery Id. This means your Google Analytics will be able to easily identify inbound links coming from specific emails sent from Adobe Campaign. It’s straightforward to set this within the delivery template as shown below.

 

add-delivery-template

Written by mark

October 8th, 2015 at 10:00 am

Personalize your EPiServer site by visit duration

without comments

Want to show a personalized message of gratitude to those users who spend a long time on your site? Now you can with the TimeOnSiteCriterion for the EPiServer Personalization/Visitor Group framework 😉

Technically – this is a straightforward implementation of a Visitor Group criteria, using some of the native Personalization API to record when the user starts their session. This is then compared to the current DateTime along with the editor selected time period to calculate a match.

The TimeOnSiteCriterion is available as part of Criteria Pack version 1.3, which is available on Codeplex (along with the source code) and of course there is an individual Nuget package on Nuget.episerver.com

An Extended example

One of the clients I’m working with at the minute had a requirement to display a modal pop-up on their site to user’s who had either visited a set number of pages, or had visited for more than a defined duration. Using the Visitor Group functionality sounded like a good match as it allows the editor’s to specify the exact criteria and mix and match with other matching criteria moving forwards.

The TimeOnSite criterion obviously forms part of this requirement, but fully meeting the requirement involves not waiting for a user to request a new page from your site and instead delegating responsibility to the browser to continue the countdown and respond (display the popup) when the criteria is matched. This is a requirement that the Visitor Group API doesn’t handle completely, as a Visitor Group is resolved as either a match or not a match when the page is delivered to the browser. There is no straightforward way of working out how ‘much’ of a match the current request may be.

Never the less, we still have the flexibility to achieve this:

  1. We need to link the site to a specific Visitor Group that will provide our criteria for when to display the popup (using a custom property like this will do the job – possibly on a site wide settings page)
  2. We need a method of working out how close we are to a match being made – in this case how many seconds are left. This data needs to be delivered to the browser.
  3. We need some client side script that will continue the countdown and trigger the popup when the countdown reaches zero.

The following public class is a part of the TimeOnSiteCriterion assembly, and contains the SecondsToMatch method to which can be used to work out how many seconds remain. The method will search a single Visitor Group – which is instantiated by Id (which is why we need a custom property to store this information), to find the first instance of a TimeOnSite model. This model contains the editor selected TimeOnSite criterion – which is then used with the user’s session start variable to calculate how long remains.


using System;
using System.Linq;
using System.Web;
using EPiServer.Personalization.VisitorGroups;

namespace CriteriaPack.TimeOnSiteCriterion
{
    public class TimeOnSiteService
    {
        public TimeOnSiteService()
        {
            Store = new VisitorGroupStore();
        }

        public TimeOnSiteService(IVisitorGroupRepository store)
        {
            Store = store;
        }

        public IVisitorGroupRepository Store { get; set; }

        private double CurrentValueInSeconds(Guid visitorGroupId)
        {
            //load visitor group by providing id
            var group = Store.Load(visitorGroupId);
            if (group == null)
                return 0;

            //return first instance of TimeOnSite Criterion
            var timeOnSiteCriteria = group.Criteria.FirstOrDefault(a => a.TypeName == VisitorGroupCriterionRepository.GetTypeName(typeof(TimeOnSiteCriterion)));
            if (timeOnSiteCriteria == null)
                return 0;

            var model = timeOnSiteCriteria.Model as TimeOnSiteModel;
            if (model == null)
                return 0;

            if (model.DurationUnit == DurationUnit.Minutes)
                return model.TimeOnSite * 60;

            if (model.DurationUnit == DurationUnit.Hours)
                return model.TimeOnSite * 60 * 60;

            return model.TimeOnSite;
        }

        private DateTime SessionStart(HttpContextBase httpContext)
        {
            return Convert.ToDateTime(httpContext.Session[TimeOnSiteCriterion.SessionStartTimeKey]);
        }

        /// <summary>
        /// Returns the number of seconds remaining until a match.
        /// </summary>
        /// <param name="httpContext">The HTTP context.</param>
        /// <param name="visitorGroupId">The visitor group id.</param>
        /// <returns></returns>
        public double SecondsToMatch(HttpContextBase httpContext, Guid visitorGroupId)
        {
            var matchTime = SessionStart(httpContext);
            matchTime = matchTime.AddSeconds(CurrentValueInSeconds(visitorGroupId));

            double timeToMatch = (matchTime - DateTime.Now).TotalSeconds;
            return timeToMatch < 0
                ? 0
                : timeToMatch;
        }
    }
}

The final part is to use this in a page:


   protected void Page_Load(object sender, EventArgs e)
   {
      var service = new TimeOnSiteService();

      //retrieve this from a page / sitewide property - allowing match from the page to the time tracking visitor group
      var visitorGroupId = new Guid("4c3bb8b2-4e8e-4fbc-bc24-5018e8c935af");

      //use the TimeOnSiteService to return the number of seconds to match
      double timeToLive = service.SecondsToMatch(new HttpContextWrapper(HttpContext.Current), visitorGroupId);
      //convert to milliseconds and write to the page
      TimeToLive.Text = (timeToLive *1000).ToString();
   }

and then write some javascript to handle this (also perhaps write it slightly better than below. Hey I’m no javascript developer – yet!)


  <script type="text/javascript">

        function setAlertTimeOut() {
          var timeToLive = '<asp:Literal id="TimeToLive" runat="server" />';
          if (timeToLive > 0) {
              setTimeout('AlertMe()', timeToLive);
          }
        }

        function AlertMe() {
            alert("Time is Up");
        }

    </script>

and also don’t forget to call the method setAlertTimeOut() when the browser loads your page. I’d also imagine that your front end will need to do something a little more fancy than pop up an alert……….

Written by mark

April 17th, 2012 at 10:00 am

Posted in C#,Code,EPiServer

A QueryString Visitor Group Criterion for EPiServer

with 2 comments

A common usage for advanced website analytics systems (Omniture, Google etc) is to track the effectiveness of external digital marketing campaigns such as banner ads, pay-per-click, sponsored search results……..

Technically, this is normally achieved by adding a querystring key to the campaign link url and then tracking requests that use this key. So for example, a Google Adwords campaign linking to your /products page would also include a querystring parameter of cid=marketingcampaign (example: http://yoursite.com/products?cid=marketingcampaign)

This querystring parameter can be picked up by your analytics implementation and used to track various aspects of the campaign.

On an EPiServer CMS6 R2 site, editors can use the Personalization/Visitor Group framework to provide a unique page experience per visitor, meaning that its possible to provide personalized content for each external marketing campaign on any page on your site.

Out-of-the-box, EPiServer provides a criterion which checks the incoming request Url or the page referrer, but not one that checks the querystring of the incoming request.

Creating a Visitor group criterion is straight forward – the only issue I’ve ever had trouble with; is when working on a .NET 4.0 project – which was solved by Ted Nyberg.

I’ve created a simple QueryString Criterion which will provide a match based on the following:

  1. Whether the current request contains a user specified querystring key
  2. and / or whether the current request contains a matching querystring key which also has the user specified value.

The source code is available as part of the Criteria Pack on Codeplex and of course there is a Nuget package on Nuget.episerver.com – (as soon as its been approved!)

Written by mark

November 7th, 2011 at 11:00 am

Posted in Code,EPiServer

GiveCamp UK – a philanthropic software development microcosm

with 2 comments

I was lucky enough to participate in the first UK GiveCamp over the course of the last weekend.

Wow, what an incredible experience!

“Pair 120 developers with a collection of UK charities each with an IT need. Lock them in a room, feed with caffeine, cooked pig and sugar. Leave to bake over the course of a weekend, peel open (and off the floor) on Sunday afternoon. Stand back and view the results.”

The concept, execution and community was superb (see @stack72’s post for a great list of thanks to those involved). A special thanks must also go to UCL for hosting the event and the generous sponsors for providing financial support and goodies! Come the Sunday afternoon ‘show and tell’, all of the project teams delivered some great work, much of which will make a tangible difference to each of the UK charities that got involved.

“For the first time in living memory, someone cried because the software we did was so good.”

What better testimonial than this? How many times have you had this reaction whilst working in your day job 😉

GiveCamp UK 2011

GiveCamp UK 2011 - photo by Bert Craven

A software development microcosm

Whilst the time-scales involved in GiveCamp make it an unreal experience, at the end of the day it’s just software development, and so the normal rules and pitfalls of software development apply.

The thing that really struck me whilst working, is that is that it’s very easy to get carried away and lose focus from the end output. Whilst much of this could be attributed to the excitement, intensity (and tiredness) that surrounds the event. Actually it is just par for the (software development) course.

If you’re ever involved in a future GiveCamp (and by all means you should), here are some of my top tips……

Deliver deliver deliver

Don’t overreach and try to build the Tower of Babel. Solving one problem well, is better than half solving many problems.

This means you constantly have to question the solution to make sure that every design decision that is made, is made for the right reasons. Do you really need that level of granular security or additional view? Focus on your core functionality only.

Remember building ‘cool’ stuff is not the output you’re looking for. Delivering a working solution that solves a real world problem is the ONLY goal….

Engagement

As ever, it is important that you have a engaged stakeholder / product owner – who is actively available to field questions and define their needs (we all know this from our day jobs right?)

Remember though, the charities will be like a kid in a sweetshop – whilst you are their ‘knight in shining armour’. It’s ok to question their requirement wish-list. 41 hours is not a long time to deliver a solution. So always make sure you stay focussed on solving a real and well defined problem.

Keep it simple stupid

After the weekend, the solutions are handed over lock and key to the charities. You need to make sure they know what you’ve built for them and that they have enough information to support it going forwards. This could mean documentation! One team produced a 40 page document on how to install a SQL Server instance. The time spent on that document was way more important than any one additional software feature. Without it, the solution wouldn’t have even been deployed.

Remember – not everybody knows the things you as a developer takes for granted. Imagine that you’re delivering a solution for your dear Nan. That’s the level you should aim for.

Focus on what you know

When you’re under pressure to deliver, don’t go off-piste and make some ‘left-field’ technology choices, so you can learn the latest new and shiny thing. Stick with what you know and what your team has capabilities with. Don’t worry, they’ll still be plenty of opportunity to learn.

For me I was working with ASP.NET / MVC but I still learnt more about Git, Entity Framework and what an awesome service AppHarbor provide.

Future me

I definitely wish I’d had ‘future me’, looking over my shoulder to remind of those things throughout the weekend (especially at 2am on Sunday morning when the adrenaline was wearing thin). However I also know that ‘future me’ would have told me how proud he was of all of us who donated time and effort to help out.

Top stuff to all involved. Here’s to GiveCamp UK 2012!

Written by mark

October 26th, 2011 at 2:00 am

Posted in Code,Community,Opinion

What are you going to do about it?

without comments

As programmers, we’re very used to dealing with systems that are easy to understand and behave predictably (though I have worked with some codebases that are the exact opposite).

Aside from code, the majority of our day-to-day interactions in the workplace are with team members, managers, clients and other colleagues. These human interactions are often much harder to predict and respond to, but can have a much greater impact on your work environment, productivity and workplace happiness.

I attended Roy Osherove’s ‘Lead Better London’ course held at Skills Matter back in July (and have been meaning to post a follow up about it ever since). Roy was / is a leading voice in the .NET community (though now he’s exploring what Ruby has to offer) and is the author of ‘The Art of Unit Testing‘, which I consider to be THE book to read if you want to learn and become better at writing test code for your software. Roy regularly blogs about software team leadership issues over on FiveWhys.com, and it is from there where I found out about the course.

The 2-day course caught my eye, as it was focussed on managing people and software teams rather than any specific project methodology, such as agile. Courses such as these give you a great opportunity to take a step back and view your workplace and your interactions within it in a different light, without the distraction of your ‘real work’. Roy is a great mentor and from the off, ably demonstrated the ‘ninja’ leadership techniques he was trying to teach to us. His ability to listen, comprehend and consolidate a problem down to a fundamental is a skill I really want to perfect.

Things I took home with me from the course:

  • Confidence to Lead: motivation to tackle the issues I see day-to-day in my workplace
  • How I react: ability to recognise my own and my colleagues behavioural traits and adjust my response to suit
  • Who I want to be: a clearer idea of the characteristics I believe make a good leader and the willpower to make sure I follow the right path.
lego-bricks

The plural of Lego is Lego, not Legos. The word Lego is a trademark and should be used as such. Here is a picture of some Lego bricks.

Roy is holding another course in London this November (2011) , so if you’re interested in learning how to lead and grow a software team, then you should do what you need to do to get yourself on his course.

For me, the challenge now is to take the skills I’ve learnt and put them into practise in my day-to-day work life.

Let the human experiment begin……

Written by mark

September 12th, 2011 at 11:00 am

Posted in Code,Opinion

Serving videos to iOS devices from EPiServer VPP folders

with 9 comments

We recently launched an EPiServer site that made moderate, but high-profile usage of video and was also designed to be iOS device friendly – meaning no Flash and Html5 used to display video. Late on in development / integration we came across an issue where the mp4 / m4v videos file that were being served from an EPiServer VPP folder did not play when viewed on an iOS device. A little investigation found that the video files would play correctly when served natively from a static / non-content managed location.

(Note: If you’re serving a large amount of media content, then you’d be better off looking at a full media hosting solution rather than just serving from the EPiServer VPP).

Http-Headers

Obviously the files were identical, so the only difference was in how IIS was serving them. Below are the Http responses (captured using FireBug) for the identical .m4v video files, one served from a VPP, and one from a native path.

The significant difference is the Accept-Ranges : bytes header. Cool – so we can just add in this header to our response and we’re sorted right? WRONG I’m afraid!

HTTP/1.1 Accept-Ranges header field

The Accept-Ranges response-header field allows the server to indicate its acceptance of range requests for a resource. This means that the server is open to partial downloads of files, and that clients (web-browsers) can download a limited ‘chunk’ of a file by requesting a specific byte-range, for-example bytes 5001-9999.

Going back to our original issue of why the files didn’t play on an iOS device, a quick look on the Apple developer notes (via StackOverflow) confirms that the web server needs to be configured to correctly handle byte-range requests. This explains why our file doesn’t work on an iOS device when served from an EPiServer VPP.

EPiServer.Web.StaticFileHandler

Our site was running using IIS7.5 in integrated pipeline mode, meaning that pretty much the whole Request / Response pipeline is handled by ASP.NET modules. Looking in the EPiServer site web.config you can see that the location path elements for VPP paths (Global /PageFiles etc) contain the HttpHandlers that are responsible for serving VPP filess. This is a bespoke EPiServer file handler implementation (EPiServer.Web.StaticFileHandler), which unlike the default IIS7 StaticFileHandler does not support Range-Requests 🙁

The Solution

Now you didn’t think that I’d explain all of this, without heroically coming up with a solution did you? Well I am providing a solution, but its hardly heroic as most of the hard work was done many years ago by Scott Mitchell – see Range-Specific Requests in ASP.NET. In his post Scott explains and provides an example of how to roll a HttpHandler with support for range-specific HTTP requests.

I’ve inherited from the RangeRequestHandlerBase class that Scott posted in the above article, and overidden a few methods adding in some EPiServer specifics, such as mapping the request path to the physical VPP file path and also using the EPiServer.Web.MimeMapping class to handle mapping from an extension to a mime type.


    public class RangeRequestFileHandler : RangeRequestHandlerBase
    {
        ///
        /// Returns a FileInfo object from the mapped VirtualPathProviderFile
        ///
        /// <param name="context" />
        ///
        public override FileInfo GetRequestedFileInfo(HttpContext context)
        {
            UnifiedFile file = GetFileInfoFromVirtualPathProvider(context);

            if (file == null)
                return null;

            PreventRewriteOnOutgoingStream();
            return new FileInfo(file.LocalPath);
        }

        private static UnifiedFile GetFileInfoFromVirtualPathProvider(HttpContext context)
        {
            return GenericHostingEnvironment.VirtualPathProvider.GetFile(context.Request.FilePath) as UnifiedFile;
        }

        ///
        /// Returns the MIME type from the mapped VirtualPathProviderFile
        ///
        /// <param name="context" />
        ///
        public override string GetRequestedFileMimeType(HttpContext context)
        {
            UnifiedFile file = GetFileInfoFromVirtualPathProvider(context);
            return MimeMapping.GetMimeMapping(file.Name);
        }

        ///
        /// Prevents episerver rewrite on outgoing stream.
        ///
        private void PreventRewriteOnOutgoingStream()
        {
            if (UrlRewriteProvider.Module != null)
                UrlRewriteProvider.Module.FURLRewriteResponse = false;
        }

This handler can be used to serve files from your VPP folders by adding the following configuration (IIS7 only folks – which if you’re not using then you really should be pushing for an upgrade!). This handler listed below is set up only to serve files of type .m4v – and remember that it does not provide any of the native EPiServer functionality for setting the staticFile cache expiration time.


<location path="Global">
     <system.webServer>
         <handlers>
              <add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader"/>
              <add name="videofiles" path="*.m4v" verb="*" type="FortuneCookie.RangeRequestFileHandler.RangeRequestFileHandler, FortuneCookie.RangeRequestFileHandler"/>
              <add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer"/>
         </handlers>
      </system.webServer>
      <staticFile expirationTime="-1.0:0:0"/>
 </location>

The full source code can be downloaded from the Code section at EPiServer World and a NuGet package is available that will auto-magically add in the code and configure your Global and PageFile VPP’s to serve .mp4, .m4v and .mov video files using the RangeRequestFileHandler.

Written by mark

July 5th, 2011 at 10:00 am

Impersonating Visitor Groups from Code

without comments

If you require that a particular HttpRequest impersonates one or more Visitor Groups,  then you can do so by adding one of two keys to the current HttpContext.

Before calling the logic to check each criteria in the VisitorGroup; and determining whether the current User is matched to the group, the VisitorGroupRole class will first check if the current HttpContext contains an item called “ImpersonatedVisitorGroupsById” or an item called “ImpersonatedVisitorGroupsByName”. If these items contain either a valid VisitorGroupRole.Id, or VisitorGroupRole.Name respectively, then the current user will be matched to that Visitor Group regardless of whether they match the Criteria rules.

This is used by EPiServer in edit mode, to allow editors to preview pages as viewed by particular Visitor Groups.

Here  is an method that will ensure that users are always evaluated as being in the Visitor Groups specified by id the method’s arguments. (Note, this method uses the MVC ControllerContext to access the current HttpContext. If you are using this method in a WebForms page you’ll need to substitute this for HttpContext.Current


protected void ImpersonateVisitorGroupsById(string[] visitorGroupIds)
{
     var impersonatedGroupsById =
               ControllerContext.HttpContext.Items["ImpersonatedVisitorGroupsById"] as List<string>
                                                                      ?? new List<string>();
     impersonatedGroupsById.AddRange(visitorGroupIds);
     ControllerContext.HttpContext.Items["ImpersonatedVisitorGroupsById"] = visitorGroupIds;
}

Written by mark

May 9th, 2011 at 9:00 am

Posted in ASP.NET,C#,Code,EPiServer

Using the DynamicDataStore to count file downloads

without comments

Increasingly CMS builds are required to deal with more than just standard page-based data and deliver an additional level of content metadata, such as the number of page views or the number of times a file has been downloaded.

The DynamicDataStore (introduced in EPiServer CMS6) provides developers with a convenient and easy-to-use persistence mechanism which is well suited to this type of content metadata.

One of the more simple requirements is to track the number of times a document is downloaded. The best place to handle this using ASP.NET is to create your own HttpHandler which will be responsible for serving the file and also updating the download count. This is better than any page based / codebehind methods as you can be sure that however the document is accessed (even via direct url) you’ll record the count. Remeber though, actually verifying that the file has been downloaded sucessfully is a whole different ball game – the count will give you just the number of times the file has been requested.

Creating a HttpHandler is simply a matter of creating a class that implements the IHttpHandler interface. However as we don’t want to alter any of the underlying behaviour for serving a file, but just supplement a call to the DDS. We’ll just inherit the existing EPiServer StaticFileHandler and override the ProcessRequestInternal and IsReusable methods.

using System.Web;
using DownloadCount.Persistence;

namespace DownloadCount
{
   public class DownloadCountHttpHandler : EPiServer.Web.StaticFileHandler
   {
       public DownloadCountHttpHandler()
       {
          ViewCountStore = new DdsDownloadCountStore();
       }

       protected IDownloadCountStore ViewCountStore { get; set; }

       protected override bool ProcessRequestInternal(HttpContext context)
       {
          var permanentPath = PermanentLinkUtility.MapRequestPathToPermanentLink(context.Request.FilePath);
          ViewCountStore.UpdateDownloadCount(permanentPath);
          return base.ProcessRequestInternal(context);
       }

       public override bool IsReusable
       {
          get { return true; }
       }
    }
}

There are some EPiServer specifics to take into account as we’re dealing with files stored in the VPP using the VirtualPathVersioningProvider. We need a unique handle to each file to accurately record the download path so we’ll map the request path to an internal EPiServer permanent link ID. This ensures that the count will be maintained should the file be renamed through the File Manager interface. I’ve put together a simple utility class with a static method, that attempts to checks whether the requested path is a UnifiedFile (aka lives withing the VPP). If so, it returns the PermanentLinkVirtualPath.

using System.Web.Hosting;
using EPiServer.Web.Hosting;

namespace DownloadCount
{
   public static class PermanentLinkUtility
   {
       public static string MapRequestPathToPermanentLink(string filePath)
       {
          var file = HostingEnvironment.VirtualPathProvider.GetFile(filePath) as UnifiedFile;
          if (file == null) return filePath;
          return string.IsNullOrEmpty(file.PermanentLinkVirtualPath)
                   ? filePath
                   : file.PermanentLinkVirtualPath;
       }
   }
}

The data we need to store is represented by the following object. The class only has two properties, an int to hold the total number of downloads and a string to represent the unique file path to the file. You’ll notice that the FilePath property is marked with the [EPiServerDataIndex] attribute, which tells the DDS to map the property to an indexed database column. We’ll use this index to perform lookups when retrieving data from the store.

using EPiServer.Data.Dynamic;

namespace DownloadCount.Persistence
{
   public class FileDownloadCount
   {
      [EPiServerDataIndex]
      public string FilePath { get; set; }
      public int Count { get; set; }
   }
}

Below is the service layer that is responsible for dealing with the DDS (following the recommended usage pattern) . It implements the two members of the IDownloadCount interface, which provide means of accessing and updating the download count information.elpful

[EDIT – updated as per Paul Smith’s helpful comment below]

using System.Web;
using EPiServer.Data.Dynamic;
using System.Linq;

namespace DownloadCount.Persistence
{
   public class DdsDownloadCountStore : IDownloadCountStore
   {
      protected DynamicDataStore GetDynamicDataStore()
      {
         return DynamicDataStoreFactory.Instance.GetStore(typeof(FileDownloadCount)) ??
               DynamicDataStoreFactory.Instance.CreateStore(typeof(FileDownloadCount));
      }

      public void UpdateDownloadCount(string path)
      {
         path = UrlDecodePath(path);
         using(var store = GetDynamicDataStore())
         {
            var fileCount = store.Find<FileDownloadCount>("FilePath", path).FirstOrDefault()
                            ?? FileDownloadCountFactory.Create(path);

            fileCount.Count++;
            store.Save(fileCount);
         }
      }

      private static string UrlDecodePath(string path)
      {
         return HttpUtility.UrlDecode(path);
      }

      public int GetDownloadCount(string path)
      {
         path = UrlDecodePath(path);
         using (var store = GetDynamicDataStore())
         {
           var fileCount = store.Find<FileDownloadCount>("FilePath", path).FirstOrDefault();
           return fileCount == null ? 0 : fileCount.Count;
         }
      }
   }
}

Finally, to use this handler we need to register it in the web.config file. Here I’m going to record counts for just .pdf and .doc files that are stored in the Globals VPP folder. You need to add one instance of the handler for each type of file you want to track. Don’t forget to chain the handlers appropriately (ie the existing wildcard handler should always be defined last).


<location path="Global">
 <system.webServer>
  <handlers>
   <add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader"/>
   <add name="countdocs" path="*.doc" verb="*" type="DownloadCount.DownloadCountHttpHandler, DownloadCount"/>
   <add name="countpdfs" path="*.pdf" verb="*" type="DownloadCount.DownloadCountHttpHandler, DownloadCount"/>
   <add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer"/>
  </handlers>
 </system.webServer>
 <staticFile expirationTime="-1.0:0:0"/>
 </location>

Accessing the latest count on page is pretty simple, again using the MapRequestPathToPermanentLink method defined earlier.


var store = new DdsDownloadCountStore();
var permanentPath = PermanentLinkUtility.MapRequestPathToPermanentLink(FilePath);
DownLoadCount.Text = string.Format("Downloaded {0} times", store.GetDownloadCount(permanentPath));

Full source code is available zipped up here, or in the EPiServer World code section.

Written by mark

September 15th, 2010 at 8:06 pm

Posted in ASP.NET,C#,Code,EPiServer