Code Voyeur
RSS
Data Access Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net Rate My Snippet

A Simple IronPython ActionFilter for ASP.NET MVC

There are times when an action filter has transient logic that will change frequently. Logging, authorization or request sanitizing rules are examples of filters that may require extensive configuration to achieve needed flexibility.

Rather than create several filters that make excessive use of appSettings, it is possible to use the Dynamic Language Runtime and script files to achieve the same result. This article will demonstrate how to create a filter that uses a simple convention to map action filter hooks to functions in IronPython script files. Like other CodeVoyeur articles, this one assumes the choice of a statically typed language for core application logic has already been made.

public class PyFilter : ActionFilterAttribute {
        
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
            ...
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext) {
            ...
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext) {
            ...
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext) {
            ...
    }
        
}
The PyFilter class will be the core file that performs the execution of the IronPython code. It will override each of the four virtual action filter methods from the ActionFilterAttribute base class.
private static ScriptRuntimeSetup _setup = null;
private static ScriptRuntime _runtime = null;
private static Dictionary<string, object> _filters = new Dictionary<string, object>(); //will hold function references
private string _name = null;

public static string RootPath { get; set; } 
public static string ScriptFile { get; set; }
This class contains static classes for caching the required scripting API class instances. It also contains a static dictionary that will hold an application level cache of the filters defined in the script files. The RootPath and ScriptFile variables allow for overriding the default location of the script file.
public PyFilter(string name) {            
    _name = name;
}

static PyFilter() {
            
    _setup = ScriptRuntimeSetup.ReadConfiguration();
    _runtime = new ScriptRuntime(_setup);

     _runtime.LoadAssembly(Assembly.GetExecutingAssembly());
     _runtime.LoadAssembly(Assembly.GetAssembly(typeof(DateTime)));

    string path = RootPath ??     HttpContext.Current.Server.MapPath("~/App_Data/");
    ScriptEngine engine = _runtime.GetEngine("IronPython");            
    engine.SetSearchPaths(new string[] { path });
    engine.ExecuteFile(Path.Combine(path, ScriptFile ?? "filters.py"), _runtime.Globals);
}
A name is required when using this filter. It is used for mapping to the appropriate script. The instance constructor forces this setting.

The static constructor is used to ensure that the scripting context is setup prior to use. By default, the executing assembly and System assembly is made available to scripts. The path of the root script path is also added to the IronPython search path.

public override void OnActionExecuted(ActionExecutedContext filterContext) {
    execute("action_executed", filterContext);
}

public override void OnActionExecuting(ActionExecutingContext filterContext) {
    execute("action_executing", filterContext);
}

public override void OnResultExecuted(ResultExecutedContext filterContext) {
    execute("result_executed", filterContext);
}

public override void OnResultExecuting(ResultExecutingContext filterContext) {
    execute("result_executing", filterContext);
}
Each of the overridden methods calls a driver method, execute, passing its ControllerContext object and a key for finding the appropriate function to execute in the script files.
private void execute(string suffix, ControllerContext context) {

    string key = string.Format("{0}_{1}", _name, suffix);

    if (!_filters.ContainsKey(key)) {
        object method;
        _runtime.Globals.TryGetVariable(key.ToLower(), out method);
        _filters[key] = method;
    }

    if (_filters[key] == null) 
        return;
    _runtime.Operations.Call(_filters[key], context);                        
}
The execute method takes the name of the filter and a suffix to generate a method name. A name of “logging” and suffix of “result_executing” would cause the filter to search for a function named “logging_result_executing.”

If the _filters dictionary already has the function reference saved, it executes it. Otherwise, it tries to find it. If it hasn't been defined, it is assumed that step in the filter should be skipped.

[PyFilter("trace")]
public class HomeController : Controller {
        
    ...

    [ValidateInput(false)]
    [PyFilter("injection")]
    public ActionResult Save(string name, string comments) {            
     ...
    }

    ...
}
For a class to use this filter, it needs to decorate the controller or the action. The sample project contains two filter scripts. One is a simplistic trace that writes to the response the time an action was called. The second is used to sanitize the request for an action that must disable validation input because posting some HTML tags is allowed.
import clr
clr.AddReference("System")

from System import *

def trace_action_executing(context):

    __write_trace(context, "Action Executign")
    
def trace_action_executed(context):

    __write_trace(context, "Action Executed")
    
    
def trace_result_executing(context):

    __write_trace(context, "Result Executing")
    
    
def trace_result_executed(context):

    __write_trace(context, "Result Executed")
        
    
def __write_trace(context, event):

    if context.HttpContext.Request.QueryString["debug"] == "true":
        context.HttpContext.Response.Write("Tracing: %s %s at %s<br />" % (event, context.RouteData.Values["action"], DateTime.Now))
The trace script is straightforward. The name of each function matches the name of the filter and its convention-based suffix. These methods show how the context is available to each script.
import clr
clr.AddReference("System")

from System.Text.RegularExpressions import *

def injection_action_executing(context):
    
    tag_pattern = r"<(.|\n)*?>"
    comments = context.HttpContext.Request["Comments"]
    
    matches = Regex.Matches(comments, tag_pattern)    
    
    invalid_tags = []
    for match in matches:
        invalid_tags.append(match.Value)
        
    if len(invalid_tags) > 0:
        context.Controller.TempData["invalid_tags"] = invalid_tags
        context.HttpContext.Response.Redirect("~/Home/InvalidPost")
The injection file is a little more complex, demonstrating how the request may be taken over (a redirect is forced on bad postings). Additionally, it demonstrates how data may be stuffed into ViewData or TempData from within the script. The InvalidPost view file may then render the invalid tags entered by the user.

Download Sample Project

Article Posted: Thursday, September 10, 2009

Comments

Posted by Jace on 7/4/2011 1:09:44 PM
Wow! That's a really neat ansewr!

Leave a Comment

Shout it

Contact Code Voyeur about this article.