Using the DynamicDataStore to count file downloads

U

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.

About the author

Mark Everard

I've worked across the digital industry for the past ten years, helping clients and colleagues across a diverse range of sectors meet numerous digital challenges, specifically focusing on web technologies, digital marketing and content management.

I've worked on large multi-supplier projects and led and managed both in-house and geographically-disperse development teams. And I've always approached my work with a smile on my face.

Mark Everard

I've worked across the digital industry for the past ten years, helping clients and colleagues across a diverse range of sectors meet numerous digital challenges, specifically focusing on web technologies, digital marketing and content management.

I've worked on large multi-supplier projects and led and managed both in-house and geographically-disperse development teams. And I've always approached my work with a smile on my face.

Get in touch