<?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: Ben Witt</title>
    <description>The latest articles on DEV Community by Ben Witt (@ben-witt).</description>
    <link>https://dev.to/ben-witt</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%2F1456445%2F4ae90e8b-6b3e-48e7-be98-2d43dacbd68d.jpeg</url>
      <title>DEV Community: Ben Witt</title>
      <link>https://dev.to/ben-witt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ben-witt"/>
    <language>en</language>
    <item>
      <title>Durable Workflows Are Great, and the Wrong Choice for Short Pipelines</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/durable-workflows-are-great-and-the-wrong-choice-for-short-pipelines-2n77</link>
      <guid>https://dev.to/ben-witt/durable-workflows-are-great-and-the-wrong-choice-for-short-pipelines-2n77</guid>
      <description>&lt;p&gt;With the Microsoft Agent Framework and its Durable Task extension, durable execution is once again a major topic in the .NET world. The idea is tempting: define agents and workflows in ordinary code, then let the runtime handle persistence, crash recovery, state management, distributed scaling, and observability.&lt;/p&gt;

&lt;p&gt;That is strong technology. At the time of writing, the Durable Task extension for Microsoft Agent Framework is still in preview, but the direction is already clear: durable execution is becoming easier to apply to agentic and workflow-based systems.&lt;/p&gt;

&lt;p&gt;Precisely because it is powerful, it will also be used in places where it does not belong.&lt;/p&gt;

&lt;p&gt;My thesis: for short, cheap, idempotent pipelines, durable workflows are usually the wrong abstraction. Not because the framework is bad, but because the nature of the problem is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Two Concepts Separate
&lt;/h2&gt;

&lt;p&gt;There is an important distinction that is easy to blur.&lt;/p&gt;

&lt;p&gt;Microsoft Agent Framework workflows let you define a graph of executors and agents. That can be useful even without durability. A workflow graph can model a sequence, a branch, a fan-out, or a handoff.&lt;/p&gt;

&lt;p&gt;Durable Task is a different decision: it adds persistence, recovery, replay semantics, and distributed coordination.&lt;/p&gt;

&lt;p&gt;Those two things are related, but they are not the same. A workflow does not automatically need to be durable. A three-step graph can still be just a three-step graph.&lt;/p&gt;

&lt;p&gt;That distinction matters because the cost of a workflow model and the cost of durable execution are not the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Durable Workflows Are Built For
&lt;/h2&gt;

&lt;p&gt;Durable execution solves a real and difficult problem: how do you keep a workflow alive when it runs for a long time, consists of several steps, waits for external events, and may fail along the way?&lt;/p&gt;

&lt;p&gt;The runtime records progress in durable state. If the process crashes, the workflow can be reconstructed from history and resumed. Work that has already completed does not have to be blindly executed again. In serverless hosting models, a workflow can wait for a human approval or external event without consuming compute for the entire waiting period.&lt;/p&gt;

&lt;p&gt;That is excellent engineering for workflows that actually have these properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;long-running processes&lt;/li&gt;
&lt;li&gt;human approvals&lt;/li&gt;
&lt;li&gt;external events&lt;/li&gt;
&lt;li&gt;distributed execution&lt;/li&gt;
&lt;li&gt;visible business state&lt;/li&gt;
&lt;li&gt;expensive intermediate steps&lt;/li&gt;
&lt;li&gt;recovery requirements that span process restarts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where durable workflows shine. The extra machinery buys you something concrete: continuity, recovery, and visibility.&lt;/p&gt;

&lt;p&gt;But durability is not magic. It does not remove the need to design side effects carefully. If a step sends an email, charges a card, writes to an external system, or publishes a message, you still need idempotency keys, deduplication, transactional boundaries, or an outbox pattern. Durable execution helps avoid repeating completed work, but it does not make the outside world exactly-once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Short Pipeline Is
&lt;/h2&gt;

&lt;p&gt;A short pipeline is not just a workflow that finishes quickly.&lt;/p&gt;

&lt;p&gt;For this discussion, a short pipeline has four properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it runs in milliseconds or a few seconds&lt;/li&gt;
&lt;li&gt;it has only a small number of steps&lt;/li&gt;
&lt;li&gt;it does not wait for humans or external events&lt;/li&gt;
&lt;li&gt;it is cheap and safe to repeat from the beginning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the decisive one.&lt;/p&gt;

&lt;p&gt;A message comes in, gets transformed, validated, enriched, and forwarded. If the process dies halfway through, the simplest recovery strategy is often to run the whole pipeline again. If the pipeline is idempotent and the cost of repetition is tiny, replaying from durable history is solving a problem that the workload barely has.&lt;/p&gt;

&lt;p&gt;For this kind of workload, durable execution can make you pay a price without giving you enough value in return.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Costs You Add
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure overhead.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Durable execution depends on a persistent backend and a hosting environment that records workflow progress. For a process that finishes in milliseconds, that can be more machinery than the problem deserves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Programming model constraints.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Replay-based orchestration requires discipline. You need clean step boundaries, deterministic orchestration logic, and careful handling of replay semantics. Those rules are worth their cost when you need recovery across time. If you do not, they become cognitive overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Every durable boundary has a cost. In a workflow that waits for 24 hours, that cost disappears in the noise. In a low-latency pipeline, it lands exactly where you care most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging distance.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You are no longer debugging simple control flow. You are debugging an orchestration with history, replay behavior, stored state, and runtime semantics. That is powerful for complex processes, but unnecessary distance from a three-step transformation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational coupling.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The pipeline now depends on the durability store, the worker model, the hosting configuration, and the runtime's interpretation of history. That can be a good trade when you need the guarantees. It is a bad trade when a normal retry would have solved the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Question
&lt;/h2&gt;

&lt;p&gt;The question before choosing durable execution is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Would reliability be nice?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reliability is always nice.&lt;/p&gt;

&lt;p&gt;The useful question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does it cost me to repeat the entire operation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is "almost nothing, because the pipeline is short, cheap, and idempotent," you probably do not need durable execution.&lt;/p&gt;

&lt;p&gt;If the answer is "a lot, because the workflow runs for hours, waits for external input, or contains expensive side effects," then the overhead may be worth every cent.&lt;/p&gt;

&lt;p&gt;I would ask five questions before introducing a durable workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the process outlive a single request or worker process?&lt;/li&gt;
&lt;li&gt;Does it wait for humans, timers, or external events?&lt;/li&gt;
&lt;li&gt;Is there meaningful intermediate state that someone needs to observe?&lt;/li&gt;
&lt;li&gt;Is repeating the whole operation expensive or dangerous?&lt;/li&gt;
&lt;li&gt;Are side effects isolated and designed for idempotency?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If most answers are no, durability is probably the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Use Instead
&lt;/h2&gt;

&lt;p&gt;For a short pipeline, the honest architecture is usually boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a few methods or handlers called in sequence&lt;/li&gt;
&lt;li&gt;validation at the boundary&lt;/li&gt;
&lt;li&gt;retry only around transient I/O&lt;/li&gt;
&lt;li&gt;idempotency keys for incoming messages&lt;/li&gt;
&lt;li&gt;dead-letter handling for poison messages&lt;/li&gt;
&lt;li&gt;tracing and metrics for observability&lt;/li&gt;
&lt;li&gt;an outbox pattern if a database write and message publish must stay coordinated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you reliability where it matters without turning ordinary control flow into orchestration.&lt;/p&gt;

&lt;p&gt;The goal is not to avoid infrastructure at all costs. The goal is to put infrastructure at the correct boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I Would Use Durable Workflows
&lt;/h2&gt;

&lt;p&gt;Durable workflows are the right choice as soon as at least one of these points applies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The process runs for minutes, hours, or days.&lt;/li&gt;
&lt;li&gt;There are waiting points for external events or human approvals.&lt;/li&gt;
&lt;li&gt;The workflow state must be externally visible while it is running.&lt;/li&gt;
&lt;li&gt;Completed steps are expensive to repeat.&lt;/li&gt;
&lt;li&gt;Side effects need strict coordination and cannot be treated as a cheap retry.&lt;/li&gt;
&lt;li&gt;The process needs distributed execution across workers or machines.&lt;/li&gt;
&lt;li&gt;Auditability and replayable history are part of the requirement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases, the durable runtime earns its place. The workflow is no longer just a short transformation. It is a stateful business process.&lt;/p&gt;

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

&lt;p&gt;The temptation with powerful frameworks is to use them everywhere because they are impressive. Durable workflows are impressive for good reasons.&lt;/p&gt;

&lt;p&gt;But durability is not a free upgrade. It is a trade: persistence, replay semantics, storage dependency, latency, and a stricter programming model in exchange for recovery, continuity, and observability.&lt;/p&gt;

&lt;p&gt;For short, cheap, idempotent pipelines, that trade is usually wrong. Repeating the whole operation is simpler than preserving every intermediate step.&lt;/p&gt;

&lt;p&gt;Sometimes the right answer to "Should we make this durable?" is simply:&lt;/p&gt;

&lt;p&gt;No. Make it repeatable.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>discuss</category>
      <category>distributedsystems</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>RabbitMQ Dead Letter Queues in .NET: Why We Chose This Approach and Where It Hit Us</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 22 Jul 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/rabbitmq-dead-letter-queues-in-net-why-we-chose-this-approach-and-where-it-hit-us-1igb</link>
      <guid>https://dev.to/ben-witt/rabbitmq-dead-letter-queues-in-net-why-we-chose-this-approach-and-where-it-hit-us-1igb</guid>
      <description>&lt;p&gt;In a message-based system, one of the first questions you have to answer is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens to a message that cannot be processed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The naive answer is: catch the exception, log it, and move on.&lt;/p&gt;

&lt;p&gt;That works until you realize what it really means: you are silently losing messages in production. No rollback, no second chance, no audit trail.&lt;/p&gt;

&lt;p&gt;RabbitMQ's Dead Letter Exchange, DLX, is a good answer to this problem. But it is not magic, and it is not a complete failure-handling strategy by itself. It is one building block in a production setup that also needs retries, persistence, publisher confirms, monitoring, and a deliberate replay process.&lt;/p&gt;

&lt;p&gt;This article explains why we used a DLX, what the setup looks like in .NET, and which mistakes caught us off guard.&lt;/p&gt;

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

&lt;p&gt;Our system processes messages from several queues through a plugin-based messaging framework. A consumer reads a message, processes it, and confirms it with &lt;code&gt;BasicAck&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If processing fails, there are three basic options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Requeue immediately.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;BasicNack&lt;/code&gt; with &lt;code&gt;requeue: true&lt;/code&gt; puts the message back on the queue. This is useful for some transient failures, but dangerous as a default. If the problem is permanent, the same message can loop forever and put unnecessary pressure on the broker and consumers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reject without requeue and without DLX.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;BasicNack&lt;/code&gt; or &lt;code&gt;BasicReject&lt;/code&gt; with &lt;code&gt;requeue: false&lt;/code&gt; discards the message if no dead-lettering is configured. That means no second chance and no operational visibility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reject without requeue and use a DLX.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If the source queue is configured with a Dead Letter Exchange, RabbitMQ republishes the message to that exchange. From there, it can be routed to a Dead Letter Queue, where it can be inspected, repaired, replayed, or archived.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Option 3 was the only acceptable default for messages that failed permanently. Messages that cannot be processed should not simply disappear.&lt;/p&gt;

&lt;p&gt;But there is an important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A DLQ is not an error handler. It is a place where failed messages wait for an error-handling decision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How Dead Lettering Works
&lt;/h2&gt;

&lt;p&gt;Dead-lettering can happen for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a consumer rejects or nacks a message with &lt;code&gt;requeue: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a message expires because of TTL&lt;/li&gt;
&lt;li&gt;a queue length limit is exceeded&lt;/li&gt;
&lt;li&gt;a quorum queue delivery limit is exceeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common case in our system was explicit rejection from the consumer.&lt;/p&gt;

&lt;p&gt;Conceptually, the flow 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;Publisher
    |
[Exchange]
    |
[my.queue] -- nack(requeue:false) --&amp;gt; [my.dlx] --&amp;gt; [my.queue.dlq]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DLQ is a normal queue. RabbitMQ adds dead-letter metadata to the message headers, including information such as the queue it came from, the reason it was dead-lettered, and death history through headers such as &lt;code&gt;x-death&lt;/code&gt;, &lt;code&gt;x-first-death-*&lt;/code&gt;, and &lt;code&gt;x-last-death-*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That metadata is useful, but it should not be your only source of truth. For production troubleshooting, log the message ID, correlation ID, queue name, exception type, and failure reason before you nack the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer Policies Over Hardcoded Queue Arguments
&lt;/h2&gt;

&lt;p&gt;There are two ways to configure dead-lettering for a queue:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;using RabbitMQ policies&lt;/li&gt;
&lt;li&gt;using queue declaration arguments such as &lt;code&gt;x-dead-letter-exchange&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;RabbitMQ recommends policies for DLX configuration because policies can be updated without redeploying applications and without deleting and recreating queues. Queue declaration arguments are less flexible because they become part of the queue's declared properties.&lt;/p&gt;

&lt;p&gt;A policy-based setup can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl set_policy my-service-dlx &lt;span class="s2"&gt;"^my&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;service&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"dead-letter-exchange":"my.dlx"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--apply-to&lt;/span&gt; queues &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--priority&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need a specific dead-letter routing key, add it to the policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rabbitmqctl set_policy my-service-dlx &lt;span class="s2"&gt;"^my&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;service&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"dead-letter-exchange":"my.dlx","dead-letter-routing-key":"my.service.failed"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--apply-to&lt;/span&gt; queues &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--priority&lt;/span&gt; 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is usually the cleaner production option.&lt;/p&gt;

&lt;p&gt;There is still a place for queue arguments: if your application fully owns the topology and you want the topology definition to live with the application. In that case, configuration is better than hardcoding, but policies should be considered first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup in CSharp
&lt;/h2&gt;

&lt;p&gt;The exchange and DLQ are normal RabbitMQ entities:&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;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExchangeDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.dlx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExchangeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Direct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.queue.dlq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueBindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.queue.dlq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.dlx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;routingKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my-routing-key"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the queue is configured through policy, the main queue declaration does not need DLX arguments:&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;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&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 intentionally configure DLX through queue arguments, the declaration must include the exact arguments:&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;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"x-dead-letter-exchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my.dlx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"x-dead-letter-routing-key"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-routing-key"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer rejects permanently failed messages with &lt;code&gt;requeue: false&lt;/code&gt;:&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;consumer&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;AsyncEventingBasicConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReceivedAsync&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ea&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;try&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;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicAckAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TransientException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Transient failure while processing message."&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicNackAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"Permanent processing failure. Message will be dead-lettered. DeliveryTag={DeliveryTag}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryTag&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicNackAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeliveryTag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&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;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicConsumeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my.queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoAck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the exact exception hierarchy. The important part is the decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transient failures may be retried&lt;/li&gt;
&lt;li&gt;permanent failures should be dead-lettered&lt;/li&gt;
&lt;li&gt;every path should be observable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Hit Us: PRECONDITION_FAILED 406
&lt;/h2&gt;

&lt;p&gt;This was the part that cost us time.&lt;/p&gt;

&lt;p&gt;After we configured dead-lettering in the broker, the next deployment failed during startup with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AMQP operation QueueDeclare failed with code 406: PRECONDITION_FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cause was not the DLX itself. The cause was RabbitMQ's property equivalence check.&lt;/p&gt;

&lt;p&gt;When a queue already exists, a later &lt;code&gt;QueueDeclare&lt;/code&gt; call must match the existing queue properties. That includes durable, exclusive, auto-delete, queue type, and optional arguments. If the declaration differs, RabbitMQ closes the channel with &lt;code&gt;PRECONDITION_FAILED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In our case, the queue already existed with &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; and &lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;. Our framework declared the same queue at startup, but did not pass those arguments. From RabbitMQ's perspective, that was a different queue declaration for the same queue name.&lt;/p&gt;

&lt;p&gt;The broker was right to reject it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;There are two clean ways to avoid this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Use policies
&lt;/h3&gt;

&lt;p&gt;If dead-lettering is configured through RabbitMQ policies, the application does not have to provide DLX queue arguments during &lt;code&gt;QueueDeclare&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is usually the better operational model because broker-level routing behavior stays broker-level configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Make queue arguments explicit configuration
&lt;/h3&gt;

&lt;p&gt;If the application owns queue declarations, then queue arguments must be part of the application configuration and passed consistently on every declaration.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;QueueConfiguration&lt;/code&gt; needed a generic &lt;code&gt;Arguments&lt;/code&gt; dictionary:&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;QueueConfiguration&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;Name&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;bool&lt;/span&gt; &lt;span class="n"&gt;IsDurable&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;true&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;bool&lt;/span&gt; &lt;span class="n"&gt;IsAutoDelete&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;false&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;bool&lt;/span&gt; &lt;span class="n"&gt;AutoAck&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;false&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;ExchangeName&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;RoutingKey&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;Arguments&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;The queue declaration then passes the dictionary directly:&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;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclareAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queue&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="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDurable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAutoDelete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Arguments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my.service.queue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsDurable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsAutoDelete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AutoAck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ExchangeName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my.exchange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RoutingKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my.routing.key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"x-dead-letter-exchange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my.dlx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"x-dead-letter-routing-key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my.service.failed"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second trap: if the broker queue has both &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; and &lt;code&gt;x-dead-letter-routing-key&lt;/code&gt;, both must match. Setting only one is not enough.&lt;/p&gt;

&lt;p&gt;You can inspect the current queue arguments in the RabbitMQ Management UI under the queue details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queue Arguments Are Not a Migration Mechanism
&lt;/h2&gt;

&lt;p&gt;Queue arguments are not a good place for values you expect to change frequently.&lt;/p&gt;

&lt;p&gt;Many queue properties are fixed at declaration time. If an existing queue was declared with different properties, you cannot simply redeclare it with new values. You either need a policy where supported, or you need a queue migration.&lt;/p&gt;

&lt;p&gt;For queue arguments that cannot be changed in place, the operational process is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop publishers and consumers, or drain traffic safely.&lt;/li&gt;
&lt;li&gt;Make sure the queue is empty or move the messages intentionally.&lt;/li&gt;
&lt;li&gt;Delete the queue.&lt;/li&gt;
&lt;li&gt;Recreate it with the correct properties.&lt;/li&gt;
&lt;li&gt;Restart consumers and publishers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In production, that means planning. A queue declaration is not just code. It is part of the broker topology.&lt;/p&gt;

&lt;h2&gt;
  
  
  DLX Does Not Replace Retries
&lt;/h2&gt;

&lt;p&gt;One mistake is sending every exception directly to the DLQ.&lt;/p&gt;

&lt;p&gt;Some failures are permanent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid schema&lt;/li&gt;
&lt;li&gt;impossible state transition&lt;/li&gt;
&lt;li&gt;missing required business data&lt;/li&gt;
&lt;li&gt;unsupported message type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those belong in a DLQ.&lt;/p&gt;

&lt;p&gt;Other failures are transient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database timeout&lt;/li&gt;
&lt;li&gt;network interruption&lt;/li&gt;
&lt;li&gt;temporary downstream outage&lt;/li&gt;
&lt;li&gt;lock contention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those usually need retries before dead-lettering.&lt;/p&gt;

&lt;p&gt;The retry strategy depends on the system. Common patterns are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;limited in-process retry for very short transient failures&lt;/li&gt;
&lt;li&gt;delayed retry queues with TTL and DLX&lt;/li&gt;
&lt;li&gt;retry counters in headers&lt;/li&gt;
&lt;li&gt;quorum queue delivery limits&lt;/li&gt;
&lt;li&gt;final dead-lettering after max attempts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important rule is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A DLQ should contain messages that need investigation, not messages that merely hit a temporary dependency failure once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  DLX Does Not Replace Message Durability
&lt;/h2&gt;

&lt;p&gt;A durable queue does not automatically make every message durable.&lt;/p&gt;

&lt;p&gt;For production systems, publishers still need to publish persistent messages where appropriate, and they should use publisher confirms if message loss is unacceptable. Otherwise, a message can be lost before it ever reaches the consumer or before the broker has safely accepted it.&lt;/p&gt;

&lt;p&gt;Dead-lettering itself is also a form of publishing. In some scenarios, especially clustered setups, dead-letter forwarding can fail if the target exchange or queue is unavailable or unroutable. Quorum queues offer stronger options for at-least-once dead-lettering, but the broader point remains:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A DLX is part of reliability. It is not the whole reliability story.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Operating a DLQ
&lt;/h2&gt;

&lt;p&gt;Creating a DLQ is easy. Operating one is the real work.&lt;/p&gt;

&lt;p&gt;At minimum, we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;alerts when DLQ depth grows&lt;/li&gt;
&lt;li&gt;dashboards for dead-letter rate and age&lt;/li&gt;
&lt;li&gt;a way to inspect payload and headers safely&lt;/li&gt;
&lt;li&gt;retention rules so DLQs do not grow forever&lt;/li&gt;
&lt;li&gt;a replay process with idempotency protection&lt;/li&gt;
&lt;li&gt;a way to quarantine messages that fail again after replay&lt;/li&gt;
&lt;li&gt;clear ownership: who looks at the DLQ and when&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without that, the DLQ becomes a message graveyard. It prevents immediate loss, but it does not create recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;These are the points I would consider from the beginning in the next project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Prefer RabbitMQ policies for DLX configuration.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Policies are easier to update and avoid baking broker behavior into application declarations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. If you use queue arguments, make them explicit configuration.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Hardcoded &lt;code&gt;x-arguments&lt;/code&gt; are painful because they cannot be changed casually after the queue exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Queue declarations must match broker state.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;PRECONDITION_FAILED 406&lt;/code&gt; during &lt;code&gt;QueueDeclare&lt;/code&gt; usually means a property equivalence mismatch. Check durable, auto-delete, exclusive, queue type, and optional arguments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Separate transient and permanent failures.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Do not send every exception directly to the DLQ. Retry transient failures, dead-letter permanent ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Use message durability and publisher confirms where loss matters.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
DLX handles consumer-side failure. It does not protect the whole publish-consume path by itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Monitor and operate the DLQ.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A DLQ without alerts, retention, ownership, and replay tooling is just delayed message loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Be deliberate with routing keys.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Separate dead-letter routing keys per queue are often useful because they make routing and replay easier. They are not mandatory, but they are a good default when multiple queues share one DLX.&lt;/p&gt;

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

&lt;p&gt;The DLX approach requires more setup than the naive exception-based solution. In return, you stop silently discarding failed messages and gain a place to inspect, repair, replay, or archive them.&lt;/p&gt;

&lt;p&gt;But the important lesson is not "add a DLQ and you are safe."&lt;/p&gt;

&lt;p&gt;The lesson is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat failed messages as part of your production workflow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means policies or consistent queue arguments, deliberate retries, persistent messages, publisher confirms where needed, monitoring, and a replay process.&lt;/p&gt;

&lt;p&gt;Once you understand that, the &lt;code&gt;PRECONDITION_FAILED&lt;/code&gt; error becomes less mysterious. It is RabbitMQ telling you that your code and broker topology disagree.&lt;/p&gt;

&lt;p&gt;Fix that disagreement early. Then make sure your DLQ is not just a place where messages go to be forgotten.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>distributedsystems</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>MCP Design Patterns: 6 Architectures for Your AI Tools</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/mcp-design-patterns-6-architectures-for-your-ai-tools-1d02</link>
      <guid>https://dev.to/ben-witt/mcp-design-patterns-6-architectures-for-your-ai-tools-1d02</guid>
      <description>&lt;p&gt;The Model Context Protocol is everywhere right now. Everyone is building MCP servers, connecting tools to agents, and exposing internal systems to AI clients.&lt;/p&gt;

&lt;p&gt;What gets less attention is the architecture behind those servers.&lt;/p&gt;

&lt;p&gt;An MCP server is not just a template you set up once and forget. Depending on the use case, the right structure can be completely different. A thin wrapper around an API, a local resource server, and an orchestration layer for long-running jobs all expose capabilities through MCP, but they should not be designed the same way.&lt;/p&gt;

&lt;p&gt;I have seen six patterns emerge in practice. This is not an academic classification. It is a decision aid:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which MCP server shape fits the problem?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is an MCP Server?
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol is an open protocol for connecting AI applications to external context and capabilities.&lt;/p&gt;

&lt;p&gt;An MCP server can expose three main kinds of primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; callable actions, usually with parameters and structured results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources:&lt;/strong&gt; readable context such as files, documents, database records, or generated views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts:&lt;/strong&gt; reusable prompt templates or interaction patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, most people start with tools because they are the most visible part of MCP. But a good MCP server is not just a bag of functions. It is a boundary between an AI client and a system that has data, behavior, permissions, and risk.&lt;/p&gt;

&lt;p&gt;The important question is not only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What can the agent call?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should the client expose, in what shape, with what permissions, and at what level of abstraction?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pattern 1: Direct API Wrapper
&lt;/h2&gt;

&lt;p&gt;This is the simplest and most common pattern. One MCP tool maps closely to one existing API endpoint. The server is mostly a thin adapter between the MCP client and an existing service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Client
  |
MCP Tool: get_user(id)
  |
REST API: GET /users/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The API is stable, well documented, and already close to what the model needs. You want quick integration without adding much domain logic to the MCP server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it does not fit:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The API exposes low-level data that the model still has to join, filter, interpret, or clean up. Then the complexity moves into the prompt and the model has to act like an API integration layer. That is fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub issue lookup&lt;/li&gt;
&lt;li&gt;Jira ticket retrieval&lt;/li&gt;
&lt;li&gt;internal REST services&lt;/li&gt;
&lt;li&gt;simple CRM queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Main risk:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Tool bloat. If every endpoint becomes a tool, the model has too many similar options and too much raw API detail to reason about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use direct wrappers for small, obvious, low-risk API operations. Move to task-level tools when the agent has to combine or interpret several raw endpoints.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 2: Composite Service
&lt;/h2&gt;

&lt;p&gt;A composite MCP server hides multiple backend calls behind one task-level tool. The agent asks for the result it actually needs, and the server performs the aggregation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Client
  |
MCP Tool: get_order_summary(order_id)
  |
  +-- REST API: GET /orders/{id}
  +-- REST API: GET /customers/{customer_id}
  +-- REST API: GET /products/{product_id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The agent repeatedly needs the same combination of data for one task. Without this pattern, it would make several tool calls and assemble the result itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it does not fit:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The required combination changes constantly. If every task needs a different shape, a fixed composite tool can become too rigid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical example:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A customer service agent that needs order data, customer profile, delivery status, and product information together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main risk:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The composite tool can become a hidden mini-application. If it starts making too many assumptions, the agent loses flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Return task-ready context, not a full backend dump. The output should be smaller, clearer, and safer than the raw data sources.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 3: Resource-Oriented MCP
&lt;/h2&gt;

&lt;p&gt;Not every capability should be a tool. Sometimes the best MCP server exposes structured resources that the client can read and inject as context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Client
  |
MCP Resource: file://project/README.md
MCP Resource: db://customer/123/profile
MCP Resource: config://service/payment
  |
Local or remote data source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The agent needs context more than action. The data is read-only or should be treated as reference material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it does not fit:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The model needs to execute a parameterized operation, mutate state, trigger a workflow, or perform a search that has meaningful side effects or cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code repositories&lt;/li&gt;
&lt;li&gt;documentation collections&lt;/li&gt;
&lt;li&gt;local knowledge bases&lt;/li&gt;
&lt;li&gt;generated system state snapshots&lt;/li&gt;
&lt;li&gt;configuration views&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Main risk:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Overexposure. Raw resource access can accidentally reveal too much. A filesystem server, for example, should not expose the entire machine by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Expose curated resources with clear roots, metadata, and permission boundaries. Prefer readable views over unrestricted raw access.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 4: Agent-Backed Tool
&lt;/h2&gt;

&lt;p&gt;In this pattern, the MCP tool does not call a normal API. It delegates to another AI agent, model, or specialized reasoning component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main AI Client
  |
MCP Tool: analyze_code(snippet)
  |
Specialized Analysis Agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A subtask benefits from a different model, narrower context, specialized tools, or a controlled reasoning workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it does not fit:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The task is simple enough for the main model. Additional agent hops add latency, cost, and debugging complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;code security review&lt;/li&gt;
&lt;li&gt;policy compliance analysis&lt;/li&gt;
&lt;li&gt;domain-specific document classification&lt;/li&gt;
&lt;li&gt;test generation with a specialized context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Main risk:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Loss of traceability. Once one agent calls another, failures become harder to explain. The main model sees a result, but not necessarily the reasoning quality behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use agent-backed tools when the subagent produces a bounded, verifiable result. Return evidence, confidence, and limitations where possible.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 5: Event-Driven Control Surface
&lt;/h2&gt;

&lt;p&gt;Some work should not happen inside a synchronous MCP tool call. The operation may take minutes, involve queues, or run in a background service.&lt;/p&gt;

&lt;p&gt;In this pattern, the MCP server exposes a control surface over asynchronous infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Client
  |
MCP Tool: start_report_generation(params)
  |
Event Queue / Job Scheduler
  |
Report Service
  |
MCP Tool: get_report_status(job_id)
MCP Resource: report://jobs/{job_id}/result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The operation is too slow or too expensive for a normal request-response call. The agent should start the job, receive a job ID, and check status later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it does not fit:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The operation is fast and deterministic. Adding a queue for a 200 ms call is unnecessary overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;report generation&lt;/li&gt;
&lt;li&gt;long-running data processing&lt;/li&gt;
&lt;li&gt;batch imports&lt;/li&gt;
&lt;li&gt;document conversion&lt;/li&gt;
&lt;li&gt;asynchronous approval workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Main risk:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Ambiguous state. If the agent cannot tell whether a job is queued, running, failed, completed, or expired, the workflow becomes unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Make job state explicit. Provide status, progress, failure reason, result location, cancellation, and retry semantics.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 6: Gateway or Federated MCP
&lt;/h2&gt;

&lt;p&gt;In larger systems, one MCP server may act as a gateway in front of several domain-specific servers or services. This is not a special MCP primitive. It is an architectural pattern.&lt;/p&gt;

&lt;p&gt;Many clients can connect to multiple MCP servers directly. A gateway is useful when you need shared concerns in one place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Client
  |
MCP Gateway: Auth, Routing, Logging, Policy
  |
  +-- Inventory MCP Server
  +-- Order MCP Server
  +-- Customer MCP Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When it fits:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The system has many domains, teams, or permission boundaries. You need central authentication, routing, audit logging, tool filtering, or rate limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it does not fit:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The system is small. A gateway then becomes an unnecessary layer between the client and a few simple tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typical examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enterprise AI platforms&lt;/li&gt;
&lt;li&gt;multi-team internal tool ecosystems&lt;/li&gt;
&lt;li&gt;regulated environments&lt;/li&gt;
&lt;li&gt;shared company-wide assistant infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Main risk:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The gateway becomes a bottleneck or a second platform. If every domain change requires gateway changes, team independence disappears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rule:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Use a gateway for cross-cutting policy, not for domain logic. Domain ownership should stay with the domain servers.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Note on Local Access
&lt;/h2&gt;

&lt;p&gt;Local resource access is often treated as its own category because it feels different from API integration. A local MCP server may read files, inspect repositories, query a local database, or expose an Obsidian vault.&lt;/p&gt;

&lt;p&gt;Architecturally, local access usually belongs to one of two patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource-Oriented MCP&lt;/strong&gt; for read-only local context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct or Composite Tools&lt;/strong&gt; for controlled local actions such as search, indexing, transformation, or file generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key issue is permission scope. Local access can be extremely powerful. A good local MCP server should restrict roots, filter results, avoid leaking secrets, and make dangerous actions explicit.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Decision Logic
&lt;/h2&gt;

&lt;p&gt;These questions help choose the right pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Do I have a simple existing API operation to expose?&lt;/td&gt;
&lt;td&gt;Direct API Wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the model need one task-level result from several sources?&lt;/td&gt;
&lt;td&gt;Composite Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the capability mostly read-only context?&lt;/td&gt;
&lt;td&gt;Resource-Oriented MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the task need another model or specialized reasoning context?&lt;/td&gt;
&lt;td&gt;Agent-Backed Tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the operation run asynchronously or take too long for one call?&lt;/td&gt;
&lt;td&gt;Event-Driven Control Surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do many teams or domains need one shared policy boundary?&lt;/td&gt;
&lt;td&gt;Gateway or Federated MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The patterns are not mutually exclusive. A production system often combines them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gateway MCP
  |
  +-- Composite tools for customer support
  +-- Resource-oriented server for documentation
  +-- Event-driven tools for report generation
  +-- Agent-backed tools for specialized analysis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more important decision is where complexity should live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in the MCP server&lt;/li&gt;
&lt;li&gt;in the client&lt;/li&gt;
&lt;li&gt;in the model prompt&lt;/li&gt;
&lt;li&gt;in backend services&lt;/li&gt;
&lt;li&gt;in a gateway layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a rule, complexity that is deterministic, security-sensitive, or repetitive should live outside the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Questions Every Pattern Needs
&lt;/h2&gt;

&lt;p&gt;Before exposing a capability through MCP, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it read-only or can it change state?&lt;/li&gt;
&lt;li&gt;Does it require user confirmation?&lt;/li&gt;
&lt;li&gt;What is the smallest permission scope that works?&lt;/li&gt;
&lt;li&gt;Can the tool leak secrets through raw output?&lt;/li&gt;
&lt;li&gt;Is the result structured enough for the model to use safely?&lt;/li&gt;
&lt;li&gt;Is the action logged with user, time, input, and result?&lt;/li&gt;
&lt;li&gt;Can the operation be replayed safely?&lt;/li&gt;
&lt;li&gt;What happens if the model calls the tool with malformed or hostile input?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP makes integration easier. It does not make trust boundaries disappear.&lt;/p&gt;

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

&lt;p&gt;Building an MCP server is easy. Building the right MCP server requires a conscious decision about shape, abstraction level, and risk.&lt;/p&gt;

&lt;p&gt;Start with the simplest pattern that satisfies the requirement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;direct wrappers for simple API operations&lt;/li&gt;
&lt;li&gt;composite tools for repeated multi-source tasks&lt;/li&gt;
&lt;li&gt;resources for readable context&lt;/li&gt;
&lt;li&gt;agent-backed tools for bounded specialist work&lt;/li&gt;
&lt;li&gt;event-driven control surfaces for long-running jobs&lt;/li&gt;
&lt;li&gt;gateways only when cross-domain policy really matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best MCP server does not expose everything the backend can do. It exposes the smallest stable capability that helps the model complete the task without giving it unnecessary power or unnecessary raw data.&lt;/p&gt;

&lt;p&gt;That is the real architecture decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Official MCP specification and documentation: &lt;a href="https://modelcontextprotocol.io/specification/latest" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP server concepts: &lt;a href="https://modelcontextprotocol.io/docs/learn/server-concepts" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/learn/server-concepts&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Official MCP GitHub repository: &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol" rel="noopener noreferrer"&gt;https://github.com/modelcontextprotocol/modelcontextprotocol&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Local AI Pipeline: Why Certain Workloads Never Leave the Machine</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 08 Jul 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/local-ai-pipeline-why-certain-workloads-never-leave-the-machine-1393</link>
      <guid>https://dev.to/ben-witt/local-ai-pipeline-why-certain-workloads-never-leave-the-machine-1393</guid>
      <description>&lt;p&gt;The default assumption is still this: AI means cloud API. You send your text, your audio, or your document to an endpoint, pay per token, and get a result back.&lt;/p&gt;

&lt;p&gt;For many tasks, that is perfectly valid.&lt;/p&gt;

&lt;p&gt;For a specific class of tasks, it is the wrong default. In my setup, those tasks run locally, on my own hardware, inside my own network. No document or transcript content is sent to an external AI endpoint.&lt;/p&gt;

&lt;p&gt;This is not a religious argument about local versus cloud. It is a decision logic. Here is how I think about it, including the real costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Reasons, in the Right Order
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Privacy, and in the DACH region this is not a side issue.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I transcribe a dictation, tag an invoice, or make private documents searchable, the content is exactly that: private. As soon as this content is sent to an external endpoint, data processing happens outside my own environment, with all the questions that come with it: Where is the data stored, who has access to it, what happens to it after processing, and which legal basis applies?&lt;/p&gt;

&lt;p&gt;In Germany, Austria, and Switzerland, this quickly touches topics such as GDPR obligations, data processing agreements, data residency, professional secrecy, and confidential business or financial documents.&lt;/p&gt;

&lt;p&gt;Local processing changes the risk profile. It does not magically make a system secure. I still need access control, encrypted backups, patching, and sane container permissions. But it removes one entire class of risk: external AI processing of the content.&lt;/p&gt;

&lt;p&gt;That is the clean part. What never goes to an external AI endpoint does not have to be governed, audited, or trusted there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Costs that do not scale per token.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cloud APIs charge by usage. For rare, large tasks, that is usually acceptable. For frequent, small tasks, every voice note, every scanned document, every small cleanup step, it adds up to a recurring cost that grows with usage.&lt;/p&gt;

&lt;p&gt;Local models have the opposite profile: higher upfront cost, mostly the hardware, and then marginal costs that are very low. Not zero, because electricity, storage, time, and maintenance still exist. But the economics change when a workflow runs many times per day.&lt;/p&gt;

&lt;p&gt;The break-even point depends less on one spectacular job and more on repetition. Hundreds of small tasks per month can change the calculation faster than one large task per quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Latency and independence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No network round trip. No rate limits. No API outage in the middle of a workflow. No external model deprecation that breaks an automation overnight.&lt;/p&gt;

&lt;p&gt;Local does not mean maintenance-free. Docker updates, GPU drivers, CUDA versions, operating system changes, and model updates can still break things. But the failure mode is different. The dependency boundary moves closer to me, into infrastructure I can inspect, pin, and control.&lt;/p&gt;

&lt;p&gt;For small interactive workflows, that matters. A push-to-talk transcription tool should feel instant. A document tagging pipeline should not depend on whether an external endpoint is reachable at that exact moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;My setup is intentionally unspectacular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama in Docker&lt;/strong&gt;, with GPU passthrough to an RTX 4070. A quantized &lt;code&gt;mistral:7b&lt;/code&gt; model serves as the workhorse for text tasks such as cleaning up transcripts and light classification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;faster-whisper&lt;/strong&gt; for transcription, integrated into a small push-to-talk tool: hold a key, speak, release it, and the cleaned-up text lands in the clipboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paperless-ngx&lt;/strong&gt; inside my local network for document management. Paperless runs on the NAS, while AI-assisted tagging is handled by a local model endpoint inside the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is always the same: small, frequent, data-sensitive tasks that do not need peak intelligence, but reliability, privacy, and low friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Part: What It Costs
&lt;/h2&gt;

&lt;p&gt;Local AI has real limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throughput.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In my setup, a quantized 7B model is fast enough for short cleanup and classification tasks. It is not something I would use for long-form heavy reasoning or large unattended batches. The exact speed depends on the model, quantization, backend, context length, GPU, and CPU offloading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quality ceiling.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A local 7B model is not a frontier model from the cloud. For cleanup, classification, tagging, and simple transformations, it can be fully sufficient. For demanding reasoning tasks, it is not the right choice.&lt;/p&gt;

&lt;p&gt;The important rule is this: the easier the output is to verify, the better the task fits local models. Spelling cleanup, tagging, formatting, and transcript polishing are good candidates. Legal, medical, financial, or strategic reasoning is a different category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware and maintenance.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The GPU costs money upfront. Docker, drivers, GPU passthrough, model updates, storage, backups, and monitoring become your responsibility. If you do not want to operate that infrastructure, you pay for someone else to operate it as an API fee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local security.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Local AI reduces external exposure, but it does not remove operational security. The local machine becomes the trust boundary. If the device is compromised, the NAS is misconfigured, backups are unencrypted, or containers have excessive permissions, the privacy story weakens quickly.&lt;/p&gt;

&lt;p&gt;Local is not automatically secure. It is only more controllable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Decision
&lt;/h2&gt;

&lt;p&gt;The useful question is not: local or cloud?&lt;/p&gt;

&lt;p&gt;The useful question is: what does this specific task need?&lt;/p&gt;

&lt;p&gt;Use a cloud API when the task is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rare&lt;/li&gt;
&lt;li&gt;large&lt;/li&gt;
&lt;li&gt;complex&lt;/li&gt;
&lt;li&gt;dependent on maximum model quality&lt;/li&gt;
&lt;li&gt;based on non-sensitive data&lt;/li&gt;
&lt;li&gt;not required to work offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep it local when the task is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;frequent&lt;/li&gt;
&lt;li&gt;small&lt;/li&gt;
&lt;li&gt;data-sensitive&lt;/li&gt;
&lt;li&gt;easy to verify&lt;/li&gt;
&lt;li&gt;part of a stable workflow&lt;/li&gt;
&lt;li&gt;dependent on offline reliability or independence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the actual split.&lt;/p&gt;

&lt;p&gt;Cloud is excellent when I need the strongest model and the data can leave my environment. Local is excellent when the task repeats often, touches private material, and does not require frontier-level reasoning.&lt;/p&gt;

&lt;p&gt;In my case, a surprising number of tasks fall into the second category. Transcription, document management, cleanup steps, tagging, formatting: these are exactly the tasks where I am glad every day that the content stays inside my own environment.&lt;/p&gt;

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

&lt;p&gt;Local AI is not better than cloud AI. It does not have to be.&lt;/p&gt;

&lt;p&gt;The interesting question is which tasks should be in the cloud at all. For data-sensitive, frequent, small, and easy-to-verify workflows, my answer is increasingly: none of them.&lt;/p&gt;

&lt;p&gt;Local AI changes the default question.&lt;/p&gt;

&lt;p&gt;Not: which API should I send this to?&lt;/p&gt;

&lt;p&gt;But: why should this leave my machine at all?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>privacy</category>
      <category>productivity</category>
      <category>security</category>
    </item>
    <item>
      <title>A Generic Table API with Dapper in 80 Lines of C#</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 01 Jul 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/a-generic-table-api-with-dapper-in-80-lines-of-c-1350</link>
      <guid>https://dev.to/ben-witt/a-generic-table-api-with-dapper-in-80-lines-of-c-1350</guid>
      <description>&lt;p&gt;Sometimes you quickly need a read-only API on top of a PostgreSQL database.&lt;/p&gt;

&lt;p&gt;No Entity Framework setup.&lt;br&gt;&lt;br&gt;
No code generation.&lt;br&gt;&lt;br&gt;
No endpoint per table.&lt;/p&gt;

&lt;p&gt;Just query a known table and return the result.&lt;/p&gt;

&lt;p&gt;The tempting version is fully dynamic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/{table}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful for demos, but dangerous if it means every table in the database suddenly becomes reachable from HTTP.&lt;/p&gt;

&lt;p&gt;The safer version is still generic, but not unlimited:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;one endpoint, known tables, allowed columns, bounded pagination, read-only database permissions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the version I would use as a practical template.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/transport_orders?$top=50&amp;amp;$skip=0&amp;amp;$orderby=created_at desc
GET /api/business_orders?$top=100
GET /api/handling_units?$top=25&amp;amp;$skip=25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each call returns a normalized JSON object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns come from PostgreSQL as &lt;code&gt;snake_case&lt;/code&gt; and are returned to the client as &lt;code&gt;camelCase&lt;/code&gt;. JSON columns can be deserialized so the frontend does not have to parse JSON strings manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package Dapper
dotnet add package Npgsql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. No ORM, no migrations, no scaffolding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Important Boundary: Known Tables Only
&lt;/h2&gt;

&lt;p&gt;The endpoint is generic, but the database surface is not.&lt;/p&gt;

&lt;p&gt;Instead of accepting every table name that matches a regex, define the tables and columns the API is allowed to expose:&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;tables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TableDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&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="s"&gt;"transport_orders"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"public"&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;"transport_orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"hu_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"source_node_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"target_node_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"is_active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"route_nodes"&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"business_orders"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"public"&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;"business_orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"order_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"created_at"&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;This is the difference between a useful internal helper and an accidental database browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Endpoint
&lt;/h2&gt;

&lt;p&gt;Here is a complete minimal API example:&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;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.RegularExpressions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Dapper&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Npgsql&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;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rawConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PostgreSQL"&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;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No connection string found."&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;connectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;NormalizeConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawConnectionString&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NpgsqlDataSource&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;connectionString&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDefaultPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;policy&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithOrigins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:5173"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AllowAnyMethod&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AllowAnyHeader&lt;/span&gt;&lt;span class="p"&gt;());&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;tables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TableDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&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="s"&gt;"transport_orders"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"public"&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;"transport_orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"hu_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"source_node_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"target_node_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"is_active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"route_nodes"&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"business_orders"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"public"&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;"business_orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"order_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"created_at"&lt;/span&gt;
        &lt;span class="p"&gt;})&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;app&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseCors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/{table}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NpgsqlDataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;tables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tableDefinition&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;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Table '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' is not exposed by this API."&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;top&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseBoundedInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"$top"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;defaultValue&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="n"&gt;min&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;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&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;skip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseBoundedInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"$skip"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100_000&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="nf"&gt;TryBuildOrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"$orderby"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;tableDefinition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderByClause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderByError&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;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderByError&lt;/span&gt;&lt;span class="p"&gt;);&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;qualifiedTable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;QualifiedTableName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableDefinition&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;columnList&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&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="n"&gt;tableDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QuoteIdentifier&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;countSql&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"SELECT COUNT(*) FROM &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qualifiedTable&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dataSql&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"""
&lt;/span&gt;        &lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;columnList&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qualifiedTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;orderByClause&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;@top&lt;/span&gt; &lt;span class="n"&gt;OFFSET&lt;/span&gt; &lt;span class="n"&gt;@skip&lt;/span&gt;
        &lt;span class="s"&gt;""";
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&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;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenConnectionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&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;totalCount&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecuteScalarAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CommandDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;countSql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;rows&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryAsync&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;CommandDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ToCamelCaseKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ok"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;TableDefinition&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;Schema&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;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlySet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Helper Functions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Normalize the connection string
&lt;/h3&gt;

&lt;p&gt;Many cloud providers expose PostgreSQL connection strings as URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://user:password@host:5432/database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Npgsql primarily documents key-value connection strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host=...;Port=...;Database=...;Username=...;Password=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper handles the common URL shape. If your provider includes important query parameters such as SSL settings, preserve them explicitly or use the key-value connection string directly.&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;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;NormalizeConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&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;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UriKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Absolute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&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;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scheme&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="s"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&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;userInfo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;':'&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="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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NpgsqlConnectionStringBuilder&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Database&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnescapeDataString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbsolutePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&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="n"&gt;Username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnescapeDataString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&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;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnescapeDataString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userInfo&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="k"&gt;return&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;ConnectionString&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;
  
  
  Bound pagination
&lt;/h3&gt;

&lt;p&gt;Never let the client decide unlimited page sizes.&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;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;ParseBoundedInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parsed&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;defaultValue&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;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&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;
  
  
  Quote identifiers
&lt;/h3&gt;

&lt;p&gt;Identifiers cannot be passed as SQL parameters. That is why the allow-list is the real protection. Quoting is still useful because it avoids problems with reserved words or unusual casing.&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;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;QuoteIdentifier&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;identifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;"\""&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&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="s"&gt;"\"\""&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;QualifiedTableName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TableDefinition&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;QuoteIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Schema&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="nf"&gt;QuoteIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&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;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Validate &lt;code&gt;$orderby&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;$orderby&lt;/code&gt; parameter is not free-form SQL. It is a small language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created_at desc
created_at desc, id asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every column must exist in the table allow-list.&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;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryBuildOrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TableDefinition&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;clause&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&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;parts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;StringSplitOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimEntries&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;StringSplitOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveEmptyEntries&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;fragments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;foreach&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;part&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&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;match&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;@"^(?&amp;lt;column&amp;gt;[a-zA-Z_][a-zA-Z0-9_]*)(\s+(?&amp;lt;direction&amp;gt;asc|desc))?$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IgnoreCase&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;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Invalid $orderby syntax."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&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;column&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"column"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Value&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;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Columns&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="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"Column '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' is not allowed in $orderby."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&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;direction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ASC"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;fragments&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="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;QuoteIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&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="n"&gt;direction&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="n"&gt;clause&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fragments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"ORDER BY "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&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="n"&gt;fragments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&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;
  
  
  Convert &lt;code&gt;snake_case&lt;/code&gt; keys to &lt;code&gt;camelCase&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Dapper returns column names as they come from the database. The API can normalize them for frontend use.&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;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ToCamelCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&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;parts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringSplitOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveEmptyEntries&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;parts&lt;/span&gt;&lt;span class="p"&gt;.&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&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;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&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="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
               &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;part&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="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ToCamelCaseKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&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;IDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ToCamelCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;pair&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;TryParseJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parse JSON strings carefully
&lt;/h3&gt;

&lt;p&gt;If you query dynamically with Dapper, JSONB values may arrive as strings. This helper parses strings that look like JSON objects or arrays.&lt;/p&gt;

&lt;p&gt;This is a convenience heuristic, not a perfect type system. If you know the schema, prefer type-aware JSON mapping.&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;static&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;TryParseJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&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="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&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;trimmed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimStart&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;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&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;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'{'&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="sc"&gt;'['&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonException&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="k"&gt;value&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;h2&gt;
  
  
  Security: What This Protects
&lt;/h2&gt;

&lt;p&gt;This version protects against the most important mistakes in a dynamic read endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unknown tables are rejected&lt;/li&gt;
&lt;li&gt;unknown columns are not selected&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$orderby&lt;/code&gt; can only use allowed columns&lt;/li&gt;
&lt;li&gt;identifiers are quoted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$top&lt;/code&gt; and &lt;code&gt;$skip&lt;/code&gt; are bounded&lt;/li&gt;
&lt;li&gt;SQL values still use parameters&lt;/li&gt;
&lt;li&gt;CORS is restricted to a known frontend origin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But this is still not a full production data access layer.&lt;/p&gt;

&lt;p&gt;For production, also use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a dedicated read-only PostgreSQL user&lt;/li&gt;
&lt;li&gt;authentication and authorization at the API boundary&lt;/li&gt;
&lt;li&gt;row-level or tenant-level filtering if needed&lt;/li&gt;
&lt;li&gt;explicit decisions about sensitive columns&lt;/li&gt;
&lt;li&gt;logging and rate limiting&lt;/li&gt;
&lt;li&gt;optional total counts for large tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important point:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A generic endpoint should not mean generic database access.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/transport_orders?$top=5&amp;amp;$orderby=created_at desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c720153c-3d9f-4764-8d5a-87bd5ed1594a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"huId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"341994"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sourceNodeName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IP01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"targetNodeName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A01-L-001-02-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-26T13:41:38.966Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"isActive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"routeNodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IP01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A01-L-001-02-1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;snake_case&lt;/code&gt; from PostgreSQL arrives at the client as &lt;code&gt;camelCase&lt;/code&gt;. JSON arrays such as &lt;code&gt;routeNodes&lt;/code&gt; can be returned as JSON arrays instead of strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Makes Sense
&lt;/h2&gt;

&lt;p&gt;This approach fits when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need a quick read-only API over existing tables&lt;/li&gt;
&lt;li&gt;the exposed tables are known and stable&lt;/li&gt;
&lt;li&gt;you want direct SQL without ORM setup&lt;/li&gt;
&lt;li&gt;the API is for an internal tool, demo, or prototype&lt;/li&gt;
&lt;li&gt;the shape is simple: list rows, page rows, sort rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;complex joins or aggregations are required&lt;/li&gt;
&lt;li&gt;business logic belongs in the API layer&lt;/li&gt;
&lt;li&gt;fine-grained access control is required&lt;/li&gt;
&lt;li&gt;rows must be filtered per tenant or user&lt;/li&gt;
&lt;li&gt;the database contains sensitive columns you cannot safely expose&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Dapper is a good fit for this kind of endpoint: small surface, direct SQL, minimal ceremony.&lt;/p&gt;

&lt;p&gt;But the naive version is too open. A regex around the table name is not enough. The safer pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one generic endpoint&lt;/li&gt;
&lt;li&gt;explicit table allow-list&lt;/li&gt;
&lt;li&gt;explicit column allow-list&lt;/li&gt;
&lt;li&gt;bounded pagination&lt;/li&gt;
&lt;li&gt;validated sorting&lt;/li&gt;
&lt;li&gt;read-only database credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps the useful part of the idea without turning the API into an accidental database browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Npgsql connection string parameters: &lt;a href="https://www.npgsql.org/doc/connection-string-parameters.html" rel="noopener noreferrer"&gt;https://www.npgsql.org/doc/connection-string-parameters.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Npgsql JSON mapping: &lt;a href="https://www.npgsql.org/doc/types/json.html" rel="noopener noreferrer"&gt;https://www.npgsql.org/doc/types/json.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Semantic Search with PostgreSQL: Pragmatism Beats Hype - Most of the Time</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 24 Jun 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/semantic-search-with-postgresql-pragmatism-beats-hype-most-of-the-time-25cg</link>
      <guid>https://dev.to/ben-witt/semantic-search-with-postgresql-pragmatism-beats-hype-most-of-the-time-25cg</guid>
      <description>&lt;p&gt;When you start adding semantic search to an application, the obvious options are often Pinecone, Weaviate, Qdrant, Milvus, or another dedicated vector database.&lt;/p&gt;

&lt;p&gt;That can be the right choice.&lt;/p&gt;

&lt;p&gt;But many applications already have a PostgreSQL database running. And for a large class of semantic search use cases, that database can do the job directly.&lt;/p&gt;

&lt;p&gt;The key is &lt;code&gt;pgvector&lt;/code&gt;, an open-source PostgreSQL extension that adds vector types and vector similarity search to Postgres. It lets you store embeddings next to your relational data and query them with SQL.&lt;/p&gt;

&lt;p&gt;The advantage is not only fewer moving parts. It is also architectural:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No separate sync pipeline, no second source of truth, and no extra infrastructure until you actually need it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Semantic Search in One Sentence
&lt;/h2&gt;

&lt;p&gt;Classic search compares words. Semantic search compares meaning.&lt;/p&gt;

&lt;p&gt;The query "How do I get over the pass?" can find a text about a mountain pass rather than a school hallway because both the query and the documents are represented as numerical vectors, also called embeddings.&lt;/p&gt;

&lt;p&gt;The basic flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Convert text into an embedding vector.&lt;/li&gt;
&lt;li&gt;Store that vector in PostgreSQL.&lt;/li&gt;
&lt;li&gt;Convert the search query into the same vector space.&lt;/li&gt;
&lt;li&gt;Find the stored vectors closest to the query vector.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The phrase "same vector space" matters. You cannot freely mix embeddings from different models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Most Important Design Decision: The Embedding Model
&lt;/h2&gt;

&lt;p&gt;Before creating the database schema, choose the embedding model.&lt;/p&gt;

&lt;p&gt;That choice determines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vector dimension&lt;/li&gt;
&lt;li&gt;embedding quality&lt;/li&gt;
&lt;li&gt;cost&lt;/li&gt;
&lt;li&gt;latency&lt;/li&gt;
&lt;li&gt;privacy profile&lt;/li&gt;
&lt;li&gt;whether text leaves your environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if you use OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt;, a &lt;code&gt;vector(1536)&lt;/code&gt; column is a common fit. If you use another model, including a local model through Ollama, the dimension may be different.&lt;/p&gt;

&lt;p&gt;This is not a detail. PostgreSQL will reject vectors with the wrong number of dimensions.&lt;/p&gt;

&lt;p&gt;If you later change the embedding model, plan to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a new vector column or table&lt;/li&gt;
&lt;li&gt;re-embed all documents&lt;/li&gt;
&lt;li&gt;rebuild the vector index&lt;/li&gt;
&lt;li&gt;avoid comparing old and new embeddings directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Embedding model changes are data migrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing pgvector
&lt;/h2&gt;

&lt;p&gt;Enable the extension once in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With managed PostgreSQL providers, check whether &lt;code&gt;pgvector&lt;/code&gt; is available on your plan and PostgreSQL version. Many providers support it, but you should verify this before designing around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema: Store Chunks, Not Just Documents
&lt;/h2&gt;

&lt;p&gt;For small records, one vector per row is fine.&lt;/p&gt;

&lt;p&gt;For real documents, it is usually better to embed chunks. A long document may cover several topics. One vector for the entire document often becomes too blurry.&lt;/p&gt;

&lt;p&gt;A practical schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;source&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;(&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;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;chunk_index&lt;/span&gt;  &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt;    &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_index&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;Use &lt;code&gt;vector(1536)&lt;/code&gt; only if your embedding model actually returns 1536-dimensional vectors. Otherwise, change the dimension to match the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration in .NET with Npgsql
&lt;/h2&gt;

&lt;p&gt;Install the packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package Npgsql
dotnet add package Pgvector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use Dapper, also add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package Pgvector.Dapper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For raw Npgsql, configure the data source with &lt;code&gt;UseVector()&lt;/code&gt;:&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;Npgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Pgvector&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;dataSourceBuilder&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;NpgsqlDataSourceBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;dataSourceBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseVector&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataSourceBuilder&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;p&gt;In production, create the extension and tables through migrations or database provisioning. If you create the extension at runtime, reload PostgreSQL types on the connection before using the new type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating Embeddings with OpenAI
&lt;/h2&gt;

&lt;p&gt;With the OpenAI .NET SDK:&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;OpenAI.Embeddings&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;embeddingClient&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;EmbeddingClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"text-embedding-3-small"&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="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&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;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetEmbeddingAsync&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;text&lt;/span&gt;&lt;span class="p"&gt;)&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;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;embeddingClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateEmbeddingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFloats&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;For local embeddings, use a local embedding model through your preferred provider. The important rule is the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The model used for indexing and the model used for querying must be the same, or at least intentionally compatible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do not index documents with one model and query them with another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing a Document Chunk
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InsertChunkAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;chunkIndex&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;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&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;embedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetEmbeddingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&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;vector&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;Vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;conn&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;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenConnectionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cmd&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;NpgsqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;        &lt;span class="n"&gt;INSERT&lt;/span&gt; &lt;span class="n"&gt;INTO&lt;/span&gt; &lt;span class="nf"&gt;document_chunks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;@documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;@chunkIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;@content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;@embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="nf"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;DO&lt;/span&gt; &lt;span class="n"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SET&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;
        &lt;span class="s"&gt;""", conn);
&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"documentId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chunkIndex"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"embedding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector&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;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteNonQueryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&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;The important part is the update path. If the content changes, the embedding must change too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic Search
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; operator calculates cosine distance. A smaller value means higher similarity. &lt;code&gt;ORDER BY ... ASC&lt;/code&gt; returns the nearest vectors first.&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;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;DocumentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;ChunkId&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;Title&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;Content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;SearchResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SearchAsync&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;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&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;queryEmbedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetEmbeddingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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;queryVector&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;Vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;conn&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;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenConnectionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cmd&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;NpgsqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;        &lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;@queryVector&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
        &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;
        &lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;@queryVector&lt;/span&gt;
        &lt;span class="n"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;@limit&lt;/span&gt;
        &lt;span class="s"&gt;""", conn);
&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"queryVector"&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;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&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;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&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;SearchResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reader&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;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteReaderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;while&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;results&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;SearchResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;DocumentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;ChunkId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt64&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;Title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&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;Content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDouble&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&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;results&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;Notice that the query orders by the distance expression directly. With pgvector indexes, the &lt;code&gt;ORDER BY embedding &amp;lt;=&amp;gt; @query LIMIT n&lt;/code&gt; shape is important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distance Operators
&lt;/h2&gt;

&lt;p&gt;pgvector supports several distance operators:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cosine distance&lt;/td&gt;
&lt;td&gt;text embeddings and semantic search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;-&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L2 / Euclidean distance&lt;/td&gt;
&lt;td&gt;general vector distance, images, spatial-like embeddings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;#&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;negative inner product&lt;/td&gt;
&lt;td&gt;inner-product search; multiply by &lt;code&gt;-1&lt;/code&gt; for the actual value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;+&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L1 distance&lt;/td&gt;
&lt;td&gt;absolute-distance use cases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;~&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hamming distance&lt;/td&gt;
&lt;td&gt;binary vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;%&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jaccard distance&lt;/td&gt;
&lt;td&gt;binary vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most text embedding use cases, cosine distance is a good default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an Index with HNSW
&lt;/h2&gt;

&lt;p&gt;Without an index, PostgreSQL scans the table for every vector search. That is exact and simple, but it becomes slow as the number of vectors grows.&lt;/p&gt;

&lt;p&gt;HNSW is often the best starting index for application search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;document_chunks_embedding_hnsw_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;m&lt;/code&gt;: maximum number of connections per layer; default is 16&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ef_construction&lt;/code&gt;: candidate list size during index construction; default is 64&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Higher values can improve recall, but increase memory usage and build time.&lt;/p&gt;

&lt;p&gt;You can tune search recall per query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ef_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or for one transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ef_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: HNSW is approximate. It trades perfect recall for speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  IVFFlat as an Alternative
&lt;/h2&gt;

&lt;p&gt;IVFFlat can use less memory and build faster than HNSW, but usually has a weaker speed-recall trade-off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;document_chunks_embedding_ivfflat_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not create an IVFFlat index on an empty table. It needs data to form the lists.&lt;/p&gt;

&lt;p&gt;A practical starting point for &lt;code&gt;lists&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rows / 1000&lt;/code&gt; for up to 1M rows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqrt(rows)&lt;/code&gt; for over 1M rows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For 10,000 vectors, start around 10 lists, not 100.&lt;/p&gt;

&lt;p&gt;At query time, tune probes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;probes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher probes improve recall and reduce speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering and Hybrid Search
&lt;/h2&gt;

&lt;p&gt;One of the strongest reasons to use pgvector is that vectors live next to relational data.&lt;/p&gt;

&lt;p&gt;You can filter before searching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;queryVector&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'documentation'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;queryVector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For true hybrid search, do not just calculate keyword rank and then ignore it. Combine semantic and keyword ranks.&lt;/p&gt;

&lt;p&gt;One practical pattern is Reciprocal Rank Fusion-style scoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;semantic&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;semantic_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts_rank_cd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;plainto_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;keyword_rank&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;plainto_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;semantic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;semantic_rank&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword_rank&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;semantic&lt;/span&gt;
&lt;span class="k"&gt;FULL&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;semantic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;semantic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;.&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;JOIN&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not the only way to do hybrid search, but it makes the ranking logic explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  When pgvector Is the Right Choice
&lt;/h2&gt;

&lt;p&gt;pgvector is a strong choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL is already your primary database&lt;/li&gt;
&lt;li&gt;vectors need to join naturally with relational data&lt;/li&gt;
&lt;li&gt;transactional consistency matters&lt;/li&gt;
&lt;li&gt;your team already knows PostgreSQL operations&lt;/li&gt;
&lt;li&gt;the dataset is small to medium sized&lt;/li&gt;
&lt;li&gt;you want to avoid another service and sync pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is especially good for internal search, product search, support tools, documentation search, and RAG systems where the relational database is already the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Dedicated Vector Store May Be Better
&lt;/h2&gt;

&lt;p&gt;A dedicated vector database is worth considering when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vector count grows into tens of millions or more&lt;/li&gt;
&lt;li&gt;query volume is very high and latency budgets are strict&lt;/li&gt;
&lt;li&gt;you need specialized managed operations around vector search&lt;/li&gt;
&lt;li&gt;you need advanced built-in hybrid search, reranking, or filtering behavior&lt;/li&gt;
&lt;li&gt;your team does not operate PostgreSQL deeply&lt;/li&gt;
&lt;li&gt;multi-tenant isolation and scaling are central requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decision is not ideology. It is operational fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete ASP.NET Core Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/search"&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NpgsqlDataSource&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queryEmbedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetEmbeddingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&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;queryVector&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;Vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;conn&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenConnectionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cmd&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;NpgsqlCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;        &lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;@v&lt;/span&gt; &lt;span class="n"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
        &lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;
        &lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;@v&lt;/span&gt;
        &lt;span class="n"&gt;LIMIT&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="s"&gt;""", conn);
&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddWithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v"&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&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="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reader&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;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteReaderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;while&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;results&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="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;documentId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;chunkId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInt64&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;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&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;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDouble&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&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;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&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;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Semantic search does not always require a separate vector database.&lt;/p&gt;

&lt;p&gt;If PostgreSQL is already your source of truth, pgvector lets you store embeddings next to relational data, query them with SQL, filter with normal PostgreSQL conditions, and keep transactions in one system.&lt;/p&gt;

&lt;p&gt;But the clean version has a few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;choose the embedding model before choosing the vector dimension&lt;/li&gt;
&lt;li&gt;do not mix embeddings from different models&lt;/li&gt;
&lt;li&gt;chunk documents when they are long or multi-topic&lt;/li&gt;
&lt;li&gt;index only when the table is large enough to need it&lt;/li&gt;
&lt;li&gt;remember that HNSW and IVFFlat are approximate&lt;/li&gt;
&lt;li&gt;treat model changes as data migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many production applications, pgvector is enough for a long time. Not because dedicated vector databases are unnecessary, but because the simplest reliable architecture is often the one that keeps the data where it already lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;pgvector README: &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;https://github.com/pgvector/pgvector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pgvector .NET support: &lt;a href="https://github.com/pgvector/pgvector-dotnet" rel="noopener noreferrer"&gt;https://github.com/pgvector/pgvector-dotnet&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI .NET SDK: &lt;a href="https://github.com/openai/openai-dotnet" rel="noopener noreferrer"&gt;https://github.com/openai/openai-dotnet&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI embeddings guide: &lt;a href="https://platform.openai.com/docs/guides/embeddings" rel="noopener noreferrer"&gt;https://platform.openai.com/docs/guides/embeddings&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft.Extensions.AI: &lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Stop Loading Your Entire Instruction System Into Every Session</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 17 Jun 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/significantly-fewer-context-tokens-through-a-modular-instruction-architecture-2g70</link>
      <guid>https://dev.to/ben-witt/significantly-fewer-context-tokens-through-a-modular-instruction-architecture-2g70</guid>
      <description>&lt;p&gt;Most people talk about better prompts. Hardly anyone talks about what happens before every prompt: the instructions the assistant loads into the context before the actual work begins.&lt;/p&gt;

&lt;p&gt;Depending on the system, you pay for that in different ways: input tokens, latency, reduced available context, or simply more noise in the assistant's active instructions. Even if the financial cost is partly reduced through prompt caching, the cognitive cost remains: the assistant still has to operate inside a larger instruction environment.&lt;/p&gt;

&lt;p&gt;At some point, my setup had become one single, constantly growing instruction file. System structure, assistant personality, workflows, session rules, special cases: everything was in one file. And everything was loaded into the context on every interaction, no matter whether I was solving a complex task or just asking a quick question.&lt;/p&gt;

&lt;p&gt;That is roughly like starting every phone call by reading the entire employee handbook before getting to the actual topic.&lt;/p&gt;

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

&lt;p&gt;A monolithic instruction file has two costs that become unpleasant when combined:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The baseline gets expensive.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Most of the file is irrelevant to the concrete task. Still, it sits in the active context. Depending on the system, that means token cost, latency, less room for the real task, or all of them at once.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The signal-to-noise ratio drops.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The more rules and special cases you add, the more the currently relevant part gets diluted. More context does not automatically mean more competence.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both scale in the wrong direction: the more mature your setup becomes, the heavier and less precise it gets, as long as everything lives in one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Insight
&lt;/h2&gt;

&lt;p&gt;Not all instructions are needed all the time.&lt;/p&gt;

&lt;p&gt;I always need the assistant's personality and basic operating principles. I only need the exact structure of my project system when I actually navigate through it. I only need the session-end rules at the end of a session, and never before that.&lt;/p&gt;

&lt;p&gt;A writing task does not need filesystem navigation rules.&lt;br&gt;&lt;br&gt;
A quick reasoning task does not need session-close workflows.&lt;br&gt;&lt;br&gt;
A debugging session does not need publishing guidelines.&lt;/p&gt;

&lt;p&gt;If that is true, it makes no sense to keep everything loaded permanently.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;I split the one large file into a lean entry point plus specialized modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.config/
├── instructions.md   -&amp;gt; compact entry point, always loaded
├── persona.md        -&amp;gt; personality, tone, behavior
├── structure.md      -&amp;gt; system structure, only relevant for navigation
└── workflows.md      -&amp;gt; session workflows, only relevant when needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main instruction file is now intentionally small. It contains the minimum that really has to be present in every session, plus clear references: which module is responsible for what, and when it should be loaded.&lt;/p&gt;

&lt;p&gt;The detail modules are not active by default. They are accessible, but they only become part of the context when the task requires them.&lt;/p&gt;

&lt;p&gt;That distinction matters. The full instruction set is not magically present for free. It is only available if the assistant knows that a module exists, recognizes that it is relevant, and loads it at the right moment.&lt;/p&gt;

&lt;p&gt;So modularization does not mean: same context, lower cost.&lt;/p&gt;

&lt;p&gt;It means: smaller baseline, with more responsibility placed on routing and loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Loading Works
&lt;/h2&gt;

&lt;p&gt;In my setup, the entry point acts as a router. It does not contain all detailed rules. It contains short loading rules such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If the task involves navigating the project system,
load structure.md before answering.

If the task involves ending or reviewing a session,
load workflows.md before making recommendations.

If the task is a quick standalone question,
do not load additional modules unless needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple, but it is also the fragile part of the system. If the entry point is vague, the assistant may fail to load the right module. If it is too broad, it loads too much again and the benefit disappears.&lt;/p&gt;

&lt;p&gt;The quality of the entry point determines the quality of the whole architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;In my setup, the baseline token load per session dropped by around &lt;strong&gt;60-80%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I measured this by comparing the files that were previously loaded unconditionally at session start with the files that are now loaded unconditionally. The important number is not the total size of all available instructions. It is the size of the always-loaded baseline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before modularization:

Always loaded:
instructions.md
persona.md
structure.md
workflows.md

Baseline load:
~4,800 tokens

After modularization:

Always loaded:
instructions.md
persona.md

Baseline load:
~1,450 tokens

Reduction:
69.8%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full instruction set still exists, but it is no longer active by default. It becomes active only when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;The trick is not compressing individual instructions. The trick is separating &lt;strong&gt;baseline load&lt;/strong&gt; from &lt;strong&gt;on-demand load&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Baseline load:&lt;/strong&gt; what is loaded in every session. This is where the savings matter most, because this cost is paid repeatedly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-demand load:&lt;/strong&gt; what is only relevant in specific situations. This can be large and detailed, as long as it is loaded only when it actually matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you are not optimizing the total size of your instructions. You are optimizing which part of them must always be present. And that is surprisingly little.&lt;/p&gt;

&lt;p&gt;Prompt caching can reduce the financial cost of repeated baseline instructions in some systems. But it does not remove the context-budget cost, the latency implications in every environment, or the signal-to-noise problem. A cached irrelevant instruction is still an irrelevant instruction in the active instruction set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Costs
&lt;/h2&gt;

&lt;p&gt;This is not a free lunch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Indirection:&lt;/strong&gt;&lt;br&gt;
The assistant sometimes has to take an extra step to load the right module. That is slightly slower and creates the risk that the right module is not loaded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Routing errors:&lt;/strong&gt;&lt;br&gt;
If the assistant does not recognize that a task requires a module, it may answer with incomplete instructions. This is the main operational risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintenance:&lt;/strong&gt;&lt;br&gt;
More files mean more places that can drift apart. If the entry point promises something that no longer exists in a module, you have a silent consistency problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rule conflicts:&lt;/strong&gt;&lt;br&gt;
Modules can contradict each other or the entry point. You need a precedence rule: general instructions define the default, specialized modules override them only within their domain, and explicit user instructions still have to be handled according to the system's hierarchy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onboarding:&lt;/strong&gt;&lt;br&gt;
An outsider first has to understand the loading logic before the system becomes readable. A single file is trivial to understand.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;You reduce baseline cost, but you give up permanent availability. You move complexity from runtime context into structure, routing, and maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It Is Worth It
&lt;/h2&gt;

&lt;p&gt;It is worth it if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your instruction set is large and has grown over time&lt;/li&gt;
&lt;li&gt;you start sessions frequently and baseline cost is noticeable&lt;/li&gt;
&lt;li&gt;clearly separable situations exist, such as navigation, session-end handling, publishing, coding, or special workflows&lt;/li&gt;
&lt;li&gt;your environment allows the assistant to load additional instruction files reliably&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not worth it if your entire setup fits into a few hundred tokens anyway. In that case, modularization is premature optimization: you trade real simplicity for imagined efficiency.&lt;/p&gt;

&lt;p&gt;It is also not worth it if the assistant cannot reliably access the modules when needed. A small always-loaded file plus inaccessible detail files is not an architecture. It is missing context with extra steps.&lt;/p&gt;

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

&lt;p&gt;The biggest lever for instruction cost is rarely a better prompt. It is the question of what you force the assistant to carry into every interaction, and what it should only load when needed.&lt;/p&gt;

&lt;p&gt;Separate baseline load from on-demand load. Keep the entry point small and turn it into a precise router. Leave the details where they only create cost when they are actually needed.&lt;/p&gt;

&lt;p&gt;In my case, that meant 60-80% less baseline load. But the important part is not just the savings. It is the trade-off: less permanent context, more deliberate loading.&lt;/p&gt;

&lt;p&gt;That is the architecture I actually want. Not less instruction, but less unnecessary instruction in the room at the wrong time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Most Dangerous Bias of Your AI Assistant Is That It Agrees With You</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 10 Jun 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/the-most-dangerous-bias-of-your-ai-assistant-is-that-it-agrees-with-you-4fhc</link>
      <guid>https://dev.to/ben-witt/the-most-dangerous-bias-of-your-ai-assistant-is-that-it-agrees-with-you-4fhc</guid>
      <description>&lt;p&gt;We talk a lot about hallucinations. But there is another failure mode we should take just as seriously: AI assistants are optimized to be helpful, polite, and cooperative. Over a longer session, that can quietly turn into agreeableness.&lt;/p&gt;

&lt;p&gt;In a system that is supposed to help you think, this is not a cosmetic problem. It is the core problem. An assistant that agrees with you simply because you are the one asking is worthless as a sparring partner. It confirms your bad ideas just as readily as your good ones.&lt;/p&gt;

&lt;p&gt;I call this &lt;strong&gt;sycophancy drift&lt;/strong&gt;, and I have added a reflective layer to my knowledge system to detect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Sycophancy Drift Is
&lt;/h2&gt;

&lt;p&gt;At the beginning of a session, the assistant still pushes back. You make a suggestion, and it gives you three reasons against it. Good.&lt;/p&gt;

&lt;p&gt;Twenty messages later, the tone is different. You suggest something, and suddenly everything is “a good point,” “absolutely reasonable,” or “a strong idea.” Not because your ideas have necessarily become better, but because the conversation context has increasingly conditioned the assistant toward agreement.&lt;/p&gt;

&lt;p&gt;To be precise: the model is not learning in the training sense. Its weights are not changing during the session. But the transcript becomes part of the active context, and that context can reward a pattern. If agreement keeps the conversation moving, the assistant may drift toward more agreement.&lt;/p&gt;

&lt;p&gt;That is the drift: a gradual shift from honest evaluation to confirmation.&lt;/p&gt;

&lt;p&gt;This is not speculation. Anthropic’s own research (Perez et al. 2022; Sharma et al. 2023) shows that RLHF training systematically rewards agreement: both human raters and preference models prefer convincingly written sycophantic answers over correct ones a non-negligible fraction of the time. The disposition is built in at training time. A long conversation does not create it - it amplifies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Do Not Notice It
&lt;/h2&gt;

&lt;p&gt;Because it feels good. That is the whole trick.&lt;/p&gt;

&lt;p&gt;Hallucinations stand out because they are wrong. Sycophancy does not stand out because it is pleasant. You feel confirmed, you move forward, the session feels productive, and nobody tells you that the quality of disagreement has quietly dropped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message 5:
Assistant:
"I see three major risks."

Message 35:
Assistant:
"That's a very strong idea."

Drift warning:
Criticism frequency dropped from 4 objections
per proposal to 0.8 objections per proposal.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Schematic illustration; the actual reports are qualitative)&lt;/p&gt;

&lt;p&gt;This is exactly why the assistant cannot reliably correct this in real time by itself. It is inside the same conversational pressure. You need an outside view, or at least a retrospective one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea: A Reflective Layer at the End of the Session
&lt;/h2&gt;

&lt;p&gt;In my setup, the assistant does not rewrite its own rules automatically. Instead, I use a dedicated reflective layer after a session has ended.&lt;/p&gt;

&lt;p&gt;The process is intentionally simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At the end of a session, the analysis layer is explicitly triggered.&lt;/li&gt;
&lt;li&gt;It reads the transcript of the entire session.&lt;/li&gt;
&lt;li&gt;It compares the session against an explicit, versioned rule set.&lt;/li&gt;
&lt;li&gt;It writes a structured proposal with four sections:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New rules:&lt;/strong&gt; what this session produced as lessons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed rules:&lt;/strong&gt; what proved useful again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drift warnings:&lt;/strong&gt; where the assistant agreed too much or weakened its criticism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation:&lt;/strong&gt; what should be added to the rule set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The decisive point is this: it is only a proposal. It is written for human review. Not a single rule changes automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Layer Actually Flags
&lt;/h2&gt;

&lt;p&gt;The layer does not look for politeness. Politeness is not the problem.&lt;/p&gt;

&lt;p&gt;It looks for disappearing resistance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer objections over time&lt;/li&gt;
&lt;li&gt;softer risk language&lt;/li&gt;
&lt;li&gt;repeated validation phrases&lt;/li&gt;
&lt;li&gt;missing alternative paths&lt;/li&gt;
&lt;li&gt;ignored counterarguments&lt;/li&gt;
&lt;li&gt;praise replacing evaluation&lt;/li&gt;
&lt;li&gt;earlier rules being quietly weakened&lt;/li&gt;
&lt;li&gt;decisions accepted without checking trade-offs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make the assistant negative. The goal is to preserve useful friction.&lt;/p&gt;

&lt;p&gt;A good thinking partner should not disagree for sport. But it should notice when the session has become too smooth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Design Decisions That Make the Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Human in the loop, not out of caution, but out of logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It would be tempting to let the analysis layer write its findings directly into the rule set. That would be the mistake. You would let a system that is prone to conversational drift rewrite its own anti-drift rules.&lt;/p&gt;

&lt;p&gt;Separating proposal from adoption is not a comfort feature. It is the safety mechanism that makes the whole concept meaningful.&lt;/p&gt;

&lt;p&gt;There is also a simple technical reality in my setup: the assistant has no autonomous background process and no unchecked memory update across sessions. Every change is conscious, triggered, and reviewable. This is not a limitation I want to bypass. It is the property that keeps the system honest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Importance and frequency are two different axes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every rule has two independent dimensions: an importance value and a frequency counter.&lt;/p&gt;

&lt;p&gt;A rule can occur rarely and still be critical. If I mix both dimensions together, the rare but important rule will eventually disappear because it looks statistically insignificant. That is why critical rules are protected from being archived, no matter how rarely they are triggered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A maximum of five new rules per session.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without a limit, the system overfits to a single conversation. One intense session could flood the rule set with special cases that may never matter again.&lt;/p&gt;

&lt;p&gt;The upper limit forces selection: what was truly a lesson, and what was just noise?&lt;/p&gt;

&lt;p&gt;Five is not a magic number. It is calibrated to my review capacity: a proposal I cannot review in five minutes is a proposal I will eventually stop reading and an unreviewed proposal pipeline is worse than none.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Part: The Recursion Problem
&lt;/h2&gt;

&lt;p&gt;The obvious weakness is this: I am using the same kind of model that drifted to detect its own drift. Can an assistant that tends to agree really expose its own agreement reliably?&lt;/p&gt;

&lt;p&gt;The honest answer is: not perfectly.&lt;/p&gt;

&lt;p&gt;A retrospective layer can itself become performative. It may learn to produce the kind of criticism the user expects, without actually identifying meaningful drift. It may become another agreeable ritual: “Here are your drift warnings,” even when the analysis is shallow.&lt;/p&gt;

&lt;p&gt;But the approach is still useful for two reasons.&lt;/p&gt;

&lt;p&gt;First, checking a finished transcript against an explicit checklist is a narrower task than generating helpful answers in the middle of a live conversation. Evaluation is not the same as participation.&lt;/p&gt;

&lt;p&gt;Second, the result is not trusted blindly. A human reviews it. The layer does not have to detect drift perfectly. It only has to make the pattern visible enough to question.&lt;/p&gt;

&lt;p&gt;Additional costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No real-time feedback.&lt;/strong&gt; Drift during the current session is detected afterwards, for the next run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review effort.&lt;/strong&gt; Reading proposals and deciding what to adopt is work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False confidence.&lt;/strong&gt; A reflective layer can look more objective than it really is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. The layer is not a guarantee. It is a mirror.&lt;/p&gt;

&lt;p&gt;The industry attacks the same problem one level deeper: Anthropic’s persona-vector research identifies activation patterns associated with sycophancy and steers models away from them during training. That is the right fix at the model level. But it cannot reach the workflow level — no lab can review whether your assistant pushed back on your architecture decision yesterday. That layer is yours to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It Is Worth It
&lt;/h2&gt;

&lt;p&gt;If you use the assistant as a thinking tool, where honest disagreement is part of the value, then drift detection is not a nice extra. It is one of the conditions under which the tool works at all.&lt;/p&gt;

&lt;p&gt;If you only use the assistant for clearly defined execution tasks, where agreement does not matter much, you probably do not need this.&lt;/p&gt;

&lt;p&gt;But if the assistant helps shape your decisions, your architecture, your writing, your strategy, or your beliefs, then you should care about how much resistance disappears over time.&lt;/p&gt;

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

&lt;p&gt;The labs are working on this at the training level, evals, steering, character training. What is largely missing is the workflow level: per-user, per-session drift detection that you control.&lt;/p&gt;

&lt;p&gt;The most pleasant bias may also be the most dangerous one: an assistant that agrees with you feels like a good assistant while it slowly stops being useful as a thinking partner.&lt;/p&gt;

&lt;p&gt;The solution is not simply a better model version. It is a layer that looks back after the work is done, names the drift, and leaves the decision with you.&lt;/p&gt;

&lt;p&gt;The assistant should not only help you move faster. It should also preserve the friction that keeps your thinking honesty.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part II will follow later&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Transitive Dependency – Challenges and Approaches in .NET</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 12 Feb 2025 09:00:00 +0000</pubDate>
      <link>https://dev.to/ben-witt/transitive-dependency-challenges-and-approaches-in-net-20a6</link>
      <guid>https://dev.to/ben-witt/transitive-dependency-challenges-and-approaches-in-net-20a6</guid>
      <description>&lt;p&gt;Imagine you are developing a .NET application that runs perfectly at first. Suddenly, however, unexpected runtime errors occur—even though your main project does not explicitly include any additional packages. How can this phenomenon be explained? Often, the cause is a transitive dependency: a library such as Entity Framework Core is silently included in your project via another NuGet package. In complex application scenarios, this issue can lead to version conflicts or even hard-to-diagnose runtime errors like the &lt;code&gt;MissingMethodException&lt;/code&gt;. Without targeted management of these dependencies, the effort required for error resolution can become significant.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Basics and Problem Statement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Transitive References vs. Transitive Dependencies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transitive References:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In a multi-layered solution structure, for example, Project A directly references Project B, which in turn uses Project C. In this way, Project A has an indirect (transitive) reference to Project C—even if it is not explicitly listed in the &lt;code&gt;.csproj&lt;/code&gt; file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transitive Dependencies:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
With external NuGet packages, the problem appears when they are brought in by a referenced package and do not show up in the main project’s &lt;code&gt;.csproj&lt;/code&gt; file. For instance, if Project A uses a package (e.g., &lt;code&gt;PackageX&lt;/code&gt;) that internally references EF Core in a specific version, then EF Core is included transitively—even though Project A never explicitly added it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Isn’t it remarkable how quickly such subtle dependency chains can emerge in large projects without being noticed?&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 A Typical Scenario in .NET
&lt;/h3&gt;

&lt;p&gt;In many .NET applications, Entity Framework Core (EF Core) is used as the data access library. Even if the main project does not directly reference EF Core, it can be silently included as a transitive dependency via another utility or data access package. Initially, everything seems to work smoothly—EF Core provides a stable API. However, when updating the .NET framework or integrating new features, version conflicts may occur that eventually lead to hard-to-diagnose runtime errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Detailed Solution Approaches
&lt;/h2&gt;

&lt;p&gt;To prevent the unwanted propagation of packages, two main approaches are available: the targeted restriction of individual packages using &lt;code&gt;PrivateAssets="all"&lt;/code&gt; or the complete deactivation of transitive project references with &lt;code&gt;&amp;lt;DisableTransitiveProjectReferences&amp;gt;true&amp;lt;/DisableTransitiveProjectReferences&amp;gt;&lt;/code&gt;. Both approaches are critically examined below.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 &lt;code&gt;PrivateAssets="all"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;By adding the attribute &lt;code&gt;PrivateAssets="all"&lt;/code&gt; in the PackageReference, you prevent the respective library from being passed on to other projects in the dependency hierarchy. This way, the package—and its transitive dependencies—remain available only within the current project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Granularity:&lt;/strong&gt; You decide on a package-by-package basis whether a dependency should be inherited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoidance of Side Effects:&lt;/strong&gt; Problematic libraries are not automatically transferred to subordinate projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Increased Configuration Effort:&lt;/strong&gt; If another project needs the same library, it must be explicitly referenced again.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"SomeDataAccessPackage"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.3"&lt;/span&gt; &lt;span class="na"&gt;PrivateAssets=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Could it be that in complex environments the administrative effort for such package-specific configuration outweighs the benefits?&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 DisableTransitiveProjectReferences
&lt;/h3&gt;

&lt;p&gt;With the global setting true in the .csproj file, you prevent the automatic propagation of all transitive project references. Every project must then explicitly list all the required packages.&lt;/p&gt;

&lt;p&gt;Advantages:&lt;br&gt;
    • Increased Transparency: All dependencies are explicitly visible and controllable.&lt;br&gt;
    • Prevention of Unintended Inclusions: No “hidden” libraries are automatically added to the project.&lt;/p&gt;

&lt;p&gt;Disadvantages:&lt;br&gt;
    • Higher Maintenance Effort: Explicitly listing all dependencies can create a significant administrative overhead, especially in larger projects.&lt;br&gt;
    • Risk of Omissions: Essential dependencies might accidentally be overlooked, leading to compile-time or runtime errors.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;PropertyGroup&amp;gt;
  &amp;lt;DisableTransitiveProjectReferences&amp;gt;true&amp;lt;/DisableTransitiveProjectReferences&amp;gt;
&amp;lt;/PropertyGroup&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;Given the additional effort, is it really practical to explicitly declare all dependencies?&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 Comparison of Both Approaches
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;PrivateAssets="all"&lt;/th&gt;
&lt;th&gt;DisableTransitiveProjectReferences&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Granularity&lt;/td&gt;
&lt;td&gt;Package-specific control&lt;/td&gt;
&lt;td&gt;Global deactivation – all dependencies must be explicitly referenced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance Effort&lt;/td&gt;
&lt;td&gt;Requires individual adjustments per project&lt;/td&gt;
&lt;td&gt;Higher maintenance effort due to separate management of all references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clarity&lt;/td&gt;
&lt;td&gt;Can lead to opaque dependency chains in complex structures&lt;/td&gt;
&lt;td&gt;Clear separation, but increases the risk of overlooking essential packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flexibility&lt;/td&gt;
&lt;td&gt;High flexibility in isolated control of individual packages&lt;/td&gt;
&lt;td&gt;Strict control, but less adaptability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3. Practical Example: Abstraction and Interface Encapsulation
&lt;/h2&gt;

&lt;p&gt;Consider a specific application: a .NET Standard library named Common encapsulates various helper methods and tools and internally includes a data access package that brings EF Core as a transitive dependency. In this case, it is advisable not to expose EF Core-specific classes in the public API; instead, these should be abstracted through interfaces.&lt;/p&gt;

&lt;p&gt;Example Implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
// In Common
public interface IDataService
{
    IEnumerable&amp;lt;string&amp;gt; GetData();
}

internal class EfDataService : IDataService
{
    public IEnumerable&amp;lt;string&amp;gt; GetData()
    {
        // Internal use of EF Core
        return new List&amp;lt;string&amp;gt; { "Data from EF Core" };
    }
}

public static class DataServiceFactory
{
    public static IDataService CreateService()
    {
        return new EfDataService();
    }
}

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

&lt;/div&gt;



&lt;p&gt;Configuration in Common.csproj:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;ItemGroup&amp;gt;
  &amp;lt;PackageReference Include="SomeDataAccessPackage" Version="1.2.3" PrivateAssets="all" /&amp;gt;
&amp;lt;/ItemGroup&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;This approach ensures that EF Core remains exclusively available internally—the main application only interacts with the interface without needing to know the underlying implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Advanced Strategies and Alternative Approaches
&lt;/h2&gt;

&lt;p&gt;In addition to the configuration options discussed above, other strategies can help minimize version conflicts and the issues of transitive dependencies:&lt;br&gt;
    • Binding Redirects:&lt;br&gt;
Especially in older .NET Framework applications, binding redirects can help resolve conflicts between different versions of the same library.&lt;br&gt;
    • Modular Architectures:&lt;br&gt;
Separating the application into clearly defined modules allows for targeted isolation of dependencies. This makes it easier to control unwanted side effects.&lt;/p&gt;

&lt;p&gt;Should we not always question whether a focus solely on .csproj configurations in highly complex systems is truly the optimal solution?&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Summary and Outlook
&lt;/h2&gt;

&lt;p&gt;Transitive dependencies represent a serious issue in complex .NET projects. Seemingly insignificant libraries like EF Core can—when included via other NuGet packages—lead to significant version conflicts and runtime errors. Consistent separation of dependencies and the targeted use of measures such as PrivateAssets="all" or  offer practical solutions. It is essential to weigh whether the administrative effort is justified by the benefits and how long-term maintainability can be ensured.&lt;/p&gt;

&lt;p&gt;By using abstractions, explicit references, and additional measures such as binding redirects, developers can not only create a more stable codebase but also reduce ongoing maintenance efforts. Regularly reviewing dependency hierarchies using tools like dotnet list package --include-transitive or NDepend is indispensable.&lt;/p&gt;

&lt;p&gt;Is it not ultimately the task of every developer to continually question whether the current architecture still meets the growing demands of modern applications?&lt;/p&gt;

&lt;p&gt;I think so….!&lt;/p&gt;

</description>
      <category>development</category>
      <category>coding</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Efficient Debugging and Precise Logging in C#: Using Caller Attributes</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 29 Jan 2025 17:01:53 +0000</pubDate>
      <link>https://dev.to/ben-witt/efficient-debugging-and-precise-logging-in-c-using-caller-attributes-3m5f</link>
      <guid>https://dev.to/ben-witt/efficient-debugging-and-precise-logging-in-c-using-caller-attributes-3m5f</guid>
      <description>&lt;p&gt;How often do you wish, while debugging a complex application, to instantly see where in the code an error was triggered? Often, you are left with basic error messages that lack valuable context or exact code lines. This is where the C# attributes [CallerMemberName], [CallerFilePath], and [CallerLineNumber] come into play.&lt;/p&gt;

&lt;p&gt;These attributes automatically provide information about the calling method’s name, the physical file path, and the line number in the source code when a method is called. This enables precise error tracking and logging, which is useful both during debugging and in production logging. Especially when troubleshooting large codebases, they prove to be indispensable tools for quick and targeted analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basics of the Attributes
&lt;/h2&gt;

&lt;p&gt;The attributes [CallerMemberName], [CallerFilePath], and [CallerLineNumber] are applied directly to method parameters. The compiler automatically replaces the arguments with the corresponding values from the call context without any extra effort from the developer:&lt;br&gt;
    • [CallerMemberName] provides the name of the calling method.&lt;br&gt;
    • [CallerFilePath] gives the full path to the file where the method call is located.&lt;br&gt;
    • [CallerLineNumber] provides the line number of the call within that file.&lt;/p&gt;

&lt;p&gt;The syntax is straightforward. A classic example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void LogInfo(
    string message, 
    [CallerMemberName] string memberName = "",
    [CallerFilePath] string filePath = "", 
    [CallerLineNumber] int lineNumber = 0)
{
    Console.WriteLine($"[{memberName}] {message} (File: {filePath}, Line: {lineNumber})");
}

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

&lt;/div&gt;



&lt;p&gt;Here, the parameters must have default values (e.g., = "" or = 0) so that the compiler can insert the values automatically. When you call the method without specifying these optional parameters, they are substituted automatically.&lt;/p&gt;

&lt;p&gt;Example Method for Capturing Call Stack Information&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
public void ProcessData([CallerMemberName] string caller = "",
                        [CallerFilePath] string path = "",
                        [CallerLineNumber] int line = 0)
{
    Console.WriteLine($"Called by: {caller}, File: {path}, Line: {line}");
    // Further data processing code...
}

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

&lt;/div&gt;



&lt;p&gt;In this simple scenario, you can already gain initial insights into the call context and source code position. This meta-information is extremely valuable both for debugging and for later evaluations in operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Examples for Practical Applications
&lt;/h2&gt;

&lt;p&gt;a) &lt;strong&gt;Simple Examples&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you are running a library or an API to manage borrowing processes. Everywhere important actions take place—such as registering a book, registering new users, or handling requests—you want to create log entries. You could use a helper class for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static class Logger
{
    public static void LogOperation(
        string operation, 
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string filePath = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        // For example, log to a file or a database
        Console.WriteLine($"Operation: {operation} in {memberName}, File: {filePath}, Line: {lineNumber}");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you call Logger.LogOperation("BookBorrowed"), the log provides much more information than if you had to add it manually.&lt;/p&gt;

&lt;p&gt;b) &lt;strong&gt;More Complex Example with a Base Class&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In larger projects, it is advisable to centralize logging and error handling routines in an abstract base class. The following example shows an abstract BaseRepository that catches errors and automatically uses the attributes for meaningful error messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
public abstract class BaseRepository
{
    protected void LogError(Exception ex, 
                            [CallerMemberName] string memberName = "",
                            [CallerFilePath] string filePath = "", 
                            [CallerLineNumber] int lineNumber = 0)
    {
        Console.WriteLine($"Error in {memberName} at {filePath} (Line {lineNumber}): {ex.Message}");
    }
}

public class BookRepository : BaseRepository
{
    public void AddBook(Book book)
    {
        try
        {
            // Simulated database operation
            throw new InvalidOperationException("Database error while adding a book.");
        }
        catch (Exception ex)
        {
            LogError(ex);
            throw;  // Re-throw the error to continue the stack
        }
    }
}

public class LibraryService
{
    private readonly BookRepository _repository = new BookRepository();

    public void Execute()
    {
        try
        {
            _repository.AddBook(new Book { Title = "C# Deep Dive" });
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error in the service layer: " + ex.Message);
            throw;
        }
    }
}

// Application
var service = new LibraryService();
service.Execute();

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

&lt;/div&gt;



&lt;p&gt;This approach allows complete tracking of the call stack. When an error is triggered in BookRepository, the LogError method precisely logs the context: the member name (AddBook), the file path, and the line number. At the same time, the error is re-thrown so that the higher layer (LibraryService) can perform further actions, such as separate logging or sending an alert.&lt;/p&gt;

&lt;p&gt;c) &lt;strong&gt;Tracking a Complete Stack&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In complex multi-layered architectures—consisting of service layers, repository layers, and database connections—it is crucial to trace the source of an error without gaps. The above example demonstrates this flow:&lt;br&gt;
    1.  The service layer calls AddBook in BookRepository.&lt;br&gt;
    2.  The repository layer throws an exception due to a database error, logs details using the caller attributes, and re-throws the error.&lt;br&gt;
    3.  The service layer catches the error and can handle it accordingly.&lt;/p&gt;

&lt;p&gt;With this technique, precise logging of each relevant layer is achieved, ensuring that no information about the actual origin is lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Application with a Library Management System
&lt;/h2&gt;

&lt;p&gt;In library software, where daily borrowings, reservations, returns, or new registrations are logged, the caller attributes are used optimally:&lt;br&gt;
    1.  &lt;strong&gt;Logging API Errors&lt;/strong&gt;: If an HTTP request fails, the log immediately provides information about the error location in the code (method, file, line).&lt;br&gt;
    2.  &lt;strong&gt;Logging Database Operations&lt;/strong&gt;: In dynamic environments, faulty SQL queries or transaction conflicts can quickly lead to confusing errors. Thanks to the caller attributes, it is possible to determine exactly where these conflicts were triggered, whether in the repository or service layer.&lt;/p&gt;

&lt;p&gt;Such practical insights into the system not only shorten debugging times but also improve the overall maintainability of the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Best Practices
&lt;/h2&gt;

&lt;p&gt;Although logging file and line details is very helpful during development, it is important to consider potential drawbacks:&lt;br&gt;
    • &lt;strong&gt;Performance Considerations&lt;/strong&gt;: With frequent log calls, excessive capturing of caller information can quickly bloat logs and impact application performance. Therefore, in production environments, it should be carefully considered where detailed logs are truly necessary.&lt;br&gt;
    • &lt;strong&gt;Security Aspects&lt;/strong&gt;: Revealing file paths may be undesirable in some cases, such as in security-critical applications or externally shared logs.&lt;br&gt;
    • &lt;strong&gt;Reusability&lt;/strong&gt;: To keep maintenance effort low, it is recommended to encapsulate the attributes in base classes or utility methods, as shown in the example (BaseRepository). This avoids redundant code and ensures consistent error messages across the entire project.&lt;/p&gt;

&lt;p&gt;With a sensible logging concept and thoughtful filtering mechanisms, the level of detail can be flexibly controlled depending on the environment (development, staging, production).&lt;/p&gt;

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

&lt;p&gt;The attributes [CallerMemberName], [CallerFilePath], and [CallerLineNumber] offer significant benefits for debugging, logging, and error tracing in C# projects. Their versatile applications—from simple logs in the development environment to complex error tracking in multi-layered architectures—make them essential tools in modern application development.&lt;/p&gt;

&lt;p&gt;So why not start implementing more precise error logging and more efficient debugging in your own projects right away? The implementation is straightforward, while the benefits are enormous—especially when errors in production systems need to be quickly isolated and resolved. With targeted use, thoughtful structuring, and a bit of pragmatism, a new level of transparency in error analysis is achieved, significantly easing the developer’s daily work.&lt;/p&gt;

</description>
      <category>development</category>
      <category>microsoft</category>
      <category>csharp</category>
      <category>programming</category>
    </item>
    <item>
      <title>A Critical Look at Cancellation Management in .NET Applications</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 29 Jan 2025 17:01:36 +0000</pubDate>
      <link>https://dev.to/ben-witt/a-critical-look-at-cancellation-management-in-net-applications-1kfg</link>
      <guid>https://dev.to/ben-witt/a-critical-look-at-cancellation-management-in-net-applications-1kfg</guid>
      <description>&lt;p&gt;The CancellationToken and the related types provided in .NET offer a central and effective foundation for carefully canceling tasks, threads, or asynchronous processes. However, the question arises whether this concept truly provides sufficient flexibility and stability in all application scenarios. But how often is this feature actually used in practice, and what risks come with improper handling? This article explains how the CancellationToken works and offers concrete recommendations for its efficient use. Additionally, potential pitfalls are highlighted and critically examined.&lt;/p&gt;

&lt;p&gt;Definition and Purpose of CancellationToken&lt;/p&gt;

&lt;p&gt;The CancellationToken is a structure from the Task Parallel Library (TPL) that is used to signal an ongoing operation that a cancellation request has been made. In a software architecture increasingly based on asynchronous and parallel processes, the CancellationToken plays a central role: it allows computationally intensive processes or long-running tasks to be terminated early, conserving resources and ensuring an improved user experience.&lt;/p&gt;

&lt;p&gt;Why is this construct indispensable? In a modern .NET application, numerous operations can run in parallel or asynchronously. Without coordinated cancellation and resource release, there is a risk that certain tasks continue running and block system resources, even though they are no longer needed.&lt;/p&gt;

&lt;p&gt;How It Works and Basic Concepts&lt;/p&gt;

&lt;p&gt;Cancellation control is managed by the CancellationTokenSource class. It acts as the central control unit and provides methods such as Cancel(), which sends a cancellation signal to all linked CancellationTokens.&lt;/p&gt;

&lt;p&gt;Resource Management with IDisposable&lt;/p&gt;

&lt;p&gt;A frequently underestimated aspect is that CancellationTokenSource implements the IDisposable interface. If this is not taken into account, it can easily lead to resource leaks—especially in scenarios where a large number of CancellationTokenSource objects are created in parallel. Why should one neglect implementing clean resource management?&lt;/p&gt;

&lt;p&gt;The recommendation is to always use the using keyword to enforce automatic resource release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using (var cts = new CancellationTokenSource())
{
    // Code to execute the operation
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failing to call the Dispose() method can leave unmanaged resources in memory, ultimately affecting the application’s stability.&lt;/p&gt;

&lt;p&gt;Methods to Cancel an Operation&lt;/p&gt;

&lt;p&gt;There are several approaches to sending a cancellation request to an ongoing operation:&lt;br&gt;
    1.  Synchronous Cancellation&lt;br&gt;
The cancellation request is triggered immediately. This means the current method actively checks the CancellationToken and reacts right away.&lt;br&gt;
    2.  Asynchronous Cancellation&lt;br&gt;
The request is performed in the background without immediately blocking the calling code’s flow.&lt;br&gt;
    3.  Delayed Cancellation&lt;br&gt;
A predefined time span is used after which the operation is automatically terminated. This is especially helpful when a user does not want to wait further, such as in network or I/O-intensive tasks.&lt;/p&gt;

&lt;p&gt;A concrete example of this is setting a timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this time span elapses, a cancellation request is triggered, ensuring that an operation does not run beyond the set limits. But is it always sensible to set a rigid timeout without being able to flexibly respond to external events?&lt;/p&gt;

&lt;p&gt;Monitoring Cancellation Requests&lt;/p&gt;

&lt;p&gt;Polling&lt;/p&gt;

&lt;p&gt;A common practice is to regularly check the CancellationToken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;while (!token.IsCancellationRequested)
{
    // Continue the operation
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When such loops are implemented in computationally intensive areas, the frequency of checks can significantly affect performance. The question arises: How often should the check be performed to recognize a cancellation promptly without excessively burdening execution time?&lt;/p&gt;

&lt;p&gt;Alternatively, the ThrowIfCancellationRequested() method can be used, which throws an OperationCanceledException if a cancellation request has been made:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token.ThrowIfCancellationRequested();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is usually cleaner but can also lead to unexpected side effects if the exception is not caught appropriately.&lt;/p&gt;

&lt;p&gt;Callback Registration&lt;/p&gt;

&lt;p&gt;In scenarios where regular polling is impractical or resource-intensive, a callback can be registered to execute automatically when a cancellation request arrives. For example, if a user is downloading a large file and cancels the process, not only should the download threads stop, but any temporary files created should also be deleted to free up space and avoid inconsistencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token.Register(() =&amp;gt; 
{
    Console.WriteLine("Operation canceled.");
    // Additional cleanup, e.g., deleting temporary files
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method has the advantage of calling cancellation actions specifically without the ongoing operation having to constantly check for the cancellation signal. However, callback registration can lead to messy code flow in complex scenarios when multiple cancellation cases and cleanup actions need to be considered.&lt;/p&gt;

&lt;p&gt;Linking Multiple CancellationTokens&lt;/p&gt;

&lt;p&gt;What happens when an application needs to meet several potential cancellation conditions at the same time? For example, an operation might need to respond to both a user event (like closing a window) and a timeout simultaneously. The CancellationTokenSource class provides the CreateLinkedTokenSource method for this purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The created instance combines multiple CancellationTokens into a new token. Once any of the involved tokens is canceled, the combined token is also triggered. This simplifies the implementation of complex cancellation logic and improves readability. However, caution is necessary: incorrect linking can cause operations to be canceled too early or not at all if the relationships between tokens are not clearly defined.&lt;/p&gt;

&lt;p&gt;Best Practices for Handling CancellationToken&lt;/p&gt;

&lt;p&gt;To ensure smooth cancellation management, the following points should be considered:&lt;br&gt;
    1.  Respect the Passed Token&lt;br&gt;
Avoid ignoring the CancellationToken. In every relevant method, actively check it or use an appropriate mechanism (e.g., callback).&lt;br&gt;
    2.  Properly Release Resources&lt;br&gt;
Always call the Dispose() method on CancellationTokenSource, preferably using the using block to prevent memory leaks.&lt;br&gt;
    3.  Avoid Unnecessary Sources&lt;br&gt;
Only create a new CancellationTokenSource if passing an existing CancellationToken is insufficient. Otherwise, you risk an unclear codebase and unnecessary resource consumption.&lt;br&gt;
    4.  Proper Error Handling&lt;br&gt;
Catch the OperationCanceledException and handle it meaningfully. Silently swallowing the exception can lead to hard-to-trace error patterns, as cancellation operations behave “invisibly.”&lt;/p&gt;

&lt;p&gt;Additional Aspects and Critical Consideration&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use Cases for Timeouts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While the use of a timeout has already been mentioned, in a highly connected world, variable external factors such as fluctuating network bandwidths or different devices can quickly render a rigid timeout obsolete. Wouldn’t it make sense to take a closer look at adaptive timeout management?&lt;/p&gt;

&lt;p&gt;Consider scenarios where the system responds to environmental parameters and dynamically adjusts timeouts. For example, a microservice that reduces wait times for certain requests under high load or grants more generous timeouts with a good connection could significantly enhance user satisfaction. Load balancers and reverse proxies also play an important role here, as they can capture both the context and the load of the respective services.&lt;/p&gt;

&lt;p&gt;In short, a purely time-based approach without context can lead to misjudgments and, at worst, prematurely cancel important operations. In the age of cloud computing and highly variable environments, it’s worthwhile to consider context-dependent timeout management with circuit breakers or retry mechanisms.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Depth of Example Implementations&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Although the article uses concise code snippets (such as the using statement), a more in-depth presentation of more complex application examples would be helpful. Especially in microservice architectures or when accessing databases, the layered cancellation logic often becomes apparent in practice.&lt;/p&gt;

&lt;p&gt;A realistic example could cover the following aspects:&lt;br&gt;
    • Multiple CancellationTokens originating from different sources (e.g., user inputs, timeouts, overload signals).&lt;br&gt;
    • A database-heavy operation accessing an external resource that, upon interruption, must not only reset local but also database transactions.&lt;br&gt;
    • Logical separation of cancellation handling at the service level and repository level to ensure a clear separation of concerns and minimize side effects.&lt;/p&gt;

&lt;p&gt;Such comprehensive implementations would allow developers to recognize and avoid common pitfalls—such as forgetting to call Dispose() or improperly catching OperationCanceledException—in a realistic context.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;More Comprehensive Error Handling&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The article already emphasizes the importance of catching OperationCanceledException. But how specifically should the error situation be handled in complex systems where multiple components respond to the same token?&lt;/p&gt;

&lt;p&gt;A possible scenario could look like this: A user-initiated cancellation request is recognized, but a downstream sub-component does not respond in time or blocks while requesting resources. Partial failures occur, which might only appear late in the logs and are difficult to attribute to a specific cause. In such cases, structured exception handling is needed, considering the following points:&lt;br&gt;
    • Targeted Logging: Every cancellation request should appear in the logs, ideally with context information (e.g., which sub-component responded, which actions were interrupted).&lt;br&gt;
    • Separation of Regular Exceptions and Cancellation Cases: An OperationCanceledException does not indicate a classic error state but a deliberate cancellation. However, the interplay of multiple CancellationTokens can create unforeseen side effects, making a consistent and well-documented error strategy essential.&lt;br&gt;
    • Fallback Mechanisms: In safety-critical or highly available environments, a cancellation should not lead to an unstable overall system but support orderly fallback procedures (e.g., graceful shutdown).&lt;/p&gt;

&lt;p&gt;Especially in distributed architectures where multiple services and applications interact, a well-documented error handling and logging process is indispensable to make cancellation processes both traceable and controlled.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;The CancellationToken is a central component in the development of modern, reactive, and resource-efficient .NET applications. However, its correct use requires a certain level of attention: underestimating the importance of clean resource management or ignoring cancellation signals in the code can lead to errors and performance problems almost by default.&lt;/p&gt;

&lt;p&gt;Is it really acceptable to forgo this central cancellation mechanism or implement it only half-heartedly? Those who value scalability and user satisfaction will clearly answer no. Consistently following the described best practices leads to robust and controlled cancellation management, enhances application performance, and improves the user experience equally. At the same time, a critical look at advanced concepts such as adaptive timeout management, comprehensive implementation examples, and structured error handling is recommended to make the use of CancellationToken even more effective and secure.&lt;/p&gt;

</description>
      <category>development</category>
      <category>csharp</category>
      <category>microsoft</category>
    </item>
    <item>
      <title>Asynchronous HTTP Requests – Explained Simply</title>
      <dc:creator>Ben Witt</dc:creator>
      <pubDate>Wed, 29 Jan 2025 17:01:17 +0000</pubDate>
      <link>https://dev.to/ben-witt/asynchronous-http-requests-explained-simply-2jkh</link>
      <guid>https://dev.to/ben-witt/asynchronous-http-requests-explained-simply-2jkh</guid>
      <description>&lt;p&gt;Imagine a waiter taking your order and waiting in the kitchen until your food is ready. During this time, the waiter cannot serve other guests. This is how a synchronous HTTP request works – it blocks the process.&lt;/p&gt;

&lt;p&gt;An asynchronous HTTP request, on the other hand, allows the waiter to serve other guests while your food is being prepared. Once your dish is ready, the waiter brings it to you without others being delayed. Sounds efficient, right?&lt;/p&gt;

&lt;p&gt;Why Are Asynchronous Requests So Important?&lt;/p&gt;

&lt;p&gt;In daily life, we encounter asynchronous HTTP requests all the time, for example:&lt;br&gt;
    • WhatsApp: Messages are loaded while you’re using other functions.&lt;br&gt;
    • Instagram: Images and videos are loaded in the background.&lt;br&gt;
    • Netflix: The next episode is preloaded while you’re still watching.&lt;br&gt;
    • Online Shopping: Products are filtered and sorted in the background.&lt;/p&gt;

&lt;p&gt;What Does Idempotence Mean in This Context?&lt;/p&gt;

&lt;p&gt;Idempotence is an essential concept in web development, particularly for HTTP requests. It describes the property of an operation that delivers the same result no matter how many times it is performed. This principle plays an important role in the reliability and repeatability of requests – especially with asynchronous HTTP requests, as they might be sent multiple times due to network failures or timeouts.&lt;/p&gt;

&lt;p&gt;Examples of idempotent HTTP methods:&lt;br&gt;
    • GET: Repeatedly fetching the same resource always returns the same data (unless the resource changes).&lt;br&gt;
    • PUT: Overwriting a resource with the same data does not change the final state.&lt;br&gt;
    • DELETE: Deleting a resource has no effect if the resource has already been removed.&lt;/p&gt;

&lt;p&gt;In contrast, POST is not idempotent because it often creates new resources or changes states, which can lead to inconsistent behavior when executed multiple times.&lt;/p&gt;

&lt;p&gt;A Simple Introduction in C#&lt;/p&gt;

&lt;p&gt;Here is an example of fetching weather data in C#, which also considers idempotence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class WetterService
{
    private readonly HttpClient _httpClient;

    public WetterService()
    {
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri("https://api.wetter.de/");
    }

    public async Task&amp;lt;WetterDaten&amp;gt; HoleWetterAsync(string stadt)
    {
        try
        {
            var antwort = await _httpClient.GetAsync($"wetter/{stadt}");

            if (!antwort.IsSuccessStatusCode)
            {
                return new WetterDaten { Fehler = "Ups! Da ist etwas schiefgelaufen." };
            }

            var json = await antwort.Content.ReadAsStringAsync();
            return JsonSerializer.Deserialize&amp;lt;WetterDaten&amp;gt;(json);
        }
        catch (Exception)
        {
            return new WetterDaten { Fehler = "Sorry, wir konnten das Wetter nicht abrufen." };
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: The use of the HTTP-GET method ensures idempotence. Even with multiple requests, the server’s state remains unchanged.&lt;/p&gt;

&lt;p&gt;Practical Example: Online Shop&lt;/p&gt;

&lt;p&gt;A typical controller processing asynchronous and idempotent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ApiController]
[Route("api/produkte")]
public class ProduktController : ControllerBase
{
    private readonly IProduktService _produktService;

    public ProduktController(IProduktService produktService)
    {
        _produktService = produktService;
    }

    [HttpGet]
    public async Task&amp;lt;ActionResult&amp;lt;List&amp;lt;Produkt&amp;gt;&amp;gt;&amp;gt; HoleAlleProdukte()
    {
        var produkte = await _produktService.HoleAlleProdukteAsync();
        return Ok(produkte);
    }

    [HttpGet("{id}")]
    public async Task&amp;lt;ActionResult&amp;lt;Produkt&amp;gt;&amp;gt; HoleProdukt(int id)
    {
        var produkt = await _produktService.HoleProduktAsync(id);

        if (produkt == null)
        {
            return NotFound("Produkt nicht gefunden");
        }

        return Ok(produkt);
    }

    [HttpDelete("{id}")]
    public async Task&amp;lt;IActionResult&amp;gt; LoescheProdukt(int id)
    {
        var erfolgreich = await _produktService.LoescheProduktAsync(id);

        if (!erfolgreich)
        {
            return NotFound("Produkt nicht gefunden oder bereits gelöscht.");
        }

        return NoContent();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idempotence in detail: Both GET and DELETE are idempotent here. Even with multiple executions, the results remain consistent, enhancing the system’s stability.&lt;/p&gt;

&lt;p&gt;Benefits of Asynchronous and Idempotent HTTP Requests&lt;br&gt;
    1.  Better User Experience: The app remains responsive and does not freeze.&lt;br&gt;
    2.  More Efficient Resource Utilization: The server can handle other tasks during wait times.&lt;br&gt;
    3.  Increased Reliability: Repeated requests do not lead to inconsistent behavior thanks to idempotence.&lt;br&gt;
    4.  Scalability: Asynchronous processing reduces bottlenecks, especially with parallel requests.&lt;/p&gt;

&lt;p&gt;Advanced Use Case: Parallel Loading&lt;/p&gt;

&lt;p&gt;Sometimes it is necessary to load multiple resources simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class DashboardService
{
    private readonly IBenutzerService _benutzerService;
    private readonly IBestellungService _bestellungService;

    public async Task&amp;lt;DashboardDaten&amp;gt; HoleDashboardDatenAsync()
    {
        var benutzerTask = _benutzerService.HoleStatistikenAsync();
        var bestellungenTask = _bestellungService.HoleStatistikenAsync();

        await Task.WhenAll(benutzerTask, bestellungenTask);

        return new DashboardDaten
        {
            BenutzerStats = await benutzerTask,
            BestellungStats = await bestellungenTask
        };
    }
}

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

&lt;/div&gt;



&lt;p&gt;Common Pitfalls and Solutions&lt;br&gt;
    1.  Forgotten await: Without await, the code remains blocking and loses its asynchronous behavior.&lt;br&gt;
    2.  Neglecting Error Handling: Network issues are common, so plan for appropriate error handling.&lt;br&gt;
    3.  Not Setting Timeouts: Avoid endless wait times by implementing timeout rules.&lt;br&gt;
    4.  Ignoring Idempotence: Non-idempotent requests can lead to hard-to-trace errors.&lt;br&gt;
    5.  Lack of Tests: Asynchronous code is prone to subtle bugs and should be thoroughly tested.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;Asynchronous HTTP requests are the foundation of modern, responsive applications. By adhering to idempotence principles, repeated requests do not produce unexpected results. Together, they not only enhance the user experience but also create robust and scalable systems. However, their proper implementation requires careful planning and execution.&lt;/p&gt;

</description>
      <category>devto</category>
      <category>developer</category>
      <category>opensource</category>
      <category>coding</category>
    </item>
  </channel>
</rss>
