Running ASP.NET Web API services under Linux and OS X

In this blog post I am going to show how you can host ASP.NET Web API services under Gentoo Linux and OS X on top of Mono's ASP.NET implementation. I will use Nginx and FastCGI to communicate between HTTP server and Mono.

A couple of months ago I've experimented with running ASP.NET Web API on a Linux box, but ran into blocking issues caused by some functionality missing from Mono. I've decided to give it another go now when more recent versions of the runtime are available.

Getting started

Yes, that is correct you should be able to run Web API services under Linux using recent versions of Mono :). The approach I am taking is to use Visual Studio to write the application and then run it on Linux.

Just a general remark - be advised that copying and running non open source assemblies (like System.Core) is probably not ok from licensing point of view (I am not a legal expert, though). This shouldn't happen under normal circumstances (unless you willingly overwrite Mono assemblies) and ASP.NET Web API is 100% open source, so it is not a problem in this scenario.

I will create a very basic Web API service, having in mind that any unnecessary dependency can create problems. Remember that not all libraries that play nicely with Web API will work happily on Mono.

Start off by adding an empty MVC4 application, remember that you can choose either to use .NET 4.0 or 4.5 when doing that. The latter supports async/await and makes writing message handlers a little bit easier.  Even though Mono 2.11 introduced support for some of 4.5 APIs I ran into issues when trying to run 4.5 Web API app against XSP 2.11. This is why I am going to use .NET 4.0 (which means you should be able to use VS2010 as well).

New Project

I am going to create a very simple model and a CRUD controller for testing purposes. Also I will not bother with any kind of IoC container and just go with a static in memory repository.

public class Beer : Entity  
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

public class Entity  
{
    public Guid Id { get; set; }
}
public class BeersController : ApiController  
{
    private IRepository<Beer> _beerRepository;

    public BeersController()
    {
        _beerRepository = WebApiApplication.BeerRepository;
    }

    public IEnumerable<Beer> Get()
    {
        return _beerRepository.Items.ToArray();
    }

    public Beer Get(Guid id)
    {
        Beer entity = _beerRepository.Get(id);
        if (entity == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return entity;
    }

    public HttpResponseMessage Post([FromBody] Beer value)
    {
        var result = _beerRepository.Add(value);
        if (result == null)
        {
            // the entity with this key already exists
            throw new HttpResponseException(HttpStatusCode.Conflict);
        }
        var response = Request.CreateResponse<Beer>(HttpStatusCode.Created, value);
        string uri = Url.Link("DefaultApi", new { id = value.Id });
        response.Headers.Location = new Uri(uri);
        return response;
    }

    public HttpResponseMessage Put(Guid id, Beer value)
    {
        value.Id = id;
        var result = _beerRepository.Update(value);
        if (result == null)
        {
            // entity does not exist
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(HttpStatusCode.NoContent);
    }

    public HttpResponseMessage Delete(Guid id)
    {
        var result = _beerRepository.Delete(id);
        if (result == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(HttpStatusCode.NoContent);
    }
}

We can seed the repository with sample data.

public class WebApiApplication : System.Web.HttpApplication  
{
    public static IRepository<Beer> BeerRepository = new InMemoryRepository<Beer>();

    protected void Application_Start()
    {
        BeerRepository.Add(new Beer()
        {
            Name = "Blue Moon",
            Description = "Belgian-style witbier. Orange-amber in color with a cloudy appearance.",
            Id = Guid.NewGuid(),
            Price = 1.99m
        });
        var config = GlobalConfiguration.Configuration;
        WebApiConfig.Register(config);
    }
}

Before continuing make sure that the project runs correctly under ASP.NET and IIS.

 

Setting up the environment

There are two main things you will need in order to run the service on Linux:

  • Working Mono installation including XSP - I will be using version 3.0.5, so this or any above should work,
  • HTTP server (unless you want to use Mono's own XSP, which should be enough for testing purposes) that supports FastCGI. I will be using Nginix. Apache should also work (through mod_mono).

Now, there are two main ways of getting Mono - either to compile and install it directly from the source code available at github (or official mono packages available at download site) or to install a package provided by your distribution. Mono support varies among different distributions and to be honest is usually pretty weak when it comes to latest versions. If you just want to experiment and don't have an existing Linux installation I would suggest choosing OpenSUSE or Gentoo (evil grin).

To compile from official tarball package use:

./configure --prefix=/usr/local  
make  
make install

And to clone and compile latest code from master branch:

git clone git://github.com/mono/mono.git  
cd mono  
./autogen.sh --prefix=/usr/local
make  
make install

In this example I will be using Gentoo Linux (as it is my favorite distro :)). Official Gentoo repository (at the time of writing this post) does not contain Mono versions above 2.X so we will need to use layman and dotnet overlay.

emerge -av layman  
layman -a dotnet

Depending on your current configuration you may also need to add USE keywords to the xsp package and unmask mono packages as newest versions are usually considered 'unstable'.

echo "dev-lang/mono ~amd64" >> /etc/portage/package.keywords  
echo "dev-dotnet/xsp ~amd64" >> /etc/portage/package.keywords

echo "dev-dotnet/xsp net40 net45" >> /etc/portage/package.use

Now cross your fingers and emerge mono:

emerge -av =mono-3.0.5

Use a higher version if available as it is likely that it contains bugfixes. This may take a while so go grab a coffee or something ;)

Portage will try to resolve dependencies and if everything goes well you should be able to run mono on your system.

ester ~ # mono --version  
Mono JIT compiler version 3.0.5 (tarball Sat Mar  2 13:04:59 Local time zone must be set--see zic manual page 2013)  
Copyright (C) 2002-2012 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com

If it does not work (e.g. compilation fails) - don't give up, try to search for the solution or use a different package (for example using official source packages instead of cutting edge latest master).

The next step is to install XSP (Mono application server). Again you can use the sources or get the package provided by your distribution. Under gentoo this means:

emerge -av xsp

If both installations were successful go ahead and copy the exported (right click on the project and Publish...) Web API application to some directory on your Linux box (eg. using WinSCP). You can use XSP to test if everything works (remember to use xsp4, which is a .NET 4 version):

xsp4 --root /home/pwalat/Piotr.WebApiMono/ --port 8082

Voila! now use your favorite browser or HTTP debugger to test the service.

Post

As  you can see VS studio compiled Web API code just works under Mono runtime. You don't even need to fiddle with web.config.

Alternatively can choose to compile the sources under Mono.

xbuild Piotr.WebApiMono.sln

Now let's set up Nginx to serve our Web API project as using XSP is good for ad-hoc testing only. First of all install the server and make sure you enable fastcgi support:

echo "www-servers/nginx fastcgi ssl nginx_modules_http_gzip_static" >> /etc/portage/package.use  
emerge -av nginx

Once you have nginx installed you will need to modify virtual host configuration to run off fastcgi (/etc/nginx/nginx.conf):

server {  
 listen   80;
 server_name  domain.com;
 access_log   /var/log/nginx/domain.com.access.log;
 location ~ / { 
   root /var/www/domain.com/;
   index index.html index.htm default.aspx default.htm;
   fastcgi_index /default.htm;
   fastcgi_pass 127.0.0.1:9002;     
   fastcgi_param  PATH_INFO          "";
   fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
   include /etc/nginx/fastcgi_params;
 } 
}

This configures Nginix to pass incoming requests to 127.0.0.1:9002 where Mono FastCGI server will be listening. Of course you can have multiple applications hosted by one Nginx instance (e.g. you can mix ASP.NET with PHP or static pages).

To run fastcgi-mono-server4 we need to provide it a list of applications. We can do it either as a command line parameter or use .webapp config files. Let's do the latter as it is more manageable approach.

mkdir /etc/webapps  
nano /etc/webapps/MonoWebApi.webapp
<apps>  
<web-application>
        <name>MonoWebApi</name>
        <vhost>domain.com</vhost>
        <vport>80</vport>
        <vpath>/</vpath>
        <path>/var/www/domain.com</path>
</web-application>
</apps>

Now we can run both servers:

/etc/init.d/nginx start  
fastcgi-mono-server4 --appconfigdir /etc/webapps /socket=tcp:127.0.0.1:9002

If everything worked correctly your service should run off Nginx and be available at http://domain.com/beers

nginx

Instead of having to run your Mono server each time from command line you probably will want to have it started during the boot time. Here is a simple /etc/init.d/mono-fastcgi script to facilitate this:

#!/bin/sh  
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin  
DAEMON=/usr/bin/mono  
NAME=mono-fastcgi  
DESC=mono-fastcgi  
PORT=9002  
MONOSERVER=$(which fastcgi-mono-server4)  
MONOSERVER_PID=$(ps auxf | grep fastcgi-mono-server4.exe | grep -v grep | awk '{print $2}')  
WEBAPPS="/etc/webapps"  
case "$1" in  
start)  
if [ -z "${MONOSERVER_PID}" ]; then  
echo "starting mono server"  
${MONOSERVER} --appconfigdir ${WEBAPPS} /socket=tcp:127.0.0.1:${PORT} &
echo "mono server started"  
else  
echo ${WEBAPPS}  
echo "mono server is running"  
fi  
;;
stop)  
if [ -n "${MONOSERVER_PID}" ]; then  
kill ${MONOSERVER_PID}  
echo "mono server stopped"  
else  
echo "mono server is not running"  
fi  
;;
esac  
exit 0
chmod +x /etc/init.d/mono-fastcgi  
rc-update add mono-fastcgi default  
/etc/init.d/mono-fastcgi start

You should have your ASP.NET Web API service running under Nginix now :)

Just to test that basic Web API functionality and message handling pipeline works correctly I've added delegating handler to calculate content's MD5 checksum and a CSV media formatter (have a look at the source code for implementation). Both work  as expected.

OS X

Generally we need to follow the same procedure as with Linux - i.e. install Mono and then install and configure Nginx.

Installing Mono on Mac should be much easier  than on Linux. Just grab OS X package (I use MDK) from mono site here and run the installer.

Optionally you can also install Xamarin Studio (aka Monodevelop 4.0) to open the solution, develop and then build and Deploy ASP.NET Web API application to a folder.

Working on a Web API project under OS X

 Now test the exported application using XSP:

xsp4 --root /Volumes/mc/ExportedApps/Piotr.WebApiMono/ --port 8080

Service running under OS X

We will install Nginx using MacPorts (if you dont have MacPorts install it from a package available here)

sudo port install nginx  
cp /opt/local/etc/nginx/nginx.conf.example /opt/local/etc/nginx/nginx.conf

If you want to run Nginx during startup execute

sudo port load nginx

Once this is done you can follow Nginx configuration instructions for Linux and configure the server to use fastcgi. The configuration file will be located at /opt/local/etc/nginx/nginx.conf. Then run fastcgi-mono-server4 and you will have ASP.NET Web API service running on OS X.

Please be advised that this example was an experiment and you still may run into issues when digging deeper :) That being said, I am sure that running under Mono will make ASP.NET Web API an appealing choice to even wider group of developers.

comments powered by Disqus