Quick Link: Download the Example Code.
OK so here’s my problem,
My client wants most of the actions in their system to work slightly differently based on the authorization role of the logged in user. For example, Moderators can edit a user, but only some fields, Administrators can edit all fields for a user.
I don’t want to have different URIs for each action, I don’t want to have to faff about with routes, I want to do this transparently, with as little pain as possible.
One approach would be to have a switch inside the method (on the current users role) and delegate the action to the private method specific to that role. However this suffers several problems:
The main action method that gets called by the controller can only have a single Resource type, which means if you want different resource types per role, youre in a mess. You therefore have to accept FormCollection, then do binding and validation manually etc. etc. urgh. You also end up with 4 methods for each action. This quickly bloats your controller
So, what can we do about this? We’ll in this post I’ll show you how you can leverage the power of you IoC container to swap out the controller instance with one specific to the current user’s role.
Here’s what you do:
Create an IXXXController for each of your controllers you want to swap based on role, like this:
public interface IUserController : IController
{
ActionResult Edit(int id);
}
Then, create your multiple implementations of this controller:
public class UserController_ForAdministrator : Controller, IUserController
{
[AcceptVerbs(HttpVerbs.Get)]
public override ActionResult Edit(int id)
{
// this would really come from a repos
var resource = new EditUser_AdministratorResource{ EmailAddress = "me@here.com"};
return View("Edit_Administrator", resource);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, EditUser_AdministratorResource resource)
{
// do admin stuff here
return RedirectToAction("Index");
}
}
public class UserController_ForModerator : HomeController
{
[AcceptVerbs(HttpVerbs.Get)]
public override ActionResult Edit(int id)
{
// this would really come from a repos
var resource = new EditUser_ModeratorResource{ EmailAddress = "me@here.com"};
return View("Edit_Moderator", resource);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, EditUser_ModeratorResource resource)
{
// save moderator stuff here
return RedirectToAction("Index");
}
}
Next, you need a custom IControllerFactory. Note in the example below I have forgone any error checking or optimisation for brevity, I have also omitted any Namespace checking.
public class CustomControllerFactory : IControllerFactory
{
private readonly IEnumerable<Type> controllerTypes;
public CustomControllerFactory()
{
this.controllerTypes = Assembly.GetExecutingAssembly().GetTypes().Where(t => (t.IsInterface || !t.IsAbstract) && t.Name.EndsWith("Controller"));
}
public IController CreateController(RequestContext requestContext, string controllerName)
{
var currentUser = ObjectFactory.GetInstance<User>();
var controllerInterface = this.controllerTypes.Where(t => t.IsInterface && t.Name.Equals("I" + controllerName + "Controller", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (controllerInterface != null)
{
return ObjectFactory.GetNamedInstance(controllerInterface, currentUser.Role) as IController;
}
var controllerClass = this.controllerTypes.Where(
t => t.IsClass &&
t.GetInterfaces().Contains(typeof(IController)) &&
t.Name.Equals(controllerName + "Controller", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if(controllerClass != null)
{
return ObjectFactory.GetInstance(controllerClass) as IController;
}
return null;
}
}
Then simply wire things up in your Application_Start:
protected void Application_Start()
{
ObjectFactory.Initialize(a =>
{
a.For<IUserController>()
.Use<UserController_ForModerator>()
.Named("Moderator");
a.For<IUserController>()
.Use<UserController_ForAdministrator>()
.Named("Administrator");
// this is obviously a hack for the purposes of this post
// here you would really be loading your real user
a.For<User>()
.Use(c => new User { Role = "Moderator" });
});
ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
}
The controller wiring can easily be done by convention in StructureMap (and other containers) so you don’t have to list each one manually.
Download the Example Code here, aren’t I nice to you?
For those of you still running IIS6 who would like to have nice SEO friendly URLs, one option is to use ISAPI Rewrite.
First, add a “.mvc” handler mapping to the asp.net dll as per Phil Haacks post.
Obviously you’ll need ISAPI Rewrite installed, then you can create the following .htaccess file:
RewriteEngine on
AllowOverride All
# Ignore Assets folder
RewriteRule ^assets/(.*?)$ /assets/$1 [NC,L]
# Rewrite everything else to have .mvc on the end of the controller name
RewriteRule ^([^/]*)(/(?:.*?)*)?$ /$1.mvc$2 [NC,L]
I keep all my CSS, images and javascript under the /assets folder. If you have these elsewhere, you probably want to modify the first rewrite rule for your specific location.
N.B. you can probably ignore these folders in a much more elegant way, perhaps with a RewriteCond on the 2nd Rule. I’m not a master of this syntax yet so this will do for the time being, it works!
Some additional rules you may also want are the following:
# Rewrite favicon
RewriteRule ^favicon.ico(.*?)$ /assets/images/icons/favicon.ico [NC,L]
# Rewrite appleicon
RewriteRule ^apple-touch-icon.png(.*?)$ /assets/images/icons/apple-touch-icon.png [NC,L]
# Ignore robots.txt
RewriteRule ^robots.txt(.*?)$ /robots.txt [NC,L]
Adding the following to your global.asax:
protected void Application_BeginRequest()
{
if (this.Request.AppRelativeCurrentExecutionFilePath.Contains(".mvc"))
this.Context.RewritePath(this.Request.Url.PathAndQuery.Replace(".mvc", string.Empty));
}
means you don’t need to have the .mvc in your route configurations, which keeps things nice and tidy!
I try to use natural IDs in my URIs wherever possible, like:
http://www.mydomain.com/some/resource/description
However sometimes this is not always practical, and for many of the non-public facing applications I work on it is simply unnecessary. In these cases I tend to use the GUID ID of the requested resource, like:
http://www.mydomain.com/resources/95801FAD-DA29-434F-B4EA-175C76266BB7
These 36 character GUIDs are rather ugly, here is a solution to shorten them down to 22 chars, which looks like:
http://www.mydomain.com/resources/DV0Ft9JPqkGV2Xne0Q64XA
not perfect, but much better.
N.B. My next step to shorten them further is to use a custom GUID algorithm with no machine-specific part to it, which should remove a significant number of bits. In the meantime, however…
First, we’ll introduce a ShortGuid struct based on the class described here.
public struct ShortGuid
{
private readonly Guid guid;
public ShortGuid(Guid guid)
{
this.guid = guid;
}
public static bool TryParse(string guid, out ShortGuid shortGuid)
{
Guid parsed;
try
{
parsed = new Guid(Convert.FromBase64String(guid.Replace("_", "/").Replace("-", "+") + "=="));
}
catch
{
try
{
parsed = new Guid(guid);
}
catch
{
shortGuid = new ShortGuid();
return false;
}
}
shortGuid = new ShortGuid(parsed);
return true;
}
public override string ToString()
{
return Convert.ToBase64String(guid.ToByteArray())
.Substring(0, 22)
.Replace("/", "_")
.Replace("+", "-");
}
public Guid ToGuid()
{
return this.guid;
}
public static implicit operator string(ShortGuid guid)
{
return guid.ToString();
}
public static implicit operator Guid(ShortGuid shortGuid)
{
return shortGuid.guid;
}
}
All pretty easy stuff. The difficulty comes when trying to get MVC to use and recognise it.
The easiest solution is to use the ShortGuid class on your resources, however the whole “short guid” concept is purely for HTTP. It certainly has no place in your domain, and has questionable presence in your resources (view models).
The next easiest solution is to use a custom route which replaces any GUID route values just before rendering, here is the code:
public class ShortGuidReplacingRoute : Route
{
public ShortGuidReplacingRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler)
{
}
public ShortGuidReplacingRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler)
{
}
public ShortGuidReplacingRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler)
{
}
public ShortGuidReplacingRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler)
{
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var dictionary = new RouteValueDictionary();
foreach(var kvp in values)
{
if (kvp.Value.GetType() == typeof (Guid))
dictionary.Add(kvp.Key, new ShortGuid((Guid)kvp.Value));
else
dictionary.Add(kvp.Key, kvp.Value);
}
return base.GetVirtualPath(requestContext, dictionary);
}
}
You then need to use this class when registering your routes, I made this convenient extension method to make life easier:
public static class RouteExtensions
{
public static Route MapGuidReplacingRoute(this RouteCollection routes, string name, string url, object defaults)
{
Route route = new ShortGuidReplacingRoute(url, new MvcRouteHandler());
route.Defaults = new RouteValueDictionary(defaults);
route.Constraints = new RouteValueDictionary();
routes.Add(name, route);
return route;
}
}
which you can use almost as normal, like so:
routes.MapGuidReplacingRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index" }
);
Then all you need is a ShortGuid model binder:
public class ShortGuidModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if(bindingContext.ValueProvider.ContainsKey(bindingContext.ModelName))
{
ShortGuid shortGuid;
var valid = ShortGuid.TryParse(bindingContext.ValueProvider[bindingContext.ModelName].AttemptedValue, out shortGuid);
if (valid)
{
if(bindingContext.ModelType == typeof(Guid))
return shortGuid.ToGuid();
if (bindingContext.ModelType == typeof(ShortGuid))
return shortGuid;
}
}
return Guid.Empty;
}
}
Which you register in the normal way:
ModelBinders.Binders[typeof (Guid)] = new ShortGuidModelBinder();
Job done
I’m fed up of writing:
<%= Html.Encode(bla) %>
throughout my views. Not only is it messy, but ASP.NET’s default behaviour of “be as insecure as possible” means you have to remember to do this everywhere. In addition to this, it simply uses:
System.Web.HttpUtility.HtmlEncode()
underneath, which isn’t particularly good at preventing XSS.
OpenRasta (a brilliant alternative to MVC which you should be using) has an excellent solution to this problem, by using a custom CSharCodeProvider to help with view compilation.
Below is a simplified version of the code OpenRasta uses, demonstrating how you can get automatic HTML encoding of all code expressions in your views. This also works for WebForms.
It uses an IoC service locator to request an arbitrary IHtmlEncoder. This allows you to use whatever encoding library you like, such as Microsoft AntiXss.
public class AutoHtmlEncodingCSharpCodeProvider : CSharpCodeProvider
{
public AutoHtmlEncodingCSharpCodeProvider()
{
}
public AutoHtmlEncodingCSharpCodeProvider(IDictionary<string, string> providerOptions) : base(providerOptions)
{
}
public override void GenerateCodeFromStatement(CodeStatement statement, TextWriter writer, CodeGeneratorOptions options)
{
var codeExpressionStatement = statement as CodeExpressionStatement;
if (codeExpressionStatement != null)
{
var methodInvokeExpression = codeExpressionStatement.Expression as CodeMethodInvokeExpression;
if (methodInvokeExpression != null)
{
if (methodInvokeExpression.Method.MethodName == "Write" && methodInvokeExpression.Parameters.Count == 1)
{
var parameter = methodInvokeExpression.Parameters[0] as CodeSnippetExpression;
if ((parameter != null) && (!string.IsNullOrEmpty(parameter.Value)))
parameter.Value = "global::" + GetType().FullName + ".PreProcessObject(this, " + parameter.Value + ")";
}
}
}
base.GenerateCodeFromStatement(statement, writer, options);
}
public static string PreProcessObject(object source, object value)
{
if(value is Raw)
return ((Raw)value).Value;
var encoder = ServiceLocator.Current.TryGetInstance<IHtmlEncoder>();
if (encoder != null)
return encoder.HtmlAttributeEncode(value.ToString());
return HttpUtility.HtmlAttributeEncode(value.ToString());
}
}
public class Raw
{
public string Value { get; set; }
public static explicit operator Raw(string text)
{
return new Raw { Value = text };
}
public static implicit operator string(Raw output)
{
return output.Value;
}
}
You need to register this in your Web.config like so:
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" warningLevel="4" type="MyAssembly.AutoHtmlEncodingCSharpCodeProvider, MyAssembly">
<providerOption name="CompilerVersion" value="v3.5" />
<providerOption name="WarnAsError" value="false" />
</compiler>
</compilers>
</system.codedom>
Then in your pages, you can do this:
<p><%= "<script>alert('i'm encoded, so i wont popup');</script>" %></p>
<p><%= (Raw)"<strong>i'm bold because im escaped with (Raw)!</strong>" %></p>
Which will be rendered as:
<script>alert(‘i’m encoded, so i wont popup’);</script>
i’m bold because im escaped with (Raw)!
There you go!
Because this code has been inspired by/copied from/a modification of code from OpenRasta, according to its license I must reproduce the copyright notice. If you also wish to use this code, you must do the same.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
In order to use the UrlHelper class in your tests, you need to construct it with a ControllerContext, which consists of an HttpRequest and an HttpResponse. Unfortunately these are quite difficult to construct, as you’ll know if you’ve ever tried. Luckily, the UrlHelper doesn’t use much of these two classes, only a few properties, making them fairly easy to mock.
The first thing you need to do is get your routes, usually declared in your Global.asax MvcApplication
var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
Then you need to mock an HttpRequest
var request = MockRepository.GenerateStub<HttpRequestBase>();
request.Stub(x => x.ApplicationPath).Return("/");
request.Stub(x => x.Url).Return(new Uri("http://localhost/a", UriKind.Absolute));
request.Stub(x => x.ServerVariables).Return(new System.Collections.Specialized.NameValueCollection());
Then you need to mock the HttpResponse
var response = MockRepository.GenerateStub<HttpResponseBase>();
response.Stub(x => x.ApplyAppPathModifier(Arg<string>.Is.Anything))
.Return(null)
.WhenCalled(x => x.ReturnValue = x.Arguments[0]);
Then you can simply stick these two mocks onto a mock context, and use that to create your UrlHelper instance and set them on the controller you are testing:
var context = MockRepository.GenerateStub<HttpContextBase>();
context.Stub(x => x.Request).Return(request);
context.Stub(x => x.Response).Return(response);
var subjectUnderTest = new MyController(); // this is the controller you are testing
subjectUnderTest.ControllerContext = new ControllerContext(context, new RouteData(), subjectUnderTest);
subjectUnderTest.Url = new UrlHelper(new RequestContext(context, new RouteData()), routes);
Job done.
Now go and use OpenRasta and not have any of these problems.