# .NET Gates, Security and Capstone

Course: [Testing & Verification](https://www.devtutorials.ai/learn/testing-verification) · Section: Testing in .NET
Source: https://www.devtutorials.ai/learn/testing-verification/dotnet/dotnet-gates-security-capstone

> 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.

```csharp runnable title="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#

```csharp runnable title="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

```yaml
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

```yaml
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

```csharp runnable title="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

```csharp runnable title="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

```csharp runnable title="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

| 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.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

- [Safe storage of app secrets, Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets): `dotnet user-secrets`.
- [Raw SQL queries in EF Core, Microsoft Learn](https://learn.microsoft.com/en-us/ef/core/querying/sql-queries): which calls are parameterized.
- [Stryker.NET documentation](https://stryker-mutator.io/docs/stryker-net/introduction/): 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.
