Runtime engineering

.NET in the browser: what WebAssembly makes possible, and where the boundaries remain

Follow a C# program from Roslyn compilation to a browser worker, then learn how to reason about packages, memory, stopping code, networking, and production boundaries.

Editorial illustration for .NET in the browser: what WebAssembly makes possible, and where the boundaries remain

Start with the execution location

A small C# program can print the same answer on a laptop, a phone, and a web page while running in three different environments. That resemblance is useful for learning, but it can hide the most important question: which machine actually executes the instructions? In a browser playground, downloading a runtime does not necessarily mean sending your program to a remote compiler service. Compilation and execution can both happen inside the browser, after the required assets have loaded.

SharpRune uses a browser-hosted .NET 9 toolchain and Roslyn for its playground. Treat that as a specific product configuration, rather than a promise that every .NET application can move unchanged into a browser tab. The useful mental model is a small, local execution environment with an explicit host interface. Your code receives the capabilities that interface exposes, and the surrounding browser supplies the rules for storage, networking, lifecycle, and resource use.

This distinction changes how you investigate errors. A missing symbol is usually a compiler or reference question. A failed fetch can be an origin policy question. A tab that stops responding may be a scheduling or memory question. Begin by locating the failure in the pipeline before changing your source program. Otherwise, an infrastructure problem can look like an unfamiliar C# rule.

Separate the compiler from the runtime

C# source is text. A runtime cannot simply execute that text because it happens to contain familiar language keywords. The compiler first parses source, resolves names against reference assemblies, checks types and language rules, and emits a managed assembly when the program is valid. Roslyn exposes these operations through APIs, which makes it possible for an editor or playground to integrate real compiler diagnostics instead of approximating the language with text matching.

The .NET runtime performs a different job. It loads managed assemblies, provides the managed execution environment, and supplies services such as garbage collection and exception handling. A browser version of that runtime is built for WebAssembly. Placing Roslyn inside it permits a loop in which managed compiler code emits another managed program, which the host then loads and invokes. The compiler and the executed program share a browser environment while retaining distinct responsibilities.

Try a deliberately misspelled identifier in a tiny example. A correct playground reports the compiler diagnostic and does not execute the invalid program. Then correct the identifier and change a literal. The new result should correspond to the new source snapshot. Those two experiments establish more than a decorative Run button: they demonstrate that the tool distinguishes analysis, successful compilation, and actual execution.

C# example

var prices = new[] { 12m, 8m, 25m };
var total = prices.Where(price => price >= 10m).Sum();
Console.WriteLine(total); // 37

References: Microsoft: Roslyn syntax analysis

Understand references before chasing packages

A compiler needs metadata describing the types your program mentions. Writing Console.WriteLine requires a reference that describes Console; writing a query requires the relevant LINQ APIs. A project file normally coordinates target frameworks, implicit references, package restore, and build rules on a development machine. A browser playground must reproduce the parts it supports, with a bounded set of files and a clear policy for additional assemblies.

This produces two separate compatibility tests. First, can the compiler resolve the API from the available metadata? Second, can the runtime execute its implementation in this host? Passing the first test does not prove the second. A package can expose a perfectly valid managed API whose implementation later calls an operating system facility unavailable in a browser. The failure can therefore appear only when a particular code path executes.

When investigating a package, reduce the program to one meaningful operation. Record the package version, the target framework, the referenced assembly, and the exact runtime failure. Avoid adding unrelated packages until the smallest example works. A carefully documented incompatibility is more valuable than a long list of package names that were restored successfully but never exercised. Restore, compilation, and runtime support are different milestones.

References: Microsoft: Roslyn syntax analysis

A worker is an execution boundary

A browser uses its main thread to coordinate much of the visible page, including user interaction and layout. Running a compiler and an arbitrary user loop there can make the editor feel frozen. A dedicated worker gives the playground a separate JavaScript execution context. The page can send a project snapshot to that worker, and the worker can return diagnostics or output through messages while the surrounding interface remains available.

A worker is not another computer, a durable process, or an unlimited background service. It belongs to the browser environment and can be suspended or destroyed as part of page lifecycle. It also does not have direct access to the page DOM. Those constraints are useful: they encourage the host to define a small message protocol instead of allowing executed code to manipulate every part of the interface implicitly.

Design messages around explicit identities. Include a request identifier, a project identifier, and a source identity so that an old reply cannot overwrite a newer editor state. A user may press Run, edit immediately, and press Run again. The results should remain attached to the source that produced them. This is ordinary concurrency reasoning even when the underlying runtime is configured without managed multithreading.

References: MDN: Using Web Workers

Asynchronous work is not automatic parallelism

The async and await keywords describe how an operation can suspend and resume without expressing its waiting as a blocked caller. They do not guarantee another CPU core or create a new operating system thread for every method. This matters especially in a browser host, where the available threading configuration depends on the runtime build, the browser, and deployment policies. An asynchronous entry point can work without promising that CPU-heavy code runs in parallel.

Consider a method that awaits a short delay and then prints a line. It yields while waiting. A method that calculates a large sum in a tight loop continues consuming execution time until it reaches a yielding operation or finishes. Adding async to the signature does not change that loop into cooperative background work. The editor remains responsive because the computation is isolated in a worker, not because the keyword made the calculation inexpensive.

For learning exercises, keep work bounded and make the bound visible. Use small data sets, reasonable iteration counts, and cancellation-aware APIs when they exist. For actual parallel algorithms, test the exact deployed browser runtime and device matrix. Treat successful desktop execution as evidence about that desktop configuration. It is not sufficient evidence about every phone, embedded web view, or browser runtime distribution.

C# example

Console.WriteLine("before");
await Task.Delay(50);
Console.WriteLine("after");

References: Microsoft: .NET WebAssembly runtime features

Stopping a program needs host support

A cancellation token is a request that cooperating code should stop. It cannot force arbitrary code to observe the request. A loop with no cancellation checks, no awaits, and no terminating condition may continue indefinitely from the program’s perspective. This makes a playground Stop control a host design problem as well as a language lesson. The surrounding environment needs a way to regain control when the program does not cooperate.

One practical browser strategy is to terminate the worker that owns the current execution and create a fresh worker when another run is needed. That is a hard boundary. It also discards the runtime state inside that worker, so the host must decide what state survives elsewhere and what should be recreated. A timeout should report that execution was interrupted, rather than pretending that the program returned normally with a successful exit code.

Never use a user program’s own output to infer that it stopped safely. A program can print a final-looking line and continue looping. Instead, track completion through the host protocol. In an exercise, intentionally create a small infinite loop only when you have a working Stop control and no valuable unsaved state. The goal is to understand the boundary, not to stress a device until it becomes unusable.

References: MDN: Using Web Workers · Microsoft: Cancellation in managed threads

The browser sandbox has a precise meaning

WebAssembly executes within the security model of its host. In a browser, that means it does not acquire unrestricted access to your desktop merely because its original language was C#. There is no general ability to start arbitrary local processes, inspect every file on the computer, or bind a public listening socket. The browser exposes carefully controlled APIs, and a managed library remains subject to those capabilities when it runs through WebAssembly.

This boundary does not make every program harmless. A tab can still consume memory, spend CPU time, make permitted network requests, and access data that its host deliberately supplies. Browser isolation is one layer of protection, not a substitute for output limits, bounded inputs, careful interop, and sensible defaults. Keep secrets out of programs you paste into shared examples, especially when those examples can make network calls.

For a teaching tool, a useful capability description names concrete operations: prepared console input, project files, captured output, supported managed packages, and preview requests. That list lets learners predict behavior. The phrase full .NET is too vague to serve the same purpose. A small environment with clear rules can teach the language accurately while being honest about the operating system features it does not provide.

References: Microsoft: Secure Blazor WebAssembly

Client code cannot keep server secrets

Anything delivered to a client should be treated as inspectable by that client. This includes managed assemblies, JavaScript glue, configuration files, and values embedded in a browser application. Obfuscation can complicate inspection, but it does not transform a distributed secret into a protected server credential. A database password or private API key does not belong in a browser playground sample or in the published assets of a client application.

When an application needs a privileged operation, place that operation behind a server boundary with appropriate authentication and authorization. The server can retain its own credentials and decide whether the caller may perform the requested action. The browser sends the necessary input and receives a limited result. This architecture also gives you a place to apply quotas, auditing, and resource ownership checks that the client cannot simply remove.

Learning projects are a good place to practice this separation. Use fictional identifiers and public test data in local examples. If a service requires a private token, model the response in memory while learning the language, then build a real backend separately. Do not publish a convenient demonstration containing a production credential. The fact that the example compiles locally says nothing about whether its data flow is appropriate to distribute.

References: Microsoft: Secure Blazor WebAssembly

Network requests still follow browser rules

An HTTP request made by managed code in a browser remains a browser-originated request. The remote service’s cross-origin policy still matters. If an endpoint does not permit the page’s origin, changing from JavaScript to C# does not bypass that restriction. Likewise, the availability of HttpClient does not imply raw socket access or permission to contact every address that a server process could reach.

Diagnose networking in layers. Confirm the destination URL, check whether HTTPS is required, inspect the browser’s network panel, and distinguish a remote HTTP error from a request blocked before the response is readable. An API returning an authorization failure is a different problem from an origin policy rejecting access. Log a useful description without printing tokens, private query values, or complete personal payloads into a shared screenshot.

For deterministic lessons, avoid making a public network service a prerequisite for understanding LINQ or records. Start with an in-memory array or a fixed JSON document. Introduce networking as its own topic, with clear failure paths and cancellation. That progression lets learners distinguish a programming mistake from a temporary service outage, a lost connection, or a deployment policy they do not control.

References: Microsoft: Secure Blazor WebAssembly

Filesystem names do not guarantee persistence

An API that looks like a normal filesystem API may be backed by host-managed or virtual storage in a browser environment. A path such as data/results.txt therefore needs an explicit interpretation. Is it an imported project file, a temporary file in the current run, a browser-persisted document, or an export offered for download? The spelling alone cannot answer that question. Your application must define the lifecycle.

SharpRune’s compiler host works with project documents and captured written files. Think in terms of that document boundary when constructing a reproducible example. Include the input file in the project, read it through the supported path rules, and inspect the produced output file. Do not assume that a path on your laptop exists in a phone’s web view, or that a worker restart preserves every file it created.

A useful exercise reads a tiny CSV document, validates each row, and produces a summary file. Run it twice and compare the results. Then deliberately omit the input file and examine the failure. This teaches error handling and data ownership together. For production browser applications, choose a persistence API deliberately and test reload, eviction, export, and recovery behavior instead of relying on temporary runtime state.

Native dependencies need a different investigation

NuGet distributes more than ordinary managed code. Some packages include native binaries, platform-specific runtime assets, build tasks, source generators, or assumptions about the machine running the application. A library that works on a server can depend on a native operating system API that has no browser equivalent. Package restore cannot resolve that conceptual mismatch by itself, even if the package advertises a broadly compatible target framework.

Start by checking whether the library is purely managed and whether its documented APIs support browser execution. Inspect transitive dependencies as well as the package you added directly. A small wrapper can conceal a platform dependency several layers below the call site. Also separate build-time behavior from runtime behavior: a source generator executed by a normal SDK build is not automatically part of a browser playground’s compilation pipeline.

If your goal is to learn an algorithm, prefer a portable implementation using arrays, spans where supported, collections, and standard text APIs. If your goal specifically requires a native database engine or imaging library, reproduce the browser deployment path with the package’s documented build process. It may require relinking native WebAssembly assets or a server component. That is a separate engineering task, not an editor toggle.

References: Microsoft: .NET WebAssembly runtime features · Microsoft: WebAssembly build tools and AOT

AOT is a deployment tradeoff

Ahead-of-time compilation is one way to prepare managed code for execution, but it is not a universal faster button. Browser WebAssembly deployments can use different combinations of interpretation and compiled code depending on their toolchain and settings. AOT may improve CPU-intensive workloads while increasing publication work and asset size. The right decision depends on what users actually do and how often they pay the initial download cost.

A dynamically compiling playground has an additional constraint: user code does not exist when the site itself is published. Compiling the host ahead of time does not automatically compile every future program a visitor types. The runtime still needs a supported way to execute the assemblies emitted during the session. Evaluate that requirement before copying performance settings from a conventional application whose entire codebase is known at build time.

Measure distinct phases instead of reporting a single vague speed number. Record first download, runtime startup, compilation, first execution, and repeated execution. Test a trivial program and a representative larger example. Keep network cache state visible in your notes. A fast warm run and a slow first visit can both be true, and each leads to a different improvement in the user experience.

References: Microsoft: WebAssembly build tools and AOT

Cross-origin isolation is an operational feature

Some browser capabilities, particularly shared memory used by multithreaded WebAssembly configurations, depend on cross-origin isolation and the corresponding HTTP policies. These are deployment properties as well as runtime build choices. A program that works on a development server can fail when published through a proxy that drops required headers, changes resource origins, or serves a third-party dependency without a compatible resource policy.

Do not add isolation headers by rote without checking the complete page. Fonts, scripts, images, embedded frames, popups, and worker imports can all interact with the policy. Hosting the necessary assets under a consistent origin often simplifies the problem. Inspect the browser’s reported isolation state and network failures after deployment. A successful request for index.html does not establish that the worker’s dependency graph loaded correctly.

For a playground, include a small boot diagnostic that identifies whether the runtime initialized and whether the required exports exist. Keep that diagnostic separate from compilation errors in user code. If an isolation requirement fails, the learner should see an actionable environment message rather than an unexplained spinner. Infrastructure policy belongs in the product’s operational documentation, while the interface should explain the practical next step.

References: Microsoft: .NET WebAssembly runtime features · MDN: Cross-origin isolation

Memory pressure is part of the product design

A compiler, runtime, metadata references, source files, emitted assemblies, and output buffers all occupy memory. A desktop browser may tolerate a workload that causes a mobile browser to reclaim a tab. The important limit is the complete working set on the target device, not merely the size of the C# array in your example. Repeated runs can reveal retention that a single successful demonstration misses.

Keep output bounded. A loop that prints a million lines creates work in the managed program, the message channel, and the rendering interface. Truncation should be visible so a learner knows that the display is incomplete. Similarly, validate project file counts and text sizes before compilation. Clear constraints help the tool remain predictable, particularly on phones where available memory changes as other applications become active.

Investigate growth with a repeatable sequence: boot, compile a known project, run, stop, replace the project, and repeat. Observe whether the host deliberately recreates its worker or retains assemblies for reuse. Either strategy has costs. A fresh worker loses warm runtime state; a persistent worker needs a retention policy. Choose based on measured behavior and explain the user-visible consequences, especially when a restart discards unsaved runtime data.

Debug the pipeline one boundary at a time

When a playground fails, collect evidence at each boundary before adding retries. Can the browser download the runtime assets with the correct content types? Does the worker boot? Can the host find the exported compile and run methods? Does Roslyn produce diagnostics? Does the emitted program enter its entry point? Does the result message carry the expected source identity? Each question narrows the investigation.

A minimal probe should have a known output and no network dependency. Print a fixed line, then run a decimal calculation, then try an asynchronous entry point, then a deliberate compilation error. Add project files and packages only after the core path works. Preserve the exact source, browser version, runtime configuration, and observable status. Screenshots are useful, but structured diagnostics and reproducible input make a bug far easier to fix.

Distinguish an exception in the user program from an exception in the host. A user InvalidOperationException should appear as a runtime failure associated with that project. A missing JavaScript export is an environment failure that the user cannot repair by renaming a C# variable. This separation is especially valuable in education: a learner needs to know when to study the program and when the tool itself needs attention.

An in-browser web preview is not Kestrel

A browser tool can provide a useful preview of route handlers without starting a real public HTTP server. SharpRune includes a focused web runtime that registers an application and dispatches preview requests through its host interface. That allows learners to explore request paths, responses, and related programming concepts within the same project workflow. It should be understood as an implemented subset with its own supported surface.

A production ASP.NET Core application normally runs under a real host such as Kestrel, often behind an ingress or reverse proxy. It must handle actual connections, request limits, authentication, TLS boundaries, logging, deployment lifecycle, and dependencies such as databases. Those operational concerns do not disappear because a route returned a JSON object in a browser preview. A successful preview proves behavior in that preview environment.

Use the preview to tighten feedback while learning, then move the project to the normal .NET SDK and a suitable server environment when production behavior matters. Recheck every framework feature and package there. This is a productive progression: the browser lowers the effort needed to begin, while the server environment supplies the capabilities and operational guarantees required for a deployed API.

References: Microsoft: Blazor hosting models

Build exercises that expose useful boundaries

A good browser exercise is small enough to understand and rich enough to reveal one new idea. Start with a deterministic transformation: parse a short list of orders, reject invalid amounts, group valid rows, and print a summary. Add a record to describe each row. Then move parsing into a method and write a few explicit checks. None of this requires a remote database or an unrestricted operating system.

Next, vary one environmental assumption at a time. Provide an empty input file. Include Unicode text. Supply malformed JSON. Cancel a waiting operation. Generate a controlled amount of output. Each variation teaches a failure mode with a concrete observation. Avoid combining unfamiliar language syntax, a new package, authentication, and an unreliable network endpoint in the same first exercise. If everything changes at once, even success teaches very little about why it worked.

Keep expected output alongside the source, but do not turn the exercise into a memorization test. Ask learners to predict the result, explain a boundary case, and make a small change. Browser execution is most valuable when it shortens the distance between a question and evidence. The goal is not to reproduce a complete workstation in a tab; it is to make a useful experiment easy to run.

Choose the browser when its strengths match the job

Browser-hosted .NET is a strong fit for interactive lessons, portable demonstrations, small data transformations, and tools that benefit from local execution after their assets load. Sharing a URL removes much of the setup that otherwise stands between curiosity and a first C# program. The same managed language can also help an existing .NET team reuse carefully chosen domain logic across client and server environments.

Choose a server or installed development environment when the task requires durable background work, broad native integration, privileged credentials, large workloads, or an authoritative multi-user data store. You can combine environments rather than forcing one to absorb every responsibility. A browser can handle editing and immediate feedback while a backend owns protected operations and an installed SDK handles full builds and deployment checks.

The most reliable decision comes from a short capability inventory. Name the required APIs, expected data sizes, supported devices, network assumptions, state lifetime, and failure recovery behavior. Test the uncertain items early. WebAssembly makes .NET available in a remarkable place, but the practical value comes from understanding that place precisely. Clear boundaries let you build useful tools and teach real C# without promising capabilities the host has not established.

Sources & further reading

Technical references checked against primary documentation. Browser playground examples target the SharpRune .NET 9 profile; production server guidance describes a separate hosting environment.

Illustration created with ImageGen for SharpRune. Article and examples are educational; validate behavior in your target runtime.
Put an idea into practice