ASP.NET 4.5 auf IIS 7.5 mit SQL Server Express 2012 einrichten

Hier mein Vorgehen nach langem und frustrierendem Herumprobieren:

IIS vorbereiten

  1. Verzeichnis für App anlegen und App hineinkopieren
  2. App im IIS-Manager zum Beispiel als neue Site hinzufügen (mit eigenem AppPool)
  3. Anonyme Authentifizierung auf „AppPool-Identity“ festlegen
  4. AppPool-Grundeinstellungen auf .NET 4.0 (integriert) festlegen
  5. Erweiterte AppPool-Einstellungen auf „Benutzerprofil laden = True“ und „Identität = AppPool-Identity“ festlegen
  6. In der Kommandozeile folgende Befehle ausführen:
    icacls <app-path> /grant "IIS APPPOOL\<apppool-name>":(OI)(CI)(RX)
    icacls <app-path>\App_Data /grant "IIS APPPOOL\<apppool-name>":(OI)(CI)(F)

SQL Server vorbereiten

  1. Mit den folgenden Befehlen den SQL Server Express aktualisieren:
    CREATE LOGIN [IIS APPPOOL\<apppool-name>] FROM WINDOWS
    GRANT CREATE DATABASE TO [IIS APPPOOL\<apppool-name>]

Der passende Connection String

Beim Connection String kann es dann wieder etwas hakelig werden, aber der folgende Eintrag funktioniert ganz gut:
Data Source=.\SQLEXPRESS; Initial Catalog=<db-name>; Integrated Security=SSPI; AttachDBFilename=|DataDirectory|<db-name>.mdf; User Instance=true

IIS 7 + SQL Server Express 2008

Leider, leider scheint das bei dieser Kombo nicht so „einfach“ zu sein, denn mit „User Instance=true“ kommt es zu Impersonation-Fehlern und ohne fehlen die Rechte im App_Data-Verzeichnis…

Letzteres lässt sich aber korrigieren, indem man dem User „<host-name>\SQLServerMSSQLUser$<host-name>$SQLEXPRESS“ Vollzugriff auf App_Data gewährt – dann kann „User Instance“ komplett weggelassen werden.

WWW-Authenticate (Basic) in ASP.NET

Mit nur wenigen Zeilen lässt sich die Basic Authentication (http://de.wikipedia.org/wiki/HTTP-Authentifizierung) in C# umsetzen:

const String KEY = "BasicAuthKey";
const String Basic = "Basic ";

if (Session.Contents[KEY] == null)
{
    Response.BufferOutput = true;

    String auth = Request.Headers["Authorization"] ?? String.Empty;
    auth = System.Text.Encoding.UTF8.GetString(
        Convert.FromBase64String(auth.StartsWith(Basic)
        ? auth.Substring(Basic.Length) : String.Empty));
    if (auth == "username:password")
    {
        Session.Contents.Add(KEY, new Object());
    }
    else
    {
        Response.Clear();
        Response.Headers.Set(
            "WWW-Authenticate",
            "Basic realm=\"RegisteredUsers@domain.tld\"");
        Response.SetStatus(HttpStatusCode.Unauthorized);
        Response.End();
    }
}

Simple Web Server bereit zum Download

Es ist soweit, mein eigener kleiner Webserver ist fertig! Im Wesentlichen versteht er HTTP 1.1, kommt aber auch mit 0.9, 1.0 und ein wenig 2.0 klar. Der Clou ist seine Einfachheit:

  • Keine Installation
  • Einfache Konfiguration von mehreren ASP.NET-Anwendungen
  • Starten per Doppelklick (optional als Windows-Dienst installierbar)

Hier geht es zur Produktseite!

ASP.NET-Runtime in der eigenen Applikation aufrufen

Nachdem die ASP.NET-Runtime in die eigene Applikation eingebunden wurde, können nun Aufrufe an diese weitergeleitet werden. Dazu ist „lediglich“ ein Aufruf der Methode HttpRuntime.ProcessRequest() nötig. Damit dieser aber durchgeführt werden kann, muss eine Implementierung der abstrakten Klasse System.Web.HttpWorkerRequest her. .NET bietet mit System.Web.Hosting.SimpleWorkerRequest eine solche Basisimplementierung, die hier erst einmal ausreichen soll – einfach den Konstruktor mit den drei Parametern aufrufen und das neue Objekt übergeben:

HttpWorkerRequest workerRequest = new SimpleWorkerRequest(page, query, output);
HttpRuntime.ProcessRequest(workerRequest);

Bei dem Page-Parameter darf kein führender Slash angegeben werden, während beim Query-Parameter das führende Fragezeichen weggelassen werden muss. Für einfachste Anwendungen kann der Output-Parameter eine Instanz vom Typ System.IO.StringWriter aufnehmen.

Ein kompletter Aufruf könnte dann im „client-seitigen“ Teil der Anwendung wie folgt aussehen:

MyApplicationHost myHost = MyApplicationHost.CreateWebHost(
    "/", Path.Combine(Environment.CurrentDirectory, "SampleWebApp"));
using (StringWriter writer = new StringWriter())
{
    myHost.ProcessRequest("Default.aspx", "q=Where are you?&a=I am here!", writer);
    EmbeddedWebBrowser.DocumentText = writer.ToString();
}

EmbeddedWebBrowser stellt dabei eine Instanz des System.Windows.Forms.WebBrowser-Controls dar.

ASP.NET-Runtime in die eigene Applikation einbinden

Im Laufe der letzten Jahre habe ich des Öfteren die ASP.NET-Runtime in Desktop-Applikationen integriert, sei es um ein komplexes HTML-Reporting umzusetzen oder um eine .NET 2.0-Anwendung Web Services veröffentlichen zu lassen. Nicht zuletzt der Wunsch nach einem kleinen, „eigenen Webserver“ hat meine Beschäftigung mit dem Thema vorangetrieben. Im Folgenden will ich kurz dokumentieren, was sich bis heute als meine „Best Practice“-Lösung herauskristallisiert hat.

Die wichtigste Erkenntnis war für mich, dass die Runtime in eine eigene AppDomain geladen werden muss und so von der restlichen Anwendung isoliert wird – das hat Vor- und Nachteile (bspw. kann die Runtime-AppDomain nach Gebrauch einfach entladen werden, auf der anderen Seite müssen Daten aus der Anwendung über eine AppDomain-Grenze transportiert werden, um im „Web-Bereich“ genutzt werden zu können). Es gibt zwar Mittel und Wege eine vorhandene AppDomain so zu manipulieren, dass sie die Runtime aufnehmen und ausführen kann, aber in der Praxis hat sich dies für mich nicht als sinnvoll erwiesen.

Laden der ASP.NET-Runtime

Das Laden der ASP.NET-Runtime beginnt mit der Klasse ApplicationManager aus dem Namespace System.Web.Hosting:

ApplicationManager appManager = ApplicationManager.GetApplicationManager();

Ist der Rückgabewert ungleich null,  kann in diesem Prozess mit dem Laden einer Ausführungsumgebung für eine Webanwendung begonnen werden.

Jetzt ist es prinzipiell nicht mehr schwer, einen physischen Pfad (das Basisverzeichnis der Webanwendung auf der Festplatte) auf einen virtuellen (der Pfad hinter der Adresse des Servers, z.B. http://domain.tld/virtual) abzubilden – dazu muss lediglich der Typ eine Objektes her, der IRegisteredObject implementiert, und mit dessen Instanz man später kommunizieren möchte, um die Runtime die gewünschten Aufgaben erledigen zu lassen. Sinnvollerweise sollte dieser Typ von MarshalByRefObject abgeleitet sein, da die Runtime in einer anderen AppDomain ausgeführt wird (siehe oben). Es besteht jetzt allerdings das Problem, dass dieser Typ sowohl in der aktuellen AppDomain, als auch in der ASP.NET-AppDomain bekannt sein muss, sein Assembly also in beiden AppDomains geladen ist/geladen werden kann. Dies ließe sich zum Beispiel dadurch bewerkstelligen, dass die entsprechende Assembly im bin-Verzeichnis der auszuführenden Webanwendung abgelegt wird oder aber jetzt hineinkopiert wird. Das finde ich allerdings wenig elegant und glücklicherweise gibt es einen anderen Weg:  System.Web.Compilation.BuildManagerHost. Dieser Typ wird nicht exportiert und kann deswegen nicht direkt zugegriffen werden. Da er aber Teil der Runtime ist, kann diese auf ihn zugreifen und ihn instanzieren:

Type buildManagerHostType = typeof(HttpRuntime).Assembly.GetType(
    "System.Web.Compilation.BuildManagerHost");
Object buildManagerHost = appManager.CreateObject(
    appId,
    buildManagerHostType,
    virtualPath,
    physicalPath,
    failIfExists);

Dabei bezeichnet die appId die auszuführende Anwendung eindeutig, man könnte also der Einfachheit halber bspw. eine Verknüpfung aus physischem und virtuellem Pfad als appId verwenden. Der Parameter failIfExists gibt desweiteren an, wie damit umgegangen werden soll, wenn das gewünschte Objekt schon existiert: bei true wird eine Ausnahme ausgelöst und bei false das bereits existierende Objekt zurückgegeben.

Die AppDomain ist nun bereit, die Runtime geladen…

Injizieren eigener Assemblies in die ASP.NET-Laufzeitumgebung

Dabei hilft nun die Methode RegisterAssembly() des zuvor registrierten BuildManagerHost:

Assembly myAssembly = typeof(MyApplicationHost).Assembly;
buildManagerHostType.InvokeMember(
    "RegisterAssembly",
    BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.NonPublic,
    null,
    buildManagerHost,
    new Object[] { myAssembly.FullName, myAssembly.Location });

Der Typ MyApplicationHost ist nun die oben erwähnte Klasse, die uns später das Steuern der Runtime ermöglichen wird. Diese Klasse sollte von MarshalByRefObject abgeleitet sein und muss IRegisteredObject implementieren.

MyApplicationHost myHost = (MyApplicationHost)appManager.CreateObject(
    appId,
    typeof(MyApplicationHost),
    virtualPath,
    physicalPath,
    failIfExists);

In einem Folgeartikel zeige ich dann, wie nun Aufrufe an die Runtime durchgeführt werden.

Die verschiedenen ASP.NET Tags

Die folgenden ASP.NET Tags habe ich im Laufe der Zeit kennengelernt. Da es hin und wieder schwer fällt, die passende Doku zu finden, liste ich sie hier kurz auf.

<%@ … %> kennzeichnet ASP.NET-Direktiven
<% … %> umschließt einen serverseitig ausgeführten Code-Abschnitt
<%= … %> dient der direkten Ausgabe eines Wertes, der nicht weiter kodiert wird
<%: … %> dient der direkten Ausgabe eines Wertes, der durch HtmlEncode() kodiert wird
<%# … %> kommt bei der Datenbindung zum Einsatz und ermöglicht unter anderem den Einsatz der Eval-Funktion
<%– … –%> bietet die Möglichkeit serverseitiger Kommentierung
<%$ … %> umschließt einen ASP.NET Ausdruck, der Zugriff auf AppSettings, ConnectionStrings oder Resources erlaubt