.NET Gates, Security and Capstone

By Syncfusion®, Inc. · Last updated

The gates for .NET teams: a test-diff guard for xUnit and Stryker.NET config, the definition of done in C#, dotnet test and Stryker.NET in CI, secrets in appsettings.json, SQL and HTML injection, NuGet package checks, and a short capstone.

Gates and Security for .NET Teams

This is the last lesson. It brings two earlier lessons to a .NET project:

  • the gates from Verification Gates for Agents
  • the checks from Security and Dependency Checks

It ends with a short capstone.

  • Gates: a test-diff guard, a definition of done, dotnet test and Stryker.NET in CI.
  • Security: secrets in appsettings.json, SQL injection, HTML encoding and NuGet package checks.
  • Capstone: judge .NET AI changes by their evidence.

Simple lesson: the same gates, written for .NET files and commands.

A Real-World Example

Think about airport security in two different countries.

  • The machines and the forms look different.
  • The checks are the same: your ticket, your ID, your bags.
  • Nobody gets on the plane without passing every check.

Your .NET gates work the same way.

What You Will Learn

In this lesson, you will learn:

  1. How the test-diff guard looks for xUnit tests and Stryker.NET config.
  2. How to write the definition of done in C#.
  3. How to wire dotnet test and Stryker.NET into GitHub Actions or Azure Pipelines.
  4. How to catch secrets, SQL injection, HTML injection and risky NuGet packages.
  5. How to judge .NET AI changes with evidence.

The Guard for .NET

What changes from the TypeScript guard:

  • Test files end in Tests.cs.
  • A skipped test is [Fact(Skip = "...")], not .skip.
  • Checks start with Assert., not expect(.
  • stryker-config.json holds a gate. Lowering "break" is faking green too.

The guard against three agent moves

using System;
using System.Collections.Generic;
using System.Linq;

var moves = new (string Name, string[] Added, string[] Removed)[]
{
    ("fix the code, add a test", new[] { "[Fact]", "public void Cap() => Assert.Equal(4800m, Apply(5000m));" }, new string[0]),
    ("skip the red test", new[] { "[Fact(Skip = \"flaky\")]" }, new[] { "[Fact]" }),
    ("loosen the check", new[] { "Assert.True(result > 0);" }, new[] { "Assert.Equal(4800m, result);" }),
};

foreach (var (name, added, removed) in moves)
{
    var problems = new List<string>();
    problems.AddRange(added.Where(a => a.Contains("Skip =")).Select(a => "skipped: " + a));
    problems.AddRange(removed.Where(r => r.StartsWith("Assert.") && !added.Contains(r)).Select(r => "removed: " + r));
    Console.WriteLine($"{name}: {(problems.Count == 0 ? "allowed" : "BLOCKED (" + string.Join("; ", problems) + ")")}");
}

Press Run.

  • The real fix: allowed.
  • Skip and loosen: blocked, with the reason.

Simple lesson: the guard reads the diff of the test files and blocks the shortcuts to green.

The Definition of Done in C#

Is it done?

using System;
using System.Collections.Generic;

var claim = new Evidence(TypeErrors: 0, Passed: 6, Failed: 0, FailedOnOldCode: false, MutationScore: 62, GuardProblems: new[] { "skipped: [Fact(Skip = \"flaky\")]" });

var reasons = IsDone(claim, threshold: 80);
Console.WriteLine(reasons.Count == 0 ? "Done ✓" : "Not done:");
foreach (var r in reasons) Console.WriteLine($"  - {r}");

static List<string> IsDone(Evidence e, int threshold)
{
    var reasons = new List<string>();
    if (e.TypeErrors > 0) reasons.Add($"{e.TypeErrors} build error(s)");
    if (e.Passed == 0) reasons.Add("no tests ran");
    if (e.Failed > 0) reasons.Add($"{e.Failed} failing test(s)");
    if (!e.FailedOnOldCode) reasons.Add("the new tests did not fail on the old code");
    if (e.MutationScore < threshold) reasons.Add($"mutation score {e.MutationScore}% is below {threshold}%");
    foreach (var p in e.GuardProblems) reasons.Add("guard: " + p);
    return reasons;
}

record Evidence(int TypeErrors, int Passed, int Failed, bool FailedOnOldCode, int MutationScore, string[] GuardProblems);

Press Run.

  • The output: "Not done", with three reasons.
  • Now try: set FailedOnOldCode: true, MutationScore: 85 and an empty guard list: new string[0]. Run again.

Simple lesson: "done" in .NET means the same evidence: build clean, tests pass, fail first, mutation score, guard clean.

Gates in CI

GitHub Actions

name: verify
on: [pull_request]
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '10.0.x' }
      - run: dotnet build --warnaserror
      - run: dotnet test --no-build
      - run: dotnet tool install -g dotnet-stryker
      - run: dotnet stryker --break-at 80

Azure Pipelines

pr:
  branches:
    include: ['*']
pool:
  vmImage: ubuntu-latest
steps:
  - task: UseDotNet@2
    inputs:
      version: '10.0.x'
  - script: dotnet build --warnaserror
  - script: dotnet test --no-build
  - script: dotnet tool install -g dotnet-stryker && dotnet stryker --break-at 80
  • --warnaserror turns warnings into errors. A null warning hidden by the AI then fails the build.
  • --break-at 80 fails the run when the mutation score is below 80%.
  • Agent hooks can run the same commands after each edit. Use the hook files from the pack you built in Capstone: Your Verification Pack, with dotnet test in place of the npm commands.

These files follow the docs as of November 2026. Check the action and task versions for your organisation before you commit them.

Simple lesson: hooks give fast feedback. CI is the barrier before merge.

Security Checks in .NET

Secrets in appsettings.json

Scan settings for secrets

using System;
using System.Text.RegularExpressions;

string settings = """
    {
      "ConnectionStrings": { "Shop": "Server=db;User Id=shop;Password=Shop@2026!" },
      "Payments": { "ApiKey": "<set with dotnet user-secrets>" }
    }
    """;

foreach (Match m in Regex.Matches(settings, "Password=[^;\"]+|(sk|rk)_live_[A-Za-z0-9]{8,}|AKIA[0-9A-Z]{16}"))
    Console.WriteLine($"Secret found: {m.Value[..12]}...");

Press Run.

  • The output: the password in the connection string.
  • The placeholder for the API key is fine.
  • Where secrets belong: dotnet user-secrets on your machine, and a vault or CI secrets in production.

SQL Injection

Pasted vs parameterized

using System;

string attack = "1' OR '1'='1";

// The AI's version: the text is pasted into the SQL.
string pasted = $"SELECT * FROM Orders WHERE Id = '{attack}'";
Console.WriteLine("Pasted:        " + pasted);

// The safe version: a parameter. The value travels separately.
string safe = "SELECT * FROM Orders WHERE Id = @id";
Console.WriteLine("Parameterized: " + safe + "   with @id = " + attack);

Press Run.

  • Pasted: the attack changes the query to "every order".
  • Parameterized: the SQL never changes. In real code: command.Parameters.AddWithValue("@id", id), or let Entity Framework build the query.

Watch for these in AI C# code: FromSqlRaw with $"..." strings, SqlCommand built with +, and ExecuteSqlRaw with user input. Entity Framework's FromSqlInterpolated and LINQ queries are parameterized for you.

HTML Encoding

Encode user text before it goes into HTML

using System;
using System.Net;

string review = "<script>steal()</script>";

Console.WriteLine("Raw:     <p>" + review + "</p>");
Console.WriteLine("Encoded: <p>" + WebUtility.HtmlEncode(review) + "</p>");

Press Run.

  • Raw: the browser would run the script.
  • Encoded: it shows as text. Razor pages encode @review for you. Html.Raw(...) turns that off, so flag it in AI changes.

NuGet Packages

Before you restore a package an AI added, check that it is real, popular and not a look-alike:

  • On nuget.org: owner, downloads, age, and a link to the source.
  • In the terminal: dotnet list package --vulnerable and dotnet list package --outdated.
  • In the lab: the third exercise builds a small checker.

Lock what you restore.

  • Turn on RestorePackagesWithLockFile to get a packages.lock.json file.
  • Keep a list of trusted package sources.
  • Then a surprise package or version shows up as a diff you can review.

Simple lesson: settings, SQL, HTML and packages: the same four security checks, in .NET form.

Capstone: Judge .NET Changes

Use your gates on each change. Accept with evidence, reject with evidence:

  1. "Added coupon cap. All tests pass." The diff adds [Fact(Skip = "flaky")]. → Reject: guard.
  2. "Refactored Price()." Golden pins pass, and old vs new matched on 500 orders. → Accept.
  3. "Faster order lookup." Uses FromSqlRaw($"... {id}"). → Reject: SQL injection.
  4. "Fixed rounding." New test failed on the old code, mutation score 91%, guard clean. → Accept.
  5. "Lowered Stryker threshold to unblock CI." → Reject: guard on stryker-config.json.
  6. "Added Newtonsofts.Json." → Reject: package check, a look-alike name.

The quiz below has more of these.

Simple lesson: you now have the gates for .NET too. "Done" is still a claim.

TL;DR

  1. .NET guard: Tests.cs files, Skip =, removed Assert. lines, and stryker-config.json.
  2. Definition of done in C#: build clean, tests pass, fail first, mutation score, guard clean.
  3. CI: dotnet build --warnaserror, dotnet test, dotnet stryker --break-at 80.
  4. Secrets: never in appsettings.json. Use user-secrets locally and a vault in production.
  5. SQL: parameters, never pasted text. Watch FromSqlRaw with $"...".
  6. HTML: encode user text. Flag Html.Raw.
  7. NuGet: check every new package before you restore it.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Guarding only test files"Config is not a test."Protect stryker-config.json too.The agent lowers the threshold to get green.
Secrets in appsettings"It is only the dev settings."Use dotnet user-secrets.The password is pushed with the repo.
FromSqlRaw with $"...""The id is just a number."Use parameters or LINQ.1' OR '1'='1 returns every order.
Html.Raw for reviews"The review needs formatting."Encode, or sanitise with a library.A script runs for every visitor.
Restoring every package"The AI chose it."Check it on nuget.org first.A look-alike package runs on your build server.

Check Your Understanding

Exercises

What to Remember

  • The .NET guard reads Tests.cs diffs and protects stryker-config.json.
  • "Done" means the evidence is there, in any language.
  • CI runs dotnet build --warnaserror, dotnet test and dotnet stryker --break-at.
  • Secrets in user-secrets or a vault. Parameters for SQL. Encoding for HTML. Checks for NuGet.
  • Accept with evidence. Reject with evidence.

Further Reading

You have finished Testing & Verification, in TypeScript and in .NET. Take your verification pack, and use it on the next AI change you see.