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).
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.
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
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.
Now test the exported application using XSP:
xsp4 --root /Volumes/mc/ExportedApps/Piotr.WebApiMono/ --port 8080
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.