My Top 5 ASP.NET MVC ActionFilters

ActionFilters have been around since the first release of ASP.NET MVC. Today, I give you my five favorite ActionFilters to use right away in your MVC code.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
Blog 'Action'

For those of you who are still in the Web Forms world, there can be a lot of HttpContext.Current calls all over the place, which can make your code look a mess. In ASP.NET MVC, the general rule is to keep your HttpContext calls self-contained in model binders and Action Filters.

The awesome thing about Action Filters is that they are a simple attribute that you can add to a controller when you want that action filter to affect all pages. If you don't want an Action Filter on all of the pages in a controller, you can make it more granular by adding it to only the methods (pages) that absolutely need it.

So since I posted my 10 Extremely Useful .NET Extension Methods and it did so well, I thought that today, I would go over my top Action Filters that I use across my website, DanylkoWeb.

These are tested ActionFilters and, of course, they are working in production (except for one...the SearchBot).

CompressFilter

An oldie, but a goodie. Been using this routine for a while (Thanks, Kazi Manzur Rashid).

If you want to compress your HTML when sending it down to the client, apply this compress filter to get the fastest package delivery available.

ActionFilters\CompressFilter.cs

public class CompressFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        bool allowCompression = false;
        bool.TryParse(ConfigurationManager.AppSettings["Compression"], out allowCompression);

        if (allowCompression)         {             HttpRequestBase request = filterContext.HttpContext.Request;
            string acceptEncoding = request.Headers["Accept-Encoding"];
            if (string.IsNullOrEmpty(acceptEncoding)) return;
            acceptEncoding = acceptEncoding.ToUpperInvariant();
            HttpResponseBase response = filterContext.HttpContext.Response;
            if (acceptEncoding.Contains("GZIP"))             {                 response.AppendHeader("Content-encoding", "gzip");                 response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);             }             else if (acceptEncoding.Contains("DEFLATE"))             {                 response.AppendHeader("Content-encoding", "deflate");                 response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);             }         }     } }

Whitespace Filter

Speaking of fast delivery, this just may be a common sense thing, but all of those spaces in your HTML is taking up bandwidth (and..uhh..space) when delivered to your audience.

So why not eliminate the spaces.

As an example of how it works, look at the HTML on the main page by viewing the source.

Usage:

[WhitespaceFilter]
public ActionResult Index()
{
}

ActionFilters\WhitespaceFilter.cs

public class WhitespaceFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var response = filterContext.HttpContext.Response;

        // If it's a sitemap, just return.         if (filterContext.HttpContext.Request.RawUrl == "/sitemap.xml") return;
        if (response.ContentType != "text/html" || response.Filter == null) return;
        response.Filter = new HelperClass(response.Filter);     }
    private class HelperClass : Stream     {         private readonly Stream _base;         StringBuilder _s = new StringBuilder();
        public HelperClass(Stream responseStream)         {             if (responseStream == null)                 throw new ArgumentNullException("responseStream");             _base = responseStream;         }
        public override void Write(byte[] buffer, int offset, int count)         {             var html = Encoding.UTF8.GetString(buffer, offset, count);             var reg = new Regex(@"(?<=\s)\s+(?![^<>]*</pre>)");             html = reg.Replace(html, string.Empty);
            buffer = Encoding.UTF8.GetBytes(html);             _base.Write(buffer, 0, buffer.Length);         }
        #region Other Members
        public override int Read(byte[] buffer, int offset, int count)         {             throw new NotSupportedException();         }
        public override bool CanRead { get { return false; } }
        public override bool CanSeek { get { return false; } }
        public override bool CanWrite { get { return true; } }
        public override long Length { get { throw new NotSupportedException(); } }
        public override long Position         {             get { throw new NotSupportedException(); }             set { throw new NotSupportedException(); }         }
        public override void Flush()         {             _base.Flush();         }
        public override long Seek(long offset, SeekOrigin origin)         {             throw new NotSupportedException();         }
        public override void SetLength(long value)         {             throw new NotSupportedException();         }
        #endregion     } }

ETag Filter

ETags are tokens that are associated with a particular web resource. The web resource is primarily a web page, but it could be a XML or JSON document.

It's primarily used to determine if the page needs updated or not. Since the server generated the ETag token, it can find out whether it needs to make a request to pull the new page or not.

Less requests = faster page displays.

For more information on ETags, InfoQ has a great article about Using ETags to Reduce Bandwidth and Workload.

Usage:

[ETag]
public ActionResult Index()
{
}

ActionFilters\ETagActionFilter.cs

public class ETagAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request);
    }
}

public class ETagFilter : MemoryStream {     private HttpResponseBase _response = null;     private HttpRequestBase _request;     private Stream _filter = null;
    public ETagFilter(HttpResponseBase response, HttpRequestBase request)     {         _response = response;         _request = request;         _filter = response.Filter;     }
    private string GetToken(Stream stream)     {         var checksum = new byte[0];         checksum = MD5.Create().ComputeHash(stream);         return Convert.ToBase64String(checksum, 0, checksum.Length);     }
    public override void Write(byte[] buffer, int offset, int count)     {         var data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        var token = GetToken(new MemoryStream(data));         var clientToken = _request.Headers["If-None-Match"];
        if (token != clientToken)         {             _response.AddHeader("ETag", token);             _filter.Write(data, 0, count);         }         else         {             _response.SuppressContent = true;             _response.StatusCode = 304;             _response.StatusDescription = "Not Modified";             _response.AddHeader("Content-Length", "0");         }     } }

SearchBot Filter

Search Engines have their spiders that crawl through your site looking for great content, but if you've been targeted by some "rogue spiders," you can make sure that they don't swipe your content.

This simple SearchBot filter shows how you can stop people from crawling through your site.

Of course, they would have to contain a user-agent, IP address, or identification of who they were for you to return "Not Found" pages.

ActionFilters\SearchBotFilter.cs

public class SearchBotFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (HttpContext.Current.Request.Browser.Crawler)
        {
            filterContext.Result = new ViewResult() { ViewName = "NotFound" };
        }
    }
}

TidyHtml Filter

And finally, for those who absolutely HAVE to keep their HTML neat and structured and indented so properly, the TidyHtml filter takes your HTML and makes it properly indented and neat when people View your source.

Personally, I think that's a lot of whitespace, but...ehh...that's just me.

NOTE: This uses the TidyNet assembly which can be found at TidyNet at sourceforge.net or you can NuGet with Install-Package TidyNetPortable

A NOTE TO THE NOTE: Thanks to a reader, there was an error because of the age of this post. They caught an error with the TidyNet assembly. Instead, I've replaced TidyNet with the TidyHtml5Managed library. I've added a Github repository below with all of the ActionFilters.

ActionFilters\TidyHtml.cs

public class TidyHtml : ActionFilterAttribute
{
    public TidyHtml()
    {
        Xhtml = true;
        IndentContent = true;
        DocType = DocType.Strict;
        XmlOut = true;
        MakeClean = true;
        HideEndTags = true;
        LogicalEmphasis = true;
        DropFontTags = true;
    }
    #region Properties
    public DocType DocType { get; set; }
    public bool DropFontTags { get; set; }
    public bool LogicalEmphasis { get; set; }
    public bool XmlOut { get; set; }
    public bool Xhtml { get; set; }
    public bool IndentContent { get; set; }
    public bool HideEndTags { get; set; }
    public bool MakeClean { get; set; }
    public bool TidyMark { get; set; }
    #endregion
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (!(filterContext.Result is ViewResult)) return;
        var tidy = new Tidy
        {
            Options =
            {
                DocType = DocType,
                DropFontTags = DropFontTags,
                LogicalEmphasis = LogicalEmphasis,
                XmlOut = XmlOut,
                Xhtml = Xhtml,
                IndentContent = IndentContent,
                HideEndTags = HideEndTags,
                MakeClean = MakeClean,
                TidyMark = TidyMark,
            }
        };
        filterContext.RequestContext.HttpContext.Response.Filter =
            new HtmlTidyFilter(filterContext.RequestContext.HttpContext.Response.Filter, tidy);
    }
    #region Nested type: HtmlTidyFilter
    private class HtmlTidyFilter : Stream
    {
        private readonly Stream _stream;
        private readonly Tidy _tidy;
        #region Properites
        public override bool CanRead
        {
            get { return true; }
        }
        public override bool CanSeek
        {
            get { return true; }
        }
        public override bool CanWrite
        {
            get { return true; }
        }
        public override long Length
        {
            get { return 0; }
        }
        public override long Position { get; set; }
        public override void Flush()
        {
            _stream.Flush();
        }
        #endregion
        #region Methods
        public override int Read(byte[] buffer, int offset, int count)
        {
            return _stream.Read(buffer, offset, count);
        }
        public override long Seek(long offset, SeekOrigin origin)
        {
            return _stream.Seek(offset, origin);
        }
        public override void SetLength(long value)
        {
            _stream.SetLength(value);
        }
        public override void Close()
        {
            _stream.Close();
        }
        public override void Write(byte[] buffer, int offset, int count)
        {
            var data = new byte[count];
            Buffer.BlockCopy(buffer, offset, data, 0, count);
            var html = Encoding.Default.GetString(buffer);
            using (var input = new MemoryStream())
            {
                using (var output = new MemoryStream())
                {
                    var byteArray = Encoding.UTF8.GetBytes(html);
                    input.Write(byteArray, 0, byteArray.Length);
                    input.Position = 0;
                    _tidy.Parse(input, output, new TidyMessageCollection());
                    var result = Encoding.UTF8.GetString(output.ToArray());
                    var outdata = Encoding.Default.GetBytes(result);
                    _stream.Write(outdata, 0, outdata.GetLength(0));
                }
            }
        }
        #endregion
        public HtmlTidyFilter(Stream stream, Tidy tidy)
        {
            _stream = stream;
            _tidy = tidy;
        }
    }
    #endregion
}

Conclusion

Ever since I was introduced to the AuthorizeAttribute in ASP.NET MVC, I had to learn more about the ActionFilters.

To secure a page with one attribute attached to a method OR an entire controller?

Or compress your payload using gzip down to the client with one line?

Yes, please!

I hope these Action Filters help you like they've helped me over the years.

Source of ActionFilters are on Github

Do you have a favorite ActionFilter? Post your comment and link below and tell us about it!

Did you like this content? Show your support by buying me a coffee.

Buy me a coffee  Buy me a coffee
Picture of Jonathan "JD" Danylko

Jonathan Danylko is a web architect and entrepreneur who's been programming for over 25 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Principal Software Engineer Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus