David Fowler vom ASP.NET-Produktteam hat vor einigen Tagen ein Beispiel [1] online gestellt, das zeigt, wie man ASP.NET 5 bzw. ASP.NET MVC 6 im Self-Hosting-Modus betreiben kann. Die bis dato bekannten Beispiele zu diesem Thema verwendeten eine ASP.NET-5-Anwendung, die unter Verwendung von Kommandozeilen-Parameter zu starten war. Die Beispiele von David zeigen hingegen, wie man mit Microsofts Entwicklungs-Web-Server Kestrel mehr Kontrolle über Self-Hosting bekommt, indem man den Self-Host bei Bedarf in einer eigenen Anwendung startet. Somit könnte man den Self-Host in einem Windows-Dienst oder innerhalb einer Windows-Anwendung starten.
Ich habe das Beispiel von David ein wenig erweitert, sodass es zeigt, wie man eine Startup-Klasse, die die gewünschten Middleware-Komponenten aufsetzt und Services bereitstellt, einbinden kann. Das ist zum einen nützlich, wenn man eine Anwendung sowohl via Self-Host als auch in IIS hosten möchte und hilft zum anderen zu verstehen, was beim Hosting in IIS tatsächlich passiert, wenn hier ASP.NET 5 bzw. ASP.NET MVC 6 hochfährt. Das ganze versehe ich hier zur Erklärung auch mit einigen zusätzlichen Anmerkungen.
Das hier betrachtete Beispiel besteht aus einer ausführbaren Program-Klasse, welche via Constructor-Injection einen IServiceProvider sowie einen ILibraryManager injiziert bekommt. Ersterer gibt via DI Zugriff auf alle standardmäßig vorherrschenden Services; letzterer veröffentlicht Informationen über sämtliche eingebundenen Bibliotheken (NuGet-Pakete).
public class Program
{
private readonly IServiceProvider _serviceProvider;
private readonly ILibraryManager _libraryManager;
public Program(IServiceProvider serviceProvider, ILibraryManager libraryManager)
{
_serviceProvider = serviceProvider;
_libraryManager = libraryManager;
}
public void Main(string[] args) { … }
Interessant wird es dann in der Main. Meine Variation prüft, ob es Kommandozeilenparameter gibt. Falls dem nicht so ist, geht sie von standardmäßigen Angaben, die die Nutzung von Port 5000 bewirken, aus. Die Kommandozeilenparameter werden in ein neues Konfigurationsobjekt mit AddCommandLine aufgenommen.
// 1. Kommandozeilenargumente auslesen
if (args.Length == 0)
{
args = new string[] { "--server.urls", "http://localhost:5000" };
}
var config = new Configuration().AddCommandLine(args);
Dann wird ein HostingEnvironment erzeugt. Damit kann u. a. die Startup-Klasse prüfen, in welcher Umgebung die Web-Anwendung läuft. So könnte man zum Beispiel in einer Test-Umgebung zusätzliche Middleware-Komponenten aufnehmen oder die verwendeten Middleware-Komponenten anders konfigurieren. Das HostingEnvironment benötigt eine Referenz auf das aktuelle IApplicationEnvironment. Dieses gibt Auskunft über die aktuelle Programmausführung und kann unter anderem genutzt werden, um den Anwendungs-Ordner zu ermitteln. Um an dieses IApplicationEnvironment zu kommen, nutzt die betrachtete Anwendung den Service-Provider. Schließlich bekommt das erzeugte Hosting-Environment auch noch einen Namen (EnvironmentName).
// 2. Create Hosting-Environment
var appEnv = _serviceProvider.GetService<IApplicationEnvironment>();
var env = new HostingEnvironment(appEnv, new IConfigureHostingEnvironment[] { });
env.EnvironmentName = "Development - SelfHost";
Anschließend wird die Startup-Klasse instanziiert. Im Gegensatz zum Hosting im IIS muss hier die Klasse nicht zur Laufzeit ermittelt werden, sondern kommt direkt zum Einsatz.
Die Methode HostingServices.Create erzeugt anschließend unter Verwendung des ServiceProviders mit den standardmäßig vorherschenden Services eine ServiceCollection, die darauf basiert. Der Unterschied zwischen ServiceProvider und ServiceCollection ist, dass ersterer zum Abrufen von Services und letztere zum Definieren von Services genutzt werden kann. Unterm Strich bewirkt dieses Vorgehen, dass man nun eine ServiceCollection hat, die zum einen die Services aus dem ServiceProvider beschreibt und zum anderen um weitere Services erweitert werden kann.
Danach wird eine LoggerFactory erzeugt und zur Vereinfachung dieser Demo der ConsoleLogger registriert. Somit kann ab sofort auf die Konsole geloggt werden. Diese LoggerFactory wird auch als Service registriert, sodass jede Klasse der gehosteten Anwendung darauf via DI Zugriff bekommt und damit Logger erzeugen kann.
Im Anschluss wird die Methode Startup.ConfigureServices aufgerufen. Diese hat die Aufgabe, jene Services, die die gehostete Anwendung benötigt, zu registrieren. Zu diesem Zweck bekommt sie die ServiceCollection übergeben. Über den zweiten Parameter erhält sie den Logger. Bei der aktuellen Implementierung für das Hosting in IIS ist dieser zweite Parameter optional, d. H. es liegt am Entwickler, ob er der Funktion ConfigureServices diesen zweiten Parameter spendiert. Hier wird zur Vereinfachung davon ausgegangen, dass er einrichtet wurde.
Nach dem Aufruf von SonfigureServices liegt die konfigurierte ServiceCollection vor. Damit die davon beschriebenen Services abgerufen werden können, wird daraus mit BuildServiceProvider ein ServiceProvider erstellt.
// 3. Create Startup-Object and init Services
var startup = new Startup(env);
var services = HostingServices.Create(_serviceProvider);
var loggerFactory = new LoggerFactory();
loggerFactory.AddConsole();
services.AddInstance<ILoggerFactory>(loggerFactory);
startup.ConfigureServices(services, loggerFactory);
var serviceProvider = services.BuildServiceProvider();
Danach wird der ApplicationBuilder erzeugt. Er bekommt den ServiceProvider übergeben. Konfiguriert wird er durch die Methode Configure der Startup-Klasse, welche zusätzlich auch das aktuelle HostingEnvironment bekommt. Configure ist bekanntlichermaßen dafür verantwortlich, die gewünschten Middleware-Komponenten zu registrieren.
Danach erzeugt die Methode Build vom ApplicationBuilder das sogenannte requestDelegate. Das ist eine Methode, die die gesamte durch den ApplicationBuilder beschriebene Pipeline repräsentiert. HTTP-Anfragen können an diese Methode übergeben werden und werden von der Pipeline bearbeitet. Das Ergebnis dieses Unterfangens ist der Rückgabewert von requestDelegate.
// 4. Init Pipeline
var app = new ApplicationBuilder(serviceProvider);
startup.Configure(app, env);
var requestDelegate = app.Build();
Jetzt muss nur noch der Web-Server gestartet und jede an ihn gerichtete Anfrage durch die Pipeline gejagt werden. Das geschieht über die ServerFactory, welche den LibraryManager benötigt, um auf referenzierte Bibliotheken zugreifen zu können. Initialize erzeugt ein Objekt, dass den Server beschreibt. Dazu wird das Konfigurationsobjekt mit dem gewünschten Port übergeben.
Factory.Start startet den Server und nimmt neben dem Info-Objekt einen Lambda-Ausdruck entgegen. Dieser wird für jede empfangene Abfrage ausgeführt. Das übergebene Objekt vom Typ ServerRequest beschreibt dabei die HTTP-Anfrage. Da die Pipeline bzw. der requestDelegate die Anfrage als Instanz von FeatureCollection erwartet, verpacken die vier nächsten Zeilen das ServerRequest-Objekt in solch einer FeatureCollection-Instanz. Diese wird dann an das requestDelegate übergeben, welche die Anfrage abarbeitet und eine HTTP-Antwort retourniert. Diese liefert den Lambda-Ausdruck retour, sodass der Web-Server sie zum Aufrufer zurücksenden kann.
// 5. Start Server
var factory = new ServerFactory(_libraryManager);
var info = factory.Initialize(config);
factory.Start(info, obj => {
var request = obj as ServerRequest;
FeatureCollection coll = new FeatureCollection();
coll.Add(typeof(IHttpRequestFeature), request);
coll.Add(typeof(IHttpResponseFeature), request);
coll.Add(typeof(IHttpUpgradeFeature), request);
var context = new DefaultHttpContext(coll);
return requestDelegate(context);
});
Console.WriteLine("Started ...");
Console.ReadLine();
Das gesamte Beispiel findet man unter [2].
[1] https://github.com/davidfowl/HostingOptions/blob/master/src/RawWeb/Program.cs
[2] https://www.dropbox.com/s/pdumci9psjp3csb/FlugDemo.zip?dl=0