Realizzare un ActionFilter per ottimizzare le nostre pagine web.

Fin dalla Preview 2 del Framework ASP.NET MVC sono stati introdotti gli ActionFilter, che permettono di variare o migliorare il comportamento di un Controller o della singola Action in esso contenuta, consentendo così un forte riutilizzo del codice.
Il loro utilizzo è piuttosto semplice: basta decorare la Action o il Controller con l'attributo e implementare la logica nei metodi esposti dall'ActionFilter base da cui tutti ereditano.
I metodi messi a disposizione sono 4:

  • OnActionExecuted;
  • OnActionExecuting;
  • OnResultExecuted;
  • OnResultExecuting;


Per capire più a fondo le potenzialità e semplicità di utilizzo di questi ActionFilter ci basti osservare lo snippet seguente che abilita l’accesso alla Action soltato agli utenti presenti nel ruolo di Administrator.

[Authorize(Roles = "Administrator")]
public ActionResult Index()
{
    //Nostra Action
}

Ovviamente nel Framework troviamo già parecchi ActionFilter come HandleError, OutputCache, Authorize, ecc, ma spesso non tutti riescono a soddisfare esigenze quali, ad esempio, l'abilitazione della compressione sulle pagine, la rimozione di spazi vuoti dal markup o l'aggiunta di una firma a fondo pagina.
Proprio dall'esigenza di rimuovere gli spazi vuoti è nata l'idea di realizzare un custom ActionFilter, che nel nostro esempio si chiamerà OptimizationFilter, che avrà il compito di ottimizzare la nostra pagina.

I requisiti per un filter di questo genere sono abbastanza semplici:

  • Possibilità di attivare la compressione dei dati;
  • Possibilità di rimuovere gli spazi vuoti dal markup;
  • Possibilità di ignorare l'esecuzione del filtro per tutti gli utenti presenti in un determinato ruolo;

Precedentemente in ASP.NET, per la rimozione degli spazi vuoti dal markup veniva realizzato un HttpModule, che aveva il compito di verificare se la richiesta effettuata era rivolta verso una pagina web, e, nel caso, eseguiva una Regular Expression per la rimozione degli spazi (maggiori info qui: http://madskristensen.net/post/A-whitespace-removal-HTTP-module-for-ASPNET-20.aspx).
Purtroppo, benchè comodissime, le Regular Expression non brillano sicuramente per le performance, quindi consiglio di abbinare l'outputcache all'utilizzo di questo ActionFilter.

Partendo da quanto mostrato in questo post (http://madskristensen.net/post/The-WebOptimizer-class.aspx) di Mads Kristensen, ho implementato il CustomFilter come mostrato di seguito:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
[AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
public class OptimizationFilter : ActionFilterAttribute
{
    private bool? executeUser;
    private string[] rolesException;

    public OptimizationFilter()
    {
        Compress = false;
        RemoveWhiteSpace = false;
    }

    public bool Compress { get; set; }
    public bool RemoveWhiteSpace { get; set; }

    public string RolesException
    {
        get { return string.Join(",", rolesException); }
        set { rolesException = value.Split(","); }
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        bool execute = Execute(filterContext);

        if (!execute)
            return;

        if (!Compress)
            base.OnActionExecuting(filterContext);

        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);
        }
    }

    private bool Execute(ControllerContext filterContext)
    {
        bool execute = true;

        if (rolesException == null)
            executeUser = true;

        if (executeUser != null)
            return executeUser.Value;

        for (int i = 0; i < rolesException.Length; i++)
            if (filterContext.HttpContext.User.IsInRole(rolesException[i]))
            {
                execute = false;
                break;
            }

        executeUser = execute;

        return execute;
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        base.OnResultExecuted(filterContext);

        if (!Execute(filterContext))
            return;

        if (RemoveWhiteSpace)
            filterContext.HttpContext.Response.Filter = new WhitespaceFilter(filterContext.HttpContext.Response.Filter);
    }
}

internal class WhitespaceFilter : Stream
{
    private static readonly Regex RegexBetweenTags = new Regex(@">\s+<", RegexOptions.Compiled);
    private static readonly Regex RegexLineBreaks = new Regex(@"\n\s+", RegexOptions.Compiled);

    private readonly Stream sink;

    public WhitespaceFilter(Stream sink)
    {
        this.sink = sink;
    }

    public override void Flush()
    {
        sink.Flush();
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return sink.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        sink.SetLength(value);
    }

    public override void Close()
    {
        sink.Close();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return sink.Read(buffer, offset, count);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        var data = new byte[count];
        Buffer.BlockCopy(buffer, offset, data, 0, count);
        string html = Encoding.Default.GetString(buffer);


        html = RegexBetweenTags.Replace(html, "> <");
        html = RegexLineBreaks.Replace(html, string.Empty);
        html = html.Replace("\r", string.Empty);
        html = html.Replace("//<![CDATA[", string.Empty);
        html = html.Replace("//]]>", string.Empty);
        html = html.Replace("\n", string.Empty); 

        byte[] outdata = Encoding.Default.GetBytes(html.Trim());
        sink.Write(outdata, 0, outdata.GetLength(0));
    }

    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; }
}

Come si potrà notare, l’ ActionFilter ha esposte tre proprietà:

  • Compress;
  • RemoveWhiteSpace;
  • RolesException;

La prima serve per impostare la compressione, la seconda a rimuovere gli spazi vuoti dal markup e la terza per disabilitare le prime solo ad alcuni ruoli; proprio quest'ultima si può rivelare molto utile nel caso si debbano effettuare controlli sul markup.
Come si può vedere dallo snippet seguente il suo utilizzo risulta molto semplice.

[OptimizationFilter(Compress = false, RemoveWhiteSpace = true, RolesException = "CSSDesigner,MarkupDesigner")]
public ActionResult Index(string page)
{
    //Nostra Action
}

Ulteriori informazioni riguardo agli Action Filter le potete trovare qui.

Ciauz


Comments