One of the things I wanted for the framework I’m building for Xero was to implement filters.
I have a first pass of those ready.. didn’t take me to long to implement either. I only tested the before filters I haven’t gotten round to testing the After filter. I have to do a demo today that demonstrates databinding (like the castle project solved it.) What I’m putting on my blog here is very much a proof of concept implementation and you should not use this in a production environment.
Here’s how I went about it. In this post I’ll show how I implemented a filter that directs anonymous users to the login page.
- Define a couple enumerations
public enum Execute
{
Before,
After,
BeforeAndAfter
}
public enum SecureFor
{
None,
Anonymous,
PerUser
}
- Create an IFilter interface
using System.Web;
namespace Xero.Mvc.Extensions.Filters
{
public interface IFilter
{
Execute WhenToExecute { get; }
IHttpContext HttpContext { get; set; }
void Execute();
}
}
- Create an AbstractFilter base class
using System;
using System.Web;
namespace Xero.Mvc.Extensions.Filters
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public abstract class AbstractFilter : Attribute, IFilter
{
private readonly Xero.Mvc.Extensions.Execute whenToExecute;
private IHttpContext httpContext;
public AbstractFilter(Xero.Mvc.Extensions.Execute whenToExecute)
{
this.whenToExecute = whenToExecute;
}
public IHttpContext HttpContext
{
get { return httpContext; }
set { httpContext = value; }
}
public Xero.Mvc.Extensions.Execute WhenToExecute
{
get { return whenToExecute; }
}
public abstract void Execute();
}
}
- Create a SecureFilter base class
using System;
using System.Web;
using System.Collections.Generic;
namespace Xero.Mvc.Extensions.Filters
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public abstract class AbstractSecureFilter : AbstractFilter, IFilter
{
private readonly SecureFor secureFor;
public AbstractSecureFilter(Xero.Mvc.Extensions.Execute whenToExecute, SecureFor secureFor)
: base(whenToExecute)
{
this.secureFor = secureFor;
}
public SecureFor SecureFor
{
get { return secureFor; }
}
protected void RedirectToLogin()
{
HttpContext.Response.Redirect("~/", true);
}
}
}
- Implement the concrete AnonymousUsersFilter
using Xero.Mvc.Extensions.Filters;
using Xero.Mvc.Tasklist.Model.EntityClasses;
using Xero.Mvc.LLBLGenIntegration.Services;
using System;
using System.Collections.Generic;
using Xero.Mvc.Extensions;
namespace Xero.Mvc.Tasklist.Filters
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class AnonymousUsersFilter : AbstractSecureFilter
{
private User[] users;
private DataService<User> userService;
public AnonymousUsersFilter(Xero.Mvc.Extensions.Execute whenToExecute, SecureFor secureFor) : this(whenToExecute, secureFor, null) { }
private AnonymousUsersFilter(Xero.Mvc.Extensions.Execute whenToExecute, SecureFor secureFor, User[] users)
: base(whenToExecute, secureFor)
{
this.users = users;
this.userService = new DataService<User>();
}
public AnonymousUsersFilter(Xero.Mvc.Extensions.Execute whenToExecute, User[] users) : this(whenToExecute, SecureFor.PerUser, users) { }
public User[] Users
{
get { return users; }
}
public override void Execute()
{
if (SecureFor == Xero.Mvc.Extensions.SecureFor.Anonymous && HttpContext.Session["userId"] == null)
{
RedirectToLogin();
}
if (SecureFor == Xero.Mvc.Extensions.SecureFor.PerUser && Users == null)
RedirectToLogin();
if (SecureFor == Xero.Mvc.Extensions.SecureFor.PerUser)
{
User currentUser = userService.FindOneById((Guid)HttpContext.Session["userId"]);
List<User> allowedUsers = new List<User>(Users);
if (allowedUsers.Find(usr => usr.Name.ToUpperInvariant() == currentUser.Name.ToUpperInvariant()) == null)
RedirectToLogin();
}
}
}
}
- Implement the filter on a controller
[AnonymousUsersFilter(Xero.Mvc.Extensions.Execute.Before, SecureFor.Anonymous)]
public abstract class SecureControllerBase : SmartXeroController
- There are a couple of places where you can implement the execution of the filter logic. I chose to do it before the actual controller class is being loaded. To do that I had to create a handler and a routehandler
7.a The MvcHandler
using System.Web.Mvc;
using System;
using Xero.Mvc.Core.Exceptions;
using Xero.Mvc.Extensions.Filters;
using System.Linq;
namespace Xero.Mvc.Extensions
{
public class XeroMvcHandler : MvcHandler
{
protected override void ProcessRequest(System.Web.IHttpContext httpContext)
{
if (this.RequestContext == null)
{
throw new NoRequestContextException();
}
string controllerName = this.RequestContext.RouteData.Values["controller"].ToString();
Type controllerType = this.GetControllerType(controllerName);
if (controllerType == null)
{
throw new NoControllerFoundException(this.RequestContext.HttpContext.Request.Path);
}
IFilter[] filters = controllerType.GetCustomAttributes(typeof(IFilter), true) as IFilter[];
filters
.Where(attr => attr.WhenToExecute == Xero.Mvc.Extensions.Execute.Before || attr.WhenToExecute == Xero.Mvc.Extensions.Execute.BeforeAndAfter)
.ToList()
.ForEach(attr =>
{
if (attr != null)
{
attr.HttpContext = httpContext;
attr.Execute();
}
});
IController controllerInstance = this.GetControllerInstance(controllerType);
ControllerContext controllerContext = new ControllerContext(this.RequestContext, controllerInstance);
controllerInstance.Execute(controllerContext);
filters
.Where(attr => attr.WhenToExecute == Xero.Mvc.Extensions.Execute.After || attr.WhenToExecute == Xero.Mvc.Extensions.Execute.BeforeAndAfter)
.ToList()
.ForEach(attr =>
{
if (attr != null)
{
attr.HttpContext = httpContext;
attr.Execute();
}
});
}
}
}
7b. The Route Handler
using System.Web.Mvc;
using System.Web;
namespace Xero.Mvc.Extensions
{
public class XeroMvcRouteHandler : IRouteHandler
{
#region IRouteHandler Members
public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new XeroMvcHandler { RequestContext = requestContext };
}
#endregion
}
}
- And the last step is to tell your application that it needs to use the new route handler. You can do that in the
global.asax.cs
protected override void SetupRoutes()
{
// Note: Change Url= to Url="[controller].mvc/[action]/[id]" to enable
// automatic support on IIS6
RouteTable.Routes.Add(new Route
{
Url = "Login/Default.aspx",
Defaults = new { controller = "Login", action = "Index", id = (string)null },
RouteHandler = typeof(XeroMvcRouteHandler) // Our custom route handler
});
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (string)null },
RouteHandler = typeof(XeroMvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "Default.aspx",
Defaults = new { controller = "Login", action = "Index", id = (string)null },
RouteHandler = typeof(XeroMvcRouteHandler)
});
}