Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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.
.gif)
In questa pagina
La pipeline di ASP.NET
Come si registrano gli handler
Il primo HttpHandler
Un caso reale: riscrivere l'indirizzo
Un caso un po' particolare: CSS server side con dati da profilo
Il ruolo di DefaultHttpHandler: non solo ASP.NET
HttpHandler asincroni
Altri scenari di utilizzo
Conclusioni
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.
.jpg)
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:
.jpg)
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.
.jpg)
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:
.jpg)
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:
Esportare dati da Excel verso XML con un HTTPHandler di ASP.NET
Un HttpHandler per la FormsAuthentication sui file con la stessa estensione
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
Aree di accesso protette con ASP.NET 2.0: Membership e Roles API – Daniele Bochicchio
Personalizzazione del profilo utente con ASP.NET 2.0: Profile API – Daniele Bochicchio
BuildProvider, ExpressionBuilder e VirtualPathProvider – Cristian Civera