Azure Service Fabric introduction - Part 2: WebAPI-based stateless service

Azure Service Fabric Introduction Part 2

-

Now that we have our local cluster running we can get started working on our actual application. If you have read part 1, you know that we are setting out to build a cloud-based solution for tracking sheep. Let's start by taking a look at the overall requirements for our sheep tracking application.

Sheep tracker requirements

The basic idea is to equip each sheep with a tracking device, a "bleater", that will interact with our application by submitting its location every 5 minutes or so.

Our application will then store the data for us, and let us access it for various purposes. We will use the data gathered to keep track of where the sheep are and to react if any sheep have gone of the radar.

The application will have three main components.

  1. A stateless API that will allow interaction with the application
  2. A stateful service for tracking overall sheep statistics
  3. An individual stateful actor for every sheep for detailed tracking

Sheepishly Azure Service Fabric Architecture

The clients will interact with the API to either send location data to our system, or retrieve information about sheep whereabouts.

The tracker service will keep track of when a sheep last reported a location. And the individual sheep actors will keep a detailed log of sheep movements for a particular sheep.

Creating the WebAPI endpoint

First things first, let us start by creating a new Service Fabric solution and our first service, the stateless WebAPI endpoint. I decided to call the API "Ingest", which has now become a bit misleading as it is also used for querying information. Feel free to choose a better name for your API.

Create Azure Service Fabric solution

Create Azure Service Fabric Stateless Service

If you take a look at the Solution Explorer you will see something like this.

Clean new Service Fabric stateless service

There are two items to note here.

  1. The "Sheepishly"-project which is the root of our new Service Fabric application.

  2. The "Sheepishly.Ingest"-project which is basically a console application. This is our stateless service.

Turning it into an API

The API-project isn't going to do much as it is right now. We need to turn our console application into a self-hosted WebAPI. Fortunately for us, that is easily achieved.

For a very thorough walkthrough you should take a look at this getting started guide over at the Microsoft Azure Service Fabric documentation Service Fabric Web API services with OWIN self-hosting.

I will be going over the steps required here as well. Please note that the majority of the code included here is identical to code found in the example from Microsoft mentioned above. It is not invented here, but included for completeness.

Installing Owin self-hosting package

We need to run our WebAPI inside our application. To achieve this we install the Microsoft.AspNet.WebApi.OwinSelfHost NuGet-package. Installing it will give us everything required to run our API.

Wiring things up

The entry point for our service is the file called Ingest.cs, the name matches the name you have chosen for the service. By default it contains two things.

    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {
        // TODO: If your service needs to handle user requests, return a list of ServiceReplicaListeners here.
        return new ServiceInstanceListener[0];
    }

    protected override async Task RunAsync(CancellationToken cancelServiceInstance)
    {
    ...
    }

We are building a service that needs to handle client requests. For our purposes right now the RunAsync()-method is unnecessary, so let's go ahead and remove it.

That leaves us with CreateServiceInstanceListeners(), where we can configure "CommunicationListeners" that will allow our stateless service to accept requests and respond to them.

Update the CreateServiceInstanceListeners()-method and have it return an instance of a new class you create called OwinCommunicationListener which implements the ICommunicationListener interface.

    protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
    {
        return new[] {
            new ServiceInstanceListener(initParams => new OwinCommunicationListener("api", new Startup(), initParams))
        };
    }

With that in place let us take a look at the implementation. Don't worry about the Startup-object now, we will get back to it later.

Owin Communication Listener

The ICommunicationListener interface requires us to implement three public methods.

  • OpenAsync() - For starting communication
  • CloseAsync() - For gracefully shutting down communication
  • Abort() - For ungracefully shutting down communication

Our new OwinCommunicationListener takes three constructor arguments that we will need as we go along.

  • appRoot - The location we want to accept communication on (like "/api")
  • startup - Our Startup-object, which implements IOwinAppBuilder
  • serviceInitializationParameters - An object that provides us with information about our service instance. We will use it to figure our what port the service instance is running on.

The IOwinAppBuilder is an interface introduced by us. Go ahead and add it now, and we will get back to it later.

public interface IOwinAppBuilder
{
    void Configuration(IAppBuilder appBuilder);
}

Then let's take a look at the code.

public class OwinCommunicationListener : ICommunicationListener
{
    private readonly IOwinAppBuilder _startup;
    private readonly string _appRoot;
    private readonly ServiceInitializationParameters _parameters;

    private string _listeningAddress;

    private IDisposable _serverHandle;

    public OwinCommunicationListener(string appRoot, IOwinAppBuilder startup, ServiceInitializationParameters serviceInitializationParameters)
    {
        _startup = startup;
        _appRoot = appRoot;
        _parameters = serviceInitializationParameters;
    }

    public Task<string> OpenAsync(CancellationToken cancellationToken)
    {
        var serviceEndpoint = 
            _parameters
            .CodePackageActivationContext
            .GetEndpoint("ServiceEndpoint");

        var port = serviceEndpoint.Port;
        var root = 
            String.IsNullOrWhiteSpace(_appRoot) 
            ? String.Empty 
            : _appRoot.TrimEnd('/') + '/';

        _listeningAddress = String.Format(
            CultureInfo.InvariantCulture, 
            "http://+:{0}/{1}", 
            port, 
            root
        );
        _serverHandle = WebApp.Start(
            _listeningAddress, 
            appBuilder => _startup.Configuration(appBuilder)
        );

        var publishAddress = _listeningAddress.Replace(
            "+", 
            FabricRuntime.GetNodeContext().IPAddressOrFQDN
        );

        ServiceEventSource.Current.Message("Listening on {0}", publishAddress);
        return Task.FromResult(publishAddress);
    }

    public void Abort()
    {
        StopWebServer();
    }

    public Task CloseAsync(CancellationToken cancellationToken)
    {
        StopWebServer();
        return Task.FromResult(true);
    }

    private void StopWebServer()
    {
        if (_serverHandle == null)
            return;

        try
        {
            _serverHandle.Dispose();
        }
        catch (ObjectDisposedException) { }
    }
}

The interesting part here is OpenAsync(). Let's take a closer look at what it does. First it builds a listening address.

var serviceEndpoint = _parameters.CodePackageActivationContext.GetEndpoint("ServiceEndpoint");
var port = serviceEndpoint.Port;
var root = String.IsNullOrWhiteSpace(_appRoot) ? String.Empty : _appRoot.TrimEnd('/') + '/';

_listeningAddress = String.Format(CultureInfo.InvariantCulture, "http://+:{0}/{1}", port, root);

The port is retrieved from an endpoint called "ServiceEndpoint". This refers to configuration in the ServiceManifest.xml file which you will find in the PackageRoot folder. Let's go ahead and add that endpoint to configuration.

<Resources>
    <Endpoints>
        <Endpoint Name="ServiceEndpoint" Type="Input" Protocol="http" Port="8100" />
    </Endpoints>
 </Resources>

We already specified the root as "api" in our constructor when we created the object. Which means the listening address will look like end up like this http://+:8100/api.

The +-sign indicates that we want our app to listen on all available addresses. Our API can run several instances on different machines in our cluster, we don't want to tie the application to a specific address.

Now we are ready to ask Owin to start our application. We will keep the server handle returned by Owin as we will need it if we want to shut the server down.

_serverHandle = WebApp.Start(_listeningAddress, appBuilder => _startup.Configuration(appBuilder));

Last we return the address of the actual instance of web app.

var publishAddress = _listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN);
return Task.FromResult(publishAddress);

This will let Service Fabric know that our instance is accepting requests.

Startup and the IOwinAppBuilder

Let's back things up a bit to when Owin starts the server.

_serverHandle = WebApp.Start(_listeningAddress, appBuilder => _startup.Configuration(appBuilder));

WebApp.Start requires two things.

  1. An address to listen on
  2. An action that will perform configuration on an IAppBuilder-object

For the latter we are giving it an anonymous action that takes an IAppBuilder as an argument and feeds it to the IOwinAppBuilder-object we took as a constructor argument when we created the class.

Let's go ahead and introduce the Startup-class which will implement the IOwinAppBuilder interface.

public class Startup : IOwinAppBuilder
{
    public static void ConfigureFormatters(MediaTypeFormatterCollection formatters)
    {
        formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
    }

    public void Configuration(IAppBuilder appBuilder)
    {
        var config = new HttpConfiguration();

        config.MapHttpAttributeRoutes();
        ConfigureFormatters(config.Formatters);

        appBuilder.UseWebApi(config);
    }
}

Configuration() is the method required by the interface, this is where we configure our WebAPI.

With Startup in place we have everything required to start accepting WebAPI requests from our stateless service. We can now start focusing on our actual API logic.

The API

We have configured our WebAPI application to map routes from attributes, which means we can introduce our controller and have it wired up automatically. We want to implement an API that does four things.

  • GET /api - Returns info about the API
  • POST /api/locations - Receives location information from sheep trackers
  • GET /api/sheep/{sheepId}/lastseen - Returns last received location timestamp for sheep
  • GET /api/sheep/{sheepId}/lastlocation - Returns last reported location for sheep

We can implement that in a single controller like so.

public class TrackerController : ApiController
{
    [HttpGet]
    [Route("")]
    public string Index()
    {
        return "Welcome to Sheepishly 1.0.0 - The Combleat Sheep Tracking Suite";
    }

    [HttpPost]
    [Route("locations")]
    public async Task<bool> Log(Object location)
    {
        throw new NotImplementedException();
    }

    [HttpGet]
    [Route("sheep/{sheepId}/lastseen")]
    public async Task<DateTime?> LastSeen(Guid sheepId)
    {
        throw new NotImplementedException();
    }

    [HttpGet]
    [Route("sheep/{sheepId}/lastlocation")]
    public async Task<object> LastLocation(Guid sheepId)
    {
        throw new NotImplementedException();
    }
}

For now, all but the welcome message are unimplemented stubs. We will fill them out with actual functionality in the next chapters.

Running it

If you run the application you will see that it starts deploying to your local Service Fabric cluster.

Open a browser window and navigate to http://localhost:8100/api and you should be greeted with a running Service Fabric WebAPI application!

Running Service Fabric WebAPI application

What's next

In the next installment of the series we will add a stateful service to the mix, allowing us start receiving and tracking sheep locations. Check out Part 3 - Stateful services.

View Comments