<?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: rahib khan</title>
    <description>The latest articles on DEV Community by rahib khan (@rahib_khan).</description>
    <link>https://dev.to/rahib_khan</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%2F4072284%2Fb06c01e3-1879-4f4e-b261-76d942eadbcc.png</url>
      <title>DEV Community: rahib khan</title>
      <link>https://dev.to/rahib_khan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahib_khan"/>
    <language>en</language>
    <item>
      <title>Thinking to create a unique dev materials hub</title>
      <dc:creator>rahib khan</dc:creator>
      <pubDate>Wed, 19 Aug 2026 11:27:18 +0000</pubDate>
      <link>https://dev.to/rahib_khan/thinking-to-create-a-unique-dev-materials-hub-8aa</link>
      <guid>https://dev.to/rahib_khan/thinking-to-create-a-unique-dev-materials-hub-8aa</guid>
      <description>&lt;p&gt;I want to have a kinda unique and different plateform where dev related information will be sharing every day by every with a exact and complete details of the issues and the solutions that really feels like a living environment and easy for AI to search and find the solution&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Hi dev I am Rahib khan .NET and Tauri developer</title>
      <dc:creator>rahib khan</dc:creator>
      <pubDate>Sat, 15 Aug 2026 11:20:00 +0000</pubDate>
      <link>https://dev.to/rahib_khan/hi-dev-i-am-rahib-khan-net-and-tauri-developer-3k4k</link>
      <guid>https://dev.to/rahib_khan/hi-dev-i-am-rahib-khan-net-and-tauri-developer-3k4k</guid>
      <description></description>
    </item>
    <item>
      <title>Your app doesn't know when another program changed your database</title>
      <dc:creator>rahib khan</dc:creator>
      <pubDate>Tue, 11 Aug 2026 04:44:57 +0000</pubDate>
      <link>https://dev.to/rahib_khan/your-app-doesnt-know-when-another-program-changed-your-database-6nc</link>
      <guid>https://dev.to/rahib_khan/your-app-doesnt-know-when-another-program-changed-your-database-6nc</guid>
      <description>&lt;p&gt;There's a bug in almost every line-of-business app I've worked on, and most teams never&lt;br&gt;
file it as a bug. It looks like this:&lt;/p&gt;

&lt;p&gt;A planner has a product list open. Somewhere else, an ERP sync job inserts a row straight&lt;br&gt;
into the same database. The planner's screen doesn't change. It won't change in five&lt;br&gt;
seconds, or five minutes. It changes when they click Refresh — and the only reason they&lt;br&gt;
ever click Refresh is that they've learned, over months, not to trust what they're looking&lt;br&gt;
at.&lt;/p&gt;

&lt;p&gt;I hit this on a manufacturing scheduling product. Heavy .NET desktop application, several&lt;br&gt;
workstations, one shared SQL Server, and an ERP integration that writes to that database&lt;br&gt;
directly. Every workstation was showing data that was minutes or hours stale, and the&lt;br&gt;
official answer was "click the refresh button."&lt;/p&gt;

&lt;p&gt;The interesting part isn't the bug. It's that &lt;strong&gt;the database already knew&lt;/strong&gt;, and there was&lt;br&gt;
no reasonable way to ask it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Every database can already tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; — Change Tracking, built into the engine, all editions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; — logical replication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — the binlog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; — &lt;code&gt;PRAGMA data_version&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four mature, documented mechanisms. The capability exists everywhere.&lt;/p&gt;

&lt;p&gt;The abstraction didn't. And that gap is why teams keep reinventing this badly.&lt;/p&gt;
&lt;h2&gt;
  
  
  What people actually do instead
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A timer.&lt;/strong&gt; Reload the grid every thirty seconds. Works, in the sense that a stopped clock&lt;br&gt;
works twice a day. Burns queries, and the user still sees stale data for up to thirty&lt;br&gt;
seconds — during which they might act on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cooperative scheme.&lt;/strong&gt; The app stamps a "something changed" row on every write, and other&lt;br&gt;
instances poll that row. This is elegant right up until a &lt;em&gt;second&lt;/em&gt; application touches the&lt;br&gt;
database, at which point it silently stops working. Ours did exactly this. An ERP writing&lt;br&gt;
directly stamped nothing, so no workstation ever learned. Nothing errored. It just quietly&lt;br&gt;
never happened.&lt;/p&gt;

&lt;p&gt;That scheme had a second cost I only noticed when I costed up what replacing it would take:&lt;br&gt;
&lt;code&gt;ExecuteUpdate&lt;/code&gt; and &lt;code&gt;ExecuteDelete&lt;/code&gt; &lt;strong&gt;bypass EF Core interceptors entirely&lt;/strong&gt;. So every&lt;br&gt;
set-based write had to remember to stamp by hand. That codebase had 19 manual stamp calls&lt;br&gt;
across 26 bulk-write sites, and a 673-line convention test whose entire job was catching&lt;br&gt;
the ones developers forgot — hundreds of lines of machinery to work around one thing EF&lt;br&gt;
doesn't intercept.&lt;/p&gt;

&lt;p&gt;Change Tracking sits inside the database engine, so it sees those writes regardless. That&lt;br&gt;
whole category of "did you remember to stamp?" review burden stops being possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SqlDependency&lt;/code&gt; / &lt;code&gt;SqlTableDependency&lt;/code&gt;.&lt;/strong&gt; The answers you'll find first, and the ones with&lt;br&gt;
the worst production stories. From documented reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple &lt;code&gt;UPDATE&lt;/code&gt;s taking &lt;strong&gt;two minutes&lt;/strong&gt;, from unexpected parallelism in the query plan&lt;/li&gt;
&lt;li&gt;Service Broker queries &lt;strong&gt;must not contain &lt;code&gt;JOIN&lt;/code&gt;s&lt;/strong&gt; — an easily-missed rule. One query
joining six tables hung a database badly enough to need restarting both the service and
the server&lt;/li&gt;
&lt;li&gt;Endpoints silently going idle, worked around with keepalive timers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SqlDependency&lt;/code&gt; &lt;strong&gt;isn't supported on SQL Server Express&lt;/strong&gt; at all&lt;/li&gt;
&lt;li&gt;Microsoft's own documentation: &lt;em&gt;"not designed for use in client applications"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More than one team has ended up standing up Kafka. That is an enormous amount of&lt;br&gt;
infrastructure to answer &lt;em&gt;"did that row change?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debezium&lt;/strong&gt; is the real answer at scale, and it's excellent — but it's JVM plus Kafka&lt;br&gt;
Connect. Even the "Kafka-less" options embed Debezium's JVM engine. There is nothing you&lt;br&gt;
can ship inside a .NET desktop app.&lt;/p&gt;
&lt;h2&gt;
  
  
  Someone had already tried this
&lt;/h2&gt;

&lt;p&gt;After I built mine, I went looking — as you should, in that order or the other, but&lt;br&gt;
definitely at some point.&lt;/p&gt;

&lt;p&gt;There &lt;strong&gt;is&lt;/strong&gt; a .NET package advertising exactly this: multi-database CDC, unified API, SQL&lt;br&gt;
Server + MySQL + PostgreSQL. Which was a genuinely uncomfortable thing to find.&lt;/p&gt;

&lt;p&gt;Then I looked closer. About a thousand downloads, dormant since September 2025, one version&lt;br&gt;
pulled for critical bugs a month after launch — and no MySQL or PostgreSQL implementation&lt;br&gt;
at all. Those two engines existed only in the README.&lt;/p&gt;

&lt;p&gt;I want to be fair to it: &lt;strong&gt;that project is evidence the need is real.&lt;/strong&gt; Someone else felt&lt;br&gt;
the same gap and started building the same shape. That's a signal, not a warning.&lt;/p&gt;

&lt;p&gt;But it taught me four things I did differently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It polls while calling itself CDC.&lt;/strong&gt; The method is literally &lt;code&gt;PollForChangesAsync()&lt;/code&gt;.
Polling is fine — I poll too. Mislabelling it isn't, because when someone opens the code
and finds a timer, they stop believing everything else on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's welded to Entity Framework.&lt;/strong&gt; The API is generic over your EF entity. Which is
backwards: the whole reason you need change notification is that &lt;em&gt;something that isn't
your EF app&lt;/em&gt; wrote the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One package pulls every driver.&lt;/strong&gt; &lt;code&gt;SqlClient&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;EF Core&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;EF Relational&lt;/code&gt;
&lt;em&gt;and&lt;/em&gt; &lt;code&gt;MySql.Data&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;Npgsql&lt;/code&gt; — always. It ships drivers for providers it never
implemented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It advertised four databases and shipped one.&lt;/strong&gt; A developer evaluates it, discovers
that, closes the tab — and never comes back to check version 3.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The design decision everything else follows from
&lt;/h2&gt;

&lt;p&gt;Here's the temptation: make every database look the same. One interface, four&lt;br&gt;
implementations, done.&lt;/p&gt;

&lt;p&gt;It's a trap, and this table is why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;SQL Server&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL&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;table + changed keys&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;whole database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;table + full row&lt;/td&gt;
&lt;td&gt;table + full row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;polled&lt;/td&gt;
&lt;td&gt;polled&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;pushed on commit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;pushed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives app restart&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives downtime&lt;/td&gt;
&lt;td&gt;within retention&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;within retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup required&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ALTER DATABASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wal_level=logical&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;binlog_format=ROW&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SQLite's &lt;code&gt;PRAGMA data_version&lt;/code&gt; is a single integer for the entire file. It cannot tell you&lt;br&gt;
which table changed, let alone which row, and it means nothing after a restart. PostgreSQL&lt;br&gt;
logical replication gives you the row before and after, and will hold changes for days&lt;br&gt;
while your app is offline.&lt;/p&gt;

&lt;p&gt;A uniform API over those two has to either &lt;strong&gt;lie about SQLite&lt;/strong&gt; or &lt;strong&gt;cripple PostgreSQL&lt;br&gt;
down to SQLite's floor&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I did neither. Every provider declares what it can actually do:&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;FeedCapabilities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ChangeDetail&lt;/span&gt; &lt;span class="n"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// DatabaseChanged &amp;lt; TableChanged &amp;lt; KeysChanged &amp;lt; RowImages&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;DurableAcrossRestart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;SurvivesDowntime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;FiltersOwnWrites&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;RequiresProvisioning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the consumer declares what it needs:&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="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireAtLeast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChangeDetail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KeysChanged&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// "I need to know WHICH rows"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point that at SQLite and &lt;strong&gt;the application refuses to start&lt;/strong&gt;, with a message naming both&lt;br&gt;
tiers. It does not quietly do half its job for six months until somebody notices the&lt;br&gt;
numbers are wrong.&lt;/p&gt;

&lt;p&gt;That's the whole idea: &lt;strong&gt;leaky and declared beats uniform and wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The abstraction admits it's leaky. In exchange, the leak is a compile-time-ish contract&lt;br&gt;
instead of a production surprise.&lt;/p&gt;
&lt;h3&gt;
  
  
  It caught its own first lie
&lt;/h3&gt;

&lt;p&gt;I originally marked SQLite as &lt;code&gt;FiltersOwnWrites: true&lt;/code&gt;. &lt;code&gt;data_version&lt;/code&gt; genuinely does ignore&lt;br&gt;
commits made on the connection that reads it, so it looked correct.&lt;/p&gt;

&lt;p&gt;It isn't. The feed's connection never writes — your application writes on its &lt;em&gt;own&lt;/em&gt;&lt;br&gt;
connection. So your writes &lt;strong&gt;do&lt;/strong&gt; surface. Declaring &lt;code&gt;true&lt;/code&gt; would have been technically&lt;br&gt;
defensible and practically a lie.&lt;/p&gt;

&lt;p&gt;Writing capabilities down honestly forced me to notice that on the very first provider.&lt;/p&gt;
&lt;h2&gt;
  
  
  Proving it, rather than claiming it
&lt;/h2&gt;

&lt;p&gt;The core asset isn't any provider. It's a &lt;strong&gt;conformance suite&lt;/strong&gt;: one set of tests every&lt;br&gt;
provider inherits and runs &lt;strong&gt;unchanged&lt;/strong&gt;, asserting only what that provider's declared&lt;br&gt;
capabilities promise.&lt;/p&gt;

&lt;p&gt;A provider implements three hooks and adds nothing:&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;protected&lt;/span&gt; &lt;span class="k"&gt;abstract&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;IChangeFeed&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateFeedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;WriteAsForeignApplicationAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// separate connection, no ORM&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;virtual&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;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;IsAvailableAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// skip when no server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule that keeps it honest: &lt;strong&gt;if a provider needs a special case in the suite to pass,&lt;br&gt;
the abstraction is wrong and the abstraction is what changes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It tests in both directions. A provider that claims more than it delivers fails. A provider&lt;br&gt;
that quietly delivers less than it declared also fails. And when a database isn't available,&lt;br&gt;
the tests &lt;strong&gt;skip visibly&lt;/strong&gt; rather than passing vacuously — because a green tick that tested&lt;br&gt;
nothing is exactly how a README ends up claiming four databases and shipping one.&lt;/p&gt;

&lt;p&gt;SQLite polls one integer and can only say "something changed." SQL Server queries change&lt;br&gt;
tables and names individual rows. PostgreSQL holds a replication connection open, is pushed&lt;br&gt;
each transaction as it commits, and reports every column before and after. &lt;strong&gt;All three pass&lt;br&gt;
the identical suite with no exemptions.&lt;/strong&gt; That's the evidence the contract holds.&lt;/p&gt;

&lt;p&gt;The third one is the one that actually tested it. Two polling providers passing the same&lt;br&gt;
tests proves less than it looks like — they have the same shape. A &lt;em&gt;streaming&lt;/em&gt; provider&lt;br&gt;
satisfying the same unmodified contract is what tells you the abstraction wasn't quietly&lt;br&gt;
built around polling.&lt;/p&gt;

&lt;p&gt;Measured detection latency on SQL Server: &lt;strong&gt;262ms against a 250ms poll interval&lt;/strong&gt; — about&lt;br&gt;
12ms of library overhead. There's a permanent test asserting that, so if anyone later adds&lt;br&gt;
per-change work that pushes it into seconds, it fails rather than being noticed by a human&lt;br&gt;
watching a console.&lt;/p&gt;
&lt;h2&gt;
  
  
  The provider that tried to prove me wrong
&lt;/h2&gt;

&lt;p&gt;With two providers, "the abstraction is sound" was really "the abstraction fits two things&lt;br&gt;
that work the same way." Both polled. Both said &lt;em&gt;something changed, go look.&lt;/em&gt; Nothing had&lt;br&gt;
tested the parts of the contract written for a database that pushes.&lt;/p&gt;

&lt;p&gt;PostgreSQL logical replication is the opposite shape in every respect. The server streams&lt;br&gt;
each transaction as it commits. It carries &lt;strong&gt;every column, before and after&lt;/strong&gt; — including&lt;br&gt;
the whole row for a &lt;code&gt;DELETE&lt;/code&gt;, which no polling provider can give you, because by the time&lt;br&gt;
you go and look the row is gone. And a replication slot retains changes on the server while&lt;br&gt;
your app is offline, so downtime survival is real rather than best-effort.&lt;/p&gt;

&lt;p&gt;It passed the conformance suite with no exemptions. That was the result I wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It also broke in four ways I would have shipped.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before writing it, I found a hole in my own test suite. Every detail check was written as&lt;br&gt;
&lt;code&gt;if (detail &amp;lt; tier) assert nothing at that tier&lt;/code&gt;. That catches a provider claiming &lt;em&gt;more&lt;/em&gt;&lt;br&gt;
than it delivers — but a provider claiming the &lt;strong&gt;top&lt;/strong&gt; tier skips every branch and is&lt;br&gt;
therefore asserted against by &lt;em&gt;nothing at all&lt;/em&gt;. A feed declaring &lt;code&gt;RowImages&lt;/code&gt; could have&lt;br&gt;
returned empty rows forever and passed all six tests.&lt;/p&gt;

&lt;p&gt;I added the positive direction. The next three bugs are the ones it caught:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Checkpoint.Now&lt;/code&gt; replayed history.&lt;/strong&gt; Postgres's slot is a backlog by design, so resuming&lt;br&gt;
"at the slot" means resuming at the oldest thing it still holds. A caller asking for &lt;em&gt;now&lt;/em&gt;&lt;br&gt;
got an arbitrary amount of the past. It reads the server's current WAL position instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every value came back as a string.&lt;/strong&gt; pgoutput defaults to text mode, so an integer column&lt;br&gt;
arrived as &lt;code&gt;"10"&lt;/code&gt;. That satisfies every structural assertion — it's non-null, it's the right&lt;br&gt;
column, the test is green — and breaks the first consumer that does arithmetic on it. One&lt;br&gt;
flag fixes it. Nothing but a typed assertion would ever have found it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The primary key was the entire row.&lt;/strong&gt; To get before-images you must set &lt;code&gt;REPLICA IDENTITY&lt;br&gt;
FULL&lt;/code&gt;, and under that setting Postgres marks &lt;em&gt;every&lt;/em&gt; column as part of the replica identity.&lt;br&gt;
So the "changed row's key" contained all of it. On a two-column demo table you'd never&lt;br&gt;
notice; on a real one, code reading &lt;code&gt;Values[0]&lt;/code&gt; gets the right answer by luck.&lt;/p&gt;

&lt;p&gt;None of the three are exotic. All three are the kind that pass review, pass a mocked test,&lt;br&gt;
and surface as someone else's incident months later. They were caught because the suite ran&lt;br&gt;
against a real PostgreSQL rather than a fake — which is the same reason the project's rule&lt;br&gt;
is that a provider isn't listed until it does.&lt;/p&gt;

&lt;p&gt;Two costs worth stating plainly, because I'd rather you read them here than discover them:&lt;br&gt;
&lt;code&gt;REPLICA IDENTITY FULL&lt;/code&gt; writes every column of every &lt;code&gt;UPDATE&lt;/code&gt; into the WAL, which is a real&lt;br&gt;
write-throughput tax on wide tables. And an abandoned replication slot retains WAL&lt;br&gt;
&lt;strong&gt;indefinitely&lt;/strong&gt; and will eventually fill the disk — that's the same mechanism that makes&lt;br&gt;
downtime survivable, pointed the wrong way. One slot per application, and drop it when the&lt;br&gt;
application is retired.&lt;/p&gt;
&lt;h2&gt;
  
  
  Back to the scheduling app
&lt;/h2&gt;

&lt;p&gt;The integration was one new class. It ends at the existing cache-refresh dispatcher, so&lt;br&gt;
every cache, every bound collection and every notification handler downstream stayed&lt;br&gt;
untouched. Behind a config switch, off by default.&lt;/p&gt;

&lt;p&gt;Then the demo: insert a row from SSMS with the switch off — nothing, forever. Click Refresh&lt;br&gt;
— the row appears, proving the data was always there and only the &lt;em&gt;signal&lt;/em&gt; was missing.&lt;br&gt;
Relaunch with the switch on, same insert — the grid updates in about a quarter of a second,&lt;br&gt;
nobody touching anything.&lt;/p&gt;

&lt;p&gt;Every existing test passed unmodified, because nothing was deleted. That mattered more than&lt;br&gt;
the latency: I wasn't asking anyone to approve a rewrite.&lt;/p&gt;

&lt;p&gt;One detail I liked. Change Tracking works on &lt;em&gt;tables&lt;/em&gt;; the app's cache map worked on &lt;em&gt;CLR&lt;br&gt;
types&lt;/em&gt;. I resolved table names through EF metadata rather than guessing from class names —&lt;br&gt;
and five entities would have been silently mis-watched otherwise. One was mapped to a&lt;br&gt;
&lt;strong&gt;singular&lt;/strong&gt; table because it had no &lt;code&gt;DbSet&lt;/code&gt;, so EF fell back to the type name. An unwatched&lt;br&gt;
table produces no error. Just a screen that never updates. Which is the bug I started with.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where it is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/rahibkhan44/DbSignal" rel="noopener noreferrer"&gt;&lt;strong&gt;DbSignal&lt;/strong&gt;&lt;/a&gt; — MIT, on NuGet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package DbSignal.SqlServer     &lt;span class="c"&gt;# or DbSignal.PostgreSql, or DbSignal.Sqlite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SqlServerFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&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;span class="nf"&gt;Watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dbo.Products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&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;batch&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;feed&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;Checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&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;table&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&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="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;key&lt;/span&gt; &lt;span class="k"&gt;in&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;Keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&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;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; row &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;Values&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="s"&gt; in &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;QualifiedName&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;p&gt;On PostgreSQL the same loop gives you the values, not just the keys:&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="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PostgreSqlFeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&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;span class="nf"&gt;Watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"public.products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&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;batch&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;feed&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;Checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&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;table&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batch&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="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;row&lt;/span&gt; &lt;span class="k"&gt;in&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;Rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Kind&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &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;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="s"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Insert:  -&amp;gt; Widget
Update: Widget -&amp;gt; Widget Mk II
Delete: Widget Mk II -&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Three providers, all proven.&lt;/strong&gt; SQL Server, PostgreSQL and SQLite. MySQL is designed and&lt;br&gt;
not written — and it stays off the list until the conformance suite passes against a real&lt;br&gt;
running instance of it.&lt;/p&gt;

&lt;p&gt;Three proven beats four promised. I learned that from someone else's README.&lt;/p&gt;




&lt;h3&gt;
  
  
  Notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Delivery is &lt;strong&gt;at-least-once&lt;/strong&gt;. Handlers must be idempotent. Exactly-once isn't achievable
across these mechanisms and claiming it would be untrue.&lt;/li&gt;
&lt;li&gt;No EF Core dependency anywhere — Dapper and raw ADO work fine.&lt;/li&gt;
&lt;li&gt;One package per provider. Install SQLite support, get a SQLite driver. Nothing else.&lt;/li&gt;
&lt;li&gt;CI runs the suite against real databases on every push: LocalDB on a Windows job,
&lt;code&gt;postgres:17&lt;/code&gt; on a Linux one. Where a database isn't present the tests &lt;strong&gt;skip visibly&lt;/strong&gt;
rather than passing quietly, so a green build never means "we tested nothing."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and it breaks, please open an issue. The one external bug report this has had&lt;br&gt;
so far was worth more than every download counter on the page.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>database</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
