Shuffleco.de

Lost in shuffled code

May 15, 2017 - 5 minute read - .Net C#

Self hosted Services mit Katana

Ich bin ja ein Fan von REST Apis, Ich mag die Lesbarkeit der Aufrufe und die Tatsache das man keinen State verwaltet. Ich kann diverse Api calls schnell mit Postman testen. An der Microsoft Implementierung MVC REST Api mag ich zudem, das es eine klare Trennung der Verantwortlichkeiten zumindest unterstützt.

Das Mittel der Wahl um leichtgewichtige RESTful Apis auf einem Windows System zu hosten ist meiner Meinung nach derzeit AspNet Katana, das OWIN basierte Dienste bauen und ohne IIS Hosten kann.

Wie ist das ganze einzurichten?

Zunächst einmal habe ich mir als Basis einen das Windows Service Template hergenommen:

Projekt anlegen

Die Klasse Program wird den Service instanzieren.

Program.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static void Main()
    {
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[]
        {
            new OwinListener()
        };
        ServiceBase.Run(ServicesToRun);
    }
}

als nächstes Installieren wir das Owin.Selfhost Package über den Nuget Paketmanager.

1
PM> Install-Package Microsoft.Owin.SelfHost 

OwinListener wird von ServiceBase abgeleitet und überschreibt die Lifecycle Methoden OnStart() und OnStop() um Setup und TearDown Aufgaben auszuführen. In meinem Fall verwende ich Sie um die WebApi zu initialisieren. Diese Methoden werden dann später vom ServiceControlManager aufgerufen um den Service zu steuern.

OwinListener.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public partial class OwinListener : ServiceBase
{
    private IDisposable _service;
    private ILogger _logger;

    public OwinListener()
    {
        InitializeComponent();
    }

    protected override void OnStart(string[] args)
    {
    	StartApi();
    }

    private void StartApi()
    {
    	var options = new StartOptions();
    	options.Urls.Add("http://localhost:9000");
    	options.Urls.Add("http://127.0.0.1:9000");
    	options.Urls.Add($"http://{Environment.MachineName}:9000");
    	try
    	{
    	    _service = WebApp.Start<OwinStartUp>(options);
    	}
    	catch (Exception ex)
    	{
            _logger.Log(ex);
            throw;
    	}
    }

    protected override void OnStop()
    {
        _service.Dispose();
    }
    
    /// <summary>
    /// Klasse für lokales debuggen (unter Projekteigenschaften als Startobjekt festlegen)
    /// </summary>
    public class LocalDebug
    {
        static void Main()
        {
            var myService = new OwinListener();
            while (true)
            {
                myService.OnStart(null);
                System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
                break;
            }
        }
    }
}

Die WebApi Help Pages von Microsoft funktionieren derzeit noch nicht mit Katana, ich habe aber hier DalSoft Help Pages ein Projekt gefunden, das hervorragend mit Katana funktioniert, und wie im Original Razor Templates für die Api Dokumentation verwendet.

1
PM> Install-Package DalSoft.WebApi.HelpPage 

Für das Exception Handling verwende ich einfach eine winzige Middleware: Hier kommen nur die Ausnahmen an, die weiter unten im Stack auftreten.

GlobalExceptionMiddleware.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class GlobalExceptionMiddleware : OwinMiddleware
{
    public GlobalExceptionMiddleware(OwinMiddleware next) : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context)
    {
        try
        {
            await Next.Invoke(context);
        }
        catch (Exception ex)
        {
            //Todo: Handle Exceptions der nachfolgenden Komponenten
        }
    }
}

Für die Authentifizierung habe ich zu demozwecken eine BasicAuth implementiert, auch Asp.Net Identity oder Active Directory Auth ist hier natürlich denkbar.

AuthenticationMiddleware.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class AuthenticationMiddleware : OwinMiddleware
{
    public AuthenticationMiddleware(OwinMiddleware next) : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context)
    {
        var request = context.Request;
        var response = context.Response;

        response.OnSendingHeaders(state => {
            var resp = (OwinResponse)state;
            if (resp.StatusCode == 401)
                resp.Headers.Set("WWW-Authenticate", "Basic");
        }, response);
        
        var header = request.Headers.Get("Authorization");

        if (!string.IsNullOrWhiteSpace(header))
        {
            var authHeader = System.Net.Http.Headers.AuthenticationHeaderValue.Parse(header);

            if ("Basic".Equals(authHeader.Scheme,StringComparison.OrdinalIgnoreCase))
            {
                string parameter = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter));
                var parts = parameter.Split(':');

                string userName = parts[0];
                string password = parts[1];

                if (userName == "bar")
                {
                    if (password == "foo")
                    {
                        var claims = new[] { new Claim(ClaimTypes.Name, "bar") };
                        var identity = new ClaimsIdentity(claims, "Basic");
                        request.User = new ClaimsPrincipal(identity);
                    }
                }
            }
        }
        
        await Next.Invoke(context);
    }
}

Füge alles zusammen

die Klasse OwinStartUp enthält nun den Code der die Middleware einbindet. Unsere Middlewarekomponenten sind nun also:

  • WebApi
  • Help Pages
  • Exception Handling
  • Authentication

Die Reihenfolge, in der die Komponenten eingebunden werden, ist wichtig. Schließlich wollen wir zum Beispiel zuerst sicherstellen das der Nutzer Authentifiziert ist, bevor er API Controller Methoden aufrufen kann. Die Default-Konfiguration gleicht der Standard WepApi Config und enthält ein Routentemplate, das greift wenn nicht mit AttributeRouting in den API Controllern gearbeitet wird. Ich bevorzuge allerdings das explizite setzen von Routen im Controller und wende daher zusätzlich

config.MapHttpAttributeRoutes();

in der Konfiguration an.

OwinStartup.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class OwinStartUp
{
    public void Configuration(IAppBuilder appBuilder)
    {
        //Web API 
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "{controller}/{id}",
            defaults: new {id = RouteParameter.Optional}
        );
        
        //Exception handling
        appBuilder.Use(typeof(GlobalExceptionMiddleware));

        //help pages
        appBuilder.UseWebApiHelpPage(config);

        //auth Middleware einbinden
        appBuilder.Use(typeof(AuthenticationMiddleware));
        config.EnsureInitialized();

        //Reihenfolge hier wichtig! (auth vor Api) 
        appBuilder.UseWebApi(config);
    }        
}

Sind diese Schritte abgeschlossen, kann es mit der Implementierung der API Controller weitergehen, Wie vorher beschrieben bevorzuge ich das Attribute Routing.

Über jeder Methode gebe ich also an auf welches HTTP Verb Sie reagieren soll. Im Beispiel 2 einfache Methoden, die bei einem Http POST an die URLs

http://<localhost:9000/owin/Start

http://<localhost:9000/owin/Stop

reagieren, und ein HTTP 200 OK mit etwas Text im Body antworten.

Das Attribute

[Authorize]

über dem Controller bewirkt nun das unsere AuthenticationMiddleware greift und nur authorisierte Benutzer Zugriff auf das API erhalten. Alle anderen werden lediglich ein HTTP 403 zu sehen bekommen.

DemoController.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[Authorize]
public class DemoController : ApiController
{
    [HttpPost]
    [Route("Owin/Start")]
    public IHttpActionResult Start()
    {
        return Ok("Start");
    }

    [HttpPost]
    [Route("Owin/Stop")]
    public IHttpActionResult Stop()
    {
        return Ok("Stop");
    }
}

Zusammengefasst:

Mit nur wenigen einfachen Klassen haben wir einen schönen Windows Service mit REST interface, einfacher Authentifizierung und automatischer API Dokumentation erstellt.

Viel Spass beim Implementieren und Happy ‘REST’ ing!