Als ich vor ca. 2 Jahren begann mich mit einem SiteMapProvider zu befassen der als SiteMapDataSource eine SQL-Datenbank benutzt, bin ich auf diesen Artikel von Jeff Prosise gestoßen. In seiner Implementierung hatte er schon fast alle meine Anforderungen erfüllt. Er benutzt die SqlCacheDependency zum überwachen von Änderungen an der Sitemap Tabelle in der Datenbank, sowie das securityTrimmingEnabled-Konfigurationsattribut welches den Wert der Eigenschaft SecurityTrimmingEnabled des SiteMapProvider setzt und somit die Verwendung von Roles unterstützt. Was mit allerdings fehlte, war eine "Entkoppelung" des SiteMapProvider von der Datenbank. Da in den meisten ASP.NET-Anwendungen exzessiver Gebrauch von der Datenbankverbindung gemacht wird wollte ich den SqlSiteMapProvider so gestalten, dass er nicht bei jedem laden der Seite eine Verbindung zur Datenbank aufbaut. Es sollte auch eine Möglichkeit geschaffen werden den Provider über einen Webdienst mit der Datenbank kommunizieren zu lassen. Dabei sollte auch gleich die Authentifizierung mit Hilfe der, in dieser Artikelreihe beschriebenen, SoapAuthHeader-Klasse ermöglicht werden. Das Ganze sollte auch noch einfach in der web.config konfiguriert werden.
Der zuletzt angesprochene Punkt: Konfiguration in der web.config ließ sich am leichtesten umsetzen. Da die Klasse SqlSiteMapProvider von der abstrakten Klasse StaticSiteMapProvider erbt, wird die Methode Initialize überschrieben. Diese Methode wird vom Framework bei der Initialisierung des Provider aufgerufen und die gesamte, den Provider betreffende, Konfiguration in der web.config als NameValueCollection an die Initialize-Methode übergeben. Jetzt braucht man nur noch die einzelnen Konfigurationsattribute auf ihren Wert zu prüfen und auszuwerten. Um zu unterscheiden ob eine direkte Datenbankverbindung genutzt wird, habe ich das Konfigurationsattribut directConnection hinzugefügt. Dieses Attribut wird in der überschriebenen Initialize-Methode in einen boolschen Wert gecastet und ausgewertet. Der Konfigurationsbereich in der web.config könnte so aussehen:
<configuration>
<siteMap defaultProvider="SqlSiteMapProvider" enabled="true">
<providers>
<clear/>
<add name="SqlSiteMapProvider" type="TestSpace.Web.SqlSiteMapProvider"securityTrimmingEnabled="true" secureQueryStringEnabled="true"directConnection="false" cacheKey="sitemapDataSet"/>
</providers>
</siteMap>
</configuration>In der Initialize-Methode wird geprüft ob das Attribut in der web.config angegeben ist und dann in eine Variable vom Typ bool gecastet.
// prüfen ob directConnection in der web.config angegeben ist
if (string.IsNullOrEmpty(config["directConnection"]))
{
config.Remove("directConnection");
config.Add("directConnection", "true");
}
bool.TryParse(config["directConnection"], out isDirectConnection);
config.Remove("directConnection");Die Variable isDirectConnection wird nun an mehreren Stellen in einem if-Statement verwendet um auf die vorliegende Konfiguration zu reagieren. Falls isDirctConnection true zurückgibt, wird, wie im Original Code, ein SqlDataReader verwendet um die Sitemap-Struktur zu erstellen. Wird false zurückgegeben, liegt im HttpRuntime.Cache ein DataSet vor. In diesem Fall wird ein DataTableReader verwendet um die Sitemap-Struktur zu erstellen. Beide Szenerien lassen sich ohne größeren Aufwand nutzen, da das Argument reader der beiden Methoden CreateSiteMapNodeFromDataReader(reader) und GetParentNodeFromDataReader(reader) vom Typ DbDataReader ist und somit sowohl einen SqlDataReader als auch einen DataTableReader akzeptiert. Insofern sind die Änderungen an der SqlSiteMapProvider-Klasse abgeschlossen.
Um das benötigte DataSet im HttpRuntime.Cache zu verwalten kann natürlich in jedem Anwendungsfall des SqlSiteMapProvider ein eigenes Datahandling geschrieben werden. Doch jedes einzelne Mal mich selbst um die Daten im Cache zu kümmern erschien mir zu mühsam. Also habe ich nach einem Weg gesucht um die benötigten Daten nur einmal, beim Start der Anwendung, zur Verfügung zu stellen. Es war klar, dass hier nur die Methode Application_Start der Global-Klasse in Betracht kam. Jetzt musste noch eine Klasse her, welche sich einfach in der Global-Klasse initialisieren lies und sich um die Cache-Verwaltung kümmerte. Um die Zugehörigkeit zum SqlSiteMapProvider zu zeigen, habe ich diese Klasse SqlSiteMapCacheHandler genannt. Da ich mir zur Aufgabe gemacht hatte die Daten von einem Webdienst liefern zu lassen, musste auch die Handhabung eines Webdienst-Proxy sowie die zugehörige Authentifizierung möglichst abstrakt gehalten werden. Des weiteren soll ebenfalls auf Änderungen in der zugrundeliegenden Datenbanktabelle reagiert werden. Hier habe ich mich für die Verwendung einer CacheDependency mit einem Cacheschlüssel entschieden. Dieses erschien mir am einfachsten, da Änderungen an der Sitemap-Tabelle sicherlich aus der Web-Anwendung heraus getätigt werden und somit der Cacheschlüssel einfach, im Zuge der Änderung, geändert werden kann.
Etwas komplexer verhält es sich mit der Verwendung eines Webdienst-Proxy und der Authentifizierung. Es müssen verschiedenen Objekte an die SqlCacheHandler-Klasse übergeben werden. Im einzelnen sind dies:
- Der eigentliche Webdienst-Proxy als Object.
- Die Webdienst-Proxy Methode, welche die benötigten Daten anfordert.
- Der für die Authentifizierung zuständige SoapHeader als Object.
- Die Gültigkeitsdauer des SoapHeader.
Also wurden in der SqlCacheHandler-Klasse die benötigten Eigenschaften des jeweiligen Typs angelegt.
#region Properties
/// <summary>
/// Der Name des Schlüssels unter dem das DataSet im Cache abgelegt werden soll.
/// Es muss der gleiche Name verwendet werden wie in der Konfiguration
/// des <see cref="T:TestSpace.Web.SqlSiteMapProvider"/>.
/// </summary>
/// <value>Setzt den Wert des privaten Feldes cacheKey oder gibt ihn zurück.</value>
/// <remarks>n/a</remarks>
public string CacheKey
{
get { return this.cacheKey; }
set { this.cacheKey = value; }
}
/// <summary>
/// Der Referenzschlüssel der überwacht werden soll. Initialisierungswert ist <c>false</c>.
/// Das bedeutet, Daten sind nicht geändert.
/// </summary>
/// <value>Setzt den Wert des privaten Feldes referenceKey oder gibt ihn zurück.</value>
/// <remarks>
/// Ein zusätzlicher Schlüssel, der für das verwendete <see cref="T:System.Web.Caching.CacheDependency"/>
/// -Objekt benötigt wird. Der Wert dieses Schlüssels, <c>true</c> oder <c>false</c>, sollte bei
/// Änderungen an der Datenbanktabelle ebenfalls auf <c>true</c>, als Daten geändert, gesetzt werden.
/// </remarks>
public string ReferenceKey
{
get { return this.referenceKey; }
set { this.referenceKey = value; }
}
/// <summary>
/// Die WebServiceProxy-Klasse, die das DataSet liefert.
/// </summary>
/// <value>Setzt den Wert des privaten Feldes proxy oder gibt ihn zurück.</value>
/// <remarks>
/// Falls der übergebene WebServiceProxy <see cref="T:System.Web.Services.Protocols.SoapHeader"/>
/// oder andere Methoden zur Authentifitierung verwendet, muss dieser an die Eigenschaft
/// <see cref="P:TestSpace.Web.SqlSiteMapCacheHandler.ProxyAuhHeader"/> übergeben werden.
/// </remarks>
public object Proxy
{
get { return this.proxy; }
set { this.proxy = value; }
}
/// <summary>
/// Die <see cref="T:System.Web.Services.Protocols.SoapHeader"/>-Klasse,
/// die zur Authentifizierung verwendet wird.
/// </summary>
/// <value>Setzt den Wert des privaten Felds proxyAuthHeader oder gibt ihn zurück.</value>
/// <remarks>n/a</remarks>
public object ProxyAuhHeader
{
get { return this.proxyAuthHeader; }
set { this.proxyAuthHeader = value; }
}
/// <summary>
/// Die Gültigkeit der AuthHeader Klasse in Sekunden.
/// </summary>
/// <value>Setzt den Wert des privaten Feldes proxyAuthHeaderExpires oder gibt ihn zurück.</value>
/// <remarks>Der Standardwert ist auf 10 Sekunden festgelegt.</remarks>
public int ProxyAuthHeaderExpires
{
get { return this.proxyAuthHeaderExpires; }
set { this.proxyAuthHeaderExpires = value; }
}
/// <summary>
/// Der Name der Methode im <see cref="P:TestSpace.Web.SqlSiteMapCacheHandler.Proxy"/>,
/// die das DataSet für den <see cref="T:TestSpace.Web.SqlSiteMapProvider"/> liefert.
/// </summary>
/// <value>Setzt den Wert des privaten Feldes getDataMethod oder gibt ihn zurück.</value>
/// <remarks>Die aufzurufende Methode darf keine Parameter benötigen.</remarks>
public string GetDataMethod
{
get { return this.getDataMethod; }
set { getDataMethod = value; }
}
#endregionAls einzige öffentliche Methode ist die Methode Initialize vorhanden die, nach der Zuweisung der Eigenschaften, einmal aufgerufen wird. In der Global-Klasse sieht das dann in etwa so aus:
public class Global : HttpApplication
{
// Neue Instanz des WebService-Proxy
private GetSiteMapProxy siteMapProxy = new GetSiteMapProxy();
// Neue Instanz vom SoapAuthHeader des WebService-Proxy
private SoapAuthHeader authHeader = new SoapAuthHeader();
// neue Instanz vom SqlSiteMapCacheHandler
private SqlSiteMapCacheHandler handler = new SqlSiteMapCacheHandler();
/// <summary>
/// Standard-Konstruktor
/// </summary>
public Global()
{
//Auskommentierung der folgenden Zeile bei Verwendung von Designkomponenten aufheben
//InitializeComponent();
}
/// <summary>
/// Wird beim Start der Anwendung ausgeführt.
/// </summary>
void Application_Start(object sender, EventArgs e)
{
// SqlSiteMapCacheHandler initialisieren
handler.CacheKey = "sitemapDataSet";
handler.ReferenceKey = "sitemapKey";
handler.Proxy = siteMapProxy;
handler.GetDataMethod = "getData";
handler.ProxyAuhHeader = authHeader;
handler.ProxyAuthHeaderExpires = 30;
handler.Initialize();
}
// hier weitere Methoden
}Wenn die Methode Initialize aufgerufen wird, prüft sich zunächst ob die privaten Felder proxy und getDataMethod mit Werten belegt sind. Anschließend wird überprüft ob die beiden Cacheschlüssel schon im Cache vorhanden sind.
public void Initialize()
{
// prüfen ob die Felder proxy und getDataMethod mit Werten belegt sind.
if (this.proxy == null || this.getDataMethod == null)
{
throw new EmptyPropertyException(Resources.EmptyProxyAndGetData);
}
// prüfen ob die beiden CacheSchlüssel schon im Cache vorhanden sind.
if (HttpRuntime.Cache[this.referenceKey] == null
&& HttpRuntime.Cache[this.cacheKey] == null)
{
HttpRuntime.Cache.Insert(this.referenceKey, false);
this.CacheSiteMap();
}
// Schlüssel sind vorhanden, aber Daten wurden geändert.
else
{
if ((bool)HttpRuntime.Cache[this.referenceKey])
{
HttpRuntime.Cache[this.referenceKey] = false;
this.CacheSiteMap();
}
}
}Sind beide Schlüssel noch nicht vorhanden werden sie ins Cache eingefügt und anschließend die private Methode CacheSiteMap ausgeführt. Sollten die Schlüssel bereits vorhanden sein, wird überprüft ob die Daten im Cache noch gültig sind. Die erwähnte private Methode CacheSiteMap kümmert sich um das eigentliche Speichern des DataSet im Cache, sowie um das erzeugen der CacheDependency.
private void CacheSiteMap()
{
// den ReferenceSchlüssel in das cacheKeys-Array schreiben.
// wird zur Überwachung der Gültigkeit des Cache-Objekt benötigt.
this.cacheKeys[0] = this.referenceKey;
// Eine Cacheabhängikeit zum referenceKey erzeugen.
CacheDependency dependency = new CacheDependency(null, this.cacheKeys);
// Das DataSet in das Cache schreiben.
HttpRuntime.Cache.Insert(
this.cacheKey,
this.GetDataSet(),
dependency,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.High,this.SitemapRemovedCallback);
}Als zweites Argument erwartet die Cache.Insert-Methode ein Objekt, welches im Cache gespeichert werden soll. Anstatt eines vorliegenden DataSet, wird hier die private Methode GetDataSet aufgerufen, welche das benötigte DataSet liefert. Da die Methode einen beliebigen Webdienst-Proxy, aus der Eigenschaft Proxy, verarbeiten muss, als auch der Methodenname welcher die benötigten Daten liefert auch jedesmal ein anderer sein kann, wird hier ein bisschen Reflection benötigt. Auf diese Art und Weiße kann auch gleich der SoapHeader des Webdienst-Proxy dynamisch erzeugt und zugewiesen werden. Für das Lesen und Zuweisen von Eigenschaften in Klassen die erst zur Laufzeit bekannt sind, bietet das Framework die Klasse PropertyInfo an. Also braucht nur von jeder Eigenschaft der SoapAuthHeader-Klasse ein PropertyInfo-Objekt erzeugt zu werden um die Werte der jeweiligen Eigenschaft zu bearbeiten.
byte[] hash = Fingerprint.CreateHash(
new object[] { DateTime.Now, this.proxyAuthHeaderExpires, false });
// mittels Reflection die Properties der SoapAuthHeader Klasse holen.
PropertyInfo proxyAuthHeaderValue = this.proxy.GetType().GetProperty("SoapAuthHeaderValue");
PropertyInfo authHeaderCreated = this.proxyAuthHeader.GetType().GetProperty("Created");
PropertyInfo authHeaderExpires = this.proxyAuthHeader.GetType().GetProperty("Expires");
PropertyInfo authHeaderIsEditMode = this.proxyAuthHeader.GetType().GetProperty("IsEditMode");
PropertyInfo authHeaderHashValue = this.proxyAuthHeader.GetType().GetProperty("HashValue");
// Die Werte in den Properties der SoapAuthHeader Klasse setzen
if (authHeaderCreated != null)
{
authHeaderCreated.SetValue(this.proxyAuthHeader, DateTime.Now, null);
}
if (authHeaderExpires != null)
{
authHeaderExpires.SetValue(this.proxyAuthHeader, this.proxyAuthHeaderExpires, null);
}
if (authHeaderIsEditMode != null)
{
authHeaderIsEditMode.SetValue(this.proxyAuthHeader, false, null);
}
if (authHeaderHashValue != null)
{
authHeaderHashValue.SetValue(this.proxyAuthHeader, hash, null);
}
// Die SoapAuthHeader-Klasse an das Property SoapAuthHeaderValue der
// WebService-Proxy Klasse übergeben
if (proxyAuthHeaderValue != null)
{
proxyAuthHeaderValue.SetValue(this.proxy, this.proxyAuthHeader, null);
}Ähnlich verhält es sich mit dem Methodenname der jeweiligen Webdienst-Proxy-Klasse. Hierfür wird allerdings die MethodInfo-Klasse verwendet. Zuerst wird ein MethodInfo-Objekt mit dem Methodennamen aus dem Feld getDataMethod erzeugt. Anschließend wird geprüft ob das MethodInfo-Objekt null ist und ob der Rückgabe-Typ der Methode ein DataSet ist. Sollten beide Bedingungen erfüllt sein, wird mittels MethodBase.Invoke die Methode im Webdienst-Proxy aufgerufen.
// mittels Reflection die Methode holen welche das DataSet liefert.
MethodInfo proxyGetData = this.proxy.GetType().GetMethod(this.getDataMethod);
// Falls die Methode gefunden wurde und der RückgabeTyp ein DataSet ist
// wird die Methode mit Invoke aufgerufen.
if (proxyGetData != null && proxyGetData.ReturnType == typeof(DataSet))
{
return (DataSet)proxyGetData.Invoke(this.proxy, new object[] { });
}Nun muss nur noch das zurückgegebene Objekt in ein DataSet gecastet und als solches zurückgegeben werden. Die oben beschriebene Methode CacheSiteMap fügt dieses jetzt in den Cache ein und kann somit vom SqlSiteMapProvider verwendet werden.
Da die hier beschriebenen Klassen Bestandteil eines größeren Projektes sind, habe ich hier kein Projekt zum Download angeboten. Sollte Interesse bestehen, kann ich die Klassen aus dem Gesamtprojekt herauslösen und zum herunterladen bereitstellen. Einfach kurz einen Kommentar schreiben.
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
