Testing .NET AI Apps with Microsoft.Extensions.AI.Evaluation

Score model output in unit tests with Microsoft.Extensions.AI.Evaluation: the packages, a first relevance evaluation, how to assert safely, and rules for keeping an LLM judge repeatable.

By Ajith Joseph · Tue Nov 03 2026 · Updated Tue Nov 03 2026 · 4 min read · intermediate

#dotnet #csharp #ai #testing

An AI feature that has no tests is a feature you only trust on the day you wrote it. The usual objection is that model output is not deterministic, so there is nothing to assert. Microsoft.Extensions.AI.Evaluation is the official answer: a set of libraries that score a response, so a test can say "relevance must be at least 4" instead of "the text equals this string".

I installed version 10.11.0 of the packages on November 7, compiled the code in this post, and ran it. One caveat up front, from that run, is covered below: the scores come from a judge model, so a test is only as stable as the judge.

What is in the family

From Microsoft's documentation, the evaluation libraries are split by purpose:

  • Microsoft.Extensions.AI.Evaluation: the core abstractions and types.
  • ...Evaluation.Quality: evaluators for qualities such as relevance and completeness. They use an LLM as the judge.
  • ...Evaluation.NLP: evaluators that compare a response with one or more reference answers using classical text techniques such as tokenisation and n-grams. No model is involved, so they are cheap and repeatable.
  • ...Evaluation.Safety: checks for harmful or inappropriate content. These rely on the Foundry Evaluation service, which uses a model fine-tuned for the job.
  • ...Evaluation.Reporting: caching of LLM responses, storage of results and report generation. An Azure variant stores results in Azure Storage, and a console package provides the dotnet aieval tool for reports and for managing cached data.

You can also write your own by implementing IEvaluator.

A first evaluation

using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Evaluation;
using Microsoft.Extensions.AI.Evaluation.Quality;

IChatClient judgeClient = /* a real model client, adapted with AsIChatClient() */;

IEvaluator relevance = new RelevanceEvaluator();
var judge = new ChatConfiguration(judgeClient);

var question = new ChatMessage(ChatRole.User, "Where is order A-1001?");
ChatResponse reply = await appClient.GetResponseAsync(question);   // the app under test

EvaluationResult result = await relevance.EvaluateAsync([question], reply, judge);
NumericMetric score = result.Get<NumericMetric>(RelevanceEvaluator.RelevanceMetricName);

Console.WriteLine(
quot;relevance: {score.Value}");

The shape is always the same: the conversation, the response you want to judge, and a ChatConfiguration that says which model does the judging.

Using it as a test

In xUnit, MSTest or NUnit, run a handful of fixed prompts through your real pipeline and assert on the score:

[Fact]
public async Task Order_question_gets_a_relevant_answer()
{
    var reply = await App.AskAsync("Where is order A-1001?");
    var result = await Relevance.EvaluateAsync([Ask("Where is order A-1001?")], reply, Judge);

    var score = result.Get<NumericMetric>(RelevanceEvaluator.RelevanceMetricName);
    Assert.NotNull(score.Value);
    Assert.True(score.Value >= 4, 
quot;relevance was {score.Value}"); }

The helpers (App, Relevance, Judge, Ask) are yours; this one is a sketch, while the first example is the code I compiled and ran. Note the Assert.NotNull first. When I ran the first example with a stand-in judge that returned plain text instead of a score, the metric came back empty rather than throwing. A test that only checks >= 4 would fail without saying why. Assert that a number exists, then assert on it.

Making it repeatable and affordable

Evaluation tests call a model, so they cost money and vary. The Reporting package exists to tame that:

  • Cache responses so a test run does not pay again for an unchanged prompt.
  • Store results so you can compare today's scores with last week's.
  • Generate a report with dotnet aieval and attach it to your build.

Use the NLP evaluators where you have a reference answer, since they are free of model variance, and keep the LLM judge for qualities you cannot express as a string comparison.

Rules I would follow

  1. Pin the judge. Use one model, one version and a low temperature for judging. If the judge changes, the scores change, and you cannot tell why.
  2. Test the pipeline, not the model. Evaluate your whole prompt, retrieval and tool setup. That is what you ship.
  3. Keep a small fixed set of twenty or so prompts that cover your failure history. Add one every time production teaches you something.
  4. Gate on trends, not single scores. One score of 3 out of 5 is noise. A falling average over the set is a regression.
  5. Evaluate safety separately, with the Safety package, and treat any failure as blocking.

What I could not verify

I ran the first example against a stand-in judge, so I did not observe real relevance scores or their variance. A third-party article from April 2026 described the evaluation package as not yet stable at that time, while NuGet listed a stable 10.x line by November. Check the current package status before you build a release gate on it.

Sources

  • Microsoft.Extensions.AI.Evaluation libraries (Microsoft Learn)
  • Microsoft.Extensions.AI.Evaluation on NuGet
  • Evaluating AI agents with Microsoft.Extensions.AI.Evaluation in C# (April 2026)
  1. AJ's Tech Notes
  2. Testing .NET AI Apps with Microsoft.Extensions.AI.Evaluation