This project demonstrates how to call Python scripts from a .NET 9 Razor Pages web application, using the recently published CSnakes library to enable Python execution within .NET.
- The application is built on .NET 9 Razor Pages and integrates with CSnakes, allowing seamless Python script execution within .NET.
- A minimal API endpoint is used, together with HTMX, to call Python scripts and return dynamically generated stock graph images (PNG format) using the matplotlib and pandas libraries in Python.
- .NET 9 SDK (the solution targets
net9.0) - Python 3.12 — optional. CSnakes is configured to fall back to a NuGet-supplied
CPython 3.12.4, so a local install is only needed if you want to run the Python
tests directly, or if you set
Python3_ROOT_DIR.
cd PythonNet.App.Web
dotnet runThen open the URL printed in the console (see Properties/launchSettings.json).
On first start CSnakes creates PythonNet.Python/.venv and pip-installs
PythonNet.Python/requirements.txt into it. This takes a minute or two; progress
is visible in the console output. Subsequent starts reuse the venv.
Run from
PythonNet.App.Web, not the solution root —Program.csdefaults the Python home to<cwd>/../PythonNet.Python, andutil.read_csvresolves data files asData/<file>relative to the working directory.
To point at a Python home elsewhere (a published layout, a shared venv), override
either setting via appsettings.json or the environment:
Python__Home=/srv/pythonnet Python__VirtualEnvironment=/srv/venv dotnet rundotnet test PythonNetSolution.sln # xUnit: endpoint validation + routing
cd PythonNet.Python && pip install -r requirements.txt pytest && python -m pytestBoth are run by .github/workflows/build.yml on every push and pull request.
PythonNet.Pythonholds the Python sources. Each one is listed as an<AdditionalFiles>entry inPythonNet.Python.csproj, which is what makes the CSnakes source generator pick it up.- At build time the generator reads the type-annotated function signatures in
util.pyandplot.pyand emits matching C# interfaces —IUtilandIPlot— plusIPythonEnvironmentextension methods (.Util(),.Plot()) that return them. Program.csconfigures the runtime (WithHome,WithVirtualEnvironment,FromNuGet("3.12.4"),WithPipInstaller) and registersIUtil/IPlotas singletons, so they can be injected anywhere.MinimalApiExtension.csinjects them intoGET /api/dashboard/stock, which validates its query parameters, then chainsread_csv → date_filter → sort_data → plot → save_plotand returns an<img>fragment for HTMX to swap into the page. Each request renders to its ownwwwroot/plots/plot_{ticker}_{guid}.png— served from/dynamic-plotswith caching disabled — so concurrent callers cannot overwrite each other's chart. Files older than five minutes are swept on the next request. The naming/ error-handling/sweep logic is shared (viaRenderPlot) with the endpoint below.GET /api/dashboard/test-plotis the same pipeline, minus the CSV: it callsutil.generate_test_data(ticker, days, start_date)— a deterministic sine-wave price series, no file or network I/O — thensort_data → plot → save_plotas above. Useful for exercising the chart/theme/date-range UI without needing a real dataset. Try it via the "Test Data" page, or directly:GET /api/dashboard/test-plot?tickerDd=1&days=30&openCb=on&closeCb=on&theme=light.POST /api/dashboard/custom-plottakes a JSON body (ticker,dates,open,close, plus optionaltitle/showOpen/showClose/theme) and callsutil.build_price_data(dates, open, close)to turn those arrays straight into the samesort_data → plot → save_plotpipeline — for plotting caller-supplied data without a CSV or the synthetic generator, e.g.:curl -X POST http://localhost:5000/api/dashboard/custom-plot \ -H "Content-Type: application/json" \ -d '{"ticker":"ACME","dates":["2024-10-24","2024-10-25"],"open":[10,12],"close":[10.5,12.5]}'
Adding a Python function is therefore: annotate it, rebuild, call the generated
C# method. Adding a new .py file also needs an <AdditionalFiles> entry.
PythonNet.App.Web/Data/ ships three static CSVs (Date,Close,Open,High,Low)
covering 24–27 Oct 2024 for AAPL, MSFT and IBM, so the dashboard works offline with
no market-data API key. The dropdown values map to them in
MinimalApiExtension.Tickers; add a ticker by dropping a CSV in Data/, adding a
<None Update> copy entry in PythonNet.App.Web.csproj, and adding one line to
that dictionary and to IndexModel.Tickers.