asp.net MVC3 Versioning of Views and Controllers

Sometimes it could be necessary to have ability to add “Versioning” to Pages on your site. For example we have URL like this – http://localhost:4444/Authentication/ShowLogin and this “ShowLogin” page used on many sites (e.g. integrated via iframes) and suddenly it is necessary to change something on this page, but not for all users of this page (required back compatibility).

First step is to use custom CSS. But when it is needed to make functional changes – completely change View, Model or Controller, in this case CSS won’t be enough. For this purpose we can request new Page via this URL – http://localhost:4444/Authentication/1001/ShowLogin where “1001” is version of the page. It is obviously that in case of “1001” version MVC Engine should find appropriate controller and views, and if no controllers or views were found – should be used default version (it sounds like View’s inheritance or overriding – this mechanism will allow to override only several views, not all).

How to implement this approach? – for this we need the following:

  • override default MVC controller’s factory – here we will define custom logic for searching and creation of controller
  • replace Views engine. Also it is needed to add new route.
  • define custom route to MVC routes
  • add ability to configure versions and other settings via web config

You can find complete example here – https://github.com/DjComandos/MvcVersioning/

Some clarifications how to do this. First of all it is needed to add new rule to Routes – it should be done in Global.asax

routes.MapRoute(
    "ControllerWithVersion",
    "{controller}/{version}/{action}/{id}",
    new {controller = string.Format(@"\{0}\", config.ControllerNameRegexForEnablingCustomControllerFactory), 
        action = "ShowLogin", id = UrlParameter.Optional},
    new {version = @"\d{4}"});

Here “config.ControllerNameRegexForEnablingCustomControllerFactory” is just a regex, something like “^Authentication|ForgotPassword$” (where Authentication or ForgotPassword are controllers names). This setting is stored in web.config.

Then it is needed to override default RouteControllerFactory – for this purpose we will create next class:

    public class RouteControllerFactory : IControllerFactory
    {
        private readonly IControllerFactory _defaultFactory = new DefaultControllerFactory();
        private readonly IAppConfiguration _configuration = new AppConfiguration();
 
        public IController CreateController(RequestContext requestContext, string controllerName)
        {
            IController controller = NeedToUseCustomFactory(controllerName, requestContext)
                                         ? CreateControllerInstance(requestContext)
                                         : _defaultFactory.CreateController(requestContext, controllerName);
 
            return controller;
        }
 
        public SessionStateBehavior GetControllerSessionBehavior(
            RequestContext requestContext, string controllerName)
        {
            return _defaultFactory.GetControllerSessionBehavior(requestContext, controllerName);
        }
 
        public void ReleaseController(IController controller)
        {
            _defaultFactory.ReleaseController(controller);
        }
 
        private IController CreateControllerInstance(RequestContext requestContext)
        {
            var currentVersion = requestContext.RouteData.Values.ContainsKey(_configuration.ApiVersionKey)
                                     ? requestContext.RouteData.Values[_configuration.ApiVersionKey].ToString()
                                     : _configuration.DefaultControllerVersion;
 
            var controllerVersionedTypeName = string.Format(
                "{0}{1}Controller",
                ControllerName(requestContext),
                _configuration.TryGetControllerPostfix(currentVersion));
            var controllerDefaultTypeName = string.Format(
                "{0}{1}Controller",
                ControllerName(requestContext),
                _configuration.TryGetControllerPostfix(_configuration.DefaultControllerVersion));
 
            var typeType = TypesFinder.FindTypeInExecutingAssembly(controllerVersionedTypeName) ??
                           TypesFinder.FindTypeInExecutingAssembly(controllerDefaultTypeName);
 
            var controller = Activator.CreateInstance(typeType) as IController;
 
            return controller;
        }
 
        protected string ControllerName(RequestContext requestContext)
        {
            return requestContext.RouteData.Values[_configuration.ControllerKey].ToString();
        }
 
        private bool NeedToUseCustomFactory(string controllerName, RequestContext requestContext)
        {
            return Regex.IsMatch(controllerName, _configuration.ControllerNameRegexForEnablingCustomControllerFactory)
                && requestContext.RouteData.Values.ContainsKey(_configuration.ApiVersionKey);
        }
    }

The main idea is that we use Custom Controller factory only in case is varsion is defined, for usual cases will be used default controllers factory. Let’s add some code to Global.asax after routing:

// register custom controller factory
ControllerBuilder.Current.SetControllerFactory(typeof(RouteControllerFactory));

Let’s override Views Rendering Engine

    public class VersionedRazorViewEngine : RazorViewEngine
    {
        private readonly IAppConfiguration _configuration = new AppConfiguration();
 
        public VersionedRazorViewEngine()
        {
            AreaViewLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}%1%/{0}.cshtml", 
                "~/Areas/{2}/Views/{1}%1%/{0}.vbhtml", 
                "~/Areas/{2}/Views/Shared%1%/{0}.cshtml", 
                "~/Areas/{2}/Views/Shared%1%/{0}.vbhtml"
            };
 
            AreaMasterLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}%1%/{0}.cshtml", 
                "~/Areas/{2}/Views/{1}%1%/{0}.vbhtml", 
                "~/Areas/{2}/Views/Shared%1%/{0}.cshtml", 
                "~/Areas/{2}/Views/Shared%1%/{0}.vbhtml"
            };
 
            AreaPartialViewLocationFormats = new[]
            {
                "~/Areas/{2}/Views/{1}%1%/{0}.cshtml", 
                "~/Areas/{2}/Views/{1}%1%/{0}.vbhtml", 
                "~/Areas/{2}/Views/Shared%1%/{0}.cshtml", 
                "~/Areas/{2}/Views/Shared%1%/{0}.vbhtml"
            };
 
            ViewLocationFormats = new[]
            {
                "~/Views/{1}%1%/{0}.cshtml", 
                "~/Views/{1}%1%/{0}.vbhtml", 
                "~/Views/Shared%1%/{0}.cshtml", 
                "~/Views/Shared%1%/{0}.vbhtml"
            };
 
            MasterLocationFormats = new[]
            {
                "~/Views/{1}%1%/{0}.cshtml", 
                "~/Views/{1}%1%/{0}.vbhtml", 
                "~/Views/Shared%1%/{0}.cshtml", 
                "~/Views/Shared%1%/{0}.vbhtml"
            };
 
            PartialViewLocationFormats = new[]
            {
                "~/Views/{1}%1%/{0}.cshtml", 
                "~/Views/{1}%1%/{0}.vbhtml", 
                "~/Views/Shared%1%/{0}.cshtml", 
                "~/Views/Shared%1%/{0}.vbhtml"
            };
        }
 
        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            return base.CreatePartialView(controllerContext, GetViewPath(controllerContext, partialPath));
        }
 
        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            return base.CreateView(
                controllerContext, GetViewPath(controllerContext, viewPath), GetViewPath(controllerContext, masterPath));
        }
 
        protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            return base.FileExists(controllerContext, GetViewPath(controllerContext, virtualPath))
                   ||
                   (IsVersioningEnabled(controllerContext) &&
                    base.FileExists(controllerContext, GetViewPath(controllerContext, virtualPath, _configuration.DefaultViewsVersion)));
        }
 
        private bool IsVersioningEnabled(ControllerContext controllerContext)
        {
            return controllerContext.RouteData.Values.ContainsKey(_configuration.ControllerKey)
                   &&
                   Regex.IsMatch(
                       controllerContext.RouteData.Values[_configuration.ControllerKey].ToString(), _configuration.ControllerNameRegexForEnablingCustomControllerFactory)
                   && controllerContext.RouteData.Values.ContainsKey(_configuration.ApiVersionKey);
        }
 
        private string GetVersion(ControllerContext controllerContext)
        {
            if (IsVersioningEnabled(controllerContext))
            {
                return controllerContext.RouteData.Values.ContainsKey(_configuration.ApiVersionKey)
                           ? controllerContext.RouteData.Values[_configuration.ApiVersionKey].ToString()
                           : _configuration.DefaultViewsVersion;
            }
 
            return string.Empty;
        }
 
        private string GetViewPath(ControllerContext controllerContext, string virtualPath, string version = null)
        {
            if (string.IsNullOrEmpty(virtualPath))
            {
                return string.Empty;
            }
 
            version = version ?? GetVersion(controllerContext);
            return base.FileExists(controllerContext, virtualPath.Replace("%1%", _configuration.TryGetViewFolderPostfix(version)))
                       ? virtualPath.Replace("%1%", _configuration.TryGetViewFolderPostfix(version))
                       : virtualPath.Replace(
                           "%1%", _configuration.TryGetViewFolderPostfix(_configuration.DefaultViewsVersion));
        }
    }

Main idea – replace mechanism that will find necessary views (just add version number to path). As a result views for default version will be in “Views/Authentication” folder, and views for “1001” version will be in “Views/Authentication1001” folder (this suffix is configurable in web.config). It’s also needed to register new view’s engine in global.asax:

// register custom razor view engine
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new VersionedRazorViewEngine());
// note: that there is no custom ViewEngine for webforms (it is need to implement it in case it will needed for API versioning)
ViewEngines.Engines.Add(new WebFormViewEngine());

Finally thats it, it is needed just to implement custom RouteControllerFactory that implements IControllerFactory and register it in Global.asax, And implement custom VersionedRazorViewEngine inherited from RazorViewEngine and also register it in global.asax. And views\controllers versioning for MVC3 is ready. It is not only versioning but also Views’ inheritance, because this approach allows us to override only views that should be changed (with reusage of old views).

Sources could be found here – https://github.com/DjComandos/MvcVersioning/

Social Share Toolbar

Fiddler’s custom rules – how to replace protocol or domain in fiddler

Almost every web developer had to deal with Fiddler – very useful utility for investigation and debugging everything connected with network. There is one additional point for “Fiddlers’ flexibility”, for case when you want to do something that couldn’t be done via fiddler’s UI. This point is “Fiddler’s custom rules”

First of all it will be useful to install FiddlerScript Editor http://www.fiddlertool.com/fiddler/fse.asp
It’s really necessary tool (at least due to the intellisense)

How to edit custom rules: open Fiddler, select “Rules” in the top menu, then choose “Custom rules” (or you could use Ctrl+R hot key to open editor)

Two simple examples:

1) Replace protocol from http to https for defined Domains.
Open “Custom rules”, find this method

static function OnBeforeRequest(oSession: Session)

add to the beginning of the method the following code:

// Custom rules:
if (oSession.HostnameIs("mikitamanko.com") 
    || oSession.HostnameIs("google.com") 
    || oSession.HostnameIs("bing.com")) {
    oSession.fullUrl = "https" + oSession.fullUrl.Substring(oSession.fullUrl.IndexOf(':'));
}

This will replace protocol for sites listed in code.

2) Replace one domain name to another (for all requests to first one)
Add to “OnBeforeRequest” method the following code:

if (oSession.HostnameIs("google.com")) {
    oSession.hostname="mikitamanko.com";
}

This code will redirect all traffic from google.com to mikitamanko.com.

It is really powerful stuff, for additional details please follow this manual http://www.fiddler2.com/Fiddler/dev/ScriptSamples.asp

P.S. also you can modify headers or body of response/request, very useful stuff.

Social Share Toolbar

Vertical reordering of blocks with CSS or How to swap two elements using CSS

It’s old trick for search engine optimization (SEO). As far as we all know, search engine parses web page and prioritizes key words founded on the page. And words that are in the beginning of the page have more priority, it’s obviously because usually in the beginning of the page is header that should describe content below.

So the obvious search engine optimization is to arrange all key words in the beginning of the page. But it is completely not cool for Design. That’s why we need to replace two blocks: block with key words should be in the beginning of the markup, but on UI it should be under the next block with page’s content.

So we got something like this

<div class="block">It is content needed for SEO, but it's not very interesting for user</div>
<div class="block">Really good and interesting for user content, but it's waste for search engines</div>

and we need to swap these two blocks on UI.

First of all we need additional wrappers, so lets modify markup to something like this:

<div class="wrapper">
    <div class="bottom">
        <div class="block">It is content needed for SEO, but it's not very interesting for user</div>
    </div>
    <div class="top">
        <div class="block">Really good and interesting for user content, but it's waste for search engines</div>
    </div>
</div>

And here is the magic:

.wrapper {
    display: table; 
    width: 100%; 
}
.top {
    display: table-header-group; 
}
.bottom {
    display: table-footer-group;
}
/* next style is not really necessary, just to show borders of swapped blocks */
.block {
    border: 1px solid black;
}

Here is the proof link http://jsfiddle.net/UJWP4/1/

Some words about browsers support – this approach works fine in IE8+ (not supported in ie7)

Social Share Toolbar