Ignoring Case For MVC Views On Linux
Running ASP.NET MVC using Mono on Linux works quite well -- but you occasionally run into some problems you wouldn't see if you were working on Windows.
For example, check out the two routes below. Both routes are matched by ASP.NET MVC as you'd expect but only one of them returns a view.
MVC uses the incoming route to determine where it will search for Views. That becomes a problem when you're using an OS with a case sensitive file system.
As far as I can tell there are ways to get around file system case-sensitivity at the OS level but that seems like an extreme response to a small problem. Additionally, Mono appears to have a setting to deal with case-sensitive file systems but I have yet to get it to work with MVC Views.
So, instead let's try some code that overrides the way Views are located.
using System; using System.Web; using System.Web.Mvc; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web.Routing; /// <summary> /// Works around case sensitive file systems by locating /// views regardless of the case they are in /// </summary> public class CaseInsensitiveViewEngine : WebFormViewEngine { private static string _Root = HttpContext.Current.Server.MapPath("~/"); //adds a new CaseInsensitiveViewEngine to the routes provided public static void Register(ViewEngineCollection engines) { //clear the existing WebForm View Engines IViewEngine[] webforms = engines.Where(engine => engine is WebFormViewEngine).ToArray(); foreach(IViewEngine engine in webforms) ViewEngines.Engines.Remove(engine); //add the new case-insensitive engine ViewEngines.Engines.Add(new CaseInsensitiveViewEngine()); } //holds all of the actual paths to the required files private static Dictionary<string, string> _ViewPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); //update the path to match a real file protected override IView CreateView (ControllerContext controllerContext, string viewPath, string masterPath) { viewPath = this._GetActualFilePath(viewPath); masterPath = this._GetActualFilePath(masterPath); return base.CreateView (controllerContext, viewPath, masterPath); } //finds partial views by detecting matches protected override IView CreatePartialView (ControllerContext controllerContext, string partialPath) { partialPath = this._GetActualFilePath(partialPath); return base.CreatePartialView (controllerContext, partialPath); } //perform a case-insensitive file search protected override bool FileExists (ControllerContext context, string virtualPath) { virtualPath = this._GetActualFilePath(virtualPath); return base.FileExists(context, virtualPath); } //determines (and caches) the actual path for a file private string _GetActualFilePath(string virtualPath) { //check if this has already been matched before if (CaseInsensitiveViewEngine._ViewPaths.ContainsKey(virtualPath)) return CaseInsensitiveViewEngine._ViewPaths[virtualPath]; //break apart the path string[] segments = virtualPath.Split(new char[] { '/' }); //get the root folder to work from var folder = new DirectoryInfo(CaseInsensitiveViewEngine._Root); //start stepping up the folders to replace with the correct cased folder name for(int i = 0; i < segments.Length; i++) { string part = segments[i]; bool last = i == segments.Length - 1; //ignore the root if (part.Equals("~")) continue; //process the file name if this is the last segment else if (last) part = this._GetFileName(part, folder); //step up the directory for another part else part = this._GetDirectoryName(part, ref folder); //if no matches were found, just return the original string if (part == null || folder == null) return virtualPath; //update the segment with the correct name segments[i] = part; } //save this path for later use virtualPath = string.Join("/", segments); CaseInsensitiveViewEngine._ViewPaths.Remove(virtualPath); CaseInsensitiveViewEngine._ViewPaths.Add(virtualPath, virtualPath); return virtualPath; } //searches for a matching file name in the current directory private string _GetFileName(string part, DirectoryInfo folder) { //try and find a matching file, regardless of case FileInfo match = folder.GetFiles().FirstOrDefault(file => file.Name.Equals(part, StringComparison.OrdinalIgnoreCase)); return match is FileInfo ? match.Name : null; } //searches for a folder in the current directory and steps up a level private string _GetDirectoryName(string part, ref DirectoryInfo folder) { folder = folder.GetDirectories().FirstOrDefault(dir => dir.Name.Equals(part, StringComparison.OrdinalIgnoreCase)); return folder is DirectoryInfo ? folder.Name : null; } }
Now, each part of our route is evaluated and the directory is crawled until we find the correct view. This way, regardless of OS or file system settings, our MVC view are able to be located no matter what the incoming route looks like.
This code also handles locating Partial Views as well even if the case of their path is incorrect.
Setup is pretty simple as well, just add this line to your Global.asax
file.
CaseInsensitiveViewEngine.Register(ViewEngines.Engines);
And now you're ready to go!
May 12, 2011
Ignoring Case For MVC Views On Linux
Case-sensitive file systems can cause problems with how ASP.NET MVC locates views with Mono.