random .NET and web development musings

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?

NO COMMENTS
Post a comment