Dan McKinley
Math, Programming, and Minority Reports

HttpModule for Performing a Custom Action for Certain URLs
January 13th, 2005

I wrote this collection of classes over the holidays as part of a side project and forgot about it. Today at work I needed basically exactly the same thing, but unfortunately I didn’t have it handy and had to recreate it from scratch.

This is a way to execute any action you please for requests against url’s matching one or more regular expressions. Today at work, I used it to install a response filter for a specific set of .aspx pages. I can think of at least a few more reasons someone might want something like this, so I’m posting it.

using System;
using System.Text.RegularExpressions;
using System.Configuration;
using System.Collections;
using System.Web;

namespace McKinley.Web
{
    /// <summary>
    /// An abstract <see cref="T:System.Web.IHttpModule"/> that checks
    /// the url of each incoming request against one or more regular
    /// expressions. When a match is found, a custom action is
    /// performed.
    /// </summary>
    public abstract class BaseRegexModule : IHttpModule
    {
        private RegexChain _chain;

        /// <summary>
        /// Reads the list of regular expressions to match from
        /// the configuration settings. Hooks the beginning of
        /// each request to perform the checks.
        /// </summary>
        public void Init(HttpApplication context)
        {
            Hashtable settings = (Hashtable)ConfigurationSettings.GetConfig(this.ConfigSectionName);
            if(settings != null)
            {
                _chain = new RegexChain();
                foreach(object val in settings.Keys)
                {
                    string pattern = val as string;
                    if(pattern != null && pattern.Length > 0)
                    {
                        _chain.Add(pattern);
                    }
                    else
                    {
                        throw new ConfigurationException(
                            "The regular expression pattern may not be empty.");
                    }
                }
            }
            context.BeginRequest += new EventHandler(BeginRequest);
        }

        /// <summary>
        /// Tries to match the raw url against the list of expressions
        /// given to the module. If a match is found, calls the abstract
        /// <see cref="M:CustomAction"/> method.
        /// </summary>
        private void BeginRequest(object sender, EventArgs e)
        {
            HttpApplication application = sender as HttpApplication;
            if(application != null)
            {
                HttpRequest request = application.Request;
                if(request != null && _chain.IsMatch(request.RawUrl))
                {
                    CustomAction(application);
                }
            }
        }

        /// <summary>
        /// This must be overridden to provide the name of the configuration
        /// section for the concrete module class. The section is presumed
        /// to use a <see cref="T:System.Configuration.DictionarySectionHandler"/>.
        /// </summary>
        protected abstract string ConfigSectionName { get; }

        /// <summary>
        /// This must be overridden to define a custom action to be performed
        /// when one of the regular expressions are matched against the
        /// request's URL.
        /// </summary>
        protected abstract void CustomAction(HttpApplication context);

        public void Dispose() { }

        public BaseRegexModule() { }
    }

    /// <summary>
    /// This is an efficient utility class for a list of
    /// regular expressions evaluated together.
    /// </summary>
    internal class RegexChain
    {
        private class Node
        {
            public Regex Expression;
            public Node Next;

            public Node(Regex r)
            {
                Expression = r;
            }
         }

        private Node _head;
        private Node _tail;

        /// <summary>
        /// Returns true if the given string is a match
        /// for any of the regular expressions in the chain.
        /// </summary>
        public bool IsMatch(string input)
        {
            Node n = _head;
            while(n != null)
            {
                if(n.Expression.IsMatch(input))
                {
                    return true;
                }
                n = n.Next;
            }
            return false;
        }

        /// <summary>
        /// Adds the given pattern to the chain of regular
        /// expressions.
        /// </summary>
        public void Add(string pattern)
        {
            Node n = new Node(new Regex(pattern));
            if(_head == null)
            {
                _head = n;
            }
            else
            {
                _tail.Next = n;
            }
            _tail = n;
        }
    }

    /// <summary>
    /// This is a <see cref="T:System.Configuration.IConfigurationSectionHandler"/>
    /// that can be used with a class derived from
    /// <see cref="T:McKinley.Web.BaseRegexModule"/>.
    /// </summary>
    public class RegexModuleSectionHandler : DictionarySectionHandler
    {
        protected override string KeyAttributeName
        {
            get { return "pattern"; }
        }
    }
}

And there you have it. Here’s an extremely simple example class that uses this to set a cookie for matching url’s.

public class ExampleRegexModule : BaseRegexModule
{
    public ExampleRegexModule() { }

    protected override void CustomAction(System.Web.HttpApplication context)
    {
        context.Response.AppendCookie(new HttpCookie("Oscar", "Is god"));
    }

    protected override string ConfigSectionName
    {
        get { return "mckinley.web/exampleRegexModule"; }
    }
}

And finally, here are the web.config entries you’d need to install this HttpModule.

<configSections>
    <sectionGroup name="mckinley.web">
        <section
            name="exampleRegexModule"
            type="McKinley.Web.RegexModuleSectionHandler, McKinley.Web"
        />
    </sectionGroup>
</configSections>

<mckinley.web>
    <exampleRegexModule>
        <add pattern="somepage.aspx" />
    </exampleRegexModule>
</mckinley.web>

<system.web>
    <httpModules>
        <add name="example"
                type="McKinley.Web.ExampleRegexModule, McKinley.Web" />
    </httpModules>
<system.web>
Back home