Create a Blazor App with ML.NET

Create a Blazor App with ML.NET

Create a Blazor App with ML.NET

I wanted to add sentiment analysis to a Blazor app without calling an external API, paying for tokens, or setting up a Python service on the side. ML.NET lets you do that with a couple of NuGet packages and zero infrastructure. The model trains and runs in-process, right inside your .NET app.

By the end of this post, you’ll have a working Blazor app that predicts whether a piece of text is positive or negative.

A complete, working example is available here: mongeon/code-examples · blazor-mlnet-sentiment.

What is ML.NET?

ML.NET is an open-source machine learning framework for .NET. It supports classification, regression, clustering, and anomaly detection, all in C#. No Python, no external service. It runs in your process, which makes deployment simple: just ship your app.

Setting up the project

Create a new Blazor Web App with server-side interactivity:

dotnet new blazor -o BlazorMLNET --interactivity Server
cd BlazorMLNET

Add the ML.NET packages:

dotnet add package Microsoft.ML --version 5.0.0
dotnet add package Microsoft.ML.FastTree --version 5.0.0

FastTree gives us a solid binary classification algorithm for sentiment analysis.

Data models

Create a Models folder and add a SentimentModel.cs file:

using Microsoft.ML.Data;

namespace BlazorMLNET.Models
{
    public class SentimentData
    {
        [LoadColumn(0)]
        public string SentimentText { get; set; } = string.Empty;

        [LoadColumn(1), ColumnName("Label")]
        public bool Sentiment { get; set; }
    }

    public class SentimentPrediction
    {
        [ColumnName("PredictedLabel")]
        public bool Prediction { get; set; }

        public float Probability { get; set; }

        public float Score { get; set; }
    }
}

SentimentData is the input. SentimentPrediction is what the model gives back: a prediction, a probability, and a raw score.

The ML service

Add MLService.cs in the same Models folder. This handles training, loading, and predicting:

using Microsoft.ML;
using Microsoft.AspNetCore.Hosting;
using System.IO;
using System;
using System.Threading.Tasks;

namespace BlazorMLNET.Models
{
    public class MLService
    {
        // Not thread-safe: fine for a demo, use PredictionEnginePool in production
        private PredictionEngine<SentimentData, SentimentPrediction>? _predictionEngine;
        private readonly string _modelPath;

        public MLService(IWebHostEnvironment env)
        {
            _modelPath = Path.Combine(env.WebRootPath, "sentiment_model.zip");
        }

        public async Task TrainAndSaveModelAsync()
        {
            await Task.Run(() =>
            {
                var mlContext = new MLContext();

                // Small dataset for demo purposes. In production, use thousands of samples
                var sampleData = new[]
                {
                    new SentimentData { SentimentText = "I love ML.NET!", Sentiment = true },
                    new SentimentData { SentimentText = "This is a great product.", Sentiment = true },
                    new SentimentData { SentimentText = "I would recommend this to my friends.", Sentiment = true },
                    new SentimentData { SentimentText = "The documentation is clear and helpful.", Sentiment = true },
                    new SentimentData { SentimentText = "The API is easy to use.", Sentiment = true },
                    new SentimentData { SentimentText = "I don't like this at all.", Sentiment = false },
                    new SentimentData { SentimentText = "This product is terrible.", Sentiment = false },
                    new SentimentData { SentimentText = "I had a bad experience with this.", Sentiment = false },
                    new SentimentData { SentimentText = "The customer service was unhelpful.", Sentiment = false },
                    new SentimentData { SentimentText = "I'm very disappointed.", Sentiment = false },
                };

                var dataView = mlContext.Data.LoadFromEnumerable(sampleData);
                var dataSplit = mlContext.Data.TrainTestSplit(dataView, testFraction: 0.2);

                var pipeline = mlContext.Transforms.Text.FeaturizeText("Features", "SentimentText")
                    .Append(mlContext.BinaryClassification.Trainers.FastTree(numberOfLeaves: 50, numberOfTrees: 50));

                Console.WriteLine("Training the model...");
                var model = pipeline.Fit(dataSplit.TrainSet);

                var predictions = model.Transform(dataSplit.TestSet);
                var metrics = mlContext.BinaryClassification.Evaluate(predictions);
                Console.WriteLine($"Accuracy: {metrics.Accuracy:P2}, F1: {metrics.F1Score:P2}");

                mlContext.Model.Save(model, dataView.Schema, _modelPath);
                _predictionEngine = mlContext.Model.CreatePredictionEngine<SentimentData, SentimentPrediction>(model);
            });
        }

        public bool LoadModel()
        {
            var mlContext = new MLContext();

            if (File.Exists(_modelPath))
            {
                var model = mlContext.Model.Load(_modelPath, out var _);
                _predictionEngine = mlContext.Model.CreatePredictionEngine<SentimentData, SentimentPrediction>(model);
                return true;
            }

            return false;
        }

        public SentimentPrediction PredictSentiment(string text)
        {
            if (_predictionEngine == null)
            {
                LoadModel();
                if (_predictionEngine == null)
                    throw new InvalidOperationException("Model is not loaded. Train the model first.");
            }

            return _predictionEngine.Predict(new SentimentData { SentimentText = text });
        }
    }
}

The service tries to load a saved model on first prediction. If no model file exists, PredictSentiment throws, so you need to train first. In the Razor component below, OnInitializedAsync handles this: it tries to load, and trains if the file is missing. In a real app, you’d train offline with a much larger dataset.

The Razor component

Create SentimentAnalysis.razor in the Components/Pages folder:

@page "/sentiment"
@using BlazorMLNET.Models
@inject MLService MLService

<div class="container mt-5">
    <h1>Sentiment Analysis with ML.NET</h1>

    <div class="row mt-4">
        <div class="col-md-12">
            <div class="card">
                <div class="card-header bg-primary text-white">
                    <h3 class="mb-0">Text Sentiment Analyzer</h3>
                </div>
                <div class="card-body">
                    <div class="mb-3">
                        <label for="sentimentText">Enter text to analyze:</label>
                        <textarea
                            id="sentimentText"
                            class="form-control"
                            rows="5"
                            @bind="InputText"
                            placeholder="Type some text here..."></textarea>
                    </div>

                    <button class="btn btn-primary mt-3" @onclick="AnalyzeSentiment">
                        Analyze Sentiment
                    </button>

                    @if (isAnalyzing)
                    {
                        <div class="mt-3">
                            <div class="spinner-border text-primary" role="status">
                                <span class="visually-hidden">Loading...</span>
                            </div>
                            <span class="ms-2">Analyzing...</span>
                        </div>
                    }

                    @if (showResult)
                    {
                        <div class="mt-4">
                            <div class="card @(isPredictionPositive ? "border-success" : "border-danger")">
                                <div class="card-header @(isPredictionPositive ? "bg-success" : "bg-danger") text-white">
                                    <h4 class="mb-0">Result</h4>
                                </div>
                                <div class="card-body">
                                    <h5 class="card-title">
                                        Sentiment: @(isPredictionPositive ? "Positive" : "Negative")
                                    </h5>
                                    <p class="card-text">
                                        <strong>Confidence:</strong> @(confidenceScore.ToString("P2"))
                                    </p>
                                    <div class="progress mt-3">
                                        <div class="progress-bar @(isPredictionPositive ? "bg-success" : "bg-danger")"
                                             role="progressbar"
                                             style="width: @(confidenceScore * 100)%"
                                             aria-valuenow="@(confidenceScore * 100)"
                                             aria-valuemin="0"
                                             aria-valuemax="100">
                                            @(confidenceScore.ToString("P0"))
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

@code {
    private string InputText { get; set; } = "";
    private bool isAnalyzing = false;
    private bool showResult = false;
    private bool isPredictionPositive = false;
    private float confidenceScore = 0;

    protected override async Task OnInitializedAsync()
    {
        if (!MLService.LoadModel())
            await MLService.TrainAndSaveModelAsync();
    }

    private async Task AnalyzeSentiment()
    {
        if (string.IsNullOrWhiteSpace(InputText))
            return;

        isAnalyzing = true;
        showResult = false;

        try
        {
            var prediction = await Task.Run(() => MLService.PredictSentiment(InputText));
            showResult = true;
            isPredictionPositive = prediction.Prediction;
            confidenceScore = prediction.Probability;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
        finally
        {
            isAnalyzing = false;
        }
    }
}

Wiring it up

Register the service in Program.cs. The Blazor template already has the AddRazorComponents call, so you just need to add the MLService singleton:

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddSingleton<MLService>();

And further down, make sure you have:

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

Add a link in Components/Layout/NavMenu.razor:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="sentiment">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Sentiment Analysis
    </NavLink>
</div>

Run it:

dotnet run

Navigate to /sentiment and try a few sentences. The model trains on first load if there’s no saved file.

Things to keep in mind

  • Cache your PredictionEngine. Creating one is expensive. The service above keeps it as a field. Good enough for a demo, but look into PredictionEnginePool for production.
  • Load models off the UI thread. The OnInitializedAsync approach works here, but for larger models you’ll want background loading.
  • Model size matters for WASM. With Blazor Server this isn’t a problem since the model stays on the server. For WebAssembly, the model ships to the browser. Watch the file size.
  • Train with real data. The 10-sample dataset above is just to get something running. A real model needs thousands of labeled examples.

Resources

ML.NET won’t replace a fine-tuned transformer model, but for straightforward classification tasks inside a .NET app, it’s hard to beat. No API key, no latency, no bill at the end of the month. Happy predicting!

This post was written with AI assistance and edited by me.

.NET  C#  Blazor  ML.NET  AI 

See also