Skip to main content

Build a Recommendation System in C# with ML.NET

Learn how to build a recommendation system in C# using ML.NET collaborative filtering. Step-by-step tutorial with full code — start building today.

Every time Netflix suggests your next binge or Amazon shows you "customers also bought," you're seeing a recommendation system at work. These systems drive an enormous share of engagement and revenue for the world's biggest platforms — and thanks to ML.NET, you can build a production-ready recommendation system in C# without leaving the .NET ecosystem or writing a single line of Python.

In this tutorial, you'll build a movie recommendation engine using collaborative filtering with ML.NET's matrix factorization trainer. We'll cover the theory (just enough to make smart decisions), the full working code, how to evaluate your model, and the pitfalls that trip up most developers on their first attempt.

What Is a Recommendation System and How Does Collaborative Filtering Work?

A recommendation system predicts how much a user will like an item they haven't interacted with yet. There are two dominant approaches:

  • Content-based filtering recommends items similar to what a user already liked, based on item attributes (genre, price, keywords). It works from day one but tends to recommend more of the same.
  • Collaborative filtering recommends items based on the behavior of similar users. If you and I rate ten movies the same way, and I loved a movie you haven't seen, there's a good chance you'll love it too. No item metadata required.

Collaborative filtering is the approach behind most large-scale recommenders, and it's what ML.NET supports natively through matrix factorization.

Matrix Factorization in Plain English

Imagine a giant spreadsheet: users as rows, movies as columns, ratings in the cells. Most cells are empty — no user has rated everything. Matrix factorization decomposes this sparse matrix into two smaller dense matrices: one describing users, one describing items, each in terms of hidden "latent factors."

You never define these factors — the algorithm learns them. One factor might roughly capture "prefers action over drama," another "likes older films." Multiply a user's factor vector by a movie's factor vector and you get a predicted rating for a cell that was empty. That's the WHY behind the magic: the model learns compressed taste profiles and uses them to fill in the blanks.

Setting Up Your ML.NET Project

You need the .NET SDK (8 or later) and two NuGet packages. Create a console app and add the packages:

dotnet new console -n MovieRecommender
cd MovieRecommender
dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.Recommender

Note that Microsoft.ML.Recommender is a separate package — forgetting it is the number one cause of "MatrixFactorization not found" errors.

For data, we'll use the classic MovieLens format: a CSV of userId,movieId,rating,timestamp. You can download the free MovieLens 100K dataset, or use any of your own user–item–rating data (purchases, clicks, and stars all work).

Defining the Data Models

ML.NET maps your data to strongly typed classes. Define one class for input and one for the prediction output:

using Microsoft.ML.Data;

public class MovieRating
{
    [LoadColumn(0)]
    public float UserId { get; set; }

    [LoadColumn(1)]
    public float MovieId { get; set; }

    [LoadColumn(2)]
    public float Label { get; set; } // the rating — ML.NET expects "Label"
}

public class MovieRatingPrediction
{
    public float Label { get; set; }
    public float Score { get; set; } // the predicted rating
}

Two details matter here. First, the rating column is named Label because ML.NET trainers look for that name by convention. Second, the IDs are float even though they're logically integers — the loader reads numeric columns as single-precision floats, and we'll convert them to keys in the pipeline.

How to Build a Recommendation System in C# — the Full Pipeline

Here's the complete training program. We load the data, split it into training and test sets, map raw IDs to keys, and train the matrix factorization model:

using Microsoft.ML;
using Microsoft.ML.Trainers;

var mlContext = new MLContext(seed: 0);

// 1. Load the data
IDataView data = mlContext.Data.LoadFromTextFile<MovieRating>(
    "ratings.csv", hasHeader: true, separatorChar: ',');

// 2. Split into train (80%) and test (20%)
var split = mlContext.Data.TrainTestSplit(data, testFraction: 0.2);

// 3. Build the pipeline: IDs must be converted to keys
var options = new MatrixFactorizationTrainer.Options
{
    MatrixColumnIndexColumnName = "UserIdKey",
    MatrixRowIndexColumnName = "MovieIdKey",
    LabelColumnName = "Label",
    NumberOfIterations = 30,
    ApproximationRank = 64,       // number of latent factors
    LearningRate = 0.1,
    Lambda = 0.05                 // regularization
};

var pipeline = mlContext.Transforms.Conversion
    .MapValueToKey("UserIdKey", nameof(MovieRating.UserId))
    .Append(mlContext.Transforms.Conversion
        .MapValueToKey("MovieIdKey", nameof(MovieRating.MovieId)))
    .Append(mlContext.Recommendation().Trainers
        .MatrixFactorization(options));

// 4. Train
Console.WriteLine("Training the model...");
ITransformer model = pipeline.Fit(split.TrainSet);

// 5. Evaluate on held-out data
var predictions = model.Transform(split.TestSet);
var metrics = mlContext.Regression.Evaluate(predictions);

Console.WriteLine($"RMSE: {metrics.RootMeanSquaredError:F3}");
Console.WriteLine($"R²:   {metrics.RSquared:F3}");

Why each step exists:

  • MapValueToKey converts arbitrary user and movie IDs into contiguous internal indices. Matrix factorization literally builds a matrix, so it needs row and column indices, not raw IDs like 90210.
  • TrainTestSplit holds back 20% of ratings the model never sees during training. Evaluating on training data would tell you nothing — any model can memorize what it was shown.
  • ApproximationRank is the number of latent factors. More factors capture subtler taste patterns but risk overfitting and slow training. Values between 32 and 128 are a sensible range for most datasets.
  • Lambda (regularization) penalizes extreme factor values, which keeps the model from over-explaining noise in sparse data.

Making Predictions and Generating Top-N Recommendations

A single prediction uses the PredictionEngine:

var engine = mlContext.Model
    .CreatePredictionEngine<MovieRating, MovieRatingPrediction>(model);

var prediction = engine.Predict(new MovieRating
{
    UserId = 6,
    MovieId = 25
});

Console.WriteLine($"Predicted rating for user 6, movie 25: {prediction.Score:F2}");

In practice, users don't want one score — they want a ranked list. To build "Top 5 movies for this user," score every candidate movie the user hasn't rated and take the best:

List<(float MovieId, float Score)> RecommendTop5(
    PredictionEngine<MovieRating, MovieRatingPrediction> engine,
    float userId,
    IEnumerable<float> candidateMovieIds)
{
    return candidateMovieIds
        .Select(movieId => (movieId,
            engine.Predict(new MovieRating { UserId = userId, MovieId = movieId }).Score))
        .OrderByDescending(x => x.Item2)
        .Take(5)
        .ToList();
}

For catalogs with millions of items, scoring everything per request is too slow — production systems precompute recommendations in a batch job (nightly, for example) and serve them from a cache or database. ML.NET's batch scoring via model.Transform on an IDataView of all user–item pairs is far faster than looping over Predict.

Saving and Loading the Model

Train once, serve many times. Persist the model to a file and load it in your API or worker service:

// Save after training
mlContext.Model.Save(model, split.TrainSet.Schema, "movie-recommender.zip");

// Load in your web app or service
ITransformer loadedModel = mlContext.Model.Load("movie-recommender.zip", out var schema);

In ASP.NET Core, register a PredictionEnginePool (from the Microsoft.Extensions.ML package) rather than creating engines per request — PredictionEngine is not thread-safe, and the pool handles that for you.

Evaluating Your Model: What Do the Numbers Mean?

Two metrics matter most for rating prediction:

  • RMSE (Root Mean Squared Error) — the average prediction error in rating units. An RMSE of 0.9 on a 1–5 scale means predictions are off by slightly under one star on average. Lower is better; on MovieLens 100K, anything under ~1.0 is respectable.
  • R² (R-squared) — how much of the rating variance the model explains. Closer to 1 is better; a negative R² means your model is worse than just predicting the average rating for everyone, which usually signals a data or configuration bug.

Remember, though: offline metrics are a proxy. A model with a slightly worse RMSE can still produce better recommendations if it surfaces diverse, relevant items. The gold standard is an online A/B test measuring clicks, watch time, or purchases.

Best Practices and Common Pitfalls

Best Practices

  • Set a seed on MLContext (new MLContext(seed: 0)) during development so runs are reproducible. Remove it in production if you want natural variance.
  • Tune hyperparameters systematically. Grid-search ApproximationRank, LearningRate, and Lambda against your test-set RMSE instead of guessing.
  • Use implicit feedback when you lack ratings. Most real applications don't have star ratings — they have clicks and purchases. ML.NET supports one-class matrix factorization (LossFunctionType.SquareLossOneClass) built exactly for "user interacted / didn't interact" data.
  • Retrain on a schedule. Tastes drift and new items arrive. A nightly or weekly retraining job keeps recommendations fresh.
  • Filter already-consumed items before presenting recommendations — nobody wants to be recommended the movie they watched yesterday.

Common Pitfalls

  • The cold-start problem. Collaborative filtering cannot recommend anything for a brand-new user or item — there's no interaction history to factorize. Mitigate it with a hybrid approach: show popular or content-based recommendations until a user has a handful of interactions, then switch to collaborative filtering.
  • Skipping MapValueToKey. Feeding raw IDs straight into the trainer either fails or silently produces a bloated, meaningless matrix. Always convert IDs to keys.
  • Evaluating on training data. You'll see a fantastic RMSE that evaporates in production. Always hold out a test set.
  • Sharing PredictionEngine across threads. It's stateful and not thread-safe. In web apps, use PredictionEnginePool.
  • Predicted scores outside the rating range. Matrix factorization can output 5.4 or 0.7 on a 1–5 scale. Clamp scores before displaying them: Math.Clamp(prediction.Score, 1f, 5f).
  • Popularity bias. The model naturally favors blockbusters everyone rates highly. If discovery matters to your product, consider re-ranking to inject diversity.

Conclusion: Your Recommendation System in C# Is Just the Beginning

You've now built a complete recommendation system in C#: loading user–item–rating data, training a collaborative filtering model with ML.NET's matrix factorization trainer, evaluating it with RMSE, generating Top-N recommendations, and shipping the model in a serialized file your ASP.NET Core app can serve.

Key takeaways:

  • Collaborative filtering learns from user behavior alone — no item metadata needed — by factorizing the sparse ratings matrix into latent taste factors.
  • ML.NET makes this a first-class C# workflow: MapValueToKeyMatrixFactorizationFitEvaluateSave.
  • Hold out a test set, tune ApproximationRank and Lambda, and judge results by RMSE offline but by user engagement online.
  • Plan for cold starts with a popularity or content-based fallback, and precompute recommendations at scale instead of scoring per request.

From here, try the one-class trainer on implicit feedback data, wrap your model in a minimal API endpoint, or blend collaborative and content-based signals into a hybrid recommender. The full power of machine learning in C# is only a NuGet package away — grab the MovieLens dataset and start experimenting today.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...