The pain of success
The heading might come off as slightly arrogant, but it is way more catchy than "The pain of trying to stay binary compatible between releases". EPiServer CMS has been very successful with a large number of installations. With every new release we want as many as possible to upgrade because every new release is always the best ever! If we didn't believe that we wouldn't release it.
Overview
There have been a few occasions where the upgrade path was not very smooth (*cough*), for instance between EPiServer CMS 4 and CMS 5. However that step was an exception - very early in the EPiServer CMS 5 development plan we decided that anything we plan to break compatibility-wise (mainly to take advantage of news in .NET 2.0) should be done in the first release of EPiServer CMS 5. However there have been other releases where changes have affected compatibility in undesirable ways. Since CMS 5 R2 we have a much stricter automated API checker in place that gives warnings as soon as we break compatibility at the binary level.
Maintaining binary compatibility simply means that upgrading EPiServer CMS will not require recompilation of your existing website. This is what our partners and customer want, so we work very hard to achieve this goal. With a lot of existing sites, even the smallest glitch can affect a lot of customers (the pain of success...).
The details
I have just finished work on a change that involved some fancy footwork to keep compatibility and improve both functionality and performance at the same time. Some background - the Friendly URL (FURL for short) feature in EPiServer CMS gives you readable and nice looking URLs that mirror the structure of your site. It is also a feature that consumes quite a lot of CPU for each request (somewhere between 5 - 25%).
We have worked on improving the performance and immediately ran into compatibility problems with rewriting the FriendlyUrlRewriteProvider (FURP) class. The easy solution is to create a new provider and leave the old in place until we can remove it completely, a very nice benefit of the provider model. This is what we did, thus the HierachicalUrlRewriteProvider was born, which basically does the same job as the FriendlyUrlRewriteProvider.
But when you use FURL you might have parts of your URL namespace that you do not want to be rewritten, maybe some special file. The solution has been to add these paths to the UnTouchedPaths property on FriendlyUrlRewriteProvider (or you can attach event handlers, but that is another story).
Here is a typical compatibility problem - the property lives on the FURP class and we know that there is code "in the wild" that uses this property. Since HierachicalUrlRewriteProvider should have this feature as well, why not simply delegate to FURP? ...or even inherit from FURP? Remember that we said that we eventually want to remove FURP - inheriting from it would mean breaking binary compatibility when FURP dies.
As it is a feature that we want both FURP and our new provider to share, it makes sense to move it to a common base class. The UrlRewriteProvider class is that base class. However simply moving UnTouchedPaths to UrlRewriteProvider is not enough. The property is simply a list of strings that was linearly scanned for matching paths and this immediately translates to poor performance as soon as the number of entries starts to increase. It is also limited to matching complete paths and we wanted to expand the feature into matching entire directory structures as well.
We ended up by implementing three methods and one property on UrlRewriteProvider:
- void AddExcludedPath(string path) - Does what the API says. The twist is that paths ending with a slash will be treated as directories and anything that starts with that string will be excluded. Other paths will be complete matches.
- bool IsExcludedPath(string path) - Returns true if the path matches anything added with AddExcludedPath
- void ClearExcludedPaths() - Once again quite self-explanatory.
- IEnumerable<string> ExcludedPaths - Expose what has been added with AddExcludedPath
That takes care of the needs for our new feature, but how do we match this with the existing UnTouchedPaths property on FURP? We fake it - or more correctly we created a new private class AddOnlyList that implements IList<string> (which is the type exposed by the UnTouchedPaths property). This class is not a complete list implementation, it only supports Add, Clear, Contains, Count and GetEnumerator. These implementations delegate to the methods/property on UrlRewriteProvider as described above. Other methods/properties will throw a NotImplemented exception.
We now have binary compatibility (UnTouchedPaths and FURP have the same public API as before) and improved functionality (ExcludedPath with directory support). What we don't have is complete semantic compatibility - there are holes in the List implementation returned by UnTouchedPaths and the actual behavior of the excluded paths is a bit different with directory matching. For example if we do
FriendlyUrlRewriteProvider.UnTouchedPaths.Add("/abc/");
then calling
FriendlyUrlRewriteProvider.UnTouchedPaths.Contains("/abc/somefile.htm");
would return true.
The solution is not perfect, but solves the vast majority of problems related to the UnTouchedPaths property.
What about the HierarchicalUrlRewriteProvider you may ask? Well, that will be the subject of another blog post.
HURL ? ;-)
It's worth keeping in mind that new releases of products like EPiServer are not only about new features :) I think people often don't realize how much attention you need to put to keep backward compatibility. Nice post, it was good reading!
Thank you for the comments!
Hello Magnus! Any chance of getting a description of the HierarchicalUrlRewriteProvider ? I can see that it is included in 6.x but i cannot seem to find any information about what it actually does?