In this article I will show a simple example of how to build a simple C# self hosted web api using Bazel.

The Bazel rules for .Net are still a work in progress, but the current state is at least mature enough to do some early experimentation.

One of the cool features of Bazel is that you can combine applications written in many different languages in the same build. This is very convenient for full stack development since you don’t need separate build tooling for frontend and backend.

In my demo I take advantage of this by having Bazel build an Angular app, an Express app and a C# web api, all in the same build. I wrote the C# code using mono on a Mac.

The C# service is a super simple self hosted web api endpoint, consisting of a C# library and console application.

The source code can be found below:

using System; using System.Collections.Generic; using System.Net; using System.Web.Http; using System.Web.Http.SelfHost; namespace selfHost { public class SelfHost { public void Initialize() { var config = new HttpSelfHostConfiguration("http://localhost:8080"); config.Routes.MapHttpRoute("API Default", "api/{controller}"); using (HttpSelfHostServer server = new HttpSelfHostServer(config)) { server.OpenAsync().Wait(); Console.WriteLine("Press Enter to quit."); Console.ReadLine(); } } } public class FriendsController: ApiController { public List<string> GetFriends() { return new List<string> { "Joe", "Mary", "Peter", "Lisa" }; } } }
using System; using selfHost; namespace myApp { class Program { static void Main(string[] args) { var selfHost = new SelfHost(); selfHost.Initialize(); } } }

Bazel Rules

To build the source code we have to add a few Bazel rules to tell Bazel how to build the application.

The rules are maintained in the rules_dotnet repo.

Here are the rules from BUILD.bazel:

package(default_visibility = [ "//visibility:public" ]) load("@io_bazel_rules_dotnet//dotnet:defs.bzl", "dotnet_library", "dotnet_binary", "dotnet_import_library") dotnet_import_library( name = "selfhost", src = "System.Web.Http.SelfHost.dll" ) dotnet_library( name = "lib", srcs = [ "lib.cs" ], deps = [ #TODO: This should be a nuget package, and not a manually downloaded dll ":selfhost", #Adding built-in .Net libs "@io_bazel_rules_dotnet//dotnet/stdlib:System.Web.dll", "@io_bazel_rules_dotnet//dotnet/stdlib:System.dll", "@io_bazel_rules_dotnet//dotnet/stdlib:System.Web.Http.dll", "@io_bazel_rules_dotnet//dotnet/stdlib:System.Net.Http.dll" ] ) dotnet_binary( name = "backend", srcs = [ "program.cs", ], deps = [ ":lib" ], )

At a high level this defines a dotnet_library rule for the code in lib.cs, and a dotnet_binary rule for the console application (program.cs).

The self host depends on a nuget package, but I struggled to get nuget integration to work, so I integrated the .dll directly. This is of course just a temporary “hack”.

Why Bazel?

The ability to combine multiple languages in the same build is very convenient, but it’s not just about simpler tooling.

Bazel supports type sharing across languages.

The benefit of cross language type sharing is that we can catch misaligned types between frontend and backend at compile time. Instead of getting runtime errors we will get compiler errors if the payload from the server no longer matches the expectations on the client.

I talk more about this here.

Another key feature of Bazel is incremental builds.

Incrementality is key for build performance. For every code change, Bazel will only rebuild the parts of the application that actually changed, or depends on the change.

There are other build performance measurers like remote caching and execution as well. I discuss this in more detail here.

Source Code

The full application consists of a frontend written in Angular, a reverse proxy written in NodeJS, and a C# backend. Everything is built using a single Bazel build.

You can download the source code from Github if you are interested in trying out. Just checkout the dotnet branch in the repo.