<?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: Ian Cowley</title>
    <description>The latest articles on DEV Community by Ian Cowley (@iancowley).</description>
    <link>https://dev.to/iancowley</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3928889%2F2cfd8346-cffe-47c8-8e57-0aef2a0a4abc.jpeg</url>
      <title>DEV Community: Ian Cowley</title>
      <link>https://dev.to/iancowley</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iancowley"/>
    <language>en</language>
    <item>
      <title>I built a C# Knowledge Layer to solve the AI Agent Memory Crisis</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Mon, 18 May 2026 17:34:40 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-c-knowledge-layer-to-solve-the-ai-agent-memory-crisis-34mj</link>
      <guid>https://dev.to/iancowley/i-built-a-c-knowledge-layer-to-solve-the-ai-agent-memory-crisis-34mj</guid>
      <description>&lt;p&gt;If you are building AI Agents today, you’ve probably noticed a glaring problem: &lt;strong&gt;The Memory Wall&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you give an agent a task (like resolving a complex customer support escalation), it doesn't just need to read a single document. It needs to look at CRM data, trace fraud relationships, read strict legal policies, and review historical tickets.&lt;/p&gt;

&lt;p&gt;The industry's current solution is to give the Agent a bunch of separate tools (APIs) and let it figure it out. The result? The Agent burns up 80% of your token budget and 5 seconds of latency just looping through databases, trying to assemble the context itself. Often, it gets confused and hallucinates.&lt;/p&gt;

&lt;p&gt;As a software engineer of 40 years, I prefer deterministic logic and bare-metal performance. Agents shouldn't assemble data; our backend architecture should assemble it for them.&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I’ve built a suite of zero-dependency, hyper-optimized C# storage engines to handle the four distinct "shapes" of enterprise data. Today, I am releasing the final piece: &lt;a href="https://github.com/ian-cowley/Glacier.Bundle" rel="noopener noreferrer"&gt;&lt;strong&gt;Glacier.Bundle&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is a unified semantic orchestration engine that compiles relational, hierarchical, tabular, and vector data into a single, high-density prompt bundle in &lt;strong&gt;under 20 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Four Pillars of AI Memory
&lt;/h3&gt;

&lt;p&gt;Before building the orchestrator, I had to build the engines. If you missed the previous articles, &lt;code&gt;Glacier.Bundle&lt;/code&gt; sits on top of these four native C# libraries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;📊 &lt;strong&gt;The Tabular Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/i-built-a-native-c-dataframe-engine-to-rival-python-polars-its-actually-faster-on-some-things-opg"&gt;I built a native C# DataFrame engine to rival Python Polars&lt;/a&gt; (&lt;code&gt;Glacier.Polaris&lt;/code&gt; / &lt;code&gt;PolarsPlus&lt;/code&gt;) for structured CRM and metric data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🧠 &lt;strong&gt;The Semantic Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/i-built-a-zero-dependency-c-vector-database-that-saturates-ddr5-ram-bandwidth-5f9a"&gt;I built a zero-dependency C# Vector Database that saturates DDR5 RAM&lt;/a&gt; (&lt;code&gt;Glacier.Vector&lt;/code&gt;) for fuzzy, historical similarity matching using AVX-512 SIMD.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🌐 &lt;strong&gt;The Relational Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/i-built-a-zero-allocation-c-knowledge-graph-because-jvm-graphs-are-too-bloated-4pej"&gt;I built a zero-allocation C# Knowledge Graph&lt;/a&gt; (&lt;code&gt;Glacier.Graph&lt;/code&gt;) to instantly map fraud rings and entity neighborhoods.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;📂 &lt;strong&gt;The Hierarchical Layer:&lt;/strong&gt; &lt;a href="https://dev.to/iancowley/why-naive-rag-is-dead-i-built-a-zero-dependency-c-semantic-tree-parser-2ph8"&gt;I built a Semantic Tree Parser because Naive RAG is dead&lt;/a&gt; (&lt;code&gt;Glacier.DocTree&lt;/code&gt;) to extract deterministic document policies without chunking them into oblivion.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Problem: Bridging the Islands
&lt;/h3&gt;

&lt;p&gt;Having four incredibly fast databases is useless if your AI Agent has to make four separate HTTP REST calls to query them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Glacier.Bundle&lt;/code&gt; acts as the single central broker. It runs in-memory alongside your Agent. You register your tabular data, your vector indices, your graph store, and your parsed markdown documents into a thread-safe &lt;code&gt;BundleContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then, instead of asking the AI to &lt;em&gt;"go find the data,"&lt;/em&gt; you use the &lt;code&gt;BundleBuilder&lt;/code&gt; to programmatically compile the absolute truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code: Compiling Context in C
&lt;/h3&gt;

&lt;p&gt;Here is how you compile a massive, multi-dimensional prompt for a customer escalation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queryVector&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer requested API rate expansion."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The fluent, zero-allocation context compiler&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;contextPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BundleBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundleCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginBundle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Customer Escalation Resolution"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Instantly pull structured CRM data (Polaris)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendTabularRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Client CRM Profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  ID: User_9021"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Name: DeltaCorp Ltd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  Tier: Enterprise"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Traverse the Knowledge Graph for suspicious IP links (Graph)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendGraphTopology&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Graph_Topology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"User_9021"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxHops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Relational Fraud Mapping"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Extract the exact SLA section without chunking errors (DocTree)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendDocumentTreeSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DocTree_SLA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Enterprise SLAs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Guaranteed SLA Policies"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Do a SIMD-accelerated math search for past tickets (Vector)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendVectorContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Vector_Tickets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Semantic Ticket History"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Output: 17 Milliseconds
&lt;/h3&gt;

&lt;p&gt;When we run this code, the C# runtime queries all four engines—extracting CRM details, tracing 2-hop relational fraud paths, pulling strict SLA Markdown structures, and executing a brute-force vector search.&lt;/p&gt;

&lt;p&gt;Here is the benchmark output from the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[5] Executing high-speed Bundle compilation...

Bundle compiled in 17.4316 ms!

======================================================================
 SYSTEM RESOLVED CONTEXT BUNDLE: CUSTOMER ESCALATION: DELTACORP LTD
 GENERATED AT: 2026-05-18 17:14:55 UTC
======================================================================

--- [DATA LAYER] CLIENT CRM PROFILE ---
  ID: User_9021
  Name: DeltaCorp Ltd
  Tier: Enterprise
  Value: £85,200.00
  Status: Active

--- [RELATIONAL LAYER] RELATIONAL FRAUD MAPPING (2 HOPS FROM User_9021) ---
Target Entity: User_9021
Connected Network Entities (3):
  SubAccount_B, SubAccount_A, Suspicious_IP_88

--- [HIERARCHICAL LAYER] GUARANTEED SLA POLICIES ---
Document Path: Document Root &amp;gt; Service Level Agreements &amp;gt; Enterprise SLAs
Content Frame:
Enterprise SLAs
Enterprise accounts enjoy a guaranteed 99.99% uptime with immediate 15-minute response times.
No hardware throttling is applied.

--- [SEMANTIC LAYER] SEMANTIC TICKET HISTORY ---
[Rank 1 | Match Score: 1.0000] Historical Ticket #4823: Customer requested custom API access rate expansion. Granted temporary bypass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Result: Zero Hallucinations
&lt;/h3&gt;

&lt;p&gt;Look at that output block. If you hand that string to an LLM, &lt;strong&gt;it cannot hallucinate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't have to guess what tier the customer is on. It doesn't have to guess if the cancellation policy applies to them. It doesn't have to guess if they are connected to a suspicious IP. The deterministic C# application code proved it all &lt;em&gt;before&lt;/em&gt; the LLM was even invoked.&lt;/p&gt;

&lt;p&gt;And it did it in &lt;strong&gt;17.4 milliseconds&lt;/strong&gt;—faster than a standard web API takes to negotiate a TLS handshake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try the Full Suite
&lt;/h3&gt;

&lt;p&gt;If you are a .NET developer building AI infrastructure, and you are tired of relying on bloated JVM containers and Python scripts, you can now run the entire AI data stack natively in C#.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Bundle" rel="noopener noreferrer"&gt;ian-cowley/Glacier.Bundle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;code&gt;dotnet add package Glacier.Bundle&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This completes the &lt;strong&gt;Glacier High-Performance Storage Suite&lt;/strong&gt;. Let's prove C# is the ultimate backend language for the AI era!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>rag</category>
    </item>
    <item>
      <title>Why Naive RAG is Dead: I built a zero-dependency C# Semantic Tree Parser</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Mon, 18 May 2026 17:07:05 +0000</pubDate>
      <link>https://dev.to/iancowley/why-naive-rag-is-dead-i-built-a-zero-dependency-c-semantic-tree-parser-2ph8</link>
      <guid>https://dev.to/iancowley/why-naive-rag-is-dead-i-built-a-zero-dependency-c-semantic-tree-parser-2ph8</guid>
      <description>&lt;p&gt;If you have built an AI Agent or a RAG (Retrieval-Augmented Generation) pipeline in the last year, you’ve almost certainly run into the exact same problem: &lt;strong&gt;Hallucinations caused by Naive Chunking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The standard industry advice for feeding documents to an AI is to take a 50-page API manual, blindly chop it up into 500-token chunks, vectorize them, and throw them into a database. &lt;/p&gt;

&lt;p&gt;This completely destroys the document's structure. &lt;/p&gt;

&lt;p&gt;If you have a paragraph that says, &lt;em&gt;"If initiated after 30 days, a 15% fee will be deducted"&lt;/em&gt;, and it gets chunked away from its &lt;code&gt;## Enterprise Cancellations&lt;/code&gt; header, the AI has no idea who that rule applies to. When a user asks about &lt;em&gt;Free Tier&lt;/em&gt; cancellations, the Vector DB might return that enterprise paragraph just because the words matched. Boom. Hallucination.&lt;/p&gt;

&lt;p&gt;As a software engineer who hates bloat and relies on deterministic logic, I needed a better way. I didn't want to use massive Python libraries to solve this.&lt;/p&gt;

&lt;p&gt;So, I built &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.DocTree" rel="noopener noreferrer"&gt;Glacier.DocTree&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;It is a zero-dependency, bare-metal C# library that parses documents into a &lt;strong&gt;Semantic Tree&lt;/strong&gt; instead of flattening them into dumb chunks. &lt;/p&gt;




&lt;h3&gt;
  
  
  The Fix: Hierarchical Parsing
&lt;/h3&gt;

&lt;p&gt;Instead of chopping text blindly by character count, &lt;code&gt;Glacier.DocTree&lt;/code&gt; reads Markdown and builds a strongly-typed parent-child object graph. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#&lt;/code&gt; (H1) becomes a root node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;##&lt;/code&gt; (H2) becomes a child of the last active H1.&lt;/li&gt;
&lt;li&gt;Standard text and code blocks become children of their most recent header.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you feed it a messy API document, it instantly compiles this beautiful, queryable hierarchy in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==========================================
 Glacier.DocTree | Semantic Parser Engine
==========================================

[1] Parsing Markdown into Semantic Tree...

[2] Visualizing the Document Structure:
└─ [Root] Document Root
  └─ [Header1] Glacier Enterprise API
    └─ [Paragraph] Welcome to the Glacier API. This docu...
    └─ [Header2] Authentication
      └─ [Paragraph] All requests to the API must be crypt...
      └─ [Header3] OAuth 2.0
        └─ [Paragraph] To authenticate via OAuth2, you must ...
        └─ [CodeBlock] ```

json {    "Authorization": "Bearer...
    └─ [Header2] Usage Policies
      └─ [Paragraph] Please adhere to the following usage ...
      └─ [Header3] Rate Limits
        └─ [Paragraph] Free tier users are limited to 100 re...
      └─ [Header3] Acceptable Use
        └─ [Paragraph] Do not use the API to train competing...

[3] Simulating Agent Query: 'Extract Rate Limits context'

--- SEMANTIC CONTEXT ---
LOCATION: Document Root &amp;gt; Glacier Enterprise API &amp;gt; Usage Policies &amp;gt; Rate Limits
--- BEGIN TEXT ---
Rate Limits
Free tier users are limited to 100 requests per minute.
Enterprise users have unlimited access.
If you exceed the limit, you will receive an HTTP 429 status code.
--- END TEXT ---


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that &lt;code&gt;LOCATION&lt;/code&gt; string. &lt;br&gt;
If an LLM reads that, it &lt;strong&gt;cannot&lt;/strong&gt; hallucinate. It knows exactly what document it's looking at, what section it is in, and who the policy applies to. The structure &lt;em&gt;is&lt;/em&gt; the meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;If you are a .NET developer building AI infrastructure, and you are tired of your Vector DB returning paragraphs completely devoid of context, you need a Semantic Layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.DocTree" rel="noopener noreferrer"&gt;ian-cowley/Glacier.DocTree&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is purely native C#. No heavy frameworks, no Python interop, no API keys required. It just parses documents at blistering speeds and gives your agents the context they actually need. &lt;/p&gt;

&lt;p&gt;Let's prove C# belongs in the modern AI ecosystem!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>rag</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>I built a Zero-Allocation C# Knowledge Graph (because JVM graphs are too bloated)</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Mon, 18 May 2026 11:53:55 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-zero-allocation-c-knowledge-graph-because-jvm-graphs-are-too-bloated-4pej</link>
      <guid>https://dev.to/iancowley/i-built-a-zero-allocation-c-knowledge-graph-because-jvm-graphs-are-too-bloated-4pej</guid>
      <description>&lt;p&gt;If you are building AI agents, you eventually hit the "Memory Wall".&lt;/p&gt;

&lt;p&gt;Your agent doesn't just need semantic text chunks (Vector Search) or structured tables (SQL). It often needs to trace &lt;strong&gt;relationships&lt;/strong&gt;. For example: &lt;em&gt;Find all suppliers connected to this failing part,&lt;/em&gt; or &lt;em&gt;Find the common connection between User_A and User_B.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To solve this, the industry tells you to spin up a massive Java-based Graph Database container.&lt;/p&gt;

&lt;p&gt;I've been writing C# and SQL for decades, and I despise unnecessary bloat. I didn't want to run a 2GB JVM container locally just to traverse a few hundred thousand relationships.&lt;/p&gt;

&lt;p&gt;So, I built &lt;a href="https://github.com/ian-cowley/Glacier.Graph" rel="noopener noreferrer"&gt;&lt;strong&gt;Glacier.Graph&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is a zero-dependency, bare-metal C# Knowledge Graph. It completely bypasses the .NET Garbage Collector and can persist 230,000 edges to disk in 30 milliseconds.&lt;/p&gt;

&lt;p&gt;Here is how I architected the engine to achieve memory-bandwidth speeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with "Object-Oriented" Graphs
&lt;/h3&gt;

&lt;p&gt;When most developers try to build a graph in memory, they immediately reach for Object-Oriented Programming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Node&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Edge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Edges&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Edge&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RelationType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have 100,000 nodes and 500,000 relationships, the .NET Garbage Collector now has to track &lt;strong&gt;600,000 object headers&lt;/strong&gt; scattered randomly across the heap. Traversing this graph means chasing pointer references through RAM, missing the CPU cache every single time, and dealing with brutal GC pauses.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: The "Forward Star" Representation
&lt;/h3&gt;

&lt;p&gt;Instead of objects, &lt;code&gt;Glacier.Graph&lt;/code&gt; uses the &lt;strong&gt;Forward Star&lt;/strong&gt; (or Compressed Sparse Row) technique.&lt;/p&gt;

&lt;p&gt;All edges are stored in flat, primitive integer arrays. When you add a relationship, the engine doesn't allocate an object; it just increments an index and writes to &lt;code&gt;int[]&lt;/code&gt; arrays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The core of the Graph Store&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_head&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Starting edge index for a node&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_to&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// Target node ID&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_relation&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Type of relation (e.g. "KNOWS")&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Index of the next edge for this node&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you traverse the graph, you aren't doing heap allocations. You are just doing sequential array index lookups. The CPU cache prefetcher loves this, resulting in near-instantaneous traversal speeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Benchmark: Traversing 150,000 Nodes
&lt;/h3&gt;

&lt;p&gt;I generated a complex synthetic graph with &lt;strong&gt;150,000 nodes&lt;/strong&gt; and &lt;strong&gt;233,000 edges&lt;/strong&gt; (including chains, branches, and back-references).&lt;/p&gt;

&lt;p&gt;I then ran a Breadth-First Search (BFS) to find the shortest path between &lt;code&gt;User_1&lt;/code&gt; and &lt;code&gt;User_99999&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the raw console output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==========================================
 Glacier.Graph | High-Performance Graph DB
==========================================

[1] Initializing Graph Engine and generating data...
    Total Nodes: 150,001 | Total Edges: 233,333

[3] Executing BFS Shortest Path (User_1 -&amp;gt; User_99999)...
    Path found in 5.3066 ms!
    Hops: 25
    Route: User_1 -&amp;gt; ... (24 intermediate nodes) ... -&amp;gt; User_99999

[4] Executing 4-Hop Neighborhood Search around User_1...
    Neighborhood scanned in 0.4416 ms!
    Discovered 11 connected entities within 4 hops.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finding a 25-hop path through 150,000 nodes took &lt;strong&gt;5.3 milliseconds&lt;/strong&gt;. Mapping an entire 4-hop neighborhood took &lt;strong&gt;0.44 milliseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can't even complete the HTTP handshake to a standard database in the time it takes this engine to traverse the entire network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blazing Fast Persistence
&lt;/h3&gt;

&lt;p&gt;Because the graph is just a collection of primitive &lt;code&gt;int[]&lt;/code&gt; arrays, saving the graph to disk is incredibly fast. We don't use JSON or heavy serialization.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;MemoryMarshal.AsBytes()&lt;/code&gt;, the engine grabs the raw bytes of the arrays directly out of RAM and blasts them to the SSD.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2] Saving raw memory arrays to disk...
    Saved 27.46 MB to 'graph_database.bin' in 30 ms.

[3] Destroying graph in memory and reloading from disk...
    Graph revived from disk in 41 ms!

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;30 milliseconds to save.&lt;/strong&gt; &lt;strong&gt;41 milliseconds to revive.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Built for AI Agents
&lt;/h3&gt;

&lt;p&gt;Like my other libraries, I didn't want to build a bloated REST API.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Glacier.Graph&lt;/code&gt; includes a built-in &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; server over standard I/O. You can point your AI agents (via AgentDevKit, Claude Desktop, or Cursor) directly at the &lt;code&gt;.dll&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The AI instantly gets JSON-RPC access to &lt;code&gt;add_node&lt;/code&gt;, &lt;code&gt;add_edge&lt;/code&gt;, &lt;code&gt;find_shortest_path&lt;/code&gt;, and &lt;code&gt;find_neighborhood&lt;/code&gt;. It allows LLMs to autonomously traverse a high-speed Knowledge Graph without any Python dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;If you are tired of massive containerized databases and want bare-metal C# performance for your relational AI data, give it a shot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Graph" rel="noopener noreferrer"&gt;ian-cowley/Glacier.Graph&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me know what your traversal times look like!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>I built a zero-dependency C# Vector Database that saturates DDR5 RAM bandwidth</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Sat, 16 May 2026 13:39:40 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-zero-dependency-c-vector-database-that-saturates-ddr5-ram-bandwidth-5f9a</link>
      <guid>https://dev.to/iancowley/i-built-a-zero-dependency-c-vector-database-that-saturates-ddr5-ram-bandwidth-5f9a</guid>
      <description>&lt;p&gt;If you’re building AI apps today, you eventually need a Vector Database for RAG (Retrieval-Augmented Generation) or giving your agents long-term memory. &lt;/p&gt;

&lt;p&gt;The current ecosystem’s answer to this problem usually involves one of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pay for a cloud service.&lt;/li&gt;
&lt;li&gt;Spin up a massive Rust, Go, or Python Docker container locally (like Qdrant, Chroma, or Milvus).&lt;/li&gt;
&lt;li&gt;Use a bloated wrapper library that pulls in 50MB of dependencies just to do math.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’ve been a software engineer for 40 years. I despise bloat. I don't use heavy data-access frameworks or massive client-side libraries. At its core, a vector database is just a massive 2D array of floats and a tight math loop. I didn't want to spin up a Docker container or rely on Python interop just to do math.&lt;/p&gt;

&lt;p&gt;So, I built &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Vector" rel="noopener noreferrer"&gt;Glacier.Vector&lt;/a&gt;&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;It is a purely native, zero-dependency, hardware-accelerated Vector Database for .NET 10. And it is fast enough to literally hit the physical limits of my motherboard.&lt;/p&gt;

&lt;p&gt;Here is how I squeezed every drop of performance out of the .NET runtime.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. The Goal: Zero Allocations
&lt;/h3&gt;

&lt;p&gt;If you have 100,000 documents, and each has a 1536-dimensional embedding (the OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; standard), you are looking at about 153.6 million floats (~600 MB of RAM). &lt;/p&gt;

&lt;p&gt;If you store this as a &lt;code&gt;float[][]&lt;/code&gt; (an array of arrays), the .NET Garbage Collector will create 100,000 object headers on the heap. Your cache locality is ruined, and your GC pause times will be brutal.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;Glacier.Vector&lt;/code&gt; uses a zero-copy memory model. It allocates flat arrays in massive chunks, or uses Memory-Mapped files, completely bypassing GC pauses. &lt;/p&gt;

&lt;p&gt;When searching, the engine pins the memory and uses &lt;code&gt;fixed&lt;/code&gt; pointers and &lt;code&gt;ReadOnlySpan&amp;lt;float&amp;gt;&lt;/code&gt;. No bounds checking. No object allocations in the hot path.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pushing the CPU: 4-Way SIMD Unrolling
&lt;/h3&gt;

&lt;p&gt;Searching a vector database requires comparing the user's query against every single document using Cosine Similarity (which, for normalized vectors, is just the Dot Product).&lt;/p&gt;

&lt;p&gt;In standard C#, a scalar &lt;code&gt;for&lt;/code&gt; loop over 150 million floats takes forever. &lt;/p&gt;

&lt;p&gt;To fix this, I wrote a custom compute kernel using .NET hardware intrinsics (&lt;code&gt;System.Runtime.Intrinsics&lt;/code&gt;). But just using &lt;code&gt;Vector256&lt;/code&gt; wasn't enough. I unrolled the loop 4-ways to feed the CPU's out-of-order execution pipeline with simultaneous Fused-Multiply-Add (FMA) instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unrolled AVX2 fast path (processing 32 floats per cycle)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;acc0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;acc1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;acc0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MultiplyAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pTarget&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pDb&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;acc0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;acc1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Fma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MultiplyAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pTarget&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Vector256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pDb&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;acc1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Benchmark: Hitting the Memory Wall&lt;br&gt;
I ran a benchmark pumping 100,000 vectors (1536 dimensions each) into the engine. Here is the raw console output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==========================================
 Glacier.Vector | SIMD Performance Engine
==========================================

[1] Initializing In-Memory Storage...
    Dimensions: 1536
    Target Count: 100,000

[2] Generating and loading synthetic vectors...
    Done! Loaded 100,000 vectors in 547 ms.

[3] Preparing search query...
[4] Executing SIMD brute-force search...

==========================================
 SEARCH COMPLETED IN: 7.152 ms
 Vectors scanned:     100,000
 Operations/sec:      13,982,298
==========================================

Top 5 Results:
  Rank 1 | Score: 0.1056 | ID: 51030 | Meta: Document_Chunk_51030
  Rank 2 | Score: 0.1019 | ID: 87632 | Meta: Document_Chunk_87632
  Rank 3 | Score: 0.1003 | ID: 52591 | Meta: Document_Chunk_52591
  Rank 4 | Score: 0.0994 | ID: 96139 | Meta: Document_Chunk_96139
  Rank 5 | Score: 0.0990 | ID: 29879 | Meta: Document_Chunk_29879
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To search the entire database—scanning 153.6 million floats—it took exactly 7.15 milliseconds.&lt;/p&gt;

&lt;p&gt;At that speed, the engine is demanding roughly 85 Gigabytes per second of memory bandwidth. The dual-channel DDR5 RAM on my motherboard physically maxes out right around 85-90 GB/s.&lt;/p&gt;

&lt;p&gt;I literally cannot make this C# code any faster without buying faster RAM. We hit the physical memory wall.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Built-in AI Integration (MCP)
A vector database isn't useful if AI agents can't talk to it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of building a bloated REST API or requiring gRPC, I built a native Model Context Protocol (MCP) server directly into the engine. It runs entirely over standard I/O (stdio).&lt;/p&gt;

&lt;p&gt;You can configure Claude Desktop, Cursor, or your own autonomous agents to point directly at the Glacier.Vector.Host.dll. The AI instantly understands how to call the add_vector and search_vectors tools via JSON-RPC. Zero Python, zero external API keys, zero network latency.&lt;/p&gt;

&lt;p&gt;Try it out&lt;br&gt;
If you are a .NET developer building RAG pipelines, AI agents, or just dealing with heavy data, and you hate bloated frameworks as much as I do, try it out.&lt;/p&gt;

&lt;p&gt;NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Glacier.Vector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/ian-cowley/Glacier.Vector" rel="noopener noreferrer"&gt;ian-cowley/Glacier.Vector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It pairs perfectly with my other recent project, &lt;a href="https://github.com/ian-cowley/AgentDevKit" rel="noopener noreferrer"&gt;AgentDevKit&lt;/a&gt; (a native C# LLM orchestration library).&lt;/p&gt;

&lt;p&gt;Drop a star on the repo, throw a few million vectors at the memory storage, and let me know how many milliseconds it takes to saturate your RAM! Let's prove C# belongs in the AI ecosystem.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>C# got left behind in the AI Agent hype. So I fixed it! AgentDevKit</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Thu, 14 May 2026 17:14:04 +0000</pubDate>
      <link>https://dev.to/iancowley/c-got-left-behind-in-the-ai-agent-hype-so-i-fixed-it-agentdevkit-4b40</link>
      <guid>https://dev.to/iancowley/c-got-left-behind-in-the-ai-agent-hype-so-i-fixed-it-agentdevkit-4b40</guid>
      <description>&lt;p&gt;If you’re a .NET developer watching the current AI landscape, you probably know the feeling.&lt;/p&gt;

&lt;p&gt;A massive new paradigm drops—in this case, autonomous AI agents—and overnight, Python and TypeScript are flooded with official SDKs, shiny frameworks like LangChain or crewAI, and endless tutorials.&lt;/p&gt;

&lt;p&gt;Meanwhile, us C# backend devs are left staring at the wall, waiting for an enterprise-grade port that will inevitably be bloated, overly abstracted, and arrive 18 months late.&lt;/p&gt;

&lt;p&gt;I’ve been writing backend software and integrations for 40 years. I don’t like waiting, and I don't like bloated frameworks. I just wanted a native, high-performance way to build AI agents in C# that can actually &lt;em&gt;do&lt;/em&gt; things—read files, query databases, and use the new Model Context Protocol (MCP) without a mountain of boilerplate.&lt;/p&gt;

&lt;p&gt;Since it didn't exist, I built it. &lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/AgentDevKit" rel="noopener noreferrer"&gt;AgentDevKit (ADK)&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is it?
&lt;/h3&gt;

&lt;p&gt;It’s a native C# Agent Development Kit designed to help you create "Agents"—AI personalities powered by Google Gemini that don't just talk, but &lt;strong&gt;think, plan, and act&lt;/strong&gt; using real-world tools.&lt;/p&gt;

&lt;p&gt;Here is a quick look at what I built into it to make it actually useful for backend environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Brain &amp;amp; The Hands (Tools)
&lt;/h3&gt;

&lt;p&gt;An LLM is just a brain. Without tools, it's just a chatbot. ADK lets you easily attach "hands" to your agent so it can interact with your systems.&lt;/p&gt;

&lt;p&gt;When you ask the agent a question, it pauses, realizes it needs more info, executes your C# tool, and uses the result to finish its answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Setup the connection&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GeminiService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create the Agent&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SysAdminBot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"You are an expert system administrator."&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Give it hands (a tool you define in C#)&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileReadTool&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Run it&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What is in the server.log file?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Model Context Protocol (MCP) Integration
&lt;/h3&gt;

&lt;p&gt;This is the real game-changer. MCP is becoming the industry standard for letting AI securely plug into local environments.&lt;/p&gt;

&lt;p&gt;Instead of writing a custom C# wrapper for every single database or file system you want your AI to touch, ADK supports MCP out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Auto-connect to a local SQLite database using MCP&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;McpService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;databaseTools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeFromConfigAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;databaseTools&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Delegation (Building Teams)
&lt;/h3&gt;

&lt;p&gt;One agent is cool. A team of agents is dangerous.&lt;br&gt;
ADK supports orchestration patterns like Pipelines and Parallel workflows. You can literally give a "Manager" agent a "Researcher" agent to use as a tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;researcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Researcher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Find facts..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Manager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Guide the project..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The Manager can now delegate tasks to the Researcher&lt;/span&gt;
&lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DelegationTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Guardrails (Because I trust no one, especially AI)
&lt;/h3&gt;

&lt;p&gt;If you've managed backend integrations as long as I have, you know that giving an AI blind access to tools is a terrible idea.&lt;/p&gt;

&lt;p&gt;ADK includes &lt;strong&gt;Human-in-the-Loop (HITL)&lt;/strong&gt; approvals and interceptors. If a tool is dangerous, wrap it in a &lt;code&gt;SensitiveTool&lt;/code&gt;. The agent &lt;em&gt;cannot&lt;/em&gt; execute it without an explicit green light from your &lt;code&gt;IApprovalService&lt;/code&gt; (which you can hook up to a console prompt, a web UI, or an email).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Block the AI from breaking out of directories&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BeforeToolCall&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SecurityException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Path breakout attempt blocked!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Resilient Parsing
&lt;/h3&gt;

&lt;p&gt;Smaller local models (and even big ones sometimes) spit out malformed JSON when trying to call tools. ADK has a built-in Self-Correction Loop. If the LLM messes up the JSON structure, the SDK catches the error, feeds the error back to the model, and tells it to fix its formatting automatically based on a retry budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;I built this to fill a massive gap in the .NET ecosystem, keeping the architecture pragmatic and heavily focused on orchestration and safety.&lt;/p&gt;

&lt;p&gt;If you are a C# dev wanting to mess around with autonomous agents, MCP, and Gemini without having to learn Python, check it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/AgentDevKit" rel="noopener noreferrer"&gt;ian-cowley/AgentDevKit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop a star, try hooking it up to your database via MCP, and let me know how it goes. If you find any bugs, open an issue—I'd love to hear how other C# devs are using it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>csharp</category>
      <category>agents</category>
      <category>google</category>
    </item>
    <item>
      <title>I built a minimal, zero-dependency PDF library for C# because I hate bloat</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Wed, 13 May 2026 19:12:26 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-minimal-zero-dependency-pdf-library-for-c-because-i-hate-bloat-58b1</link>
      <guid>https://dev.to/iancowley/i-built-a-minimal-zero-dependency-pdf-library-for-c-because-i-hate-bloat-58b1</guid>
      <description>&lt;h1&gt;
  
  
  I built a minimal, zero-dependency PDF library for C# because I hate bloat
&lt;/h1&gt;

&lt;p&gt;If you’ve been writing C# for a while, you know the drill. You need to generate a simple PDF—maybe an invoice, a quick report, or a receipt.&lt;/p&gt;

&lt;p&gt;So, you open up NuGet, search for "PDF", and suddenly you are staring down the barrel of a 50MB dependency, a labyrinth of complex object models, and licensing terms that require a law degree to understand.&lt;/p&gt;

&lt;p&gt;I’ve been a developer for 40 years. I mostly work on backend and integration software for the printing industry. I like keeping things close to the metal, and I generally despise pulling in massive frameworks when a pragmatic, lightweight solution will do the trick.&lt;/p&gt;

&lt;p&gt;I didn't want a bloated library. I just wanted to draw some text, a few shapes, and maybe throw an image on a page.&lt;/p&gt;

&lt;p&gt;So, I ported a minimal PDF generator to .NET 10. Meet &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/tinypdf-csharp" rel="noopener noreferrer"&gt;tinypdf-csharp&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is it?
&lt;/h3&gt;

&lt;p&gt;It is a C# port of the original TypeScript &lt;code&gt;tinypdf&lt;/code&gt; by Lulzx. But I didn't just translate it; I added a few quality-of-life features that make it significantly more useful for day-to-day backend development.&lt;/p&gt;

&lt;p&gt;The entire library is &lt;strong&gt;just over 1,000 lines of code&lt;/strong&gt;. It has &lt;strong&gt;zero external dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Batteries Included" Additions
&lt;/h3&gt;

&lt;p&gt;While the original architecture was beautifully simple, I needed it to do a bit more heavy lifting for real-world tasks. Here is what I added over the original:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown to PDF:&lt;/strong&gt; This is my favorite addition. You can pass a raw Markdown string into the library, and it handles the layout and spits out a formatted PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flate (Deflate) Compression:&lt;/strong&gt; Automatically compresses the PDF streams so your file sizes stay tiny. (You can toggle it off if you need raw streams).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clickable Links:&lt;/strong&gt; Full support for clickable hyperlinks, complete with optional underline styling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, it still has all the basics: text rendering (Helvetica, Times, Courier), shapes (rectangles, circles, wedges, lines), and JPEG image embedding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show me the code
&lt;/h3&gt;

&lt;p&gt;It is designed to be ridiculously simple to use. Here is how you generate a PDF with some text and a red box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TinyPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TinyPdfCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;650&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"#FF0000"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you want to use the Markdown converter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TinyPdf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"# Header\n\nThis is a paragraph.\n\n- List item"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TinyPdfCreate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"markdown.pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Is it fast?
&lt;/h3&gt;

&lt;p&gt;Because there's no massive object tree or bloated memory footprint, it flies. I ran a benchmark generating 1,000 multi-page invoice PDFs concurrently, and it finished in &lt;strong&gt;~270 milliseconds&lt;/strong&gt;. That’s 0.27ms per PDF. If you are generating receipts or pie charts, it drops to around &lt;strong&gt;0.03ms&lt;/strong&gt; per PDF.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;If you are building a microservice, an AWS Lambda, or a simple backend integration and you just need to spit out PDFs without sacrificing your app's payload size, give it a try.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package TinyPdf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/tinypdf-csharp" rel="noopener noreferrer"&gt;ian-cowley/tinypdf-csharp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop by the repo, take a look at the code (again, it's only ~1000 lines, so you can read the whole thing on your lunch break), and let me know what you think. If it saves you from installing a massive legacy PDF framework today, my job is done.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>pdf</category>
      <category>dotnetcore</category>
      <category>markdown</category>
    </item>
    <item>
      <title>I built a native C# DataFrame engine to rival Python Polars (it's actually faster on some things)</title>
      <dc:creator>Ian Cowley</dc:creator>
      <pubDate>Wed, 13 May 2026 10:32:44 +0000</pubDate>
      <link>https://dev.to/iancowley/i-built-a-native-c-dataframe-engine-to-rival-python-polars-its-actually-faster-on-some-things-opg</link>
      <guid>https://dev.to/iancowley/i-built-a-native-c-dataframe-engine-to-rival-python-polars-its-actually-faster-on-some-things-opg</guid>
      <description>&lt;h1&gt;
  
  
  I built a native C# DataFrame engine to rival Python Polars (it's actually faster on some things)
&lt;/h1&gt;

&lt;p&gt;If you work in data science or heavy data engineering, you already know about &lt;a href="https://pola.rs/" rel="noopener noreferrer"&gt;Polars&lt;/a&gt;. It’s the Rust-backed powerhouse that took the Python ecosystem by storm, leaving Pandas in the dust.&lt;/p&gt;

&lt;p&gt;But if you’re a .NET developer, the data manipulation story has always been a bit… frustrating. We have &lt;code&gt;Microsoft.Data.Analysis&lt;/code&gt;, but it lacks the expressive lazy API and raw speed we crave. We often end up exporting data to Python just to process it, only to bring it back to C#.&lt;/p&gt;

&lt;p&gt;I got tired of waiting for a native .NET solution. So, I decided to build one from scratch.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;&lt;a href="https://github.com/ian-cowley/Glacier.Polaris" rel="noopener noreferrer"&gt;Glacier.Polaris&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is a high-performance, strongly-typed DataFrame library for C# (.NET 10). It features SIMD-accelerated compute kernels, a lazy execution engine, native nullability (Kleene logic), and it currently passes 135/135 golden-file parity tests against Python Polars.&lt;/p&gt;

&lt;p&gt;And after weeks of fighting the .NET JIT compiler and CPU caches, it is actually beating Polars in several key benchmarks.&lt;/p&gt;

&lt;p&gt;Here is how I pushed C# to its physical limits to pull this off.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Zero-Allocation and SIMD String Filtering
&lt;/h2&gt;

&lt;p&gt;In standard C#, string operations are heavy. If you filter a DataFrame with &lt;code&gt;df.Filter(Expr.Col("Status") == "Completed")&lt;/code&gt;, checking materialized &lt;code&gt;.NET&lt;/code&gt; string objects one by one will instantly ruin your performance due to pointer-chasing and heap allocations.&lt;/p&gt;

&lt;p&gt;To beat Polars (which uses Arrow's contiguous memory format), I couldn't use C# strings.&lt;/p&gt;

&lt;p&gt;Instead, Glacier.Polaris stores strings as flat UTF-8 byte arrays. When you execute an equality filter, the engine loads your target string into a &lt;code&gt;Vector256&amp;lt;byte&amp;gt;&lt;/code&gt; register. As it scans the 10-million row DataFrame, it fires a single AVX2 instruction (&lt;code&gt;Vector256.Equals&lt;/code&gt;) that compares entire words simultaneously against the target bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; String exact-match filtering in Glacier runs in &lt;strong&gt;3.62 ms&lt;/strong&gt; for 1 million rows, beating Polars (&lt;strong&gt;~4.2ms&lt;/strong&gt;) with zero string allocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Breaking the 4ms Barrier: The Float64 Sorting War
&lt;/h2&gt;

&lt;p&gt;The hardest fight I had was with &lt;code&gt;ArgSort&lt;/code&gt; on Float64 data.&lt;/p&gt;

&lt;p&gt;Initially, I wrote a highly optimized, single-threaded Radix sort. I managed to drop the sorting time for 1 million floats to &lt;strong&gt;13.11 ms&lt;/strong&gt;—a massive 5.4x speedup over the standard .NET &lt;code&gt;Array.Sort&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But Polars was doing it in &lt;strong&gt;4.21 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At 13ms, my C# code had officially maxed out the physical capabilities of a single CPU core. Moving 200MB of data (keys and indices) over 8 radix passes requires about 47 GB/s of memory bandwidth. A single core physically taps out around 15-20 GB/s.&lt;/p&gt;

&lt;p&gt;I needed to parallelize it. But .NET's &lt;code&gt;Parallel.For&lt;/code&gt; has too much overhead; spinning up the ThreadPool state machine takes 1-2ms alone, which is a death sentence when your target is 4ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: The Parallel Block Tournament Merge&lt;/strong&gt;&lt;br&gt;
Instead of using standard .NET parallel loops, I built a custom generic parallel block merge engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The engine slices the 1M array into isolated chunks and hands them to raw &lt;code&gt;Task&lt;/code&gt; objects.&lt;/li&gt;
&lt;li&gt;Each core executes a single-threaded Radix sort entirely inside its &lt;strong&gt;L2 Cache&lt;/strong&gt;, meaning it never talks to system RAM, avoiding Translation Lookaside Buffer (TLB) thrashing.&lt;/li&gt;
&lt;li&gt;The engine merges the sorted chunks using a stable, parallel pairwise tournament merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; Float64 sorting dropped to &lt;strong&gt;12.05 ms&lt;/strong&gt; for 1M rows, and successfully scaled to sort 10 Million rows in just &lt;strong&gt;84.71 ms&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The Benchmarks (C# vs Polars)
&lt;/h2&gt;

&lt;p&gt;I ran these benchmarks on the same machine, comparing Glacier.Polaris (.NET 10 Release build) against Polars 1.40.1.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: Times are in milliseconds. Lower is better).&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation (1M Rows)&lt;/th&gt;
&lt;th&gt;Glacier.Polaris (C#)&lt;/th&gt;
&lt;th&gt;Python Polars&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DataFrame Creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.02 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5.33 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (~266x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sum (Int32)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.14 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.45 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (3.2x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard Deviation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.33 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.55 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (1.7x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GroupBy Sum (Int32)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.56 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5.20 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (3.3x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inner Join (Small Right)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2.29 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4.61 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (2.0x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rolling StdDev&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3.15 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;12.92 ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 C# (4.1x)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By utilizing single-pass Welford algorithms for variance, contiguous memory, and a custom Fibonacci-hashing hash map for joins, C# absolutely flies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;Glacier.Polaris covers ~98% of the Python Polars core surface area, including LazyFrames, query optimization (predicate/projection pushdowns), and full temporal operations.&lt;/p&gt;

&lt;p&gt;If you are building high-performance data pipelines, backtesting financial algorithms, or doing ML preprocessing in .NET, I’d love for you to try it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ian-cowley/Glacier.Polaris" rel="noopener noreferrer"&gt;https://github.com/ian-cowley/Glacier.Polaris&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Glacier.Polaris

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Star the repo, try to break the lazy execution engine, and let me know what features you want to see next! Let's bring world-class data engineering to .NET.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>polars</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
