Projekt „Benutzerdefinierter Azure-Anspruchsanbieter für SharePoint“ – Teil 2
Veröffentlichung des Originalartikels: 15.02.2012
In Teil 1 dieser Reihe habe ich kurz die Ziele dieses Projekts umrissen, das auf einer allgemeinen Ebene in der Verwendung des Windows Azure-Tabellenspeichers als Datenspeicher für einen benutzerdefinierten SharePoint-Anspruchsanbieter besteht. Der Anspruchsanbieter verwendet das CASI Kit zum Abrufen der Daten von Windows Azure, die benötigt werden, um die Personenauswahl (d. h. das Adressbuch) und die Namensauflösung für das Eingabesteuerelement bereitzustellen.
In Teil 3 erstelle ich alle Komponenten, die in der SharePoint-Farm verwendet werden. Dazu gehört auch eine auf dem CASI Kit basierende benutzerdefinierte Komponente, die die gesamte Kommunikation zwischen SharePoint und Azure verwaltet. Es gibt ein benutzerdefiniertes Webpart, das Informationen über neue Benutzer erfasst und in einer Azure-Warteschlange ablegt. Schließlich gibt es einen benutzerdefinierten Anspruchsanbieter, der mit dem Azure-Tabellenspeicher über WCF – über die benutzerdefinierte CASI Kit-Komponente – kommuniziert, um die Funktionalität des Eingabesteuerelements und der Personenauswahl zu ermöglichen.
Lassen Sie uns nun dieses Szenario etwas weiter ausbauen.
Beispielsweise möchten Sie, dass Ihre Partner oder Kunden auf eine Ihrer Websites zugreifen, ein Konto anfordern und dann automatisch dieses Konto „bereitstellen“ können…wobei „bereitstellen“ für verschiedene Personen höchst unterschiedliche Bedeutung haben kann. Wir verwenden dies als Basisszenario, aber natürlich lassen wir unsere öffentlichen Cloudressourcen einen Teil der Arbeit erledigen.
Werfen wir zunächst einen Blick auf die Cloudkomponenten, die wir selbst entwickeln werden:
- Eine Tabelle zur Nachverfolgung aller Anspruchstypen, die wir unterstützen möchten
- Eine Tabelle zur Nachverfolgung aller eindeutigen Anspruchswerte für die Personenauswahl
- Eine Warteschlange, in die wir Daten senden können, die der Liste der eindeutigen Anspruchswerte hinzugefügt werden sollen
- Einige Datenzugriffsklassen zum Lesen und Schreiben von Daten in Azure-Tabellen und zum Schreiben von Daten in die Warteschlange
- Eine Azure-Workerrolle, die Daten aus der Warteschlange lesen und die Tabelle der eindeutigen Anspruchswerte auffüllen wird
- Eine WCF-Anwendung als Endpunkt, über den die SharePoint-Farm kommuniziert, um die Liste der Anspruchstypen abzurufen, nach Ansprüchen zu suchen, einen Anspruch aufzulösen und der Warteschlange Daten hinzuzufügen
Lassen Sie uns nun die einzelnen Komponenten etwas genauer betrachten.
Tabelle der Anspruchstypen
In der Tabelle der Anspruchstypen speichern wir alle Anspruchstypen, die der benutzerdefinierte Anspruchsanbieter verwenden kann. In diesem Szenario verwenden wir nur einen Anspruchstyp, den Identitätsanspruch – in diesem Fall ist dies die E-Mail-Adresse. Sie sollten andere Ansprüche verwenden, aber der Einfachheit halber verwenden wir hier nur einen. Bei der Azure-Tabellenspeicherung fügen Sie einer Tabelle Instanzen von Klassen hinzu, daher müssen wir eine Klasse zur Beschreibung der Anspruchstypen erstellen. Beachten Sie auch hier, dass Sie einer Tabelle in Azure Instanzen verschiedener Klassentypen hinzufügen können, aber um die Dinge zu vereinfachen, verzichten wir hier darauf. Die Klasse, die von der Tabelle verwendet wird, sieht folgendermaßen aus:
namespace AzureClaimsData
{
public class ClaimType : TableServiceEntity
{
public string ClaimTypeName { get; set; }
public string FriendlyName { get; set; }
public ClaimType() { }
public ClaimType(string ClaimTypeName, string FriendlyName)
{
this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimTypeName);
this.RowKey = FriendlyName;
this.ClaimTypeName = ClaimTypeName;
this.FriendlyName = FriendlyName;
}
}
}
Ich gehe hier nicht auf alle Grundlagen der Verwendung des Azure-Tabellenspeichers ein, da es eine Menge diesbezüglicher Ressourcen gibt. Wenn Sie also mehr Details zu PartitionKeys oder RowKeys und deren Verwendung benötigen, kann Ihnen Ihre freundliche lokale Bing-Suchmaschine weiterhelfen. Worauf ich hier nur hinweisen möchte ist, dass ich den Wert, den ich für den PartitionKey speichere, URL-codiere. Warum? Nun, in diesem Fall ist der PartitionKey der Anspruchstyp, der verschiedene Formate annehmen kann: urn:foo:blah, https://www.foo.com/blah usw. Bei einem Anspruchstyp, der Schrägstriche enthält, kann der PartitionKey nicht mit diesen Werten in Azure gespeichert werden. Daher codieren wir sie stattdessen in einem Format, das Azure versteht. Wie oben erwähnt, verwenden wir den E-Mail-Anspruch, der Anspruchstyp dafür lautet also https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.
Tabelle der eindeutigen Anspruchswerte
In der Tabelle der eindeutigen Anspruchswerte werden alle eindeutigen Anspruchswerte gespeichert, die wir erhalten. In unserem Fall speichern wir nur einen Anspruchstyp – den Identitätsanspruch – daher sind definitionsgemäß alle Anspruchswerte eindeutig. Ich habe diesen Ansatz jedoch aus Gründen der Erweiterbarkeit gewählt. Angenommen, Sie möchten später damit beginnen, Rollenansprüche mit dieser Lösung zu verwenden. Es würde keinen Sinn machen, den Rollenanspruch „Mitarbeiter“ oder „Kunde“ oder was auch immer tausendmal zu speichern; für die Personenauswahl muss nur bekannt sein, dass der Wert vorhanden ist, damit er in der Auswahl bereitgestellt werden kann. Danach hat ihn, wer ihn hat – wir müssen nur zulassen, dass er verwendet wird, wenn Rechte auf einer Website erteilt werden. Vor diesem Hintergrund sieht die Klasse, in der die eindeutigen Anspruchswerte gespeichert werden, folgendermaßen aus:
namespace AzureClaimsData
{
public class UniqueClaimValue : TableServiceEntity
{
public string ClaimType { get; set; }
public string ClaimValue { get; set; }
public string DisplayName { get; set; }
public UniqueClaimValue() { }
public UniqueClaimValue(string ClaimType, string ClaimValue, string DisplayName)
{
this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimType);
this.RowKey = ClaimValue;
this.ClaimType = ClaimType;
this.ClaimValue = ClaimValue;
this.DisplayName = DisplayName;
}
}
}
Hier möchte ich auf ein paar Dinge hinweisen. Erstens: Wie in der vorherigen Klasse verwendet der PartitionKey einen URL-codierten Wert, da er den Anspruchswert darstellt, der Schrägstriche enthält. Zweitens werden die Daten, wie häufig bei der Azure-Tabellenspeicherung, denormalisiert, da es kein JOIN-Konzept wie in SQL gibt. Technisch gesehen können Sie einen JOIN in LINQ ausführen, aber so vieles in LINQ ist bei der Arbeit mit Azure-Daten verboten (oder zeigt schlechte Performance), dass ich es einfacher finde, eine Denormalisierung durchzuführen. Wenn Sie anders darüber denken, schreiben Sie entsprechende Kommentare – ich bin gespannt auf Ihre Meinung. In unserem Fall der lautet der Anzeigename „E-Mail“, da das der Anspruchstyp ist, der in dieser Klasse gespeichert wird.
Die Anspruchswarteschlange
Die Anspruchswarteschlange ist ziemlich einfach – wir speichern Anforderungen für „neue Benutzer“ in der Warteschlange, und dann werden sie von einem Azure-Workerprozess aus der Warteschlange gelesen und die Daten in die Tabelle der eindeutigen Anspruchswerte übertragen. Der Hauptgrund dafür ist, dass die Arbeit mit dem Azure-Tabellenspeicher manchmal ziemlich verzögert erfolgt, das Einfügen eines Elements in eine Warteschlange dagegen ziemlich schnell geht. Auf diese Weise können wir also die Auswirkungen auf die SharePoint-Website minimieren.
Datenzugriffsklassen
Ein ziemlich profaner Aspekt der Verwendung des Azure-Tabellenspeichers und der Warteschlangen ist, dass Sie immer Ihre eigene Datenzugriffsklasse schreiben müssen. Für den Tabellenspeicher müssen Sie eine Datenkontextklasse und eine Datenquellenklasse schreiben. Ich verschwende nicht viel Zeit darauf, denn Sie können Unmengen darüber im Internet lesen. Außerdem füge ich meinen Quellcode für das Azure-Projekt an diesen Beitrag an, damit Sie ihn nach Belieben verwenden können.
Dennoch möchte ich auf eines hinweisen, wobei es nur um eine persönliche Vorliebe geht. Ich speichere meinen gesamten Azure-Datenzugriffscode gern in einem separaten Projekt. So kann ich ihn in eine eigene Assembly kompilieren und auch in Projekten außerhalb von Azure verwenden. Beispielsweise finden Sie in dem Beispielcode, den ich hochlade, eine Windows-Formularanwendung, mit der ich die verschiedenen Teile des Azure-Back-Ends getestet habe. Sie weiß nichts über Azure, außer dass sie einen Verweis auf einige Azure-Assemblys und meine Datenzugriffsassembly enthält. Ich kann sie in diesem Projekt und genauso einfach in meinem WCF-Projekt verwenden, das ich als Front-End für den Datenzugriff für SharePoint verwende.
Hier einige Besonderheiten der Datenzugriffsklassen:
- · Ich verwende eine separate Containerklasse für die Daten, die ich zurückgeben möchte – die Anspruchstypen und die eindeutigen Anspruchswerte. Was ich mit Containerklasse meine, ist eine einfache Klasse mit einer öffentlichen Eigenschaft vom Typ List<> . Diese Klasse gebe ich anstelle einer einfachen List<> von Ergebnissen zurück, wenn die Daten angefordert werden. Der Grund ist folgender: Wenn ich List<> von Azure zurückgebe, erhält der Client nur das letzte Element in der Liste (wenn Sie dasselbe von einem lokal gehosteten WCF tun, funktioniert es problemlos). Daher gebe ich als Problemumgehung Anspruchstypen in einer Klasse wie der folgenden zurück:
namespace AzureClaimsData
{
public class ClaimTypeCollection
{
public List<ClaimType> ClaimTypes { get; set; }
public ClaimTypeCollection()
{
ClaimTypes = new List<ClaimType>();
}
}
}
Und die Klasse zur Rückgabe der eindeutigen Anspruchswerte sieht so aus:
namespace AzureClaimsData
{
public class UniqueClaimValueCollection
{
public List<UniqueClaimValue> UniqueClaimValues { get; set; }
public UniqueClaimValueCollection()
{
UniqueClaimValues = new List<UniqueClaimValue>();
}
}
}
- · Die Datenkontextklasse ist ziemlich einfach – nichts Großartiges; sie sieht wie folgt aus:
namespace AzureClaimsData
{
public class ClaimTypeDataContext : TableServiceContext
{
public static string CLAIM_TYPES_TABLE = "ClaimTypes";
public ClaimTypeDataContext(string baseAddress, StorageCredentials credentials)
: base(baseAddress, credentials)
{ }
public IQueryable<ClaimType> ClaimTypes
{
get
{
//this is where you configure the name of the table in Azure Table Storage
//that you are going to be working with
return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);
}
}
}
}
- · In den Datenquellenklassen verwende ich einen etwas anderen Ansatz zum Herstellen der Verbindung mit Azure. In den meisten Beispielen, die ich im Internet gefunden habe, werden die Anmeldeinformationen mit einer Art Registrierungseinstellungsklasse ausgelesen (das ist nicht der genaue Name, ich kann mich nicht mehr genau erinnern). Das Problem dabei ist, dass ich keinen Azure-spezifischen Kontext habe, da meine Datenzugriffsklasse außerhalb Azure funktionieren soll. Daher erstelle ich stattdessen eine Einstellung in den Projekteigenschaften und schließe darin den Kontennamen und den Schlüssel ein, die zum Herstellen der Verbindung mit meinem Azure-Konto benötigt werden. Damit enthalten die beiden Datenquellenklassen Code wie den folgenden, um die Verbindung mit dem Azure-Speicher herzustellen:
private static CloudStorageAccount storageAccount;
private ClaimTypeDataContext context;
//static constructor so it only fires once
static ClaimTypesDataSource()
{
try
{
//get storage account connection info
string storeCon = Properties.Settings.Default.StorageAccount;
//extract account info
string[] conProps = storeCon.Split(";".ToCharArray());
string accountName = conProps[1].Substring(conProps[1].IndexOf("=") + 1);
string accountKey = conProps[2].Substring(conProps[2].IndexOf("=") + 1);
storageAccount = new CloudStorageAccount(new StorageCredentialsAccountAndKey(accountName, accountKey), true);
}
catch (Exception ex)
{
Trace.WriteLine("Error initializing ClaimTypesDataSource class: " + ex.Message);
throw;
}
}
//new constructor
public ClaimTypesDataSource()
{
try
{
this.context = new ClaimTypeDataContext(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);
this.context.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
Trace.WriteLine("Error constructing ClaimTypesDataSource class: " + ex.Message);
throw;
}
}
- · Die tatsächliche Implementierung der Datenquellenklassen enthält eine Methode zum Hinzufügen eines neuen Elements für einen Anspruchstyp und für einen eindeutigen Anspruchswert. Das ist ganz einfacher Code:
//add a new item
public bool AddClaimType(ClaimType newItem)
{
bool ret = true;
try
{
this.context.AddObject(ClaimTypeDataContext.CLAIM_TYPES_TABLE, newItem);
this.context.SaveChanges();
}
catch (Exception ex)
{
Trace.WriteLine("Error adding new claim type: " + ex.Message);
ret = false;
}
return ret;
}
Ein wichtiger Unterschied in der Add-Methode für die Datenquelle für eindeutige Anspruchswerte ist, dass sie weder einen Fehler auslöst noch false zurückgibt, wenn beim Speichern von Änderungen eine Ausnahme auftritt. Der Grund ist, dass ich bewusst damit rechne, dass Benutzer versehentlich oder warum auch immer mehrfach versuchen sich anzumelden. Sobald ihr E-Mail-Anspruch verzeichnet wurde, löst jeder weitere Versuch, ihn hinzuzufügen, eine Ausnahme aus. Da Azure nicht den Luxus stark typisierter Ausnahmen bietet und ich nicht möchte, dass sich das Ablaufprotokoll mit sinnlosem Zeug füllt, kümmere ich mich nicht um diese Situation.
- · Die Suche nach Ansprüchen ist insofern interessanter, als sie ebenfalls Dinge umfasst, die Sie in LINQ, aber nicht in LINQ mit Azure ausführen können. Ich füge den Code hier hinzu und erkläre dann einige Entscheidungen, die ich getroffen habe:
public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)
{
UniqueClaimValueCollection results = new UniqueClaimValueCollection();
UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();
const int CACHE_TTL = 10;
try
{
//look for the current set of claim values in cache
if (HttpRuntime.Cache[ClaimType] != null)
results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];
else
{
//not in cache so query Azure
//Azure doesn't support starts with, so pull all the data for the claim type
var values = from UniqueClaimValue cv in this.context.UniqueClaimValues
where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)
select cv;
//you have to assign it first to actually execute the query and return the results
results.UniqueClaimValues = values.ToList();
//store it in cache
HttpRuntime.Cache.Add(ClaimType, results, null,
DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,
System.Web.Caching.CacheItemPriority.Normal,