Showing posts with label Enterprise. Show all posts
Showing posts with label Enterprise. Show all posts

Monday, August 3, 2009

Storing business objects in EPiServer

I recently had a discussion with someone about how best to store collections in EPiServer, such as RSS feeds, advertisements, frequently asked questions, customer details etc. Here are my thoughts...

Using custom properties

If you wanted to display a collection of objects on a page, then usually you might create a custom property with a suitable interface so that editors can add to, and maintain the list. You could then register this property for a particular page type - so that each page of this page type can have its own list, or as a dynamic property - so that it is available as a shared list, and available across multiple page types.

This probably makes the most sense. However, the problem with this is that there is often a lot of development that needs to be done, e.g.:

  • Developing a suitable editor for the collection (which might need to allow sorting, grouping, searching, validation etc.)
  • Developing the web control that will render the list from the serialised data.
  • Adding the more advanced features that your business might need, such as workflows for approvals, scheduling the inclusion and deletion of items from the list, security and so on.

Even if you are sure that you know exactly what your business requires, there is still the chance for scope creep, and a future of upgrades and maintenance.

Using pages

One of the ways to make your life easier is to create your collections as EPiServer pages. This instantly gives you a huge amount of the functionality you might require to manage the items. You will have the ability to:
  • Easily define the properties of the items through their page type
  • Add workflows such as approvals
  • Set up access for different users and groups with different privileges
  • Use the many EPiServer page list controls that are available to render the list

And your editors will be able to:

  • Perform the usual C.R.U.D. operations
  • Organise items in a list or hierarchy, with grouping and sorting.
  • Search for items etc. etc.

In short, EPiServer is designed to manage a large collection, and this is what it does well! Of course, an item (in these sorts of collections) is a kind of content that does not directly correlate to a page on a website, so it might seem a bit foreign to re-use this functionality. However, it is a simple matter make sure that no-one can browse to an item (which now has its own URL) by adding some redirection or error trapping logic to the load event of the item’s page type ASPX, or through an event handler on the BeginRequest event. Actually it might be good to have the ability to display a page per item, if you wanted to give editors a preview how the item might look when displayed in a list! :)

There are some down sides though. EPiServer makes some assumptions about your items. It thinks they are pages of a site! This means adding a huge amount of overhead to them. The database stores a lot of information about pages in EPiServer that you might not want, and this, combined with the fact that the relational model isn’t going to be entirely optimised for your needs, will make data access slower than it needs to be. The API used to get page data back from the database is optimised in certain ways (such as retrieving the properties of the page), but this won’t do you any good if you have related objects in your list. You will often need several database reads on each page request. Caching may only get you so far as well.

Nowhere can potential performance issues been seen better, than in the page tree in the EPiServer edit mode. Imagine if you had thousands, or even millions of items in your collection. This interface is not designed to support lists of this scale. Even when in optimised mode, if your items do not lend themselves nicely to being stored in a hierarchy, and end up all under one parent node, then the interface will eventually become unusable, and you’ll need to write your own.

With a large collection of pages, you may also face performance issues on individual page requests, since the default EPiServer URL provider will have to search through all of your pages to find the page ID which corresponds to the requested URL.

If performance and scale are issues for you then the last resort is to start...

Using a custom data access layer

In the end, creating your own tables and stored procedures etc, may be the way to go, since you have so much extra control over the functionality, but generally I wouldn’t recommend it unless you really need to. EPiServer has so much functionality that can be re-used, it would seem silly to me not to try.

Summary

To summarize, using pages for your business objects is often a very good solution which is quick to implement and easily adopted by your editors. But what you need to bear in mind is that EPiServer is not a content management system in the literal sense. ‘Page management system’ would be more accurate, and if you’re thinking of using pages to manage your collections, you need to be sure that performance not be a problem.

It would be nice to see EPiServer change (or expand) their architecture in the future for enterprise applications in need of managing more than just pages. Of course, for specific markets, products and plugins are already available (EPiServer Community for instance).

Friday, July 31, 2009

EPiServer Enterprise Virtual Path Provider

This article demonstrates how to create a virtual path provider which can be used to serve site specific files, while still using the same application directory for your sites.

Introduction to EPiServer Enterprise

For those of you who have been able to play with a copy of EPiServer running an enterprise license, you can skip the next couple of paragraphs. For those who haven’t, here’s how it works:

Each site defined in IIS has the same home directory. When a page of one of the sites requested, EPiServer looks at the host header to see which set of pages (site) should be served. Multiple sites can be defined in the EPiServer section of web.config, and the host header(s) and start page for each site are configured.

When pages are created, EPiServer handles the fact that you might want to use the same URLs for pages under different sites. There are also many site settings which allow you to keep control over the way each site works, despite the fact they share the same code. Users, roles, access, workflow, categories, custom properties, etc, can all be administered from within a single interface, but you still have the ability to specify how these work over different sites. One of the main benefits of this architecture is that you can share resources, such as images, CSS, web controls, and page types over all your sites.

The Problem

The problem is, that there is no built in ability in EPiServer, for creating site specific resources for a given URL, or virtual path. Say for instance that you would like to use a common URL for two sites;

‘site1.com/logo.gif’, and
‘site2.com/logo.gif’.

Because these sites share the same application directory, this is impossible, without both sites having the same logo. You could separate all your site specific files into different sub directories, but this would give you URLs like these;

‘site1.com/site1-files/logo.gif’ and
‘site2.com/site2-files/logo.gif’.

This works fine but it’s not very elegant to look at. More importantly than how it looks though, are the implications for using these URLs in your application, and for virtual paths used on the server. Any ASP.NET page or control which wants to use a particular image, would have to be implemented once for each site, or, it would have to get the logo location from somewhere that can change per site, such as page properties, or some other custom site-aware system.

Now consider that you might want something like a page type to be common across multiple sites (because they work in exactly the same way and use the same controls) but you want the layout and the style of the page type to be different on each site. You should be able to specify different master pages for each site, and maybe different css files, without having to create more than one ASPX, and define more than one page type in EPiServer. Unfortunately, if you’re using a common ASPX for your page type, it will have to use the same resources. This is also true of things like web controls.

The Solution

One solution is to create a virtual path provider (VPP) which serves different files depending on which site is being requested. The logic is simple. When a request for a file is received, we check to see if there is a site-specific version of that file available. If there is, then we serve that file. If there isn’t, then we don’t serve anything from the VPP, instead allowing the file to be served by ASP.NET from the main application directory (the shared resources).

Here's the code for the virtual path provider and related classes:


using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;

namespace EPiDave.Web.Hosting
{

/// <summary>
/// Virtual Path Provider designed to expose a file system for a
/// particular directory which contains files specific to a
/// particular website.
/// </summary>
/// <remarks>
/// This class cannot be used with the EPiServer file manager UI.
/// </remarks>

public class SiteSpecificPathProvider : VirtualPathProvider
{

private string _baseVirtualPath;

/// <summary>
/// Constructor - Loads the virtualPath property from supplied config settings
/// as the base virtual path to be used for the lifetime of the class instance.
/// </summary>
/// <param name="str">Name of the VPP (unused but required for construction by EPiServer)</param>
/// <param name="nvc">Collection of config properties for the VPP (from web.config)</param>

public SiteSpecificPathProvider(string name, NameValueCollection configProperties)
: base()
{

string virtualPath = configProperties["VirtualPath"];

if (String.IsNullOrEmpty(virtualPath))
_baseVirtualPath = "/";
else
_baseVirtualPath = VirtualPathUtility.AppendTrailingSlash(VirtualPathUtility.ToAbsolute(virtualPath));

}

/// <summary>
/// IsVirtual tests to see if the virtual path provided is one this VPP can serve
/// based on the loaded base virtual path.
/// </summary>
/// <param name="virtualPath">The virtual path requested</param>
/// <returns>True or false</returns>
/// <example>
/// Should return true if supplied path is under
/// "/base/" or is "/base" directory itself.
/// </example>

private bool IsVirtual(string virtualPath)
{

if (virtualPath.StartsWith(_baseVirtualPath, StringComparison.InvariantCultureIgnoreCase) ||
virtualPath.Equals(VirtualPathUtility.RemoveTrailingSlash(_baseVirtualPath), StringComparison.InvariantCultureIgnoreCase))
return true;
else
return false;

}

/// <summary>
/// ConvertVirtualToReal converts a virtual path to a real file system path
/// </summary>
/// <param name="virtualPath">The virtual path</param>
/// <param name="SiteResourceMapping">The mapped directory</param>
/// <returns>Server file path</returns>
/// <example>
/// Converting "~/Resources/logo.gif" with a directory mapping of "~/SiteResources/Site1/"
/// will result in "C:\ApplicationPath\SiteResources\Site1\Resources\logo.gif"
/// </example>

internal string ConvertVirtualToReal(string virtualPath)
{

string hostName = HttpContext.Current.Request.ServerVariables["HTTP_HOST"];

string path = "SiteSpecific/" + hostName + VirtualPathUtility.ToAbsolute(virtualPath);
path = HttpContext.Current.Server.MapPath(path);

return path;

}

internal bool FileAvailable(string virtualPath)
{
if (IsVirtual(virtualPath))
{
if (File.Exists(ConvertVirtualToReal(virtualPath)))
return true;
}
return false;
}

internal bool DirectoryAvailable(string virtualPath)
{
if (IsVirtual(virtualPath))
{
if (Directory.Exists(ConvertVirtualToReal(virtualPath)))
return true;
}
return false;
}

/// <summary>
/// FileExists tests to see whether or not a file exists at
/// a particular virtual path.
/// </summary>
/// <param name="virtualPath">The virtual path of the file</param>
/// <returns>True / false</returns>

public override bool FileExists(string virtualPath)
{

// If the file exists return true,
// else pass to the next provider in the list.
if (FileAvailable(virtualPath))
return true;

return Previous.FileExists(virtualPath);

}

/// <summary>
/// DirectoryExists tests to see whether or not a directory exists at
/// a particular virtual path.
/// </summary>
/// <param name="virtualDir">The virtual path of the directory</param>
/// <returns>True / false</returns>

public override bool DirectoryExists(string virtualDir)
{

// If the directory exists return true,
// else pass to the next provider in the list.
if (DirectoryAvailable(virtualDir))
return true;

return Previous.DirectoryExists(virtualDir);

}

/// <summary>
/// GetFile gets the specified file as SiteResourceFile object.
/// </summary>
/// <param name="virtualPath">The virtual path of the file</param>
/// <returns>SiteResourceFile</returns>

public override VirtualFile GetFile(string virtualPath)
{

// If the file exists return it,
// else pass to the next provider in the list.
if (FileAvailable(virtualPath))
return new SiteResourceFile(virtualPath, this);

return Previous.GetFile(virtualPath);

}

/// <summary>
/// GetDirectory gets the specified directory as VirtualDirectory object.
/// </summary>
/// <param name="virtualDir">The virtual path of the directory</param>
/// <returns>SiteResourceDirectory</returns>

public override VirtualDirectory GetDirectory(string virtualDir)
{

// If the directory exists return it,
// else pass to the next provider in the list.
if (DirectoryAvailable(virtualDir))
return new SiteResourceDirectory(virtualDir, this);

return Previous.GetDirectory(virtualDir);

}

/// <summary>
/// GetFileHash provides a hash (including version) of the path that should be used now.
/// If this hash is different to that in memory, the cache will be invalidated.
/// </summary>
/// <remarks>
/// The reason we do not use a cache dependency is:
/// a) There is no clear event that can be used to fire the notification of invalidation.
/// b) The site specific directory needs to be checked despite a core cache that may still be valid.
/// </remarks>
/// <param name="virtualPath">The virtual path of the file or directory</param>
/// <param name="virtualPathDependencies">The virtual path dependencies already known</param>
/// <returns>File or directory hash</returns>

public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
{
if (IsVirtual(virtualPath))
{
string timestamp;
string realpath;

// Try to find a file by that name
if (FileExists(virtualPath))
{
// If we find a file, return the hash of the latest version.
realpath = ConvertVirtualToReal(virtualPath);
timestamp = File.GetLastWriteTimeUtc(realpath).ToString();
return realpath + timestamp;
}

// Finding file has failed. Try to find a directory by that name
if (DirectoryExists(virtualPath))
{
// If we find a directory, return the hash of the latest version.
realpath = ConvertVirtualToReal(virtualPath);
timestamp = Directory.GetLastWriteTimeUtc(realpath).ToString();
return realpath + timestamp;
}

// No file or directory exists, so return null.
return null;
}
return Previous.GetFileHash(virtualPath, virtualPathDependencies);
}

/// <summary>
/// GetCacheDependency returns null for paths within our virtual path,
/// since we're using the GetFileHash function for invalidating our cache.
/// </summary>
/// <param name="virtualPath">The virtual path</param>
/// <param name="virtualPathDependencies">The virtual path dependencies already known</param>
/// <param name="utcStart">The date and time</param>
/// <returns>null</returns>

public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsVirtual(virtualPath))
return null;

return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}

}

/// <summary>
/// SiteResourceDirectory - Class representing a virtual directory
/// </summary>

public class SiteResourceDirectory : VirtualDirectory
{

string _virtualPath;
SiteSpecificPathProvider _provider;

/// <summary>
/// Constructor
/// </summary>
/// <param name="virtualPath">The virtual path requested</param>
/// <param name="provider">The SiteSpecificPathProvider</param>

public SiteResourceDirectory(string virtualPath, SiteSpecificPathProvider provider)
: base(virtualPath)
{
_virtualPath = virtualPath;
_provider = provider;
}

/// <summary>
/// Children property - Returns the complete list
/// of files and sub directories within this directory
/// </summary>

public override IEnumerable Children
{
get
{
List<object> children = new List<object>();
foreach (object dir in Directories)
{
children.Add(dir);
}
foreach (object file in Files)
{
children.Add(file);
}
return children;
}
}

/// <summary>
/// Directories property - Returns a list of sub
/// directories within this directory
/// </summary>

public override IEnumerable Directories
{
get
{
// Create a new list of directories
IList<SiteResourceDirectory> directories = new List<SiteResourceDirectory>();

// Get REAL path from virtual path
string realDir = _provider.ConvertVirtualToReal(_virtualPath);

// If current directory exists, get sub directories
if (realDir != null)
{

// Find directories in this directory
string[] subDirectoryEntries = Directory.GetDirectories(realDir);
foreach (string subDirectory in subDirectoryEntries)
{
string s = subDirectory.Replace(HttpRuntime.AppDomainAppPath, "~/").Replace(Path.DirectorySeparatorChar, '/');
directories.Add(new SiteResourceDirectory(VirtualPathUtility.AppendTrailingSlash(VirtualPathUtility.Combine(_virtualPath, s)), _provider));
}

}

return directories;
}
}

/// <summary>
/// Files property - Returns a list of files
/// within this directory
/// </summary>

public override IEnumerable Files
{
get
{
// Create a new list of files
IList<SiteResourceFile> files = new List<SiteResourceFile>();

// Get REAL path from virtual path
string realDir = _provider.ConvertVirtualToReal(_virtualPath);

// If current directory exists, get files
if (Directory.Exists(realDir))
{

// Find files in this directory
string[] fileEntries = Directory.GetFiles(realDir);
foreach (string file in fileEntries)
{
string f = file.Replace(HttpRuntime.AppDomainAppPath, "~/").Replace(Path.DirectorySeparatorChar, '/');
files.Add(new SiteResourceFile(VirtualPathUtility.Combine(VirtualPathUtility.AppendTrailingSlash(_virtualPath), f), _provider));
}

}

return files;
}
}

}

/// <summary>
/// SiteResourceFile - Class representing a virtual file
/// </summary>

public class SiteResourceFile : VirtualFile
{

string _virtualPath;
SiteSpecificPathProvider _provider;

/// <summary>
/// Constructor
/// </summary>
/// <param name="virtualPath">The virtual path of the file</param>
/// <param name="provider">The SiteSpecificPathProvider</param>

public SiteResourceFile(string virtualPath, SiteSpecificPathProvider provider)
: base(virtualPath)
{
_virtualPath = virtualPath;
_provider = provider;
}

/// <summary>
/// Exposes a stream to the file in our virtual file system
/// </summary>
/// <returns>Stream</returns>

public override Stream Open()
{

string realPath = _provider.ConvertVirtualToReal(_virtualPath);

// If the file exists put the content on the stream.
if (File.Exists(realPath))
{
return new FileStream(realPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
return null;

}

}

}
EPiServer kindly gives us the ability to register this virtual path provider in the web.config.




<add name="SiteSpecificFiles" virtualpath="~/Resources/" type="EPiDave.Web.Hosting.SiteSpecificPathProvider, EPiDave" />


Of course, there are other solutions to this problem, but I find this one is very easy to work with. With images and other binary files, it means you don't need any special http handlers. With ASPXs and ASCXs etc, it means that you can have clean markup. You don't have to implement some site specific logic behind each part of your page or control. All you need to do, is create a different resource, and put it in the correct directory on the server.