Vorsicht beim Öffnen von SiteCollections bei mehreren AAM‘s

Ich musste heute feststellen, dass das Öffnen von SiteCollections über die Site ID nicht immer die beste Lösung ist. Natürlich ist es die schnellste, das bleibt mir als Entwickler erstmal im Gedächtnis. Nur manchmal ist es nicht möglich oder sinnvoll eine SiteCollection über die ID zu öffnen.
Doch erstmal zum Problem…

Bei mir trat ein Problem beim Arbeiten mit Bilder und Links auf. Links haben auf eine falsche URL gezeigt und Bilder wurden nicht geladen, auch wegen einer falschen URL.
Den Bösewicht, der das Problem verursacht hat, habe ich schnell gefunden. Das Portal hat mehrere AAM’s (Alternate Access Mappings) zum Zugriff auf die SharePoint-Seite zu Verfügung gestellt. Eine URL für den internen Zugriff und eine URL für den öffentlichen Zugriff.

Auf dem Portal gibt es eine „Über“-SiteCollection, die den Einstiegspunkt auf die Website markiert. Diese SiteCollection enthält Listen mit allgemeinen Daten die auch in Sub-SiteCollection verfügbar sein müssen. Dahinter können beliebige Teamsites liegen. Alle Teamsites brauchen Infos aus der „Über“-SiteCollection. Damit dieser Zugriff einfacher wird habe ich in dem Projekte einen Wrapper gebaut, der einen Context zu der „Über“-SiteCollection aufbaut und diese öffnet. Und wie schon oben erwähnt natürlich über IDs.

Portalstruktur visualisiert

Portalstruktur visualisiert


public class UeberContext : IDisposable
{
	private readonly SPContext _spContext;
	private SPSite _uSite;
	private SPWeb _uWeb;

	private UeberContext(SPContext spContext)
	{
		_spContext = spContext;
	}

	private static UeberContext Instance
	{
		get
		{
			var ueberContext = HttpContext.Current.Items["UeberSiteInstance"] as UeberContext;
			if (ueberContext == null)
			{
				var spContext = SPContext.Current;
				var instance = new UeberContext(spContext);
				HttpContext.Current.Items["UeberSiteInstance"] = instance;
				return instance;
			}
			return ueberContext;
		}
	}

	public static UeberContext Current
	{
		get { return Instance; }
	}

	public SPSite Site
	{
		get
		{
			var site = _spContext.Site;
			var rootWeb = site.RootWeb;
			if (IsMasterSite(rootWeb))
				return site;
			else
			{
				var siteID = new Guid(rootWeb.Properties["UeberSiteID"]);
				_uSite = new SPSite(siteID);
				return _uSite;
			}
		}
	}

	public SPWeb Web
	{
		get
		{
			var site = _spContext.Site;
			var rootWeb = site.RootWeb;
			if (IsMasterSite(rootWeb))
				return rootWeb;
			else
			{
				var webID = new Guid(rootWeb.Properties["UeberWebID"]);
				_uWeb = Site.OpenWeb(webID);
				return _uWeb;
			}
		}
	}

	private bool IsMasterSite(SPWeb supposedWeb)
	{
		// Check if the supposedWeb is already the rootweb of the Über sitecollection
		// simplified it's always true for demo code
		var _isMasterSite = true;
		return _isMasterSite;
	}

	public void Dispose()
	{
		if (_uWeb != null)
			_uWeb.Dispose();
		if (_uSite != null)
			_uSite.Dispose();
	}
}

Wie aus dem Code hervorgeht, speichere ich die IDs zur „Über“-SiteCollection im PropertyBag des RootWebs der aktuell besuchten SiteCollection. Ist die aktuelle SiteCollection schon die „Über“-SiteCollection, gebe ich das SPWeb-Objekt und das SPSite-Objekt des SharePoint-Context’s zurück. Wenn nicht wird die „Über“-SiteCollection geöffnet.

Nun wieder zurück zu dem Problem mit den multiplen AAM’s…

Da die „Über“-SiteCollection über die ID geöffnet wird, gibt diese immer den Context des Default AAM zurück. Das bedeutet es wird der Default AAM benutzt obwohl die Seite über den Internet AAM besucht wird.

Ein Beispiel:
Ich habe 2 AAM’s in meiner WebApplication konfiguriert .Einen Default und einen für Internet.
    Default = http://intern.dlindemann.de
    Internet = http://extern.dlindemann.de

Besucht wird eine Sub-SiteCollection über den Internet AAM http://extern.dlindemann.de/meine-subsite. Auf dieser Seite befindet sich ein WebPart, der Daten aus einer Liste in der „Über“-SiteCollection unter http://extern.dlindemann.de abrufen soll. Die IDs zur „Über“-SiteCollection stehen im PropertyBag der Sub-SiteCollection und ich kann mit oben beschriebenem UeberContext auf die „Über“-SiteCollection zugreifen (Variable ueberWeb).

var ueberWeb = UeberContext.Current.Web;
SPList list = ueberWeb.GetList("/lists/einfacheListe");
// ... do something with the list

Das funktioniert soweit ganz gut.
Doch nun enthält diese Liste ein URL-Feld zum verlinken von Bildern, die ich gerne mit dem WebPart ausgeben möchte. Egal ob das URL Feld mit einer vollen URL oder einer server-relativen URL befüllt wurde, SharePoint speichert diese Bilder-URLs immer server-relativ ab, wenn sie innerhalb derselben SharePoint-WebApplication liegen. Externe Bilder werden natürlich mit vollem Pfad gespeichert.

Liest man nun ein Bild aus, wird die URL von SharePoint zu einer vollwertigen URL umgewandelt und das im Context des geöffneten SPSite-Objekts.
Mein Bild mit der URL /SiteCollecionImages/mein-bild.png wird zu http://intern.dlindemann.de/SiteCollectionImages/mein-Bild.png obwohl ich die Website über die URL http://extern.dlindemann.de besuche. Denn mein UeberContext der die SiteCollection mittels ID öffnet gibt mir die URL des Default AAM’s zurück.
Die Folge davon ist, dass das Bild nicht geladen werden kann (HTTP Status 502 Bad Gateway).

var imageItem = list.GetItemById(1);
var imageItemField = item.Fields.GetFieldByInternalName("MyImage");
string fieldValue = item[imageItemField.Id].ToString();
var imageFieldValue = new SPFieldUrlValue(fieldValue);
this.Controls.Add(new Image() { ImageUrl = imageFieldValue.Url });

Die Lösung um das Problem zu beheben ist die URL des aktuellen Context (hier http://extern.dlindemann.de/meine-subsite) zu benutzen, was viel URL zerschneiden mit sich bringt. Oder man benutzt die Methode RebaseUriWithAlternateUri um an die URL des richtigen AAM’s heranzukommen statt die ID zu benutzen. Benutzt man die URL um das SPSite-Objekt zu öffnen dauert es ein paar Millisekunden länger. Doch dafür hat Context garantiert die richtige URL. Benutzt man die Methode RebaseUriWithAlternateUri dauert es nochmal etwas länger und man muss auf die Farm zugreifen, deshalb sollte man bei dieser Methode den AAM Zwischenspeichern.

public SPSite Site
{
	get
	{
		var site = _spContext.Site;
		var rootWeb = site.RootWeb;
		if (IsMasterSite(rootWeb))
			return _spContext.Site;
		else
		{
			var ueberSiteUrl = rootWeb.Properties["UeberSiteUrl"].ToString();

			// getting current aam        
			var rootWebUri = new Uri(rootWeb.Url, UriKind.Absolute);
			var webAppUri = new Uri(rootWebUri.GetLeftPart(UriPartial.Authority));
			SPAlternateUrl currentAam = null;
			SPSecurity.RunWithElevatedPrivileges(() =>
			{
				currentAam = SPFarm.Local.AlternateUrlCollections.LookupAlternateUrl(webAppUri);
			});

			// get url for site    
			var ueberSiteUri = SPFarm.Local.AlternateUrlCollections.RebaseUriWithAlternateUri(new Uri(ueberSiteUrl), currentAam.UrlZone);
			_uSite = new SPSite(ueberSiteUri.ToString());

			// open site with correct url    
			return _uSite;
		}
	}
}

Durch diesen Code wird aus der URL http://intern.dlindemann.de die in dem PropertyBag unter UeberSiteUrl gespeichert wurde automatisch http://extern.dlindemann.de wenn die Seite über den Internet AAM besucht wird.

Ich weiß, der Code gewinnt keinen Schönheitswettbewerb 🙂 . Das liegt daran, dass alles stark vereinfacht ist und kein Caching benutzt wird. Ich hoffe ich kann damit trotzdem die Problematik von URLs und verschiedenen AMM’s aufzeigen.

Happy Coding!