blog post image
Andrew Lock avatar

Andrew Lock

~8 min read

Exploring .NET interactive notebooks with VS Code

In a recent series I described the process of creating a Simple Moving Average calculator. A couple of days later Khalid messaged me on twitter:

This was something that I knew wouldn't be hard, but I wanted to validate the code before sending a reply. My normal go-to approach would be to create a console app for testing, but instead I decided to try out something I'd heard about recently: .NET Interactive Notebooks.

What are .NET Interactive Notebooks?

.NET Interactive Notebooks have grown out of two things: .NET Interactive, and Jupyter notebooks.

.NET Interactive is the new name for "Try .NET", a project from Microsoft to make .NET available in more places, and with much lower ceremony than is typically required for .NET projects. It's a group of APIs and CLI tools that let users easily try out and work with .NET. One great example is the .NET in-browser tutorial (powered by Blazor) that lets you try .NET in your browser.

Jupyter notebooks came from the Python community, and they are a way of creating documents that mix code with formatted text, by way of Markdown. This can be a great for exploratory work in particular, as you combine your code with textual descriptions of what you're trying. You can document your steps, write some code, print the results, and iterate. This workflow makes them very popular for data science and analysis in particular.

Example of a .NET interactive notebook

Jupyter notebooks run as client-server applications, though you typically run both the client and server on your own machine. You author your notebook on the client in an IDE/editor, and your code is executed by sending it to the server (called the kernel). The results are then displayed in your notebook.

.NET Interactive Notebooks essentially adds a .NET story for Jupyter notebooks. The original Jupyter notebooks supported 3 language (Julia, Python, and R; hence the name Jupyter) but these days you can find kernels for many different lanuguages. .NET Interactive provides a .NET kernel and VS Code extension called .NET Interactive Notebooks.

Installing .NET Interactive Notebooks

Jupyter notebooks have been on my radar for a while, and there has been a .NET Core kernel for Jupyter since 2019. The thing that always put me off was the fact you had to install Jupyter, which requires installing Anaconda, which requires installing Python… Yawn, yeah, managing a Python environment was 100% something I had no interest in doing, so I left it alone.

Luckily my patience paid off, and they've massively simplified the requirements, by making .NET Interactive Notebooks a simple VS Code extension! 🎉 This is still in preview at the moment, so it will likely hit some bumps along the road, but so far the experience has been super slick for me.

To get started, make sure you have:

I'm going to assume most people reading these already have those installed, so the only other step required is to install the .NET Interactive Notebooks Extension. You can install this from VS Code by going to Extensions > .NET Interactive Notebooks > Install, or install it directly from the VS Code Marketplace.

.NET Interactive Notebook extension in VS Code

And that's it! No need to install Python or anything else for that matter. The .NET Interactive Notebooks Extension (currently in preview) relies on functionality built-in to VS Code so it Just Works™.

Creating your first .NET Interactive Notebook

To create your first notebook, open the command pallet (F1 or Ctrl+Shift+P) on Windows, and choose .NET Interactive: Create new blank notebook:

Create new blank notebook

Next you'll be presented with a choice of file formats for your notebook:

Choosing a notebook format

  • .ipynb — This is a widely used format, very commonly used by Jupyter notebooks.
    • Very common format.
    • There are lots of tools for working with them.
    • Also stores the output of your code executions, as well as the text of the notebook, so you need to be careful about sharing the notebooks if you're working with sensitive data.
    • You can (apparently) run into "file ownership" issues if you have other Jupyter extensions installed in VS Code
  • .dib — This as an experimental format used by .NET Interactive only (in discussion on GitHub).
    • Currently only used by the VS Code extension.
    • Enables additional features, such as multi-language code cells.
    • Currently doesn't store the results of code execution

I went with the .dib format in this case, as I don't need to interact with other things, and I'm not worried about saving the results in the file itself.

Working with cells in notebooks

After creating the file, you're presented with an empty notebook, consisting of a single cell:

A blank notebook

Cells can be either code or markdown, and the code can (currently) be C#, F#, HTML, PowerShell, JavaScript, or SQL. You'll get IntelliSense inside the code cells, and when you run the code (using the play ▷ icon by the cell), the results (if any) will be displayed below the cell:

My first notebook

You can add additional cells below by clicking the "Code" or "Markdown" buttons at the bottom of the cell, so you can easily flick back and forth between code and description.

Using multiple cells, I was able to quickly piece together the bits of code I needed to solve the original question - combining the SimpleMovingAverage with IAsyncEnumerable (I show the complete solution at the end of this post).

Solving the SMA problem

I actually, really like this approach. It's like a blog post, but with actual runnable code. Normally I would create a sample project, and then copy-paste bits into the blog post. That inevitably ends up with typos as I tweak things, but a notebook would avoid that issue entirely!

It would be awesome if there was some way of embedding this experience (in read only form) on my blog. Something…something…WebAssembly… Probably too big an ask, but would be a really nice experience I think!

Keyboard shortcuts in .NET interactive notebooks

As someone who likes to stick to keyboard shortcuts where possible, it took me a little while to get the hang of moving about in the notebook. Some of the shortcuts listed in the command pallet don't seem to work (I'm looking at you, "Insert cell below"), but these are the ones I found useful:

  • Start editing highlighted cell: Enter
  • Stop editing highlighted cell: Esc
  • Delete highlighted cell: Delete
  • Execute cell and insert new cell below: Alt+Enter
  • Execute cell only: Ctrl+Alt+Enter

Annoyingly, a lot of the built-in shortcuts didn't seem to work for me, but the only ones I found I needed to be productive were to switch between Markdown/Code, so I added additional keyboard shortcuts so that Alt+m would toggle between the modes on a cell.


{
    "key": "alt+m",
    "command": "notebook.cell.changeToMarkdown",
    "when": "notebookEditorFocused && !inputFocus && activeEditor == 'workbench.editor.notebook' && notebookCellType == 'code'"
}
{
    "key": "alt+m",
    "command": "notebook.cell.changeToCode",
    "when": "notebookEditorFocused && !inputFocus && activeEditor == 'workbench.editor.notebook' && notebookCellType == 'markdown'"
}

Overall, I'm very impressed with the notebook experience, and I might well start doing more of my code exploration in them, we'll see!

Using a Simple Moving Average with IAsyncEnumerable

For completeness, I've included the output of my exploratory notebook below. You can find the original at this link.

Simple Moving Average and IAsyncEnumerable

Khalid said:

I decided to look into it!

First step, we have the existing Simple Moving Average calculator from my post.

public class SimpleMovingAverage
{
    private readonly int _k;
    private readonly int[] _values;

    private int _index = 0;
    private int _sum = 0;

    public SimpleMovingAverage(int k)
    {
        if (k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "Must be greater than 0");

        _k = k;
        _values = new int[k];
    }

    public double Update(int nextInput)
    {
        _sum = _sum - _values[_index] + nextInput;
        _values[_index] = nextInput;
        _index = (_index + 1) % _k;
        return ((double) _sum) / _k;
    }
}

Khalid said he wanted to take an IAsyncEnumerable of inputs, and retrieve an IAsyncEnumerable of outputs. This is essentially just a mapping from one IAsyncEnumerable to another.

There's a few ways we could do that, but to keep everything encapsulated, I created an SmaConverter class that wraps the SimpleMovingAverage to do the conversion:

public class SmaConverter
{
    private readonly SimpleMovingAverage _sma = new(k: 3);
    public async IAsyncEnumerable<double> GetAverage(IAsyncEnumerable<int> inputs)
    {
        await foreach (int input in inputs)
            yield return _sma.Update(input);
    }
}

That's all we need to do the conversion, so now lets create a class to get our IAsyncEnumerable input:

async IAsyncEnumerable<int> GetInputs()
{
    await Task.Yield();
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 2;
    yield return 2;
    yield return 1;
    yield return 1;
    yield return 3;
    yield return 3;
    yield return 3;
}

And finally, lets put it all together

var converter = new SmaConverter();
var values = GetValues();
await foreach(var average in converter.GetAverage(values))
{
    Console.WriteLine($"Next average is {average:0.00}");
}
Next average is 0.33
Next average is 1.00
Next average is 2.00
Next average is 2.33
Next average is 2.33
Next average is 1.67
Next average is 1.33
Next average is 1.67
Next average is 2.33
Next average is 3.00

And voila!

Summary

In this post I looked at the new .NET Interactive Notebooks extension available for VS Code. To use it you can just install the extension directly—you don't need to install Python or Anaconda, which has been a barrier to me trying it before now. I found the experience of working with Notebooks to be really good, like writing a blog post with code verification, and without the friction of flicking back and forth from a console project! I'll definitely think about them when exploring ideas, and maybe as the backing for future blog posts:

Andrew Lock | .Net Escapades
Want an email when
there's new posts?