WebAssembly for .NET Developers: Introducing the Spin .NET SDK

.NET is a free, cross-platform, open source, developer platform for building applications. With over 5 million developers worldwide, it’s an established, proven technology with a wide audience. WebAssembly, a relatively newer technology, is rapidly progressing and offers many benefits including efficiency, safety and portability; which can translate into improved performance and potential cost savings. With WebAssembly for .NET developers, old meets new: developers familiar with a high-productivity platform gain the option of deploying to the low-overhead Wasm environment.

In this post, we introduce a new Spin SDK for .NET developers, run through how to build a Wasm application in C#, and peek a little behind the curtain into how the SDK is built. Read on to discover the intriguing synergy between WebAssembly and .NET.

The Spin SDK for .NET is an experimental library which allows you to:

  • Build Web applications and microservices using the Spin executor
  • Send HTTP requests to external hosts
  • Read and update Redis stores
  • Query and update PostgreSQL databases

This is an evolution from a few months ago when we wrote about Microsoft’s experimental kit for building C# and other .NET applications to WASI. The .NET WASI SDK focused primarily on building self-contained applications such as console applications or ASP.NET sites. That meant you could use it to build Spin applications using WAGI, but you couldn’t import Spin host components or subscribe to non-WAGI Spin triggers. The new SDK bridges those gaps, allowing for .NET Spin apps to take advantage of the kind of services a real-world application needs.

Writing a Spin HTTP handler in C#

In Spin, a trigger handler - such as an HTTP request handler - is a function which receives event data as a structured argument. In traditional C# terms, it’s a public method in a DLL.

To tell Spin which function in the DLL is the handler entry point, the SDK provides the HttpHandler attribute. So an HTTP handler function looks like this:

using System.Net;
using Fermyon.Spin.Sdk;

namespace Microservice;

public static class Handler
{
    [HttpHandler]
    public static HttpResponse HandleHttpRequest(HttpRequest request)
    {
        return new HttpResponse
        {
            StatusCode = HttpStatusCode.OK,
            BodyAsString = "Hello from .NET",
        };
    }
}

The name of the class and function don’t matter, but the function does have to have this signature (including being static).

Inside your function you can write whatever .NET code you like, subject to the limitations of the current WASI implementation. Here’s an example that echoes some of the request information:

using System.Net;
using System.Text;
using Fermyon.Spin.Sdk;

namespace EchoService;

public static class Handler
{
    [HttpHandler]
    public static HttpResponse HandleHttpRequest(HttpRequest request)
    {
        var responseText = new StringBuilder();

        responseText.AppendLine($"Called with method {request.Method} on {request.Url}");
        foreach (var h in request.Headers)
        {
            responseText.AppendLine($"Header '{h.Key}' had value '{h.Value}'");
        }

        var bodyInfo = request.Body.HasContent() ?
            $"The body was: {request.Body.AsString()}\n" :
            "The body was empty\n";
        responseText.AppendLine(bodyInfo);

        return new HttpResponse
        {
            StatusCode = HttpStatusCode.OK,
            Headers = new Dictionary<string, string>
            {
                { "Content-Type", "text/plain" },
            },
            BodyAsString = responseText.ToString(),
        };
    }
}

Let’s take a bit more of a look at those HttpRequest and HttpResponse objects.

As you can see, HttpRequest has the Method and Url properties you might expect. It also provides the request headers via the Headers property, which you can treat as a .NET Dictionary. Under the surface, though, it’s implemented a little differently: that doesn’t matter here but we’ll come back to it when we talk about making outbound HTTP requests. The Body property is similar to the .NET Content property, and has helper methods to make it convenient to access in different ways. In this example, we use AsString() to treat it as text; in other cases we might use AsBytes() to access the raw binary content.

Similarly, HttpResponse provides two accessors to set the body. For text responses, you can set the BodyAsString property; for binary responses, set BodyAsBytes.

You might be curious why the Spin SDK does things this way. The answer is that the HttpRequest and HttpResponse types are, internally, unmanaged structures in WebAssembly linear memory. We’re continuing to look at the best way of surfacing the Spin API, but this direct mapping of Wasm memory provides a very high-performance, low-overhead path between Spin and your code. We’ll talk about this more generally in future posts about .NET and the Wasm component model.

Another effect of the Spin SDK types being unmanaged structures is that they’re value types. Mutable value types in .NET come with some gotchas, so be aware of this if you write methods that build or modify SDK objects.

Calling other services

You can also make outbound HTTP requests from Spin applications, using Spin’s WASI HTTP facility. The outbound HTTP service is surfaced through a static class in the SDK called HttpOutbound. To make a request, you construct a HttpRequest object, and call HttpOutbound’s Send method:

using Fermyon.Spin.Sdk;

// ...

var onboundRequest = new HttpRequest
{
    Method = HttpMethod.Get,
    Url = "http://spin.fermyon.dev/",
    Headers = new Dictionary<string, string>
    {
        { "Accept", "text/html" },
    },
};

var response = HttpOutbound.Send(onboundRequest);

If you need to send a request body, the syntax for setting the Body property is:

var request = new HttpRequest
{
    // ...
    Body = Optional.From(Buffer.FromString("your text here")),
};

(This syntax for setting the body is, again, a place where the Wasm binary interface intrudes into the API, and we’re continuing to refine this.)

A similar interface is available to perform Redis operations. The SDK defines a RedisOutbound static class with Get, Set, and Publish methods:

var address = "redis://127.0.0.1:6379";
var key = "mykey";
var channel = "messages";

var payload = ComputeRedisPayload();
RedisOutbound.Set(address, key, payload);

var value = RedisOutbound.Get(address, key).ToUTF8String();

RedisOutbound.Publish(address, channel, payload);

Working with relational databases

C# has traditionally been popular for writing business applications, often backed by relational databases. So we’re proud to offer our first RDBMS support as part of the Spin .NET SDK.

The PostgresOutbound class provides Query and Execute methods. Query is for running SELECT-like operations that return values from the database; Execute is for operations like INSERT, UPDATE and DELETE that modify the database but don’t return values:

var connectionString = "user=ivan password=my$Pa55w0rd dbname=pgtest host=127.0.0.1";

var result = PostgresOutbound.Query(connectionString, "SELECT id, author, title FROM posts WHERE author = $1", "Ivan Towlson");

var summary = new StringBuilder();

summary.AppendLine($"Wrote {result.Rows.Count} posts(s)");

foreach (var row in result.Rows)
{
    var title = row[2];
    responseText.AppendLine($"- {title.Value()}");
}

We admit it’s not exactly Entity Framework yet. But if you want to build database-backed applications on Spin, give it a try and share your feedback!

Pre-warmed startup

Spin runs a separate instance of your Wasm module for each request. As we noted in our earlier post, .NET applications have significant startup costs: the runtime can take tens of milliseconds to start. The Spin SDK works around this by using a tool called Wizer to run your application through one request at build time, then snapshot the state of the Wasm module after that request. So when your module runs in Spin to handle a real request, it’s as if it was actually handling its second request - the runtime is already loaded and the code path is warmed up.

This is usually really nice, but you need to handle that build-time request carefully. This means:

  • Don’t call any external services in the warmup request.
  • Be aware if the warmup request could initialise static members.

See the SDK read-me for more details about the wizerisation process and how to handle it, and an example of a typical warmup handler.

Where do I get it?

The Spin .NET SDK is available as a NuGet package, Fermyon.Spin.Sdk.

  1. You’ll need a couple of prerequisites:
  • You must have .NET 7 Preview 5 or above.
  • Install Wizer from https://github.com/bytecodealliance/wizer/releases and place it on your PATH. If you have Rust installed, you can also do this by running cargo install wizer --all-features.
  1. Install the C# template:
spin templates install --git https://github.com/fermyon/spin-dotnet-sdk --branch main --update
  1. Create a new Spin application using the http-csharp template:
spin new http-csharp MyTestProject

If you don’t use the Spin template, but instead use the .NET classlib template, you’ll need to manually add references to the Wasi.Sdk (version 0.1.1) and Fermyon.Spin.Sdk packages using dotnet add package.

  1. Run spin build --up to test that everything is set up correctly. If not, check that the WASI SDK is working and that the path to the Spin SDK is correct.

  2. Start working on your project!

Acknowledgments

We’re deeply indebted to Steve Sanderson of Microsoft for his guidance and contributions to the SDK. Steve put considerable effort into an early iteration of the SDK, and his work greatly improved both performance and simplicity.

Summary

If you’re interested in trying out Spin with .NET, now’s a great time to start. Although the whole .NET WASI ecosystem is at an early stage, it’s a significant milestone in opening up WebAssembly to ‘business’ developers working at a higher level than C and Rust.

As Steve mentioned in his brilliant talk ‘Bringing WebAssembly to the .NET Mainstream’, 3 in 10 .NET developers use the .NET technology in a professional capacity. You already build professional, business applications with .NET, and are productive with the languages and tools. With Spin, we offer you a way to deploy them in a way that’s as reliable as containers or functions-as-a-service, but faster, simpler and more cost-effective. We’re excited for what happens when .NET meets WebAssembly - we hope you are too!

Let us know!

We would love to hear about what applications you are building, and what features you would like to see developed next. If you face any issues along the way please contact us via Discord, Twitter or GitHub.

Interested in learning more?

Get Updates