<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Selçuk GÜRAL</title>
    <description>The latest articles on DEV Community by Selçuk GÜRAL (@selcukgural).</description>
    <link>https://dev.to/selcukgural</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2504265%2Fefaad0e8-8552-42d4-b02d-6edf59361594.png</url>
      <title>DEV Community: Selçuk GÜRAL</title>
      <link>https://dev.to/selcukgural</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/selcukgural"/>
    <language>en</language>
    <item>
      <title>I built Kule: AI agents work on a copy of your folder, and you decide what comes back</title>
      <dc:creator>Selçuk GÜRAL</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:10:54 +0000</pubDate>
      <link>https://dev.to/selcukgural/i-built-kule-ai-agents-work-on-a-copy-of-your-folder-and-you-decide-what-comes-back-1ik2</link>
      <guid>https://dev.to/selcukgural/i-built-kule-ai-agents-work-on-a-copy-of-your-folder-and-you-decide-what-comes-back-1ik2</guid>
      <description>&lt;p&gt;Hi, I'm Selçuk. I've been building an open-source tool called &lt;strong&gt;Kule&lt;/strong&gt;, and its first preview release is out. I'd like to tell you what it does, why I built it, and what it can't do yet.&lt;/p&gt;

&lt;p&gt;The short version: &lt;strong&gt;Kule is a control room for AI agents that work on your folders.&lt;/strong&gt; You point an agent (Claude Code, Codex, Gemini, or any agent that speaks &lt;a href="https://agentclientprotocol.com" rel="noopener noreferrer"&gt;ACP&lt;/a&gt;) at a folder. Kule gives it a copy-on-write copy of that folder inside a sandbox, looks at what every command really changed, and lets nothing reach the real folder until a policy, or you, says so. Every commit can be undone.&lt;/p&gt;

&lt;p&gt;("Kule" means &lt;em&gt;tower&lt;/em&gt; in Turkish. Think control tower.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

&lt;p&gt;Coding agents are good now, good enough that we let them run commands. When an agent runs a command, it runs as &lt;em&gt;you&lt;/em&gt;: your files, your &lt;code&gt;~/.ssh&lt;/code&gt; and &lt;code&gt;~/.aws&lt;/code&gt;, your &lt;code&gt;~/.zshrc&lt;/code&gt;, your network, your API keys.&lt;/p&gt;

&lt;p&gt;Most agents ask before they run something, but they ask about the &lt;strong&gt;command text&lt;/strong&gt;: "Run &lt;code&gt;npm install&lt;/code&gt;?" You can't tell from that text what a postinstall script, a Python one-liner or a build script will actually touch. So after the twentieth prompt you start clicking "yes" without reading, or you switch on the mode that stops asking. And if a file the agent reads contains a prompt injection, the agent is on the attacker's side, and all that's left is what you approve.&lt;/p&gt;

&lt;p&gt;I wanted something else: &lt;strong&gt;let the agent work freely, somewhere nothing it does is real until I've seen it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;Kule has one loop, in the CLI and in the desktop app:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;make a task → give the agent a prompt → watch → review → commit or roll back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here it is without an agent, on a scratch folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/kule-demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/kule-demo/notes.md

kule new ~/kule-demo                                    &lt;span class="c"&gt;# prints a task id, e.g. 1a2b3c4d&lt;/span&gt;
kule &lt;span class="nb"&gt;exec &lt;/span&gt;1a2b3c4d &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "more" &amp;gt;&amp;gt; notes.md'&lt;/span&gt;  &lt;span class="c"&gt;# runs in the copy, in the sandbox&lt;/span&gt;
kule status 1a2b3c4d                                    &lt;span class="c"&gt;# what changed, and the policy's verdict&lt;/span&gt;
kule commit 1a2b3c4d                                    &lt;span class="c"&gt;# apply it, keeping a checkpoint&lt;/span&gt;
kule undo 1a2b3c4d                                      &lt;span class="c"&gt;# put the folder back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with an agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kule run 1a2b3c4d &lt;span class="nt"&gt;--agent&lt;/span&gt; claude-code &lt;span class="s2"&gt;"Turn notes.md into a checklist"&lt;/span&gt;
kule steps 1a2b3c4d     &lt;span class="c"&gt;# every tool call the agent made, and what it changed&lt;/span&gt;
kule commit 1a2b3c4d    &lt;span class="c"&gt;# or: kule rollback 1a2b3c4d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The desktop app (Tauri) does the same thing with a live stream, an approval inbox, a review screen and a timeline. A small daemon keeps runs going when the window is closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually between the agent and your stuff
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your folder.&lt;/strong&gt; The agent works in a copy-on-write copy: an APFS clone on macOS, a reflink on Btrfs/XFS. Its changes reach the real folder only through a commit, and every commit keeps a checkpoint, so &lt;code&gt;kule undo&lt;/code&gt; really puts things back. The history of commits and undos is signed and hash-chained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your computer.&lt;/strong&gt; Every command runs in the OS sandbox: Seatbelt on macOS, Landlock + seccomp + namespaces on Linux. It can write only to the copy and its own temp folders. &lt;code&gt;~/.ssh&lt;/code&gt;, &lt;code&gt;~/.aws&lt;/code&gt;, your Documents and the projects next to the folder are hidden. If the sandbox can't be set up, the command doesn't run. Kule never quietly falls back to "no sandbox".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What gets in.&lt;/strong&gt; This is the part I care about most. Kule doesn't judge the command; it judges the &lt;strong&gt;effects&lt;/strong&gt;. After a run, it compares the copy with your folder: files created, modified, deleted or moved, git branches moved, hooks added. Then a &lt;a href="https://www.cedarpolicy.com" rel="noopener noreferrer"&gt;Cedar&lt;/a&gt; policy gives each effect a verdict: &lt;em&gt;pass&lt;/em&gt;, &lt;em&gt;ask&lt;/em&gt; or &lt;em&gt;deny&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With the default ("balanced") policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ordinary edits to source, notes, documents and data &lt;strong&gt;pass&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;manifests and lockfiles, CI files, editor settings, agent instructions (&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;.mcp.json&lt;/code&gt;…), git config changes, history rewrites and mass deletes &lt;strong&gt;wait for you&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;git hooks are &lt;strong&gt;denied&lt;/strong&gt;, because they would run on your next &lt;code&gt;git&lt;/code&gt; command, as you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a harmless-looking command that rewrites &lt;code&gt;package.json&lt;/code&gt; still waits for you, and a scary-looking &lt;code&gt;rm -rf&lt;/code&gt; that only touches the copy changes nothing real. When something waits, Kule tells you which rule stopped it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;policy: waits for approval
  package.json · waits for approval (manifests-and-lockfiles)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a "careful" policy where every change waits. You can write your own, and a repository can add refusals of its own, but never loosen yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your network.&lt;/strong&gt; Every connection goes through the task's proxy. Package registries pass, the agent reaches its own model API while it runs, and anything else waits for your answer. Every connection is logged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your secrets.&lt;/strong&gt; The agent sees a placeholder, never the key. The proxy puts the real value into requests going to the hosts you named, and nowhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your home.&lt;/strong&gt; A write to &lt;code&gt;~/.zshrc&lt;/code&gt;, &lt;code&gt;~/.gitconfig&lt;/code&gt; or &lt;code&gt;~/.config&lt;/code&gt; lands in the task's &lt;em&gt;home layer&lt;/em&gt; with a diff for you to review. It doesn't touch your real home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your wallet and your machine.&lt;/strong&gt; Cost budgets per task, per group and per day (with a notice at 80%), CPU/memory/disk/time limits per run, and a Stop button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not only for code
&lt;/h2&gt;

&lt;p&gt;Kule works on any folder. The review shows what changed &lt;em&gt;inside&lt;/em&gt; a file (functions and methods, Markdown sections, JSON/YAML keys), before-and-after pictures, and previews for CSV, Excel and Word. If you let an agent tidy your notes, rename photos or clean up a data folder, the same loop applies.&lt;/p&gt;

&lt;p&gt;Two tasks can work on the same folder. A file both of them changed merges method by method, and only the parts that were changed twice conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part: what it can't do (yet)
&lt;/h2&gt;

&lt;p&gt;I'd rather you hear this from me than find out after an hour of setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a preview&lt;/strong&gt; (&lt;code&gt;0.2.0-preview.1&lt;/code&gt;). Things will change between previews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS 13+ and Linux x86_64 only.&lt;/strong&gt; Windows is planned, not started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The macOS app isn't notarized.&lt;/strong&gt; It's signed ad hoc, so Gatekeeper will complain the first time. Every release file comes with &lt;code&gt;SHA256SUMS&lt;/code&gt;, a build provenance attestation and an SBOM, and the &lt;a href="https://kulehq.github.io/kule/docs/getting-started/install" rel="noopener noreferrer"&gt;install guide&lt;/a&gt; shows how to check them and open the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux has some requirements.&lt;/strong&gt; On ext4 the copy is a full copy, not a clone. The sandbox needs Landlock (Linux 6.2+, 6.7+ for network rules) and unprivileged user namespaces, which Ubuntu 24.04 restricts by default. Kule tells you what's missing and won't run an agent with weaker isolation unless you choose that knowingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The proxy doesn't do HTTP/2 or WebSockets yet.&lt;/strong&gt; Clients fall back to HTTP/1.1. A few tools on macOS have certificate quirks; the &lt;a href="https://kulehq.github.io/kule/docs/platforms" rel="noopener noreferrer"&gt;Platforms page&lt;/a&gt; lists them all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kule doesn't make the agent right.&lt;/strong&gt; It makes the agent's mistakes cheap and visible. You still review, and Kule's job is to make that review short and honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's built by one person.&lt;/strong&gt; I take security seriously (every boundary has a test that fails without it, on real processes and the real filesystem, no mocks), but one person can still miss things. That's part of why it's open.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/kulehq/kule/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt; (universal &lt;code&gt;.dmg&lt;/code&gt;, &lt;code&gt;.deb&lt;/code&gt;, &lt;code&gt;.rpm&lt;/code&gt;, and the CLI alone as &lt;code&gt;.tar.gz&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Or build from source:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  git clone https://github.com/kulehq/kule &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;kule
  cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; kule &lt;span class="nt"&gt;-p&lt;/span&gt; kule-desktop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt; (English and Turkish), with a quick start, a page for every command and a three-minute video tour: &lt;a href="https://kulehq.github.io/kule/" rel="noopener noreferrer"&gt;kulehq.github.io/kule&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/kulehq/kule" rel="noopener noreferrer"&gt;github.com/kulehq/kule&lt;/a&gt; · Apache-2.0 · written in Rust&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What would help me most
&lt;/h2&gt;

&lt;p&gt;Try it on a real folder and tell me where it got in your way. A policy that asks too often, a confusing screen, a tool that breaks inside the sandbox: that's exactly the feedback I'm looking for. Open an issue, or just reply here.&lt;/p&gt;

&lt;p&gt;If you find a way through one of the boundaries, please &lt;strong&gt;don't&lt;/strong&gt; post it publicly. Report it privately as &lt;a href="https://github.com/kulehq/kule/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt; explains. I'll be grateful, and I'll fix it.&lt;/p&gt;

&lt;p&gt;And if Kule ends up keeping your folders safe and you'd like to help it keep going, there's &lt;a href="https://github.com/sponsors/selcukgural" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;. That's completely optional. A star, an issue or a "this worked for me" means a lot too.&lt;/p&gt;

&lt;p&gt;Thanks for reading. 🙏&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Stop running your whole .NET test suite on every pull request</title>
      <dc:creator>Selçuk GÜRAL</dc:creator>
      <pubDate>Fri, 16 Oct 2026 18:30:36 +0000</pubDate>
      <link>https://dev.to/selcukgural/stop-running-your-whole-net-test-suite-on-every-pull-request-5606</link>
      <guid>https://dev.to/selcukgural/stop-running-your-whole-net-test-suite-on-every-pull-request-5606</guid>
      <description>&lt;p&gt;Every .NET codebase I have worked on hits the same wall sooner or later. The test suite grows, CI goes from three minutes to twenty, and every pull request pays the full price, even a one-line rename deep inside a helper class.&lt;/p&gt;

&lt;p&gt;Splitting the suite or buying bigger runners helps for a while. But most of those tests could never have noticed the change in the first place. They run anyway because nothing tells CI which ones matter.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;TestDetta&lt;/strong&gt;: a .NET tool and GitHub Action that reads your diff and runs only the test classes that can observe it. It follows one rule throughout: &lt;strong&gt;when it cannot be sure, it runs more tests, never fewer.&lt;/strong&gt; A missed test is a bug. An extra test only costs CI time.&lt;/p&gt;

&lt;p&gt;This post covers how it works, how to try it in a few minutes, and how to check that it holds up on your own code before you trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;Here is a real change to &lt;a href="https://github.com/open-telemetry/opentelemetry-dotnet" rel="noopener noreferrer"&gt;OpenTelemetry .NET&lt;/a&gt;: a local variable renamed inside one method. It changes no behaviour, but a normal pipeline still runs all 30 test projects and 274 test classes.&lt;/p&gt;

&lt;p&gt;TestDetta works it out like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Projects:&lt;/strong&gt; 2 of 30 test projects are affected. The other 28 are not even built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbols:&lt;/strong&gt; 1 member changed, &lt;code&gt;WarnIfConfigFileEnvVarDiffers&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage map:&lt;/strong&gt; 8 test classes ran that method on &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; 8 of 274 classes run, 266 are skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those 8 classes are exactly the ones that fail when that method is broken. Recorded test time went from &lt;strong&gt;5 m 59 s to 22.4 s&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in two minutes
&lt;/h2&gt;

&lt;p&gt;TestDetta is a .NET tool (it runs on the .NET 10 SDK; your projects can target older frameworks):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; TestDetta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask what your branch affects, and why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;td affected &lt;span class="nt"&gt;--base&lt;/span&gt; origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;code&gt;dotnet test&lt;/code&gt; commands it would run, without running anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;td &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--base&lt;/span&gt; origin/main &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run only the affected tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;td &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--base&lt;/span&gt; origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ td test --base origin/main
TestDetta 0.2.2 · 1 changed file · 2 affected test projects
Tests to run
┌──────────────────┬──────────┬─────────┬──────────────────────────────────────────────────────────┐
│ Test project     │ Run      │ Classes │ Why                                                      │
├──────────────────┼──────────┼─────────┼──────────────────────────────────────────────────────────┤
│ tests/Shop.Tests │ filtered │  3 / 41 │ 3 of 41 test class(es) run or reference the changed code │
│ tests/Api.Tests  │ skipped  │  0 / 12 │ no test class runs or references the changed code        │
└──────────────────┴──────────┴─────────┴──────────────────────────────────────────────────────────┘
  tests/Shop.Tests → 3 classes
    Shop.Tests.CartTests
    Shop.Tests.CheckoutTests
    Shop.Tests.OrderTests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a coverage map, TestDetta decides by tracing names through your source. For sharper selection, record a map once on your default branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;td record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs every test with coverage and writes one map per test project to &lt;code&gt;.testdetta/coverage&lt;/code&gt; (add &lt;code&gt;.testdetta/&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Need machine-readable output? &lt;code&gt;td affected --format json&lt;/code&gt; gives you the selection for your own scripts, &lt;code&gt;--format paths&lt;/code&gt; lists the test projects, and &lt;code&gt;--format commands&lt;/code&gt; prints the &lt;code&gt;dotnet test&lt;/code&gt; commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it decides
&lt;/h2&gt;

&lt;p&gt;Each step narrows the selection only when it has evidence. Otherwise it falls back to something wider.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Projects.&lt;/strong&gt; Changed files are mapped to their projects and followed through &lt;code&gt;ProjectReference&lt;/code&gt;s. Build-wide files such as &lt;code&gt;Directory.Build.props&lt;/code&gt;, &lt;code&gt;global.json&lt;/code&gt; or &lt;code&gt;.editorconfig&lt;/code&gt; affect every project below them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symbols.&lt;/strong&gt; For C#, Visual Basic and F# files, the old and new syntax trees are compared (Roslyn for C# and VB). Comment and whitespace edits select nothing. Otherwise TestDetta knows exactly which types and members changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage.&lt;/strong&gt; A map of which test class ran which method, recorded at the base commit, gives the test classes for changed methods, properties and constructors directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names.&lt;/strong&gt; What coverage cannot see, such as new code, fields and constants, is traced through the source by name until it reaches test classes or code the map knows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallbacks.&lt;/strong&gt; When a change cannot be traced, the whole test project runs. That covers non-code files the build uses (&lt;code&gt;appsettings.json&lt;/code&gt;, &lt;code&gt;.csproj&lt;/code&gt;, &lt;code&gt;.resx&lt;/code&gt;), global usings, assembly attributes, and types a framework discovers through reflection (controllers, message handlers, EF configurations).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is one more safety net: if a filter matches no test at all, TestDetta treats that as its own bug and runs the project again without the filter.&lt;/p&gt;

&lt;p&gt;A few details took a lot of work to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maps don't have to be fresh.&lt;/strong&gt; The best map is one recorded at your branch's merge base. A map up to 50 commits older still works: TestDetta also selects the classes that ran anything changed in between that could reach your change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source generators are handled.&lt;/strong&gt; Generated code is read from the PDBs during recording. When a change reaches a generator in your repository, TestDetta builds the test projects that use it and compares what is generated, framework by framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#if&lt;/code&gt; code is not missed.&lt;/strong&gt; C# files are parsed with each target framework's real preprocessor symbols, and with every symbol their &lt;code&gt;#if&lt;/code&gt;s name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your test configuration is respected.&lt;/strong&gt; Your own &lt;code&gt;--filter&lt;/code&gt;, run settings and runner arguments are combined with TestDetta's class filter, so it only ever narrows your runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The usual setup records a map on every push to &lt;code&gt;main&lt;/code&gt; and runs only the affected tests on pull requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt; &lt;span class="c1"&gt;# lets pull requests find the newest coverage map in the Actions cache&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt; &lt;span class="c1"&gt;# pin to a commit SHA&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;# TestDetta compares with the merge base&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v6&lt;/span&gt; &lt;span class="c1"&gt;# pin to a commit SHA&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;global-json-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;global.json&lt;/span&gt;

      &lt;span class="c1"&gt;# Full test run of main, which also records the coverage map&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TestDetta/action@a2f55a8252840a367f2f7a9ebd6733de3ebfb2f5&lt;/span&gt; &lt;span class="c1"&gt;# v0.2.2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;record&lt;/span&gt;

      &lt;span class="c1"&gt;# Pull requests run only what the change can affect&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TestDetta/action@a2f55a8252840a367f2f7a9ebd6733de3ebfb2f5&lt;/span&gt; &lt;span class="c1"&gt;# v0.2.2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-test-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--logger trx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maps live in the Actions cache, keyed by commit, and pull requests read the map of their merge base. Every run writes a job summary explaining what ran and why.&lt;/p&gt;

&lt;p&gt;Not on GitHub? GitLab CI, Azure DevOps, Jenkins and Bitbucket use the same two commands (&lt;code&gt;td record&lt;/code&gt; and &lt;code&gt;td test&lt;/code&gt;) with a folder of per-commit maps on a shared disk or object storage (&lt;code&gt;--maps-dir&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't trust it, check it
&lt;/h2&gt;

&lt;p&gt;A tool that decides which tests to skip has to earn your trust on your own codebase, not on a marketing page. TestDetta ships two ways to check it, and neither needs a license:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shadow mode&lt;/strong&gt; (&lt;code&gt;td test --shadow&lt;/code&gt;, or &lt;code&gt;mode: shadow&lt;/code&gt; in the action) keeps running your full suite and compares the result with what the selection would have run. When a test fails outside the selection, it reruns that class to tell a real miss from a flaky or pre-existing failure. Run it for a couple of weeks of normal pull requests and you have evidence from your own code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit&lt;/strong&gt; (&lt;code&gt;td audit&lt;/code&gt;, or &lt;code&gt;mode: audit&lt;/code&gt;) doesn't wait for real failures. It breaks your code on purpose, one member at a time with IL mutants in a copy of the working tree, runs every test that can see each change, and reports any failing class the selection left out. It also reports the 95% upper bound of the miss rate. Your sources and build output are only read.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is tested
&lt;/h2&gt;

&lt;p&gt;I run the same check at scale on &lt;strong&gt;53 open-source .NET repositories&lt;/strong&gt;, including Polly, Newtonsoft.Json, AutoMapper, Serilog, Markdig, Refit, Autofac, NodaTime, FastEndpoints, FluentValidation and OpenTelemetry. Members are changed one at a time, and every test class that fails must be one TestDetta selected.&lt;/p&gt;

&lt;p&gt;From the latest round (2026-10-13 to 2026-10-15):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3,071&lt;/strong&gt; changes broke at least one test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5&lt;/strong&gt; misses, each a documented limitation (more on those below). Earlier real misses were fixed, each with a regression test that fails without the fix.&lt;/li&gt;
&lt;li&gt;The median repository ran &lt;strong&gt;35%&lt;/strong&gt; of its test classes per change. In 20 of the 53 repositories it was a quarter or less, and Polly got down to 3%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be upfront: some repositories gain much less. Mapperly ran 97% and AutoMapper 86%, because nearly every test there goes through the same central code paths. If most of your tests touch everything, test impact analysis can't save much, and the evaluation shows that honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;TestDetta is static and coverage-based analysis, not magic. Here is what it can't fully see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code that runs once per process&lt;/strong&gt; (static constructors, lazy singletons, EF model building) is credited to the first test class that triggers it. TestDetta traces changes to such code by name instead, but a cache you wrote by hand (a &lt;code&gt;Dictionary&lt;/code&gt; checked with &lt;code&gt;TryGetValue&lt;/code&gt;) can still hide its users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflection and dynamic loading&lt;/strong&gt; are only partly visible. Types a framework discovers this way run their whole test project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;F# signature files&lt;/strong&gt; (&lt;code&gt;.fsi&lt;/code&gt;) are not analysed, so a change to one runs its project's dependents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests skipped while recording&lt;/strong&gt; have no coverage, for example a missing Docker daemon. Record where your tests actually run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is on the &lt;a href="https://testdetta.com/docs/limitations/" rel="noopener noreferrer"&gt;Limitations page&lt;/a&gt;. My recommendation: keep running the full suite on your default branch. Recording does that anyway, so anything a selection could ever hide is still caught before release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's supported
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test frameworks:&lt;/strong&gt; xUnit v2, xUnit v3, NUnit, MSTest and TUnit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runners:&lt;/strong&gt; VSTest and Microsoft.Testing.Platform (read from &lt;code&gt;global.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Languages:&lt;/strong&gt; C#, Visual Basic and F#&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI:&lt;/strong&gt; GitHub Actions, GitLab CI, Azure DevOps, Jenkins, Bitbucket and on-premises setups&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy and licensing
&lt;/h2&gt;

&lt;p&gt;None of your code leaves your CI. Analysis, coverage maps and test results stay on your machines. The only network call is an optional license renewal, at most once a day, that sends the license and nothing else.&lt;/p&gt;

&lt;p&gt;TestDetta is &lt;strong&gt;free for public repositories on GitHub Actions and GitLab CI&lt;/strong&gt;, with every feature and no sign-up. It detects public repositories from the CI itself. Private repositories need a paid license, with a 30-day free trial that needs no card. A license problem never fails your build: without a valid license, every test simply runs, as it did before TestDetta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Website and docs: &lt;a href="https://testdetta.com" rel="noopener noreferrer"&gt;testdetta.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How it decides, step by step: &lt;a href="https://testdetta.com/docs/how-it-works/" rel="noopener noreferrer"&gt;testdetta.com/docs/how-it-works&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Validate it on your repository: &lt;a href="https://testdetta.com/docs/validate/" rel="noopener noreferrer"&gt;testdetta.com/docs/validate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Action: &lt;a href="https://github.com/TestDetta/action" rel="noopener noreferrer"&gt;github.com/TestDetta/action&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/TestDetta" rel="noopener noreferrer"&gt;nuget.org/packages/TestDetta&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, I would love to hear how it goes, especially if it &lt;strong&gt;misses&lt;/strong&gt; a test. A miss is the one bug I care about most. Leave a comment here or write to &lt;a href="mailto:support@testdetta.com"&gt;support@testdetta.com&lt;/a&gt;, and I'll dig in.&lt;/p&gt;

&lt;p&gt;How does your team deal with a slow .NET test suite today? I'm curious what has worked for you.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Anka: Designing a Minimal HTTP Server for Native AOT, Its Architecture, and Intentional Constraints</title>
      <dc:creator>Selçuk GÜRAL</dc:creator>
      <pubDate>Fri, 01 May 2026 16:48:33 +0000</pubDate>
      <link>https://dev.to/selcukgural/anka-designing-a-minimal-http-server-for-native-aot-its-architecture-and-intentional-constraints-284o</link>
      <guid>https://dev.to/selcukgural/anka-designing-a-minimal-http-server-for-native-aot-its-architecture-and-intentional-constraints-284o</guid>
      <description>&lt;p&gt;In modern backend systems, being “fast” alone is no longer enough. How quickly your service responds to the first request, how little memory it consumes at startup, and how predictably it behaves are now just as important as raw throughput. Especially in serverless, short-lived container, edge worker, and bursty internal service scenarios, the real problem is often not steady-state throughput, but cold start performance, startup allocations, and operational simplicity.&lt;/p&gt;

&lt;p&gt;Anka approaches this problem from the opposite direction of the traditional ASP.NET Core model: no middleware pipeline, no built-in routing, no TLS, no HTTP/2. Instead, it provides a Native AOT-friendly, HTTP/1.x-focused, allocation-conscious HTTP server core built on .NET 8+. The goal is straightforward: once the process starts, begin listening on the socket within milliseconds and keep the connection/request lifecycle as deterministic as possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The traditional .NET web stack is extremely powerful, but that power comes with a cost. Middleware chains, reflection-heavy surfaces, dynamic extensibility, broad protocol support, and JIT warmup all introduce significant overhead on the cold-start path. If your actual requirement is simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Open a socket, parse HTTP/1.1, execute a handler, and write the response,”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;then the general-purpose framework model can become unnecessarily expensive.&lt;/p&gt;

&lt;p&gt;The core question Anka focuses on is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If we do not need a full-featured web framework, but only a fast-starting, low-memory, Native AOT-compatible HTTP listener, what should the minimum architecture look like?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer becomes visible in the performance characteristics as well. In measured environments, Anka reaches a time-to-ready of approximately 2.3 ms, startup allocations around 124.5 KB, and a steady-state RSS of roughly 15 MB. Kestrel, by comparison, offers a much broader feature set, but carries a heavier cold-start and memory profile. Interestingly, throughput differences are not dramatic; Anka’s real advantage is not raw requests-per-second, but startup cost and footprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Minimal Layers, Maximum Control
&lt;/h2&gt;

&lt;p&gt;Anka’s architecture is intentionally narrow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server.StartAsync()
-&amp;gt; parallel accept loops
-&amp;gt; Connection.RunAsync() (one task per TCP connection)
-&amp;gt; SocketReceiver.ReceiveAsync()
-&amp;gt; HttpParser
-&amp;gt; RequestHandler(request, response, ct)
-&amp;gt; HttpResponseWriter.WriteAsync()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important aspect of this flow is that there is very little abstraction. Request processing starts directly at the socket layer and ends directly at the socket layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Server: The Acceptance Layer
&lt;/h2&gt;

&lt;p&gt;The server opens the listening socket, configures the backlog, and runs multiple parallel accept loops instead of a single accept loop. This may sound like a small detail, but under burst traffic it prevents a single accept loop from serializing connection acceptance.&lt;/p&gt;

&lt;p&gt;Additionally, the minimum thread pool size is increased to reduce thread injection latency during initial load. This may look like benchmark tuning, but in reality it is an operational decision intended to make cold-path behavior more deterministic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connection: The Real Unit of Execution
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Connection&lt;/code&gt; task is created for each TCP connection. This object:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rents a pooled receive buffer,&lt;/li&gt;
&lt;li&gt;acquires a pooled &lt;code&gt;HttpRequest&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;creates a connection-scoped &lt;code&gt;HttpResponseWriter&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;reads and parses data from the socket,&lt;/li&gt;
&lt;li&gt;invokes the request handler,&lt;/li&gt;
&lt;li&gt;and if keep-alive is enabled, continues with the next request using the same resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critical idea here is resource ownership at the connection level rather than the request level. This minimizes repetitive buffer rental and return operations across keep-alive requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  SocketReceiver: Async I/O with Allocation Discipline
&lt;/h2&gt;

&lt;p&gt;Instead of using the more convenient &lt;code&gt;Socket.ReceiveAsync(Memory)&lt;/code&gt; APIs, Anka uses &lt;code&gt;SocketAsyncEventArgs&lt;/code&gt; together with &lt;code&gt;IValueTaskSource&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is not micro-optimization for its own sake. The goal is to avoid state machine overhead and additional allocations when socket reads complete synchronously.&lt;/p&gt;

&lt;p&gt;In loopback or LAN scenarios, data is often already available in the kernel buffer. In those cases, the receive operation completes synchronously and the system proceeds with almost direct function-call overhead. If data is not available, the OS I/O completion thread finishes the operation, while continuations are posted back to the thread pool using &lt;code&gt;RunContinuationsAsynchronously = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result: I/O threads are not blocked by request-processing work.&lt;/p&gt;




&lt;h2&gt;
  
  
  HttpParser: Single-Pass Parsing
&lt;/h2&gt;

&lt;p&gt;The parser’s real value is not merely that it parses HTTP, but that it does so in a single pass using pooled buffers.&lt;/p&gt;

&lt;p&gt;It performs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request line parsing,&lt;/li&gt;
&lt;li&gt;path/query separation,&lt;/li&gt;
&lt;li&gt;lowercase normalization of header names during ingestion,&lt;/li&gt;
&lt;li&gt;inline &lt;code&gt;Content-Length&lt;/code&gt; tracking,&lt;/li&gt;
&lt;li&gt;request body reading when necessary,&lt;/li&gt;
&lt;li&gt;chunked request body decoding and reassembly into &lt;code&gt;req.Body&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;keep-alive decision calculation during parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach provides two important benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No second header scan is required.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;HttpRequest&lt;/code&gt; passed to the handler is already in a usable, near zero-copy state.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  HttpResponseWriter: Not a Framework, Just Wire Format
&lt;/h2&gt;

&lt;p&gt;The response layer follows the same philosophy. &lt;code&gt;HttpResponseWriter&lt;/code&gt; constructs the HTTP response header block inside a pooled buffer, inlines small bodies into the same buffer whenever possible, and writes directly through &lt;code&gt;Socket.SendAsync()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Another important design choice here is the absence of chunked response encoding. All responses use &lt;code&gt;Content-Length&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This constraint simplifies the response pipeline significantly and narrows the execution path for AOT and low-allocation goals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Design Decisions
&lt;/h2&gt;

&lt;h2&gt;
  
  
  A Single RequestHandler Delegate
&lt;/h2&gt;

&lt;p&gt;Anka has no middleware chain. There is only one &lt;code&gt;RequestHandler&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a deliberate form of radical simplification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;shorter call chains,&lt;/li&gt;
&lt;li&gt;fewer abstraction layers,&lt;/li&gt;
&lt;li&gt;lower dispatch overhead,&lt;/li&gt;
&lt;li&gt;a cleaner surface for Native AOT.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Costs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;cross-cutting concerns must be implemented manually,&lt;/li&gt;
&lt;li&gt;routing, authentication, logging, and metrics are not framework-managed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not an “incomplete framework”; it is a primitive-oriented design.&lt;/p&gt;




&lt;h2&gt;
  
  
  No Built-in Routing
&lt;/h2&gt;

&lt;p&gt;Path dispatching is left entirely to user code. A simple &lt;code&gt;switch&lt;/code&gt; statement is considered sufficient for many scenarios.&lt;/p&gt;

&lt;p&gt;At first glance this may appear primitive, but routing costs are not limited to path matching itself. Routing systems also bring metadata graphs, endpoint conventions, and large API surfaces. Anka rejects that cost upfront.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lowercasing Header Names During Ingestion
&lt;/h2&gt;

&lt;p&gt;Instead of performing case-insensitive lookups repeatedly, header names are normalized during parsing. This allows lookups to use straightforward &lt;code&gt;SequenceEqual&lt;/code&gt; comparisons.&lt;/p&gt;

&lt;p&gt;It may seem like a small design detail, but on the hot path it reduces both branching and processing overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reusing HttpRequest and Buffers Across Connections
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HttpRequest.ResetForReuse()&lt;/code&gt; approach avoids discarding request objects after every request. The same object is reused throughout the connection lifetime.&lt;/p&gt;

&lt;p&gt;This becomes especially effective under keep-alive workloads, because steady-state allocation reductions often come not from huge optimizations, but from many small, systematic reuse decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Small CAS-Based Request Pool
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HttpRequestPool&lt;/code&gt; is lock-free and intentionally small.&lt;/p&gt;

&lt;p&gt;The goal is not theoretical “infinite pooling,” but a practical, cache-friendly, low-overhead reuse mechanism. If all slots are occupied, objects are simply discarded.&lt;/p&gt;

&lt;p&gt;This is another important engineering decision: trying to pool everything can sometimes become more expensive than not pooling at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-offs: What This Architecture Gains — and Intentionally Gives Up
&lt;/h2&gt;

&lt;p&gt;One of Anka’s strengths is that it clearly communicates what it is not.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No middleware&lt;/td&gt;
&lt;td&gt;Shorter hot path, fewer abstractions&lt;/td&gt;
&lt;td&gt;Developers handle cross-cutting concerns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No built-in routing&lt;/td&gt;
&lt;td&gt;Lower startup cost, simpler model&lt;/td&gt;
&lt;td&gt;Reduced ergonomics in large APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/1.x only&lt;/td&gt;
&lt;td&gt;Narrower execution path, simpler AOT surface&lt;/td&gt;
&lt;td&gt;No modern protocol features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No built-in TLS&lt;/td&gt;
&lt;td&gt;Keeps the core minimal&lt;/td&gt;
&lt;td&gt;Reverse proxy becomes mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No chunked responses&lt;/td&gt;
&lt;td&gt;Simpler response writer&lt;/td&gt;
&lt;td&gt;Limited streaming ergonomics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw socket approach&lt;/td&gt;
&lt;td&gt;Full control over allocations and behavior&lt;/td&gt;
&lt;td&gt;Higher maintenance burden&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most important trade-off is this:&lt;/p&gt;

&lt;p&gt;Anka does not exist to replace Kestrel. It exists to occupy a different optimization point.&lt;/p&gt;

&lt;p&gt;If you need HTTP/2, TLS termination, middleware ecosystems, advanced hosting integrations, or rich observability plugins, then Kestrel is probably still the correct answer.&lt;/p&gt;

&lt;p&gt;But if your priority is low footprint, rapid readiness, and a highly controlled request path, Anka becomes compelling.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Strongest Part of This Design: Predictability, Not Performance
&lt;/h2&gt;

&lt;p&gt;In my opinion, the real value here is not just “0 B allocations” or “2.3 ms startup.”&lt;/p&gt;

&lt;p&gt;The real value is how easy it becomes to mentally model the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one task per connection,&lt;/li&gt;
&lt;li&gt;connection-scoped buffers,&lt;/li&gt;
&lt;li&gt;parse → handle → write pipeline,&lt;/li&gt;
&lt;li&gt;very few failure states,&lt;/li&gt;
&lt;li&gt;limited protocol surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecturally, systems like this become valuable not because of benchmark presentations, but because of operational clarity. You know exactly what the system does, what it does not do, where behavior changes under load, and where costs are paid.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example Use Cases
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Serverless or Short-Lived Container APIs
&lt;/h2&gt;

&lt;p&gt;If cold start is critical and the endpoint surface is small, Anka’s minimal startup overhead becomes a major advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sidecar / Internal Control Plane Endpoints
&lt;/h2&gt;

&lt;p&gt;For health checks, readiness probes, metrics gateways, or simple control APIs, a full framework is often unnecessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edge Compute and Appliance Deployments
&lt;/h2&gt;

&lt;p&gt;Low RSS and Native AOT compatibility matter significantly in resource-constrained environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmarking and Protocol Research
&lt;/h2&gt;

&lt;p&gt;Anka provides a clean environment for teams that want to observe HTTP pipeline behavior without framework noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Narrowly Scoped, High-Control Services
&lt;/h2&gt;

&lt;p&gt;It is well suited for single-purpose webhook receivers, internal callback endpoints, or lightweight ingestion services.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I Would Not Use It
&lt;/h2&gt;

&lt;p&gt;Personally, I would avoid this architecture for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large general-purpose product APIs developed by big teams,&lt;/li&gt;
&lt;li&gt;applications requiring rich middleware ecosystems,&lt;/li&gt;
&lt;li&gt;systems where streaming, TLS, or HTTP/2 are first-class requirements,&lt;/li&gt;
&lt;li&gt;organizations where framework integrations matter more than low-level control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because what you gain here is not “freedom.”&lt;/p&gt;

&lt;p&gt;It is control.&lt;/p&gt;

&lt;p&gt;And control always comes with increased engineering responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The most accurate technical summary of Anka is this:&lt;/p&gt;

&lt;p&gt;It is not an HTTP server that gains performance by adding optimizations on top of features; it gains simplicity and predictability by deliberately reducing scope.&lt;/p&gt;

&lt;p&gt;Native AOT compatibility, low startup allocations, connection-scoped resource management, single-pass parsing, and direct socket writing all serve the same goal:&lt;/p&gt;

&lt;p&gt;a small, fast-starting, understandable request pipeline.&lt;/p&gt;

&lt;p&gt;This approach is not for everyone — and it is not trying to be.&lt;/p&gt;

&lt;p&gt;From an experienced backend engineering perspective, its most mature characteristic is precisely this: it defines the problem narrowly, optimizes aggressively for that problem, and consciously leaves everything else outside the boundary.&lt;/p&gt;

&lt;p&gt;And often, that is a far more valuable engineering decision than building a system that tries to do everything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Originally published in Turkish on my personal blog:&lt;br&gt;&lt;br&gt;
&lt;a href="https://selcukgural.com/2026/04/17/anka-native-aot-icin-minimal-http-sunucusu-tasarlamak-ankanin-mimarisi-ve-bilincli-kisitlari/" rel="noopener noreferrer"&gt;https://selcukgural.com/2026/04/17/anka-native-aot-icin-minimal-http-sunucusu-tasarlamak-ankanin-mimarisi-ve-bilincli-kisitlari/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>anka</category>
      <category>kestrel</category>
      <category>httpframework</category>
      <category>aot</category>
    </item>
  </channel>
</rss>
