ML.NET is Microsoft's open-source, cross-platform machine learning framework that lets .NET developers train, evaluate and run custom models entirely in C#, with no Python service on the side. It targets the classic machine learning problems that still drive most business value: classification, regression, forecasting, anomaly detection, clustering and recommendations on your own data. This guide explains how ML.NET's pipeline model works, walks through training and evaluating models, covers AutoML, serving with PredictionEnginePool and ONNX interop, and shows when ML.NET is a better choice than a large language model.

What Is ML.NET?#

ML.NET has three parts: the API (a family of Microsoft.ML.* NuGet packages), Model Builder (a Visual Studio extension with a guided UI), and the ML.NET CLI (a .NET global tool named mlnet). Models run in-process, on Windows, Linux and macOS, so predictions need no network call and work offline.

The current stable release is ML.NET 5.0, shipped in November 2025. Previews of 6.0 appeared through 2026, with the latest in September 2026. Version 5.0 added a deterministic option for LightGBM training, more tokenizer models in Microsoft.ML.Tokenizers (including SentencePiece Unigram and Phi-4 support), and a CausalLMPipelineChatClient that exposes ML.NET's TorchSharp-based generative pipelines through the IChatClient interface. The core packages you will meet most often are:

PackagePurposeStable version (Sept 2026)
Microsoft.MLCore API: MLContext, data loading, transforms, linear trainers5.0.0
Microsoft.ML.FastTree, Microsoft.ML.LightGbmGradient-boosted tree trainers5.0.0
Microsoft.ML.TimeSeriesForecasting, spike and change-point detection5.0.0
Microsoft.Extensions.MLPredictionEnginePool for ASP.NET Core and DI5.0.0
Microsoft.ML.OnnxTransformerScoring ONNX models inside pipelines5.0.0
Microsoft.ML.AutoMLAutomated model and hyperparameter search0.23.0
Microsoft.ML.OnnxConverterExporting ML.NET models to ONNX0.23.0

The 0.x version numbers on AutoML, the ONNX converter and the TorchSharp-based deep learning packages reflect separate versioning, not abandonment. Still, check release notes before you upgrade them.

How ML.NET Works: MLContext, IDataView and Pipelines#

Everything starts with an MLContext. It is similar in spirit to a DbContext: a shared environment for logging and randomness, plus a set of catalogs that act as factories. mlContext.Data loads and splits data, mlContext.Transforms prepares features, and task catalogs such as BinaryClassification, MulticlassClassification, Regression, Clustering, AnomalyDetection, Forecasting, Ranking and Recommendation expose trainers and evaluators. mlContext.Model saves and loads models.

Data flows through IDataView, a lazily evaluated, schema-aware, columnar view over rows. Nothing is read until training, evaluation or prediction pulls data through the view. As a result, ML.NET can stream datasets larger than memory, but it also means errors surface late, at Fit time.

The programming model has two central abstractions:

  • Estimators (IEstimator<T>) describe what to do: "featurize this text column" or "train logistic regression". You chain them with Append into a pipeline. Creating a pipeline executes nothing.
  • Transformers (ITransformer) are the result of calling Fit on an estimator with data. A trained model is simply the fitted transformer chain, and Transform applies it to new data.

By convention, trainers read a numeric vector column named Features and a column named Label, and they add output columns whose names depend on the task, such as Score, Probability and PredictedLabel for binary classification. Most pipeline code consists of transforms that turn your raw columns into that shape.

Getting Started: A Binary Classification Model#

The following console app trains a sentiment classifier for product reviews: load, split, featurize, train, evaluate and save. It needs only the Microsoft.ML package.

C#
using Microsoft.ML;
using Microsoft.ML.Data;

var ml = new MLContext(seed: 42); // Fixed seed for reproducible splits and training.

IDataView data = ml.Data.LoadFromTextFile<ReviewInput>(
    "reviews.tsv", hasHeader: true, separatorChar: '\t');

DataOperationsCatalog.TrainTestData split = ml.Data.TrainTestSplit(data, testFraction: 0.2);

var pipeline = ml.Transforms.Text.FeaturizeText("Features", nameof(ReviewInput.Text))
    .Append(ml.BinaryClassification.Trainers.SdcaLogisticRegression(
        labelColumnName: nameof(ReviewInput.IsPositive)));

ITransformer model = pipeline.Fit(split.TrainSet);

CalibratedBinaryClassificationMetrics metrics = ml.BinaryClassification.Evaluate(
    model.Transform(split.TestSet), labelColumnName: nameof(ReviewInput.IsPositive));

Console.WriteLine($"Accuracy {metrics.Accuracy:P1} | AUC {metrics.AreaUnderRocCurve:F3} | " +
                  $"F1 {metrics.F1Score:F3}");

ml.Model.Save(model, data.Schema, "sentiment.zip");

public sealed class ReviewInput
{
    [LoadColumn(0)] public string Text { get; set; } = "";
    [LoadColumn(1)] public bool IsPositive { get; set; }
}

public sealed class ReviewPrediction
{
    [ColumnName("PredictedLabel")] public bool IsPositive { get; set; }
    public float Probability { get; set; }
    public float Score { get; set; }
}

FeaturizeText normalizes the text, tokenizes it, and produces word and character n-gram counts in a single vector. The SDCA logistic regression trainer is fast, linear and calibrated, so Probability is meaningful. Always evaluate on the held-out test set, never on the training data.

Loading and Transforming Data#

ML.NET can load delimited files with LoadFromTextFile, in-memory collections with LoadFromEnumerable, and relational data with CreateDatabaseLoader over any ADO.NET provider. In production, training data usually comes from a database query or a data lake export, so keep the loading code separate from the pipeline definition.

Real datasets mix text, categories and numbers. Each kind needs a different transform before the columns are concatenated into Features:

TransformCatalog methodUse it for
Text featurizationTransforms.Text.FeaturizeTextFree text such as titles, descriptions and comments
One-hot encodingTransforms.Categorical.OneHotEncodingLow-cardinality categories such as country or plan
Hash encodingTransforms.Categorical.OneHotHashEncodingHigh-cardinality categories such as product IDs
NormalizationTransforms.NormalizeMinMax, NormalizeMeanVarianceNumeric features for linear trainers and k-means
Missing valuesTransforms.ReplaceMissingValuesNumeric columns with NaN values
Key mappingTransforms.Conversion.MapValueToKey, MapKeyToValueString labels for multiclass classification
ConcatenationTransforms.ConcatenateBuilding the final Features vector

Here is a multiclass pipeline that routes support tickets to teams:

C#
var pipeline = ml.Transforms.Conversion.MapValueToKey("Label", nameof(Ticket.Team))
    .Append(ml.Transforms.Text.FeaturizeText("TitleFeats", nameof(Ticket.Title)))
    .Append(ml.Transforms.Text.FeaturizeText("BodyFeats", nameof(Ticket.Body)))
    .Append(ml.Transforms.Categorical.OneHotEncoding("ProductFeats", nameof(Ticket.Product)))
    .Append(ml.Transforms.NormalizeMinMax("AgeFeat", nameof(Ticket.AccountAgeDays)))
    .Append(ml.Transforms.Concatenate("Features",
        "TitleFeats", "BodyFeats", "ProductFeats", "AgeFeat"))
    .Append(ml.MulticlassClassification.Trainers.SdcaMaximumEntropy())
    .Append(ml.Transforms.Conversion.MapKeyToValue("PredictedLabel"));

public sealed class Ticket
{
    [LoadColumn(0)] public string Title { get; set; } = "";
    [LoadColumn(1)] public string Body { get; set; } = "";
    [LoadColumn(2)] public string Product { get; set; } = "";
    [LoadColumn(3)] public float AccountAgeDays { get; set; }
    [LoadColumn(4)] public string Team { get; set; } = "";
}

MapValueToKey converts string labels into the key type that multiclass trainers require, and MapKeyToValue turns the predicted key back into a team name. Because the transforms are part of the saved model, the same featurization runs at prediction time, which removes a whole class of training-serving skew bugs.

Choosing a Trainer for Your Task#

Pick the task first, then the trainer. As a rule of thumb, start with a fast linear trainer as a baseline, then try gradient-boosted trees, which usually win on tabular data.

TaskTypical questionGood first trainersPackage
Binary classificationWill this customer churn?SdcaLogisticRegression, FastTree, LightGbmMicrosoft.ML, FastTree, LightGbm
Multiclass classificationWhich team should get this ticket?SdcaMaximumEntropy, LightGbmMicrosoft.ML, LightGbm
RegressionWhat will this order cost to ship?FastTree, LightGbm, SdcaMicrosoft.ML, FastTree, LightGbm
ClusteringWhich customer segments exist?KMeansMicrosoft.ML
Anomaly detectionIs this transaction unusual?RandomizedPcaMicrosoft.ML
Time-series forecastingHow many rentals next week?ForecastBySsaMicrosoft.ML.TimeSeries
Spike and change-point detectionDid call volume suddenly jump?DetectIidSpike, DetectEntireAnomalyBySrCnnMicrosoft.ML.TimeSeries
RecommendationWhich products will this user like?MatrixFactorizationMicrosoft.ML.Recommender
RankingIn which order should results appear?LightGbm ranking, FastTree rankingLightGbm, FastTree

Deep learning scenarios, such as text classification with a transformer model, sentence similarity, named entity recognition, question answering, image classification and object detection, are available through TorchSharp-based and image packages. They typically benefit from a GPU for training.

Time-series forecasting with SSA#

Forecasting uses Singular Spectrum Analysis, which learns trend and seasonality from a single series and returns a forecast with confidence bounds:

C#
using Microsoft.ML.Transforms.TimeSeries;

IDataView history = ml.Data.LoadFromEnumerable(LoadDailyDemand()); // oldest first

var forecasting = ml.Forecasting.ForecastBySsa(
    outputColumnName: nameof(DemandForecast.Forecast),
    inputColumnName: nameof(DailyDemand.Rentals),
    windowSize: 7,        // weekly seasonality
    seriesLength: 30,
    trainSize: 365,
    horizon: 7,           // predict the next 7 days
    confidenceLevel: 0.95f,
    confidenceLowerBoundColumn: nameof(DemandForecast.Lower),
    confidenceUpperBoundColumn: nameof(DemandForecast.Upper));

SsaForecastingTransformer model = forecasting.Fit(history);

TimeSeriesPredictionEngine<DailyDemand, DemandForecast> engine =
    model.CreateTimeSeriesEngine<DailyDemand, DemandForecast>(ml);

DemandForecast next = engine.Predict();
engine.CheckPoint(ml, "demand-model.zip"); // saves model state, including recent history

public sealed class DailyDemand { public float Rentals { get; set; } }

public sealed class DemandForecast
{
    public float[] Forecast { get; set; } = [];
    public float[] Lower { get; set; } = [];
    public float[] Upper { get; set; } = [];
}

A time-series engine is stateful: it remembers the latest observations. Feed it new actual values as they arrive, and checkpoint it so a restart does not lose that state.

Evaluating Models: Metrics That Matter#

Each task catalog has an Evaluate method that returns task-specific metrics. Knowing which ones to trust matters more than knowing all of them:

  • Binary classification: Accuracy misleads on imbalanced data (99% "not fraud" is easy). Prefer AreaUnderRocCurve, AreaUnderPrecisionRecallCurve for rare positives, and F1Score when precision and recall both matter. An AUC of 0.5 means the model is no better than chance.
  • Multiclass classification: MicroAccuracy weights every example equally, while MacroAccuracy weights every class equally and exposes poor performance on small classes. LogLoss should approach zero.
  • Regression: RSquared measures explained variance, while MeanAbsoluteError and RootMeanSquaredError are in label units. RMSE punishes large errors more heavily.
  • Clustering: AverageDistance and DaviesBouldinIndex (lower is better) compare cluster tightness and separation.

A perfect score is usually a bug, typically label leakage (a feature that encodes the answer) or evaluation on training data. Use CrossValidate on smaller datasets to get a more stable estimate, and use permutation feature importance (PFI) to see which features actually drive predictions before you trust a model with business decisions.

AutoML and Model Builder#

Choosing trainers and hyperparameters by hand is tedious, and AutoML automates it. The Microsoft.ML.AutoML API infers column types, builds a featurization pipeline, and runs a time-boxed search across trainers and hyperparameters:

C#
using Microsoft.ML;
using Microsoft.ML.AutoML;

var ml = new MLContext(seed: 0);

ColumnInferenceResults columns = ml.Auto().InferColumns(
    "orders.csv", labelColumnName: "IsReturned", groupColumns: false);

IDataView data = ml.Data.CreateTextLoader(columns.TextLoaderOptions).Load("orders.csv");
DataOperationsCatalog.TrainTestData split = ml.Data.TrainTestSplit(data, testFraction: 0.2);

SweepablePipeline pipeline = ml.Auto()
    .Featurizer(data, columnInformation: columns.ColumnInformation)
    .Append(ml.Auto().BinaryClassification(labelColumnName: "IsReturned"));

AutoMLExperiment experiment = ml.Auto().CreateExperiment()
    .SetPipeline(pipeline)
    .SetBinaryClassificationMetric(BinaryClassificationMetric.AreaUnderRocCurve, "IsReturned")
    .SetTrainingTimeInSeconds(300)
    .SetDataset(split);

TrialResult best = await experiment.RunAsync();
Console.WriteLine($"Best AUC: {best.Metric:F3}");
ml.Model.Save(best.Model, data.Schema, "returns.zip");

Model Builder wraps the same engine in a Visual Studio UI. You pick a scenario (data classification, value prediction, text or image classification, recommendation, forecasting and others), point it at a file or SQL Server table, and it generates an .mbconfig file plus C# code for training and consumption. The mlnet CLI does the same from a terminal, which makes it suitable for scripting and CI. Treat the generated training code as a starting point that you review and own, not as a black box.

Saving, Loading and Serving Models in ASP.NET Core#

mlContext.Model.Save writes the whole transformer chain, including featurization, to a zip file. mlContext.Model.Load reads it back together with its input schema. For single predictions, ML.NET offers PredictionEngine<TIn, TOut>, but it is not thread-safe, so never share one across requests. In web apps, use PredictionEnginePool from Microsoft.Extensions.ML, which keeps a pool of engines and can reload the model when the file changes:

C#
using Microsoft.Extensions.ML;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPredictionEnginePool<ReviewInput, ReviewPrediction>()
    .FromFile(modelName: "sentiment", filePath: "models/sentiment.zip", watchForChanges: true);

var app = builder.Build();

app.MapPost("/reviews/score", (ReviewInput review,
    PredictionEnginePool<ReviewInput, ReviewPrediction> pool) =>
{
    ReviewPrediction p = pool.Predict(modelName: "sentiment", review);
    return Results.Ok(new { p.IsPositive, p.Probability });
});

app.Run();

FromUri polls a remote location instead, every five minutes by default, which fits models published to blob storage by a training pipeline. Only load models from trusted sources, because a model file is executable logic. For endpoint design details, see the Minimal APIs guide.

ONNX Interop: Importing and Exporting Models#

ONNX (Open Neural Network Exchange) is the common format for moving models between frameworks. ML.NET works with it in both directions:

  • Import: ApplyOnnxModel from Microsoft.ML.OnnxTransformer scores an ONNX model inside an ML.NET pipeline. This is how a .NET service can use a model that data scientists trained in PyTorch or scikit-learn. It runs on ONNX Runtime, with optional GPU execution.
  • Export: ConvertToOnnx from Microsoft.ML.OnnxConverter writes an ML.NET model to ONNX, so it can run in other runtimes and languages. Not every transform and trainer is exportable, so compare the exported model's predictions with ML.NET's on a test set.
C#
// Import: column names must match the ONNX graph's input and output names.
var onnxPipeline = ml.Transforms.ApplyOnnxModel(
    outputColumnName: "probabilities",
    inputColumnName: "float_input",
    modelFile: "fraud_model.onnx");
ITransformer onnxModel = onnxPipeline.Fit(ml.Data.LoadFromEnumerable(Array.Empty<FraudFeatures>()));

// Export: pass sample data so the converter can infer the input schema.
using FileStream stream = File.Create("sentiment.onnx");
ml.Model.ConvertToOnnx(model, split.TrainSet, stream);

public sealed class FraudFeatures
{
    [VectorType(30), ColumnName("float_input")] public float[] Values { get; set; } = [];
}

For running ONNX models directly, including small language models, see the local AI with ONNX Runtime guide.

Best Practices#

  • Establish a baseline first. Train a simple linear model before anything complex, so you know whether the extra complexity pays off.
  • Put all featurization in the pipeline. Transforms saved with the model guarantee identical preprocessing during training and serving.
  • Set a seed. new MLContext(seed: ...) makes splits and training reproducible, which matters for reviews and regression tests.
  • Version models like code. Store model files with their training data snapshot, metrics and git commit, and promote them through environments.
  • Monitor drift. Log predictions and outcomes, and retrain when feature distributions or accuracy shift.
  • Keep training out of request paths. Train in a background job or CI pipeline, and let PredictionEnginePool pick up new models.

Common Pitfalls#

  • Sharing a PredictionEngine across threads. It corrupts state under load. Use PredictionEnginePool or one engine per thread.
  • Judging imbalanced problems by accuracy. Use AUC-PR, F1 or per-class metrics.
  • Leaking the label. Features computed after the outcome (such as "refund issued" when predicting returns) inflate metrics and fail in production.
  • Schema mismatches. Input classes must match the column names and types used in training, or prediction throws at runtime.
  • Missing native dependencies. Microsoft.ML.TimeSeries and Microsoft.ML.AutoML depend on Intel MKL, which needs libomp on Linux and macOS. LightGBM, TensorFlow and ONNX features require 64-bit processes.

ML.NET vs LLMs: When to Use Which#

With LLMs everywhere, it is tempting to send every prediction to a chat model. For structured prediction, that is usually slower, more expensive and less accurate than a trained model.

CriterionML.NET modelLLM through Microsoft.Extensions.AI
Best inputsTabular features, short text, time seriesUnstructured language, documents, images
OutputScores, labels and numbers with measurable accuracyFree text or structured JSON
Data needsLabeled historical examplesWorks zero-shot or few-shot
Cost per predictionIn-process CPU time, no per-call feeTokens plus network latency
DeterminismDeterministic given the same modelVaries, even at low temperature
ExplainabilityMetrics, feature importanceHard to explain or audit
PrivacyData never leaves the processHosted by default unless run locally

The approaches combine well. An LLM can extract structured fields from emails that an ML.NET model then scores, or ML.NET can flag anomalies that an LLM explains in plain language. The AI in .NET overview maps where each tool fits, and the Microsoft.Extensions.AI guide covers the LLM side.

Frequently Asked Questions#

Is ML.NET still maintained in 2026?#

Yes. ML.NET 5.0 shipped in November 2025 as the current stable release, and 6.0 previews were published through 2026, including one in September. The repository also ships Microsoft.ML.Tokenizers, which many .NET LLM applications use for token counting.

Do I need Python or data science expertise to use ML.NET?#

No Python is required: training, evaluation and inference all run in C#. You still need to understand your data, choose sensible features and read evaluation metrics correctly. AutoML and Model Builder reduce the algorithm-selection work, but they do not replace judgment about data quality and leakage.

Can ML.NET use models trained in PyTorch or TensorFlow?#

Yes, through ONNX. Export the model to ONNX from the Python framework and score it in ML.NET with ApplyOnnxModel, or call ONNX Runtime directly for full control. ML.NET can also load TensorFlow models through its TensorFlow package.

How do I serve ML.NET models in ASP.NET Core?#

Register PredictionEnginePool with AddPredictionEnginePool<TInput, TOutput>() and load the model with FromFile or FromUri. The pool is thread-safe, supports multiple named models, and can reload a model when the file changes without restarting the app.

Should I use ML.NET or an LLM for classification?#

For well-defined labels with historical training data, such as churn, fraud, routing or pricing, a trained ML.NET model is usually cheaper, faster and more accurate. Use an LLM when you lack labeled data, when the categories change often, or when the input is long and unstructured.

Summary#

  • ML.NET brings training and inference into C#, with ML.NET 5.0 as the current stable release and 6.0 in preview.
  • Pipelines of estimators become transformers when fitted, and IDataView streams data lazily with a strict schema.
  • Choose the task first, start with a linear baseline, and evaluate with metrics suited to your data's balance.
  • AutoML and Model Builder automate trainer selection. PredictionEnginePool serves models safely in ASP.NET Core.
  • ONNX connects ML.NET to the wider ML ecosystem, and for structured prediction, ML.NET often beats an LLM on cost, latency and accuracy.

Further Reading#