SelfHosted ASP.NET Core 2.0 Application

Robert Meyer 22 Sep 2017 .NET Core .NET Standard Autofac NLog SelfHosted Website ServerFabric Windows Service permalink

Auf Grund einer Projektanforderung musste ich mich die Tage damit auseinandersetzen, wie man eine ASP.NET Core 2.0 Website und Web API in einem eigenen Prozess als Windows Service bereitstellt.

Hierbei gibt es einige Besonderheiten zu beachten, auf welche ich in diesem Post gern näher eingehen möchte.

Folgende Technologien kommen zum Einsatz:

  • Alle Projekte basieren auf .NET Core 2.0 oder .NET Standard 2.0
  • Autofac als IoC Container
  • NLog für das Logging
  • Peter Kottas WindowsService als ServerFabric (GitHub)
Grundlegende Architektur der Anwendung

Architecture

Zu finden ist dieses Beispiel auf GitHub.

Grundlegendes Vorgehen

Die Konsolenapplikation kann als WindowsService oder direkt ausgeführt werden und hostet die Website und die API. Die Website und die API sind beides .NETStandard 2.0 Projekte, während die Console eine .NET Core 2.0 Anwendung darstellt.

Aufbau der SelfHosted.Console

IApplication definiert ein Interface zum Starten und Stoppen der Applikationen. Eine Applikation kann eine Website oder eine WebApi sein.

IApplication

Diese Applikationen werden in Autofac registriert und einem gekapselten MicroService geladen und gestartet.

public class SelfHostedWindowService : MicroService, IMicroService
{
	private IServiceProvider _provider;

	public void Start()
	{
		this.StartBase();

		var builder = new ContainerBuilder();
		builder.RegisterType<WebApplication>().As<IApplication>();
		builder.RegisterType<WebApiApplication>().As<IApplication>();
		var applicationContainer = builder.Build();

		_provider = new AutofacServiceProvider(applicationContainer);

		foreach (var app in _provider.GetServices<IApplication>())
		{
			app.Start();
		}

		System.Console.WriteLine("Windows services started.");
	}

	public void Stop()
	{
		this.StopBase();

		foreach (var app in _provider.GetServices<IApplication>())
		{
			app.Stop();
		}

		System.Console.WriteLine("Windows services stopped.");
	}
}

Beim Starten der Konsole oder des Services, wird der MicroService registriert und in einer ServiceFactory geladen. Dadurch starten alle Applikationen, welche in den jeweiligen MicroService definiert sind.

ServiceRunner<SelfHostedWindowService>.Run(config =>
{
	var serviceName = "SelfHosted.WindowsService";

	config.SetName(serviceName);
	config.Service(serviceConfig =>
	{
		serviceConfig.ServiceFactory((service, extraArguments) =>
		{
			return new SelfHostedWindowService();
		});

		serviceConfig.OnStart((service, extraArguments) =>
		{
			System.Console.WriteLine("Service {0} started", serviceName);
			service.Start();
		});

		serviceConfig.OnStop((service) =>
		{
			System.Console.WriteLine("Service {0} stopped", serviceName);
			service.Stop();
		});

		serviceConfig.OnError(e =>
		{
			System.Console.WriteLine($"Service '{serviceName}' errored with exception : {e.Message}");
		});
	});
});
Besonderheiten in der ASP.NET Core 2.0 Website

Es gibt jedoch beim hosten seiner ASP.NET Core 2.0 Website in einer Console noch drei wichtige Dinge zu beachten.

  1. Alle Views müssen Embedded werden. Dafür habe ich folgende Extension Methode geschrieben, welche im Startup bei AddRazorOptions aufgerufen wird.
public static RazorViewEngineOptions AddViews(this RazorViewEngineOptions options)
{
	options.FileProviders.Add(new EmbeddedFileProvider(typeof(ServiceCollectionExtensions).GetTypeInfo().Assembly, "SelfHosted.Website"));
	return options;
}

2. Danach stellte sich heraus, dass noch einige Assemblies fehlten. Diese werden ebenfalls per Extension Methode im Startup geladen.

public static RazorViewEngineOptions AddCompilationAssemblies(this RazorViewEngineOptions options)
{
	var myAssemblies = AppDomain
	.CurrentDomain
	.GetAssemblies()
	.Where(x => !x.IsDynamic)
	.Concat(new[] { // additional assemblies used in Razor pages:
		typeof(HtmlString).Assembly, // Microsoft.AspNetCore.Html.Abstractions
		typeof(IViewLocalizer).Assembly, // Microsoft.AspNetCore.Mvc.Localization
		typeof(IRequestCultureFeature).Assembly // Microsoft.AspNetCore.Localization
	})
	.Select(x => MetadataReference.CreateFromFile(x.Location))
	.ToArray();

	var previous = options.CompilationCallback;

	options.CompilationCallback = context =>
	{
		previous?.Invoke(context);
		context.Compilation = context.Compilation.AddReferences(myAssemblies);
	};

	return options;
}
  1. Jetzt müssen noch alle statischen Files, welche z.B. im wwwroot liegen embedded werden. Auch hier gibt es wieder eine passende Extension Methode.
public static IServiceCollection AddStaticFiles(this IServiceCollection collection)
{
	// static files are embedded resources in the "wwwroot" folder
	collection.Configure<StaticFileOptions>(options =>
	{
		options.FileProvider = new EmbeddedFileProvider(typeof(Startup).Assembly, typeof(Startup).Namespace + ".wwwroot");
	});
	return collection;
}

Aufgerufen werden die Extension Methods im Startup der Website, in der Funktion ConfigureServices wie folgt:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
	services.AddSingleton<IConfiguration>(Configuration);
	services.AddMvc()
	.AddRazorOptions(options =>
	{
		options.AddViews();
		options.AddCompilationAssemblies();
	});
	services.AddStaticFiles();

	var builder = new ContainerBuilder();
	builder.Populate(services);
	this.ApplicationContainer = builder.Build();

	return new AutofacServiceProvider(this.ApplicationContainer);
}
Zusammenfassung

Auf diesem Weg erreicht man eine sehr leichtgewichtige Anwendung, welche komplett in einem eigenen Prozess unabhängig von dem Betriebssystem und Webserver installiert werden kann. Somit erreicht man bei seiner Produktentwicklung, welchen bei Kunden vor Ort installiert werden muss, eine sehr hohe Flexibilität und ich unabhängig der Umgebung.