WebAssembly for .NET Developers: Introducing the Spin .NET SDK
Ivan Towlson
csharp
dotnet
webassembly
spin
.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
.
- 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
.
- Install the C# template:
spin templates install --git https://github.com/fermyon/spin-dotnet-sdk --branch main --update
- 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
.
-
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.
-
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.