<?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: Mahmood Al Sarraj</title>
    <description>The latest articles on DEV Community by Mahmood Al Sarraj (@mahmood-alsarraj).</description>
    <link>https://dev.to/mahmood-alsarraj</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%2F3946589%2F3bb3060e-112c-486a-9c8f-169c67dc4b9d.png</url>
      <title>DEV Community: Mahmood Al Sarraj</title>
      <link>https://dev.to/mahmood-alsarraj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mahmood-alsarraj"/>
    <language>en</language>
    <item>
      <title>Monitor and Manage Redis Without Ever Leaving Your ASP.NET Core Dashboard</title>
      <dc:creator>Mahmood Al Sarraj</dc:creator>
      <pubDate>Sun, 13 Sep 2026 18:10:06 +0000</pubDate>
      <link>https://dev.to/mahmood-alsarraj/monitor-and-manage-redis-without-ever-leaving-your-aspnet-core-dashboard-ocn</link>
      <guid>https://dev.to/mahmood-alsarraj/monitor-and-manage-redis-without-ever-leaving-your-aspnet-core-dashboard-ocn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd123v6w8efkdau3k5h8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyd123v6w8efkdau3k5h8.png" alt="AsGuard — Redis" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;No RDM. No separate Redis GUI. No&lt;/em&gt; &lt;code&gt;_redis-cli KEYS *_&lt;/code&gt; &lt;em&gt;against production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://medium.com/@mahmood.alsarraj/asguard-observability-dashboard-aspnet-core-cc03ba91b8e0" rel="noopener noreferrer"&gt;last article&lt;/a&gt;, I showed how AsGuard drops a live request and exception dashboard into any ASP.NET Core app with four lines of config. This one covers what happens after someone on your team asks: “can we also see what’s actually in Redis?”&lt;/p&gt;

&lt;p&gt;You already have Redis wired into your app somewhere — session state, a distributed cache, a rate limiter. And you probably already have a terminal tab open with &lt;code&gt;redis-cli&lt;/code&gt;, typing &lt;code&gt;KEYS user:*&lt;/code&gt; and hoping nobody notices the instance hang for a second.&lt;/p&gt;

&lt;p&gt;That command is the problem. &lt;code&gt;KEYS&lt;/code&gt; blocks the entire Redis server for as long as it takes to walk every key. On a small dev instance you won't notice. On a production cache with a few million keys, you just caused an incident.&lt;/p&gt;

&lt;p&gt;AsGuard ships a Redis tab inside the dashboard you already have — and if AsGuard is already in your app for request and exception logging, turning it on costs one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What “Wiring Redis Into AsGuard” Actually Means
&lt;/h2&gt;

&lt;p&gt;AsGuard doesn’t stand up a new service, a new port, or a new login screen. It adds a Redis tab to the dashboard you’re already authenticated into. That tab gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  A key browser that pages through your keyspace with &lt;code&gt;SCAN&lt;/code&gt; — never &lt;code&gt;KEYS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  A value inspector and editor for String, Hash, List, Set, and Sorted Set types&lt;/li&gt;
&lt;li&gt;  TTL viewing and editing&lt;/li&gt;
&lt;li&gt;  Live server stats (memory, ops/sec, hit rate, clients) streamed over the same SSE connection the rest of the dashboard already uses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s opt-in. Every other AsGuard feature defaults to on. Redis management defaults to off, because it’s the one module that touches your application’s actual data instead of just observability data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85kgpekg9oiktxdi4pkp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F85kgpekg9oiktxdi4pkp.png" alt="captionless image" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Turn It On
&lt;/h2&gt;

&lt;p&gt;If AsGuard is already in your app, this is the entire integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddRequestLogging(options =&amp;gt;
{
    options.DatabaseProvider = LoggingDatabaseProvider.Sqlite;
    options.ConnectionString = "Data Source=asguard.db";
    options.DashboardUsername = "ops";
    options.DashboardPassword = builder.Configuration["AsGuard:Password"]!;options.EnableRedisManagement = true;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Redis connection string to type in. No second set of credentials. Which raises the obvious question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Where Does It Get Your Redis Connection From?
&lt;/h2&gt;

&lt;p&gt;AsGuard reuses the connection your app already built. It never asks you to configure Redis twice.&lt;/p&gt;

&lt;p&gt;If you’ve registered a &lt;code&gt;StackExchange.Redis&lt;/code&gt; &lt;code&gt;IConnectionMultiplexer&lt;/code&gt; in DI — which almost every ASP.NET Core app wiring up Redis already does — AsGuard finds it automatically the first time you open the tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Your existing Redis registration — nothing AsGuard-specific here
builder.Services.AddSingleton&amp;lt;IConnectionMultiplexer&amp;gt;(sp =&amp;gt;
    ConnectionMultiplexer.Connect("localhost:6379"));
builder.Services.AddRequestLogging(options =&amp;gt;
{
    options.EnableRedisManagement = true;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discovery runs once, lazily, on first use, and checks several places in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;**IAsGuardRedisConnectionProvider**&lt;/code&gt; &lt;strong&gt;in DI&lt;/strong&gt; — custom connection abstractions AsGuard can't see any other way: token-refreshing wrappers (e.g. Entra ID), multi-tenant multiplexers.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**IConnectionMultiplexer**&lt;/code&gt; &lt;strong&gt;in DI&lt;/strong&gt; — the common case: &lt;code&gt;AddSingleton&amp;lt;IConnectionMultiplexer&amp;gt;&lt;/code&gt;, or Aspire's &lt;code&gt;AddRedisClient&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Keyed&lt;/strong&gt; &lt;code&gt;**IConnectionMultiplexer**&lt;/code&gt; — Aspire's &lt;code&gt;AddKeyedRedisClient("cache")&lt;/code&gt;, or multiple named instances.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**AddStackExchangeRedisCache(...)**&lt;/code&gt; — apps that only wired up &lt;code&gt;IDistributedCache&lt;/code&gt;, nothing lower-level.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;**options.RedisConnections**&lt;/code&gt; — no Redis client registered anywhere; AsGuard builds its own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every rung is additive and fault-isolated: if one source is misconfigured, it doesn’t take the others down with it. AsGuard also never disposes or reconfigures a connection it borrows from rungs 1 through 4 — closing your application’s own multiplexer to satisfy a dashboard tab would be a worse bug than the one you’re trying to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 1 is the escape hatch&lt;/strong&gt;, and it’s the one you actually need if your app doesn’t hold a plain &lt;code&gt;IConnectionMultiplexer&lt;/code&gt; in DI. If you connect to Redis through your own abstraction — a wrapper that lazily authenticates (say, against Azure Cache for Redis via Microsoft Entra ID, where there's a token to refresh instead of a static password) or picks an endpoint per tenant — implement the interface and hand AsGuard the multiplexer once it's built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal sealed class AsGuardRedisConnectionProvider : IAsGuardRedisConnectionProvider
{
    private readonly RedisConnectionProvider _connectionProvider;
    private readonly RedisOptions _options;
    public AsGuardRedisConnectionProvider(RedisConnectionProvider connectionProvider, IOptions&amp;lt;RedisOptions&amp;gt; options)
    {
        _connectionProvider = connectionProvider;
        _options = options.Value;
    }
    public IEnumerable&amp;lt;AsGuardRedisConnection&amp;gt; GetConnections()
    {
        if (!_connectionProvider.IsConfigured)
        {
            return [];
        }
        var multiplexer = _connectionProvider.GetAsync().GetAwaiter().GetResult();
        var name = string.IsNullOrWhiteSpace(_options.KeyPrefix) ? "matensa" : _options.KeyPrefix;
        return [new AsGuardRedisConnection(name, multiplexer)];
    }
}
builder.Services.AddSingleton&amp;lt;IAsGuardRedisConnectionProvider, AsGuardRedisConnectionProvider&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth calling out. First, &lt;code&gt;GetConnections()&lt;/code&gt; is synchronous by contract, but acquiring an Entra token is inherently asynchronous — blocking on &lt;code&gt;GetAwaiter().GetResult()&lt;/code&gt; is fine here specifically because discovery runs lazily, once, on the first dashboard request, and the result is cached for the process lifetime. It never blocks startup and never blocks a real user request. Second, returning an empty sequence when the provider isn't configured is deliberate: AsGuard reports "no connection discovered" instead of the dashboard implying a Redis nobody is actually using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing registered yet?&lt;/strong&gt; Rung 5 covers that — a plain connection string with no DI abstraction at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options.RedisConnections.Add(new RedisConnectionDescriptor
{
    Name = "sessions",
    ConnectionString = "localhost:6379,ssl=False"
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters More Than It Looks: Azure Cache With Entra ID
&lt;/h2&gt;

&lt;p&gt;If your Redis is Azure Cache for Redis authenticated via Microsoft Entra ID, there’s no password to put in a connection string. The multiplexer authenticates with a short-lived access token that your app’s startup code refreshes on a timer, configured directly on a &lt;code&gt;ConfigurationOptions&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;A connection string can’t reconstruct that. Reusing the multiplexer your app already built and already keeps authenticated is the only thing that works — which is exactly why AsGuard checks DI before it ever looks at a connection string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Browse Keys Safely
&lt;/h2&gt;

&lt;p&gt;Open the Redis tab, pick a connection, and page through keys. A few things happen under the hood that you’d otherwise have to build yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Enumeration uses &lt;code&gt;SCAN&lt;/code&gt; with cursor-based paging — never &lt;code&gt;KEYS&lt;/code&gt;. AsGuard even checks the server version at connect time and refuses to enumerate on anything old enough that StackExchange.Redis would silently fall back to &lt;code&gt;KEYS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Type and TTL for each row are pipelined in a single round trip batch, so a 100-key page doesn’t cost you 200+ requests.&lt;/li&gt;
&lt;li&gt;  Binary keys (not valid UTF-8) show up base64-encoded instead of being mangled or hidden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click into any key to inspect and edit it. Strings, hashes, lists, sets, and sorted sets are all supported, with a &lt;code&gt;MATCH&lt;/code&gt; filter for large hashes and sets, and a byte cap so one giant value can't blow up the dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options.MaxRedisValueBytes = 262_144; // default: 256 KB, larger values become read-only
options.RedisScanPageSize = 100;      // keys per SCAN round trip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Locking It Down for Production
&lt;/h2&gt;

&lt;p&gt;Three settings decide how far the Redis tab can reach once it’s live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options.EnableRedisManagement = true;
options.RedisManagementReadOnly = true;   // browse only — no edits, deletes, or flushes
options.AllowRedisFlushDatabase = false;  // FLUSHDB stays off unless you explicitly opt in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RedisManagementReadOnly&lt;/code&gt; is enforced server-side, in the same code path every mutating operation passes through — hiding the edit buttons in the UI is cosmetic on top of that, not the actual gate. And &lt;code&gt;FLUSHDB&lt;/code&gt;, if you ever turn it on, requires the connection to have been built with &lt;code&gt;allowAdmin=true&lt;/code&gt; by your own application code, plus a typed confirmation phrase on the request body naming the exact database. AsGuard never issues &lt;code&gt;FLUSHALL&lt;/code&gt;, and there's no bulk pattern-delete — a mistyped filter against production shouldn't be a one-click mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get for Free
&lt;/h2&gt;

&lt;p&gt;Because the Redis tab lives inside the dashboard you already authenticated into, it also inherits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  CSRF protection on every mutating request&lt;/li&gt;
&lt;li&gt;  Live server stats (memory, ops/sec, hit rate, connected clients, eviction rate) streamed over the dashboard’s existing SSE connection — no second polling loop, no extra background service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stats sampler is demand-driven too. If nobody has the tab open, AsGuard isn’t quietly polling &lt;code&gt;INFO&lt;/code&gt; against your Redis server every few seconds for no one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you already have AsGuard installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;options.EnableRedisManagement = true;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don’t yet:&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 AsGuard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then follow the &lt;a href="https://github.com/mahmood-alsarraj/asguard#-quick-setup" rel="noopener noreferrer"&gt;Quick Setup&lt;/a&gt; in the README, add the line above, and open the Redis tab next to your request logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full reference:&lt;/strong&gt; &lt;a href="https://github.com/mahmood-alsarraj/asguard/blob/main/docs/features/redis-management.md" rel="noopener noreferrer"&gt;docs/redis-management.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mahmood-alsarraj/asguard" rel="noopener noreferrer"&gt;mahmood-alsarraj/asguard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If this saved you from typing&lt;/em&gt; &lt;code&gt;_KEYS *_&lt;/code&gt; &lt;em&gt;in production one more time, give the repo a star.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>dotnet</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Mahmood Al Sarraj</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:25:12 +0000</pubDate>
      <link>https://dev.to/mahmood-alsarraj/-2f6h</link>
      <guid>https://dev.to/mahmood-alsarraj/-2f6h</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/mahmood-alsarraj/stop-letting-bots-register-accounts-add-cloudflare-turnstile-to-your-net-app-34ak" class="crayons-story__hidden-navigation-link"&gt;Stop Letting Bots Register Accounts: Add Cloudflare Turnstile to Your .NET App&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/mahmood-alsarraj" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3946589%2F3bb3060e-112c-486a-9c8f-169c67dc4b9d.png" alt="mahmood-alsarraj profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/mahmood-alsarraj" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mahmood Al Sarraj
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mahmood Al Sarraj
                
              
              &lt;div id="story-author-preview-content-4353054" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/mahmood-alsarraj" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3946589%2F3bb3060e-112c-486a-9c8f-169c67dc4b9d.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mahmood Al Sarraj&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/mahmood-alsarraj/stop-letting-bots-register-accounts-add-cloudflare-turnstile-to-your-net-app-34ak" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/mahmood-alsarraj/stop-letting-bots-register-accounts-add-cloudflare-turnstile-to-your-net-app-34ak" id="article-link-4353054"&gt;
          Stop Letting Bots Register Accounts: Add Cloudflare Turnstile to Your .NET App
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/mahmood-alsarraj/stop-letting-bots-register-accounts-add-cloudflare-turnstile-to-your-net-app-34ak#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Stop Letting Bots Register Accounts: Add Cloudflare Turnstile to Your .NET App</title>
      <dc:creator>Mahmood Al Sarraj</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:23:17 +0000</pubDate>
      <link>https://dev.to/mahmood-alsarraj/stop-letting-bots-register-accounts-add-cloudflare-turnstile-to-your-net-app-34ak</link>
      <guid>https://dev.to/mahmood-alsarraj/stop-letting-bots-register-accounts-add-cloudflare-turnstile-to-your-net-app-34ak</guid>
      <description>&lt;p&gt;Your signup form sends an SMS OTP. Each one costs you a few cents.&lt;/p&gt;

&lt;p&gt;A bot loops &lt;code&gt;POST /api/register&lt;/code&gt; with a rotating IP pool and throwaway numbers, and you pay for every message. Registration is the one endpoint you leave open to strangers — no auth, no API key, no rate limit that survives a fresh IP. It's the only place where someone else can spend your money in a loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Turnstile&lt;/strong&gt; closes it. Free, invisible to most users, and it works even if your site doesn’t sit behind Cloudflare: the widget issues a token, and your backend asks Cloudflare whether that token is real.&lt;/p&gt;

&lt;p&gt;That second half is the whole thing. A token you never verify server-side is decoration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flughjg43j3xh6epfapjc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flughjg43j3xh6epfapjc.webp" alt=".NET with Turnstile" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the widget
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Sign in at &lt;code&gt;dash.cloudflare.com&lt;/code&gt;, open &lt;strong&gt;Turnstile&lt;/strong&gt;, click &lt;strong&gt;Add Widget&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Hostnames:&lt;/strong&gt; your domain, plus &lt;code&gt;localhost&lt;/code&gt; for testing.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Widget mode:&lt;/strong&gt; Managed, it decides per visitor whether to show anything.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create&lt;/strong&gt;, then copy the &lt;strong&gt;Site Key&lt;/strong&gt; and &lt;strong&gt;Secret Key&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The site key is public. The secret key is not, keep it out of &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet user-secrets set "Turnstile:SecretKey" "YOUR_SECRET_KEY"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production it belongs in Key Vault or your platform’s equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the widget to your form
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;form method="post" action="/api/register"&amp;gt;
  &amp;lt;input name="email" type="email" required /&amp;gt;
  &amp;lt;input name="password" type="password" required /&amp;gt;
  &amp;lt;div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-theme="auto"&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;button type="submit"&amp;gt;Create account&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The widget injects a hidden field named &lt;code&gt;cf-turnstile-response&lt;/code&gt;. On a SPA, read it with &lt;code&gt;turnstile.getResponse()&lt;/code&gt; and send it as a header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Verify on the server
&lt;/h2&gt;

&lt;p&gt;One reusable service, not an inline call in your handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private sealed record TurnstileResult
{
    [JsonPropertyName("success")] public bool Success { get; init; }
    [JsonPropertyName("error-codes")] public string[]? ErrorCodes { get; init; }
    [JsonPropertyName("hostname")] public string? Hostname { get; init; }
    [JsonPropertyName("challenge_ts")] public string? ChallengeTs { get; init; }
}
public sealed class TurnstileVerifier(
    HttpClient http,
    IOptions&amp;lt;TurnstileOptions&amp;gt; options,
    ILogger&amp;lt;TurnstileVerifier&amp;gt; logger)
{
    private const string VerifyUrl =
        "https://challenges.cloudflare.com/turnstile/v0/siteverify";
    public async Task&amp;lt;bool&amp;gt; IsHumanAsync(
        string? token, string? remoteIp, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(token))
            return false;
        var payload = new Dictionary&amp;lt;string, string&amp;gt;
        {
            ["secret"] = options.Value.SecretKey,
            ["response"] = token
        };
        if (!string.IsNullOrWhiteSpace(remoteIp))
            payload["remoteip"] = remoteIp;
        using var response = await http.PostAsync(
            VerifyUrl, new FormUrlEncodedContent(payload), ct);
        if (!response.IsSuccessStatusCode)
        {
            // Cloudflare unreachable — fail closed, on purpose.
            logger.LogWarning("Turnstile unavailable: {Status}", response.StatusCode);
            return false;
        }
        var result = await response.Content
            .ReadFromJsonAsync&amp;lt;TurnstileResult&amp;gt;(cancellationToken: ct);
        return result?.Success == true;
    }
}
&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;builder.Services.Configure&amp;lt;TurnstileOptions&amp;gt;(
    builder.Configuration.GetSection("Turnstile"));
builder.Services.AddHttpClient&amp;lt;TurnstileVerifier&amp;gt;(c =&amp;gt;
    c.Timeout = TimeSpan.FromSeconds(5));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Guard the endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public sealed class TurnstileFilter(TurnstileVerifier verifier) : IEndpointFilter
{
    public async ValueTask&amp;lt;object?&amp;gt; InvokeAsync(
        EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
    {
        var req = ctx.HttpContext.Request;
        var token = req.Headers["cf-turnstile-response"].FirstOrDefault()
            ?? (req.HasFormContentType
                ? req.Form["cf-turnstile-response"].FirstOrDefault()
                : null);
        var ip = req.Headers["CF-Connecting-IP"].FirstOrDefault()
            ?? ctx.HttpContext.Connection.RemoteIpAddress?.ToString();
        if (!await verifier.IsHumanAsync(token, ip, ctx.HttpContext.RequestAborted))
            return Results.Problem("Human verification failed",
                statusCode: StatusCodes.Status403Forbidden);
        return await next(ctx);
    }
}
&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;app.MapPost("/api/register", RegisterHandler)
   .AddEndpointFilter&amp;lt;TurnstileFilter&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line per endpoint. On MVC, the same logic fits into an &lt;code&gt;IAsyncActionFilter&lt;/code&gt; behind a &lt;code&gt;[VerifyHuman]&lt;/code&gt; attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that will bite you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tokens are single-use and expire in about five minutes.&lt;/strong&gt; A retry with the same token fails — call &lt;code&gt;turnstile.reset()&lt;/code&gt; on error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick your failure mode deliberately.&lt;/strong&gt; If Cloudflare is unreachable, do you block signups or let them through? Payments product: fail closed. Newsletter: fail open. The wrong answer is not having thought about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turnstile is not a rate limiter.&lt;/strong&gt; It answers “human?”, not “how many times?”. Keep your rate limiting — Turnstile removes the cheap, high-volume noise, which is most of it.&lt;/p&gt;

&lt;p&gt;For CI, Cloudflare publishes dummy keys: site key &lt;code&gt;1x00000000000000000000AA&lt;/code&gt; passes and &lt;code&gt;2x00000000000000000000AB&lt;/code&gt; blocks; secret &lt;code&gt;1x0000000000000000000000000000000AA&lt;/code&gt; passes and &lt;code&gt;2x0000000000000000000000000000000AA&lt;/code&gt; fails. Write the failing test first.&lt;/p&gt;

&lt;p&gt;Registration, password reset, OTP resend — anything a script can call a thousand times at your expense.&lt;/p&gt;

&lt;p&gt;A few minutes of work, and you stop paying for other people’s loops.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Add a Live Observability Dashboard to Your ASP.NET Core App in 5 Minutes — No External Services Needed</title>
      <dc:creator>Mahmood Al Sarraj</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:56:34 +0000</pubDate>
      <link>https://dev.to/mahmood-alsarraj/add-a-live-observability-dashboard-to-your-aspnet-core-app-in-5-minutes-no-external-services-4k6p</link>
      <guid>https://dev.to/mahmood-alsarraj/add-a-live-observability-dashboard-to-your-aspnet-core-app-in-5-minutes-no-external-services-4k6p</guid>
      <description>&lt;h2&gt;
  
  
  No Grafana. No Datadog. No YAML. Just visibility.
&lt;/h2&gt;

&lt;p&gt;You've shipped your ASP.NET Core API. It's live. And then your product manager pings you: "Something seems slow - can you check?"&lt;/p&gt;

&lt;p&gt;You open the logs. Wall of text. No correlation IDs. No timing. No idea which request threw that exception at 2 AM.&lt;br&gt;
Sound familiar?&lt;/p&gt;

&lt;p&gt;That's the problem AsGuard solves.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Is&amp;nbsp;AsGuard?
&lt;/h2&gt;

&lt;p&gt;AsGuard is a lightweight NuGet package that plugs directly into your ASP.NET Core middleware pipeline and gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full HTTP request/response logging&lt;/strong&gt; with timing, method, status, and body capture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exception tracking&lt;/strong&gt; with stack traces, severity, and trends - no Sentry account required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Host ILogger capture&lt;/strong&gt; - your existing LogWarning and LogError calls get stored and searchable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A built-in live dashboard&lt;/strong&gt; with dark/light mode, SSE-powered real-time updates, and filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire thing runs inside your app. No external services. No Docker compose file with six containers. No licensing tiers. Just a NuGet package and four lines of configuration.&lt;/p&gt;


&lt;h2&gt;
  
  
  Getting It Running (For Real, Under 5&amp;nbsp;Minutes)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - Install the&amp;nbsp;package&lt;/strong&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="n"&gt;dotnet&lt;/span&gt; &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;AsGuard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 - Configure in Program.cs&lt;/strong&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;AsGuard.Extensions&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;AsGuard.Domain.RequestLogging&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="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;AddRequestLogging&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="n"&gt;DatabaseProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LoggingDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sqlite&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="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Data Source=asguard.db"&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="n"&gt;DashboardRoute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/logs"&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="n"&gt;DashboardUsername&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="s"&gt;"AsGuard:Username"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"admin"&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="n"&gt;DashboardPassword&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="s"&gt;"AsGuard:Password"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"change-me"&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="n"&gt;EnableExceptionLogging&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CaptureHostLogs&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogResponseBody&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogRequestBody&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 - Register the middleware&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The placement matters. Add &lt;code&gt;UseRequestLogging()&lt;/code&gt; after HTTPS/CORS but before auth and your endpoints:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttpsRedirection&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;UseExceptionHandler&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;UseRequestLogging&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ← right here&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;UseAuthentication&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;UseAuthorization&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;MapControllers&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 - Hit the dashboard&lt;/strong&gt;&lt;br&gt;
Run your app, open a browser, and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://localhost:&amp;lt;port&amp;gt;/logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxygozh19krtfka66vg5x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxygozh19krtfka66vg5x.png" alt="AsGuard main dashboard" width="799" height="623"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter your credentials and you'll see a live dashboard with every request your app has handled. That's it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Middleware Order Actually&amp;nbsp;Matters
&lt;/h2&gt;

&lt;p&gt;One gotcha worth knowing upfront: where you place &lt;code&gt;UseRequestLogging()&lt;/code&gt; in the pipeline determines what gets captured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HTTP Request In]
       │
       ▼
┌──────────────────────┐
│ Exception Handler    │  ← register first so AsGuard can intercept exceptions
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│ HTTPS / CORS / HSTS  │
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  UseRequestLogging() │  ← HERE: correlation IDs generated, body stream opened
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Auth &amp;amp; Authorization│  ← 401s and 403s will still be logged
└──────────┬───────────┘
           │
           ▼
┌──────────────────────┐
│  Your Endpoints      │
└──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Placing it after auth means you lose logs for unauthorized requests. Placing it before exception handlers means unhandled exceptions won't have their HTTP context correctly linked. The order shown above gets you everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing a&amp;nbsp;Database
&lt;/h2&gt;

&lt;p&gt;AsGuard supports four providers out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite -&lt;/strong&gt; zero setup, file-based; perfect for local dev, small apps, and quick demos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server -&lt;/strong&gt; the natural fit for production on Azure or Windows-hosted workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL -&lt;/strong&gt; ideal for Linux environments and cloud-native stacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory -&lt;/strong&gt; no persistence, no config; great for integration tests and ephemeral environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Switching is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// PostgreSQL&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DatabaseProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LoggingDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PostgreSql&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="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Host=localhost;Database=asguard;Username=postgres;Password=pass"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// In-Memory (no persistence, great for tests)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DatabaseProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LoggingDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InMemory&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="n"&gt;MaxInMemoryEntries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No migrations to run manually. AsGuard auto-creates its tables on startup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Masking Sensitive Data
&lt;/h2&gt;

&lt;p&gt;If you capture request bodies (and you should, for debugging), you don't want passwords showing up in your log viewer. AsGuard gives you two ways to handle this.&lt;br&gt;
&lt;strong&gt;Per-property attribute:&lt;/strong&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginRequest&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;Username&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="n"&gt;AsGuardMasked&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;Password&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="c1"&gt;// stored as "[REDACTED]"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Global key list in config:&lt;/strong&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SensitiveBodyKeys&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;"creditCardNumber"&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="n"&gt;SensitiveBodyKeys&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;"ssn"&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="n"&gt;SensitiveBodyKeys&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;"token"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs at the middleware level before anything hits the database - you're never storing what you shouldn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You'll Actually Use It&amp;nbsp;For
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Debugging slow requests in development
&lt;/h3&gt;

&lt;p&gt;The request log view shows method, path, status code, and duration for every call. You can filter by status, search by path, and click into any row to see the full request/response bodies, headers, and the correlation ID chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracking down production exceptions
&lt;/h3&gt;

&lt;p&gt;Exception logging captures the full stack trace, the HTTP context it occurred in, the severity, and when it happened. The dashboard includes trend charts so you can see if your error rate just spiked after a deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capturing ILogger&amp;nbsp;output
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;CaptureHostLogs = true&lt;/code&gt;, any &lt;code&gt;LogWarning&lt;/code&gt; or &lt;code&gt;LogError&lt;/code&gt; your existing services emit gets stored alongside the HTTP logs. You don't need to change a single line of your existing logging code - AsGuard just intercepts the host &lt;code&gt;ILogger&lt;/code&gt; pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  APM for EF Core and HttpClient
&lt;/h3&gt;

&lt;p&gt;AsGuard auto-instruments EF Core queries and outbound &lt;code&gt;HttpClient&lt;/code&gt; calls, showing them as Gantt-style trace timelines per request. If one endpoint is slow because it's firing 12 database queries, you'll see exactly which ones and how long each took.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwg1wzpz5tvgivj1lxey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvwg1wzpz5tvgivj1lxey.png" alt="AsGuard AMP, Http Calls, EF queries" width="728" height="835"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Seeing what's live, right&amp;nbsp;now
&lt;/h3&gt;

&lt;p&gt;The dashboard uses Server-Sent Events for real-time updates - no page refresh, no polling interval to configure. While you're load testing or manually poking an endpoint, the request log updates in front of you.&lt;/p&gt;




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

&lt;p&gt;A few things to know before deploying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Change the default credentials.&lt;/strong&gt; AsGuard actively rejects the literal string "admin" as a username. Store credentials in environment variables or user secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# environment variable approach&lt;/span&gt;
&lt;span class="s"&gt;ASGUARD__USERNAME=your_secure_user&lt;/span&gt;
&lt;span class="s"&gt;ASGUARD__PASSWORD=your_secure_password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The write path is non-blocking.&lt;/strong&gt; AsGuard uses a queue-based architecture - logs are written to an in-process channel and flushed asynchronously. Your API response times are not affected by database write latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up retention policies.&lt;/strong&gt; For high-traffic apps, configure auto-cleanup so the log table doesn't grow unbounded:&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetentionDays&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;SQLite&lt;/strong&gt; is fine for low-to-medium traffic. For anything handling hundreds of requests per second in production, switch to PostgreSQL or SQL Server.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bigger&amp;nbsp;Picture
&lt;/h3&gt;

&lt;p&gt;Most&amp;nbsp;.NET teams reach for Sentry for exceptions, Datadog or Azure Monitor for metrics, and a custom Kibana setup for logs. That's three services, three SDKs, three dashboards, and three monthly bills.&lt;/p&gt;

&lt;p&gt;AsGuard doesn't replace that stack for large-scale production systems. But for the majority of apps - internal tools, SaaS products under 100k requests/day, staging environments, side projects - it gives you 80% of the observability value at 0% of the infrastructure overhead.&lt;/p&gt;

&lt;p&gt;Install it in your existing app today and you'll have a searchable, live view of every request and exception by the time your coffee's ready.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mahmood-alsarraj/asguard" rel="noopener noreferrer"&gt;mahmood-alsarraj/asguard&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;a href="https://www.nuget.org/packages/AsGuard/" rel="noopener noreferrer"&gt;nuget/AsGuard&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this saved you a debugging session, give the repo a star ⭐ - it helps.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>datadog</category>
      <category>observability</category>
      <category>grafana</category>
    </item>
  </channel>
</rss>
