Skip to content

Yes, You Can Run C# Code in Microsoft Fabric Notebooks

Photo by Negative Space: https://www.pexels.com/photo/grayscale-photo-of-computer-laptop-near-white-notebook-and-ceramic-mug-on-table-169573/

As of today the Microsoft Fabric Notebooks supports following languages:

  • PySpark (Python)
  • Spark (Scala)
  • Spark SQL
  • SparkR

If you are coming to Fabric world with an OOP programming background (like I do), those aren’t the languages you have most likely used. Of course, Python is not that hard of a language to learn, and the dataframes are sooo good for data manipulation that you will probably want to learn it anyway. Still, what if you want to develop things with TDD? What if you want to have a good testing suite around the code you are running in the notebook? Then Jupyter Notebooks aren’t the most practical tool. From this background, I had an idea: what if you could run C# code in Fabric Notebooks? Well, I found out that you can!

Pythonnet

The whole solution is build around a very interesting Python package called pythonnet. Pythonnet is a package that enables nearly seamless integration with the .NET Common Language Runtime (CLR). It allows Python code to interact with the CLR meaning, that we can call .NET methods directly from Python and that is exactly what we want to do.

Pythonnet is not part of Fabric Spark Pool, so we need to either preinstall it into Fabric Environment or install it with pip. You can install the pip package by placing this cell on top of your Notebook.

pip install pythonnet

Do We Have Dotnet?

Next big thing to know is that do we have a dotnet? If we don’t have any dotnet runtime installed we are pretty much screwed, because we cannot manually install software into our spark pools. Luckily the Fabric Spark pools comes with two different dotnet versions. I used following code to check what dotnet versions are available.

import subprocess

def check_dotnet():
    try:
        result = subprocess.run(
            ["dotnet", "--list-runtimes"],
            capture_output=True,
            text=True,
            check=True
        )
        print("✅ .NET runtimes installed:")
        print(result.stdout)
        return True
    except FileNotFoundError:
        print("❌ dotnet command not found.")
        return False
    except subprocess.CalledProcessError as e:
        print("⚠️ dotnet found but error running it:", e)
        return False

check_dotnet()

The code returns following output stating that we have .NET 6.0 and 8.0 available. + of course this code returns the True statement to state that .NET is available. How nice.

✅ .NET runtimes installed:
Microsoft.AspNetCore.App 6.0.36 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 8.0.20 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 6.0.36 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
Microsoft.NETCore.App 8.0.20 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

True

Python Shared Library

Ok, now we know that we have .NET 8.0 available and we can use it as our CLR. The next possible showstopper is that before we do anything with pythonnet we need to set PYTHONNET_PYDLL environment variable. The CLR needs to know the exact Python shared library to link with. If we don’t do this, we aren’t able to run the pythonnet properly. By running some simple outputs code we can find out that the python libraries lies in /home/trusted-service-user/cluster-env/trident_env/lib folder.

import os

libdir = "/home/trusted-service-user/cluster-env/trident_env/lib"

for f in os.listdir(libdir):
    if "libpython" in f:
        print(f)

The code above outputs following details about Python shared libraries. Now this code hard-codes us into this specific version of Python, but we can solve that later if we need more robust solution.

libpython3.11.so
libpython3.so
libpython3.11.so.1.0

Now we know what .NET versions we have and where the Python shared library is located, so we can set the PYTHONNET_PYDLL environment variable and initialize the CLR. Finally lets add print statement to the end of this Notebook cell to indicate that everything is initialized correctly.

import sys, sysconfig, os

# 1. Auto-detect the correct libpython shared object
libdir = "/home/trusted-service-user/cluster-env/trident_env/lib"
libname = "libpython3.11.so"
libpath = os.path.join(libdir, libname)

if not os.path.exists(libpath):
    raise FileNotFoundError(f"Python shared lib not found at {libpath}")

# 2. Set PYTHONNET_PYDLL BEFORE touching pythonnet
os.environ["PYTHONNET_PYDLL"] = libpath

# You can place this code in separate cell also.
# 3. Now safely import pythonnet
from pythonnet import load
load("coreclr")

import clr
print("✅ pythonnet initialized with", libpath)

.NET Code

Before we move on lets look what my C# sample code looks like. I wanted to use some simple code in this sample to avoid confusion with parameter handling and return types. I decide to create a simple calculator class which has methods to add, subtract, multiply and divide numbers. The parameters are just integers and return types are basic numbers. I think you can use any of the parameter types that pythonnet supports, but I haven’t yet tried them out.

To run the C# code in Fabric we need a DLL file that in this sample we will load it directly from Lakehouse. Of course most easiest way to get DLL is to create a class library project and build it. Note that in this sample I have Calculator as namespace and class name.

namespace Calculator
{
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        public int Subtract(int a, int b) => a - b;
        public int Multiply(int a, int b) => a * b;
        public double Divide(int a, int b) => b != 0 ? (double)a / b : throw new DivideByZeroException();

    }
}

Run The Code

As stated above, I think the most practical place to store the code (or DLL file) is in Fabric Lakehouse. I uploaded the DLL file into Lakehouse through Fabric UI and attached the Lakehouse as Data source for Notebook. This way I can easily load the DLL from Lakehouse by simply referencing it as a path.

dll_path = "/lakehouse/default/Files/Calculator.dll"
clr.AddReference(dll_path)

Finally we can invoke the C# code simply by importing class from a namespace and calling the methods with C# method names.

from Calculator import Calculator
obj = Calculator()

result = obj.Add(15, 10)
print(result) # 25

# How cool is that?!

Summary

I personally think that this is really cool. We could have a GitHub Action that builds the C# code on every Git commit, uploads the built DLL file into Fabric Lakehouse using Azure Data Lake NuGet package and this way update the C# code for our Notebook. All automated nicely.

As of my knowledge this is currently the only way to run C# code in Microsoft Fabric. You could of course create Azure Function with C# and call it from Fabric, but if you want to run the code without any extra Azure resources I don’t know any other way.

2 thoughts on “Yes, You Can Run C# Code in Microsoft Fabric Notebooks”

    1. You are absolutely right on that and the title might be a bit lively, but you can deploy the C# code DLL into Lakehouse, load it with CLR and invoke class methods without running the whole “app”. I think calling it “running the C# code” is not that big of a lie.

Comments are closed.