.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 testand 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:
- How the test-diff guard looks for xUnit tests and Stryker.NET config.
- How to write the definition of done in C#.
- How to wire
dotnet testand Stryker.NET into GitHub Actions or Azure Pipelines. - How to catch secrets, SQL injection, HTML injection and risky NuGet packages.
- 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., notexpect(. stryker-config.jsonholds 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: 85and 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
--warnaserrorturns warnings into errors. A null warning hidden by the AI then fails the build.--break-at 80fails 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 testin 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-secretson 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
@reviewfor 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 --vulnerableanddotnet list package --outdated. - In the lab: the third exercise builds a small checker.
Lock what you restore.
- Turn on
RestorePackagesWithLockFileto get apackages.lock.jsonfile. - 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:
- "Added coupon cap. All tests pass." The diff adds
[Fact(Skip = "flaky")]. → Reject: guard. - "Refactored Price()." Golden pins pass, and old vs new matched on 500 orders. → Accept.
- "Faster order lookup." Uses
FromSqlRaw($"... {id}"). → Reject: SQL injection. - "Fixed rounding." New test failed on the old code, mutation score 91%, guard clean. → Accept.
- "Lowered Stryker threshold to unblock CI." → Reject: guard on
stryker-config.json. - "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
- .NET guard:
Tests.csfiles,Skip =, removedAssert.lines, andstryker-config.json. - Definition of done in C#: build clean, tests pass, fail first, mutation score, guard clean.
- CI:
dotnet build --warnaserror,dotnet test,dotnet stryker --break-at 80. - Secrets: never in
appsettings.json. Use user-secrets locally and a vault in production. - SQL: parameters, never pasted text. Watch
FromSqlRawwith$"...". - HTML: encode user text. Flag
Html.Raw. - NuGet: check every new package before you restore it.
Common mistakes
| Mistake | Wrong idea | Correct idea | What 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.csdiffs and protectsstryker-config.json. - "Done" means the evidence is there, in any language.
- CI runs
dotnet build --warnaserror,dotnet testanddotnet 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
- Safe storage of app secrets, Microsoft Learn:
dotnet user-secrets. - Raw SQL queries in EF Core, Microsoft Learn: which calls are parameterized.
- Stryker.NET documentation: thresholds and
--break-at.
You have finished Testing & Verification, in TypeScript and in .NET. Take your verification pack, and use it on the next AI change you see.