Condividi tramite


Modificare il comportamento di ASP.NET: HttpHandler

di Daniele Bochicchio - Microsoft MVP

Gli HttpHandler rappresentano il metodo migliore con il quale estendere la pipeline di ASP.NET, aggiungendo un tocco personale agli URL e potendo contare sulla possibilità di controllare in maniera centralizzata l'accesso alle varie funzionalità del sito.La personalizzazione degli URL, ad esempio, rientra tra le funzionalità offerte da questi sistemi, così come l'applicazione di tecniche di controllo degli accessi o meccanismi di logging. Prima di andare oltre, però, occorre analizzare il flusso di richiesta e risposta di una tipica risorsa gestita da ASP.NET, così da capire meglio come gli HttpHandler si incastrino in questo scenario.

In questa pagina

La pipeline di ASP.NET La pipeline di ASP.NET
Come si registrano gli handler Come si registrano gli handler
Il primo HttpHandler Il primo HttpHandler
Un caso reale: riscrivere l'indirizzo Un caso reale: riscrivere l'indirizzo
Un caso un po' particolare: CSS server side con dati da profilo Un caso un po' particolare: CSS server side con dati da profilo
Il ruolo di DefaultHttpHandler: non solo ASP.NET Il ruolo di DefaultHttpHandler: non solo ASP.NET
HttpHandler asincroni HttpHandler asincroni
Altri scenari di utilizzo Altri scenari di utilizzo
Conclusioni Conclusioni
Approfondimenti Approfondimenti

La pipeline di ASP.NET

Quando effettuiamo una richiesta a una risorsa ASP.NET otteniamo a video un risultato che nella maggior parte dei casi è essenzialmente HTML, perché siamo abituati a pensare che ASP.NET sia limitato solo alle pagine web.

In realtà è possibile associare qualsiasi estensione e produrre dunque qualsiasi tipo di risultato, da immagini a testo, passando per la normale pipeline di ASP.NET.

La pipeline altro non è che il susseguirsi di determinati passi nella fase che include la richiesta alla risorsa, la generazione del risultato da parte di ASP.NET e quindi l'invio della risposta.

*

Figura 1: Schema della richiesta e della risposta a una pagina ASP.NET

Richiesta e risposta, che sono molto rapide tanto da non risultare visibili a occhio nudo, sono in realtà intervallate dalla generazione dell'ouput da parte di un pezzo particolare dell'architettura di ASP.NET che prende appunto il nome di HttpHandler.

Possiamo considerare un handler come un gestore fisico della richiesta, che prende in carico le informazioni inviate dall'utente e produce un risultato, che sarà poi inviato al client.

Da un punto di vista formale, un HttpHandler è una classe (così come qualsiasi altra cosa che riguardi ASP.NET) istanziata a runtime quando viene fatta una richiesta per il percorso per cui è registrata.

Un handler è infatti invocato in base ad un URL a cui è associato, così da poter produrre un risultato solo quando è effettivamente richiamato.

Il motivo per cui una pagina con estensione .aspx produce un certo tipo di risultato risiede appunto nell'handler che è associato a questa estensione, che è la classe PageHandlerFactory contenuta nel namespace System.Web.UI.

Questa classe verifica che il file esista su disco e nel caso esegue quello che è il contenuto, lavorando insieme al Page Parser per produrre il risultato che conosciamo.

Il comportamento di un handler è personalizzabile in maniera del tutto completa e il modo in cui funziona PageHanlderFactory è essenzialmente dettato da quelle che sono state le scelte, in tal senso, da parte di chi ha creato ASP.NET. Potendo specificare però handler custom, questo comportamento diventa del tutto modificabile a proprio piacimento, così da poter aggiungere un controllo centralizzato alle richieste, implementando nient'altro che il pattern Front Controller.

Per essere caricati dinamicamente, gli HttpHandler hanno tutti una radice in comune che è stabilita dall'interfaccia IHttpHandler, che devono implementare così da poter essere caricati a runtime a prescindere dal tipo effettivo. Di seguito la definizione dell'interfaccia:

C#

public interface IHttpHandler
{
    // Methods
    void ProcessRequest(HttpContext context);

    // Properties
    bool IsReusable { get; }
}

VB

Public Interface IHttpHandler
    ' Methods
    Sub ProcessRequest(ByVal context As HttpContext)

    ' Properties
    ReadOnly Property IsReusable As Boolean
End Interface

 

Come si registrano gli handler

ASP.NET contiene già un numero prefissato di handler di default, associati alle varie estensioni.

Come già detto, a seconda del percorso specificato viene instanziata la classe corrispondente e sull'oggetto risultante viene poi invocato il metodo ProcessRequest, che contiene la logica specifica per l'handler specificato, a cui viene passata un'istanza dell'HttpContext corrente, che rappresenta il contesto di esecuzione di richiesta e risposta.

Ecco un estratto del tipico web.config globale, contenuto nella directory %windir%\Microsoft.NET\Framework\v2.0.50727\CONFIG\ (dove a seconda del numero di versione, potrebbe leggermente variare il percorso):

<configuration>
<system.web>
<httpHandlers>
<add path="*.svc" verb="*" type="System.ServiceModel.Activation.HttpHandler,
System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="false" />
<add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="true" />
<add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="true" />
<add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="true" />
<add path="*.asmx" verb="*" type="System.Web.Services.Protocols.WebServiceHandlerFactory,
System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" validate="false" />
<add path="*.ascx" verb="*" type="System.Web.HttpForbiddenHandler" validate="true" />
<add path="*" verb="GET,HEAD,POST" type="System.Web.DefaultHttpHandler" validate="true" />
<add path="*" verb="*" type="System.Web.HttpMethodNotAllowedHandler" validate="true" />
</httpHandlers>
</system.web>
</configuration>

Ecco svelato, ad esempio, il motivo per cui digitando nel browser un indirizzo con una richiesta ad una risorsa con estensione .ascx, tutto quello che si ottiene è una pagina che avvisa che quel tipo di estensione non è servita. HttpForbiddenHandler è infatti una semplice classe che nel metodo ProcessRequest contiene (più o meno) questo codice:

C#

public void ProcessRequest(HttpContext context)
{
    throw new HttpException(0x193, SR.GetString("Path_forbidden", new object[] { context.Request.Path }));
}

VB

Public Sub ProcessRequest(ByVal context As HttpContext)
    Throw New HttpException(&H193, SR.GetString("Path_forbidden", New Object() { context.Request.Path }))
End Sub

L'effetto che si ottiene è dunque del tutto legittimato dal codice presentato, che come si può vedere di fatto scatena un'eccezione.

Da qui a cominciare a personalizzare il risultato associato a certe estensioni il passo è davvero breve, perché di fatto basta agire su questo metodo, implementando una propria classe e registrandola nel web.config.

In alcuni scenari particolari, è d'aiuto utilizzare un HttpHandlerFactory anzichè un HttpHandler "normale". Attraverso l'interfaccia IHttpHandlerFactory, le classi generate hanno la possibilità di specificare a runtime un handler da utilizzare, attraverso il metodo GetHandler che restituisce appunto un tipo IHttpHandler. Questa tecnica si applica quando è necessario associare un'estensione a un solo handler e poi decidere da codice qual è effettivamente l'handler da dedicare alla richiesta.

 

Il primo HttpHandler

Creare un HttpHandler è un'operazione abbastanza semplice perché come visto l'interfaccia IHttpModule lascia ampio spazio alla fantasia dello sviluppatore.

Per dimostrare quanto sia facile, il primo HttpHandler consiste in una classe che scrive a video l'ora corrente:

*

Il codice associato a questo handler è semplicissimo ed è riportato di seguito.

C#

using System;
using System.Web;

public class FirstHandler:IHttpHandler 
{

public bool IsReusable
{
get {return true; }
}

public void ProcessRequest(HttpContext context)
{
context.Response.Write(DateTime.Now.ToString());
}

}

VB

Imports System 
Imports System.Web 

Public Class FirstHandler 
Implements IHttpHandler 

Public ReadOnly Property IsReusable() As Boolean 
   Get 
     Return True 
   End Get 
End Property 

Public Sub ProcessRequest(ByVal context As HttpContext) 
   context.Response.Write(DateTime.Now.ToString()) 
End Sub 
End Class

La proprietà IsReusable indica se l'istanza della classe può essere riutilizzata per più richieste e tendenzialmente ha valore true, a meno che non si abbiano dati instanziati nel costruttore della classe che debbano differire per ogni richiesta.

Il metodo ProcessRequest contiene una semplice istruzione che attraverso lo stream di output della pagina scrive a video la data e l'ora corrente.

Perché l'handler possa essere invocato, va registrato nel web.config, nella già nominata sezione configuration\system.web\httpHandlers.

<configuration>
    <system.web>
      <httpHandlers>
        <add verb="HEAD,GET,POST" path="*.time" validate="true" type="FirstHandler, App_Code"/>
      </httpHandlers>
</system.web>
</configuration>

La registrazione prevede l'inserimento degli attributi verb, che indica i verb dell'HTTP supportati, path che indica il percorso (* è la wildcard), mentre type prevede la specifica del tipo (nome della classe completo di namespace, che nel caso manca) e dell'assembly che lo contiene. A proposito di quest'ultimo punto, per semplicità si è scelto di salvare il sorgente della classe direttamente nellla directory /App_Code/ posta sotto la root. Questa scelta fa sì che l'esempio sia testabile anche da chi ha Visual Web Developer Express 2005, ma in una soluzione reale è preferibile creare una class library e fare il deployment dell'assembly corrispondente.

 

Un caso reale: riscrivere l'indirizzo

Generalmente un handler è utilizzato per scenari più interessanti, come ad esempio nel cosiddetto UrlRewriting. Questa tecnica consiste nel sostituire ai soliti percorsi con querystring dei corrispondenti parlanti, che abbiano la capacità, ad esempio, di favorire una migliore indicizzazione da parte dei motori di ricerca.

Il tipico esempio potrebbe essere questo indirizzo:

http://www.miosito.ext/content.aspx?ID=15&Cat=16

che diventa un bel più esplicativo:

http://www.miosito.ext/content/15/16/Articolo-su-HttpHandler.aspx

Va da sé che la seconda versione continua a possedere i metadati che prima erano inviati attraverso la querystring, ma ha una forma decisamente più comoda da ricordare anche per l'utente, che può ritornare su quella pagina navigando nella history, semplicemente leggendone il nome.

In questo caso la logica dell'handler è leggermente più sofisticata, ma tutto sommato ancora semplice:

C#

using System;
using System.Web;

public class UrlRewriteHandler:IHttpHandler 
{

public bool IsReusable
{
get {return true; }
}

public void ProcessRequest(HttpContext context)
{
string url = context.Request.Path.ToLower(); // ignora il dominio e la querystring

// il pezzo iniziale dell'url
string baseUrl = "content/";

if (url.IndexOf(baseUrl) == -1)
return;

// prendo solo i pezzi che mi interessano
url = url.Substring(url.IndexOf(baseUrl) + baseUrl.Length);

if (url.IndexOf("/") == -1)
return;

url = url.Substring(0, url.LastIndexOf("/"));

// prendo le informazioni dall'url
string categoryID = null;
string ID = null;

ID = url.Split('/')[0];
categoryID = url.Split('/')[1];

// eseguo la richiesta alla pagina vera
context.Server.Execute(String.Concat("~/content.aspx?ID=", ID, "&CatID=", categoryID), false);
}

}

VB

Imports System 
Imports System.Web 

Public Class UrlRewriteHandler 
Implements IHttpHandler 

Public ReadOnly Property IsReusable() As Boolean 
Get 
  Return True 
End Get 
End Property 

Public Sub ProcessRequest(ByVal context As HttpContext) 
Dim url As String = context.Request.Path.ToLower() ' ignora il dominio e la querystring

' il pezzo iniziale dell'url
Dim baseUrl As String = "content/" 
If url.IndexOf(baseUrl) = -1 Then 
Return 
End If 

url = url.Substring(url.IndexOf(baseUrl) + baseUrl.Length) 
If url.IndexOf("/") = -1 Then 
Return 
End If

' prendo solo i pezzi che mi interessano
url = url.Substring(0, url.LastIndexOf("/")) 

' prendo le informazioni dall'url
Dim categoryID As String = Nothing 
Dim ID As String = Nothing 

ID = url.Split("/"C)(0) 
categoryID = url.Split("/"C)(1)

' eseguo la richiesta alla pagina vera
context.Server.Execute(String.Concat("~/content.aspx?ID=", ID, "&CatID=", categoryID), False)
 
End Sub 
End Class

Nel metodo ProcessRequest questa volta viene preso l'URL richiesto, ripulito dalle informazioni aggiuntive e poi richiamata una pagina esterna, inviando via querystring i parametri, sfruttando il metodo Execute della classe HttpServerUtility, che ha la possibilità di eseguire nello stesso contesto una chiamata ad una risorsa esterna, potendo tra l'altro inviare anche informazioni via querystring.

L'effetto è che ora le chiamate a pagine con l'URL riscritto sembreranno effettivamente puntare a quella pagina (basta verificarlo tra le proprietà nel proprio browser), ma in realtà saranno gestite comunque da una pagina unica.

*

 

Un caso un po' particolare: CSS server side con dati da profilo

L'unico vero limite degli HttpHandler è la fantasia. In questo esempio ho deciso di sfruttare Profile API (oggetto di un articolo disponibile a questo indirizzo) per valorizzare alcune informazioni lette direttamente dal profilo, prima che il file venga servito al browser.

Per prima cosa è necessario mappare l'estensione .cssx sotto il nostro handler, così da non entrare in conflitto con quella .css, utilizzata di default. Nel caso si utilizzi IIS con estensioni personalizzate, è necessario mappare la stessa così come viene fatto per quelle gestite da ASP.NET, attraverso IIS Manager.

Per convenzione il file avrà una notazione come la seguente:

body
{
background-color: [BackgroundColor];
color: [TextColor];
}

Il risultato è visibile nella finestra seguente:

*

L'handler in questione verifica che il file su disco esista, che l'utente sia autenticato, poi provvede a effettuare le sostituzioni trovando le occorrenze che abbiamo i simboli [] a racchiudere il nome della proprietà del profilo, mostrando a video il risultato ottenuto.

C#

using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.IO;
using System.Web.Profile;
using System.Configuration;

public class CssProfileHandler:IHttpHandler
{
public bool IsReusable
{
get { return true; }
}

public void ProcessRequest(HttpContext context)
{
// controllo che il file esista su disco
if (!File.Exists(context.Request.PhysicalPath))
throw new HttpException(404, "Not found");

// leggo tutto il contenuto e lo salvo
string text = File.ReadAllText(context.Request.PhysicalPath);

// solo se l'utente ha un profilo registrato...
if (context.User.Identity.IsAuthenticated)
{
ProfileBase profile = ProfileBase.Create(context.User.Identity.Name);

Object currentValue = null;
foreach (SettingsProperty prop in ProfileBase.Properties)
{
currentValue = profile.PropertyValues[prop.Name];
if (currentValue == null)
currentValue = prop.DefaultValue;

text = text.Replace("[" + prop.Name + "]",
currentValue.ToString());
}
}

// scrivo a video il risultato
context.Response.ContentType = "text/css";
context.Response.Write(text);
}

}

VB

Imports System 
Imports System.Web 
Imports System.Web.Security 
Imports System.Web.UI 
Imports System.IO 
Imports System.Web.Profile 
Imports System.Configuration 

Public Class CssProfileHandler 
Implements IHttpHandler 

Public ReadOnly Property IsReusable() As Boolean 
Get 
  Return True 
End Get 
End Property 

Public Sub ProcessRequest(ByVal context As HttpContext) 
' controllo che il file esista su disco
If Not File.Exists(context.Request.PhysicalPath) Then 
  Throw New HttpException(404, "Not found") 
End If

' leggo tutto il contenuto e lo salvo
Dim text As String = File.ReadAllText(context.Request.PhysicalPath) 

' solo se l'utente ha un profilo registrato...
If context.User.Identity.IsAuthenticated Then 
  Dim profile As ProfileBase = ProfileBase.Create(context.User.Identity.Name)
  Dim currentValue As Object = Nothing 
  
  For Each prop As SettingsProperty In ProfileBase.Properties 
 currentValue = profile.PropertyValues(prop.Name) 
 If currentValue Is Nothing Then 
currentValue = prop.DefaultValue 
 End If 
 text = text.Replace("[" + prop.Name + "]", currentValue.ToString) 
  Next 
  
End If

' scrivo a video il risultato
context.Response.ContentType = "text/css" 
context.Response.Write(text) 
End Sub 
End Class

 

Il ruolo di DefaultHttpHandler: non solo ASP.NET

Chiunque abbia dato un'occhiata alle novità di IIS 7.0 (potete trovare una panoramica in questo articolo) rimane favorevolmente colpito dalla possibilità di sfruttare alcuni dei meccanismi, come la FormsAuthentication che consente di proteggere l'accesso all'applicazione, anche su risorse non ASP.NET, come pagine ASP o documenti.

E' possibile sfruttare questo modo anche con IIS 6.0 e ASP.NET 2.0, sin da subito, perché tra i Chandler di default, come riportato a inizio articolo, c'è questa riga:

<add path="*" verb="GET,HEAD,POST" type="System.Web.DefaultHttpHandler" validate="true" />

Se si associa sotto IIS 6.0 l'estensione *.* tra le wildcard mappings, l'effetto è che ogni richiesta passerà attraverso questo handler di ASP.NET, che è in grado, se non interviene nessuna complicazione, di richiamare nuovamente IIS, che si occupa di gestire la risposta nel modo classico.

Questa tecnica può salvare la vita in diversi situazioni in cui si voglia fare integrazione tra sistemi esistenti, uno nuovo ed uno legacy, senza necessità di mettere in piedi complicati meccanismi di gestione di single sign-on, dato che Membership e Roles API possono essere usati con la protezione dichiarativa da web.config direttamente su risorse non ASP.NET, come pagine ASP!

 

HttpHandler asincroni

Spesso gli handler devono far riferimento a risorse esterne, come database o web service. In certi casi questi componenti possono agire in modalità asincrona, evitando che l'handler aspetti, come fa di default, che l'esecuzione del codice contenuto sia avvenuta.

In questo caso, così come avviene con la AsyncPage di ASP.NET 2.0, il codice viene eseguito in un thread separato, sempre utilizzato dal ThreadPool di ASP.NET, slegando la risposta dal tempo necessario affinché il risultato venga elaborato.

Creare un HttpHandler asincrono necessita che l'interfaccia utilizzata sia IHttpAsyncHandler e i metodi BeginProcessRequest e EndProcessRequest servono rispettivamente per intercettare l'inizio e la fine dell'invocazione.

Per il resto, la registrazione è del tutto analoga a quella di un HttpHanlder "normale" e un esempio si può trovare a questo indirizzo della library MSDN.

 

Altri scenari di utilizzo

Ecco alcune idee con cui poter implementare ulteriori soluzioni che facciano uso di HttpHandler:

Probabilmente in giro ci sono ancora tanti altri esempi e, come già detto, l'unico limite che ha questa tecnica è il risultato che vi prefiggete di raggiungere**.**

 

Conclusioni

Estendere ASP.NET 2.0 è stato reso ancora più semplice grazie a diversi entrypoint che si possono sfruttare. Da questo punto di vista gli HttpHandler rappresentano senza dubbio uno dei sistemi migliori per raggiungere un'estendibilità totale e sono presenti sin dalla prima versione, garantendo un'ampia possibilità di manovra.

Utilizzarli per le proprie necessità non ha limiti né vincoli particolari e anzi molto spesso riesce a rendere più semplice la personalizzazione di parti o dell'intera applicazione.

 

Approfondimenti