<?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: David Gates</title>
    <description>The latest articles on DEV Community by David Gates (@dgates82).</description>
    <link>https://dev.to/dgates82</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%2F4030586%2F2d421068-422a-476b-8675-f7b3b4882494.jpg</url>
      <title>DEV Community: David Gates</title>
      <link>https://dev.to/dgates82</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dgates82"/>
    <language>en</language>
    <item>
      <title>A GitHub Template for Shipping NuGet Packages with Trusted Publishing</title>
      <dc:creator>David Gates</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:58:36 +0000</pubDate>
      <link>https://dev.to/dgates82/a-github-template-for-shipping-nuget-packages-with-trusted-publishing-3dpc</link>
      <guid>https://dev.to/dgates82/a-github-template-for-shipping-nuget-packages-with-trusted-publishing-3dpc</guid>
      <description>&lt;p&gt;The second time I set up CI/CD for a NuGet package, I copied the workflows from the first one — and spent an afternoon fixing everything that didn't transfer cleanly. I didn't want every new package to start with the same afternoon of cleanup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dgates82/dotnet-nuget-release-template" rel="noopener noreferrer"&gt;&lt;code&gt;dotnet-nuget-release-template&lt;/code&gt;&lt;/a&gt; is the result: a public GitHub template repo for packing and publishing multi-target .NET NuGet packages. It's MIT licensed, verified end-to-end with a real publish, and has since scaffolded a real package — &lt;a href="https://github.com/dgates82/DGates.Identity.NotificationProviders" rel="noopener noreferrer"&gt;&lt;code&gt;DGates.Identity.NotificationProviders&lt;/code&gt;&lt;/a&gt;, now stable on NuGet.org and consumed in production by &lt;a href="https://github.com/dgates82/angular-dotnet-auth-template" rel="noopener noreferrer"&gt;&lt;code&gt;angular-dotnet-auth-template&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's what's in it and the decisions behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  No API keys anywhere
&lt;/h2&gt;

&lt;p&gt;The release workflow publishes via NuGet's &lt;a href="https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing" rel="noopener noreferrer"&gt;Trusted Publishing&lt;/a&gt;, using &lt;code&gt;NuGet/login@v1&lt;/code&gt;. The workflow requests &lt;code&gt;id-token: write&lt;/code&gt; permission and exchanges a short-lived OIDC token for a NuGet API key at publish time. The only repo secret is &lt;code&gt;NUGET_USER&lt;/code&gt; — your NuGet.org profile name, not a credential.&lt;/p&gt;

&lt;p&gt;That means nothing long-lived is stored in the repo. No key to rotate, no key to leak, no key that quietly expires and breaks your release six months from now. Setup is two steps: register the repo and workflow as a trusted publisher on NuGet.org, add the &lt;code&gt;NUGET_USER&lt;/code&gt; secret. Done.&lt;/p&gt;

&lt;p&gt;The important part of the authentication flow is just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-and-test&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# checkout, .NET setup, and pack steps omitted&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGet login (OIDC)&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGet/login@v1&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NUGET_USER }}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to NuGet.org&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;dotnet nuget push ./artifacts/*.nupkg \&lt;/span&gt;
          &lt;span class="s"&gt;--api-key ${{ steps.login.outputs.NUGET_API_KEY }} \&lt;/span&gt;
          &lt;span class="s"&gt;--source https://api.nuget.org/v3/index.json \&lt;/span&gt;
          &lt;span class="s"&gt;--skip-duplicate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;id-token: write&lt;/code&gt; is what lets the job request the OIDC token in the first place; &lt;code&gt;NuGet/login@v1&lt;/code&gt; trades it for a temporary API key that expires in about an hour — nothing outlives the release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version comes from the tag
&lt;/h2&gt;

&lt;p&gt;Pushing a tag matching &lt;code&gt;v*&lt;/code&gt; (say, &lt;code&gt;v1.2.0&lt;/code&gt;) triggers the release workflow, which packs and publishes with the version taken from the tag itself. Nothing is hardcoded in the &lt;code&gt;.csproj&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This removes an entire class of mistakes — the tag says one thing, the project file says another, and NuGet.org now has a version you didn't intend. The tag is the single source of truth, and the release process is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag v1.2.0 &amp;lt;sha&amp;gt;
git push origin v1.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One release discipline matters here: create the tag first, let CI validate that exact tag, then create the GitHub Release by selecting the tag that already exists. It's the difference between a release tag backed by a successful build and a tag that exists only because someone typed it into a release form.&lt;/p&gt;

&lt;p&gt;One wrinkle worth knowing: the template repo itself needs version tags, but those tags must not trigger the publish workflow — the example library is deliberately disposable and shouldn't land on NuGet.org. The template's own releases use a &lt;code&gt;template-v*&lt;/code&gt; prefix (&lt;code&gt;template-v1.0.0&lt;/code&gt;), which does not match the &lt;code&gt;v*&lt;/code&gt; release trigger. Repos generated from the template use plain &lt;code&gt;v*&lt;/code&gt; tags and publish normally. Small convention, but it's the kind of thing that only bites you once you've already published something by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;TODO(template)&lt;/code&gt; — every edit point is grep-able
&lt;/h2&gt;

&lt;p&gt;Every placeholder and decision point in the repo — package metadata, license copyright, &lt;code&gt;DOTNET_VERSION&lt;/code&gt;, the Mono step, the LocalStack block — is marked with a &lt;code&gt;TODO(template)&lt;/code&gt; comment. Generating a repo and running one grep gives you the complete customization checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"TODO(template)"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
.github/workflows/release.yml:6:  &lt;span class="c"&gt;# TODO(template): update to match your TargetFrameworks.&lt;/span&gt;
.github/workflows/release.yml:16:  &lt;span class="c"&gt;# TODO(template): ExampleLibrary multi-targets net48;net10.0 ...&lt;/span&gt;
.github/workflows/release.yml:24:  &lt;span class="c"&gt;# TODO(template): LocalStack block is required for this&lt;/span&gt;
    template&lt;span class="s1"&gt;'s own example (S3NoteStore). Optional once you replace ExampleLibrary ...
src/ExampleLibrary/ExampleLibrary.csproj:4:  &amp;lt;!-- TODO(template): package metadata below --&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hunting through workflow YAML wondering whether some value is a placeholder or load-bearing — work the list top to bottom and you're done.&lt;/p&gt;

&lt;p&gt;Template repos have a property that libraries don't: the source code &lt;em&gt;is&lt;/em&gt; the pitch. Nobody reads a library's internals before installing it, but everybody reads a template before adopting it, because they're about to own a copy. Unexplained hacks and unmarked placeholders cost more here than anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  An integration testing pattern, not just unit tests
&lt;/h2&gt;

&lt;p&gt;Most CI templates demonstrate unit tests and stop there. Real packages often have dependencies — databases, queues, object storage — and that's where the CI story usually gets hand-wavy, leaving you to figure out the container story yourself.&lt;/p&gt;

&lt;p&gt;The example library ships with two components on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NoteValidator&lt;/code&gt; — pure logic, covered by ordinary unit tests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;S3NoteStore&lt;/code&gt; — talks to S3, covered by integration tests backed by &lt;a href="https://localstack.cloud/" rel="noopener noreferrer"&gt;LocalStack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI runs both. The LocalStack block (&lt;code&gt;docker-compose.yml&lt;/code&gt; plus a seed script) demonstrates the shape of "my package touches an external service and I want CI to actually exercise that" — and once you replace the example library with your own, it's entirely optional. Keep it, point it at a different container (anything Testcontainers supports works the same way), or delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-targeting, including the awkward part
&lt;/h2&gt;

&lt;p&gt;The example library multi-targets &lt;code&gt;net48;net10.0&lt;/code&gt;. The &lt;code&gt;net10.0&lt;/code&gt; half is unremarkable. The &lt;code&gt;net48&lt;/code&gt; half is there because plenty of real-world .NET libraries still need to support .NET Framework — my own &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;&lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt;&lt;/a&gt; targets 4.8 — and running &lt;code&gt;net48&lt;/code&gt; tests on a Linux runner means installing Mono to host the test run.&lt;/p&gt;

&lt;p&gt;That step is clearly marked, and the README says exactly when to delete it: if you don't target a pre-.NET-Core framework, it's dead weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verified the boring way
&lt;/h2&gt;

&lt;p&gt;Before tagging &lt;code&gt;template-v1.0.0&lt;/code&gt;, I ran the whole pipeline for real: generated a repo from the template, pushed a &lt;code&gt;v*&lt;/code&gt; tag, published an actual throwaway package to NuGet.org, confirmed it, and unlisted it. Some publishing failures — especially around packaging or platform-specific behavior — only appear during a real publish, so "it should work" wasn't good enough for a repo whose entire job is publishing.&lt;/p&gt;

&lt;p&gt;Since then, the template has done its actual job. &lt;code&gt;DGates.Identity.NotificationProviders&lt;/code&gt; was scaffolded from it and shipped to a stable &lt;code&gt;1.0.0&lt;/code&gt;, and that package is consumed as a real dependency by a released project — not a demo pipeline, a working one.&lt;/p&gt;

&lt;p&gt;That's the test that mattered: whether the second package could reuse the pipeline without me re-explaining anything the first one had taught me the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;"Use this template"&lt;/strong&gt; on the &lt;a href="https://github.com/dgates82/dotnet-nuget-release-template" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, grep for &lt;code&gt;TODO(template)&lt;/code&gt;, replace the example library, set up Trusted Publishing, push a tag. The README walks through each step, including the repo Ruleset configuration I'd recommend replicating.&lt;/p&gt;

&lt;p&gt;The goal isn't to be the only way to publish NuGet packages. It's to remove the repeated setup work every package author ends up doing. If you hit something the &lt;code&gt;TODO(template)&lt;/code&gt; markers didn't cover, open an issue — that's a gap in the template, and I'd like to close it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>nuget</category>
      <category>githubactions</category>
      <category>opensource</category>
    </item>
    <item>
      <title>DGates.AwsSecretsManager 1.0.0: Stable</title>
      <dc:creator>David Gates</dc:creator>
      <pubDate>Fri, 24 Jul 2026 14:49:10 +0000</pubDate>
      <link>https://dev.to/dgates82/dgatesawssecretsmanager-100-stable-59a7</link>
      <guid>https://dev.to/dgates82/dgatesawssecretsmanager-100-stable-59a7</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://dev.to/dgates82/modern-secrets-for-legacy-net-building-a-typed-cached-aws-secrets-manager-library-2mpp"&gt;building a typed, cached AWS Secrets Manager library for .NET Framework 4.8&lt;/a&gt; — the design decisions, the trade-offs, and proving it in a real ASP.NET MVC 5 application. That post covered the journey. This one is shorter: &lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt; 1.0.0 is now available on NuGet.&lt;/p&gt;

&lt;p&gt;If you haven't read the first post, the one-paragraph version: modern .NET gets typed, cached AWS Secrets Manager access essentially for free. .NET Framework 4.8 applications get the raw SDK and nothing else. This library closes that ergonomics gap — typed retrieval via &lt;code&gt;GetSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt;, TTL-based (time-to-live) caching, Polly-backed retry for transient failures, and a DI-agnostic factory that works just as well in a &lt;code&gt;Global.asax&lt;/code&gt;-era app as in one with a container.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;source&lt;/a&gt;, &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager.Examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt;, and &lt;a href="https://www.nuget.org/packages/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;NuGet package&lt;/a&gt; are all available if you'd like to follow along.&lt;/p&gt;

&lt;p&gt;Two things changed on the way from beta to stable, and both are worth a few words.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured Logging
&lt;/h2&gt;

&lt;p&gt;The beta was silent. You could see what the library did by observing its results, but not why — whether a value came from cache or a live call, when a retry fired, when the local fallback engaged.&lt;/p&gt;

&lt;p&gt;1.0.0 adds logging through &lt;code&gt;Microsoft.Extensions.Logging.Abstractions&lt;/code&gt; — the abstractions package only, deliberately. A library targeting .NET Framework 4.8 has to be careful about what it drags into a consumer's dependency tree, and a full logging framework is exactly the kind of baggage a legacy application doesn't want imposed on it. The abstractions package is a small dependency that dictates nothing about which logging framework — if any — the consuming application uses.&lt;/p&gt;

&lt;p&gt;The wiring is simple: &lt;code&gt;SecretsManagerServiceFactory.Create(...)&lt;/code&gt; takes an optional &lt;code&gt;ILogger&lt;/code&gt;, and defaults to a no-op logger when you don't pass one. Consumers with an existing logging pipeline — NLog, Serilog, log4net via an adapter — can plug in; consumers without one pay nothing and change nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail Fast on Missing Credentials
&lt;/h2&gt;

&lt;p&gt;The beta had a sharp edge: if no AWS credentials were available anywhere, you didn't find out at startup. You found out several calls deep, on the first &lt;code&gt;GetSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt;, as a generic AWS authentication error with no obvious connection to configuration.&lt;/p&gt;

&lt;p&gt;1.0.0 validates the credential chain at &lt;code&gt;SecretsManagerServiceFactory.Create(...)&lt;/code&gt; time. The important detail is &lt;em&gt;how&lt;/em&gt;: rather than checking only the library's own settings surface, it resolves credentials through &lt;code&gt;DefaultAWSCredentialsIdentityResolver&lt;/code&gt; — the same mechanism the v4 AWS SDK uses internally — to determine whether any source in the credential chain resolves successfully: explicit settings, environment variables, &lt;code&gt;appSettings&lt;/code&gt;, an IAM role. If nothing does, you get a clear, descriptive &lt;code&gt;InvalidOperationException&lt;/code&gt; at the one place you'd actually look for a configuration problem. If anything does — including credential sources this library knows nothing about — validation passes, because the SDK's chain is the source of truth, not a reimplementation of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stable Means
&lt;/h2&gt;

&lt;p&gt;From 1.0.0 forward, the library follows semantic versioning. The public API surface — &lt;code&gt;GetSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;GetSecretStringAsync&lt;/code&gt;, &lt;code&gt;RefreshSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;InvalidateCache&lt;/code&gt;, the settings model, and the factory — is committed. Breaking changes mean a major version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Getting started is the same as it was during beta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Install-Package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DGates.AwsSecretsManager&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Startup is one factory call; retrieval is one typed call:&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;secrets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SecretsManagerServiceFactory&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SecretsManagerSettings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"us-west-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CacheTtl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&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;secret&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;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MySecret&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev/MyApp/MySecret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository ships with a LocalStack &lt;code&gt;docker-compose.yml&lt;/code&gt; and a JSON fallback path for Docker-free environments, so you can try everything locally without an AWS account. The &lt;a href="https://dev.to/dgates82/modern-secrets-for-legacy-net-building-a-typed-cached-aws-secrets-manager-library-2mpp"&gt;first post&lt;/a&gt; covers both in detail, and the &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager.Examples" rel="noopener noreferrer"&gt;examples repo&lt;/a&gt; has a full MVC 5 app wired up end to end.&lt;/p&gt;

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

&lt;p&gt;Cache stampede protection, serializer abstraction, and rotation handling are all candidates for the 1.x line, but the &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager/issues" rel="noopener noreferrer"&gt;issue tracker&lt;/a&gt; remains the source of truth for what's actually planned.&lt;/p&gt;

&lt;p&gt;Building this against a real MVC 5 application shaped this release. If you're using it in a legacy codebase, I'd genuinely like to hear what works well — and what doesn't. That kind of feedback will shape where the library goes next.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aws</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Modern Secrets for Legacy .NET: Building a Typed, Cached AWS Secrets Manager Library</title>
      <dc:creator>David Gates</dc:creator>
      <pubDate>Thu, 16 Jul 2026 10:53:27 +0000</pubDate>
      <link>https://dev.to/dgates82/modern-secrets-for-legacy-net-building-a-typed-cached-aws-secrets-manager-library-2mpp</link>
      <guid>https://dev.to/dgates82/modern-secrets-for-legacy-net-building-a-typed-cached-aws-secrets-manager-library-2mpp</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you're building on modern .NET, retrieving a secret from AWS Secrets Manager as a strongly typed, cached configuration value is pretty much turnkey. AWS's supporting NuGet packages handle TTL-based (time-to-live) caching out of the box, and let you bind secrets straight into &lt;code&gt;IConfiguration&lt;/code&gt; with only a few lines of code.&lt;/p&gt;

&lt;p&gt;If you're maintaining a .NET Framework 4.8 application — and there are more of these in production than any of us would like to admit — none of that tooling exists. The AWS SDK for .NET supports .NET Framework 4.8; you can certainly call &lt;code&gt;AmazonSecretsManagerClient.GetSecretValueAsync&lt;/code&gt; from a Framework app today. What doesn't exist is the supporting ecosystem that modern .NET developers take for granted: no typed binding, no caching client, and nothing comparable to &lt;code&gt;IConfiguration&lt;/code&gt;'s abstraction over configuration sources. You're left hand-rolling JSON deserialization, deciding for yourself whether and how to cache secrets, and wiring the rest together by hand.&lt;/p&gt;

&lt;p&gt;A lot of what's still running out there is classic ASP.NET MVC 5 — &lt;code&gt;System.Web.Mvc&lt;/code&gt;, &lt;code&gt;Global.asax&lt;/code&gt;-based startup, with no built-in dependency injection (DI). That's not a niche scenario; it's the reality for many enterprise applications across organizations of every size.&lt;/p&gt;

&lt;p&gt;I ran into this gap firsthand when my organization decided to standardize on AWS Secrets Manager across both modern and legacy .NET applications. The modern apps were straightforward: install the packages, bind to &lt;code&gt;IConfiguration&lt;/code&gt;, and you're done. The first .NET Framework 4.8 application was a different story. It needed the same typed retrieval, caching, and clean integration, but none of that infrastructure existed. Every legacy application in our portfolio would face the same challenge. I built &lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt; to solve it once, rather than repeating the same solution across every legacy application.&lt;/p&gt;

&lt;p&gt;So the goal here wasn't to invent AWS Secrets Manager support for .NET Framework; that already existed. The goal was to close the &lt;em&gt;ergonomics&lt;/em&gt; gap: build the kind of typed, cached, DI-friendly wrapper that modern .NET developers get for free today, in a form that a legacy .NET Framework app — including one with no DI container and no modern configuration pipeline — can actually adopt without a rewrite.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;source&lt;/a&gt;, &lt;a href="https://www.nuget.org/packages/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;NuGet package&lt;/a&gt;, and &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager.Examples" rel="noopener noreferrer"&gt;example applications&lt;/a&gt; are all available if you'd like to follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Decisions &amp;amp; Trade-offs
&lt;/h2&gt;

&lt;p&gt;This is the part of the library that matters — not "I called an AWS SDK method," but the decisions around &lt;em&gt;how&lt;/em&gt; to expose it. Every decision came back to one constraint: the library had to feel natural in a .NET Framework 4.8 application, without requiring that application to become more "modern" to use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typed retrieval, not string wrangling.&lt;/strong&gt; The library's API centers on &lt;code&gt;GetSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt; — deliberately mirroring the typed configuration binding modern .NET developers already expect. Tell it what shape you expect back, and it hands you an object — not a JSON blob you deserialize yourself three call sites later. &lt;code&gt;GetSecretStringAsync&lt;/code&gt; exists too, for the cases where a secret genuinely is just a string and wrapping it in a POCO (plain old CLR object) would be overkill. &lt;code&gt;RefreshSecretAsync&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;InvalidateCache&lt;/code&gt; round out the API — force a refresh when you know a secret rotated, or drop it from cache entirely without waiting for TTL expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching: &lt;code&gt;ConcurrentDictionary&lt;/code&gt;, not &lt;code&gt;IMemoryCache&lt;/code&gt;.&lt;/strong&gt; This one is a deliberate departure from what a lot of people would reach for first. &lt;code&gt;Microsoft.Extensions.Caching.Memory&lt;/code&gt; is the obvious modern choice, but pulling it into a .NET Framework 4.8 library means dragging in a dependency chain built for a different runtime generation, plus the DI registration ceremony that comes with it. That's exactly the kind of friction we want to avoid with this library. A &lt;code&gt;ConcurrentDictionary&lt;/code&gt; keyed by secret name, storing the value alongside its expiry, does everything needed here: thread-safe reads and writes, no extra package, no DI requirement. &lt;code&gt;CacheTtl&lt;/code&gt; is exposed as a &lt;code&gt;TimeSpan&lt;/code&gt; rather than an int, because "how long should this live" is a duration, and forcing a caller to remember whether an int means seconds or milliseconds is the kind of API ambiguity that causes production incidents at 2 a.m.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilience: Polly, scoped narrowly.&lt;/strong&gt; Secrets Manager calls can fail transiently, and retrying blindly can be worse than not retrying at all if you're not careful about &lt;em&gt;what&lt;/em&gt; you retry. Polly 8 wraps the AWS calls specifically for transient failure modes — throttling, temporary network failures, and similar conditions — not as a blanket "wrap everything and hope" policy. Non-transient failures, like invalid credentials or a secret that does not exist, surface immediately instead of being retried into a longer, more confusing failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DI-agnostic by design.&lt;/strong&gt; This decision came directly from the target audience. A modern ASP.NET app has a DI container built in. A classic ASP.NET MVC 5 app, running through &lt;code&gt;Global.asax&lt;/code&gt;, does not. Forcing a specific container (Autofac, Unity, whatever) onto a legacy app just to use this library would be limiting and defeat the purpose. Instead, &lt;code&gt;SecretsManagerServiceFactory&lt;/code&gt; builds a configured &lt;code&gt;SecretsManagerService&lt;/code&gt; directly, and &lt;code&gt;SecretsManagerService&lt;/code&gt; itself is directly instantiable — no container required. Consumers who &lt;em&gt;do&lt;/em&gt; have a DI container can register it as usual; consumers who don't can call the factory once at startup and hold the result in a static accessor. Both are first-class, not one blessed path and one workaround.&lt;/p&gt;

&lt;p&gt;The same restraint applies to authentication. &lt;code&gt;SecretsManagerSettings&lt;/code&gt; accepts an explicit &lt;code&gt;AccessKey&lt;/code&gt; and &lt;code&gt;SecretKey&lt;/code&gt; for the cases that genuinely need them, but when those values are omitted, the library defers to the AWS SDK's standard credential chain — environment variables, an &lt;code&gt;.aws/credentials&lt;/code&gt; profile, or an IAM role. Rather than inventing its own authentication model, the library relies on the one AWS developers already know and expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local development without AWS.&lt;/strong&gt; None of the above matters much if testing it locally means burning AWS API calls against a real Secrets Manager instance every time you hit F5, or requiring an AWS account just to try the library out. The library ships with a &lt;code&gt;docker-compose.yml&lt;/code&gt; that spins up LocalStack and a seed script that populates it with test secrets, so a fresh checkout can run the integration tests against a fully local AWS emulation with a single command: no AWS account, no manual setup. The examples repo follows the same pattern.&lt;/p&gt;

&lt;p&gt;For environments where Docker isn't available — nested VMs without virtualization passthrough are a common setup in practice — there's a &lt;code&gt;LocalJsonFallbackPath&lt;/code&gt; that reads secrets from a local JSON file instead. It's not a mock; it's a genuine fallback path, wired the same way the real client is, so switching between "talk to LocalStack," "talk to real AWS," and "read a local file" is a configuration change, not a code change. (To be clear: this fallback is a local development and testing convenience, not something intended for production use.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving It in a Real App: MvcExample
&lt;/h2&gt;

&lt;p&gt;Could every application implement this itself? Absolutely — the AWS SDK makes that possible, if not particularly pleasant. But once you're standardizing Secrets Manager across dozens of legacy applications, the problem isn't writing another JSON deserializer. It's making sure every application gets the same caching behavior, retry policy, configuration model, and local development experience — instead of a dozen slightly different, independently maintained versions of the same idea.&lt;/p&gt;

&lt;p&gt;A library intended for ASP.NET MVC 5 isn't really proven by a console sample alone. To validate &lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt; in the kind of environment it was actually built for, I created a classic ASP.NET MVC 5 application — &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager.Examples/tree/main/src/MvcExample" rel="noopener noreferrer"&gt;MvcExample&lt;/a&gt; — where a user enters a city, the app retrieves an OpenWeatherMap API key from Secrets Manager, geocodes the city, and fetches live weather. A "how this worked" panel shows the secret name, the retrieved URL, and which backend served the request — real AWS, LocalStack, or the JSON fallback.&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%2F89diyqr6q6tubdy1e5kw.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%2F89diyqr6q6tubdy1e5kw.png" alt="MvcExample showing weather results for a searched city, with the " width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Startup integration is a single factory call — no DI container required:&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;// Global.asax.cs — Application_Start&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SecretsManagerServiceFactory&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SecretsManagerSettings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"us-west-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CacheTtl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;LocalJsonFallbackPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"~/App_Data/secrets.local.json"&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;Retrieval is one typed call — the secret comes back as a POCO, already deserialized, already cached:&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;secret&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;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OpenWeatherMapSecret&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev/MvcExample/OpenWeatherMap"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the ergonomics gap from the start of this post, closed: typed, cached, and wired into a &lt;code&gt;Global.asax&lt;/code&gt;-era app without a rewrite.&lt;/p&gt;

&lt;p&gt;In MvcExample, both calls live behind a small static accessor class: it reads the settings from &lt;code&gt;Web.config&lt;/code&gt; instead of hardcoding them, guards initialization so the service is built exactly once, and wraps retrieval results with the metadata the "how this worked" panel displays. None of that is required by the library itself — it's the app's own glue — but the pattern is worth copying if you want environment-specific configuration without a recompile. See &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager.Examples/blob/main/src/MvcExample.Infrastructure/SecretsManagerAccessor.cs" rel="noopener noreferrer"&gt;SecretsManagerAccessor.cs&lt;/a&gt; for the full version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legacy packaging friction.&lt;/strong&gt; MVC5 projects use &lt;code&gt;packages.config&lt;/code&gt;, and &lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt; brings the AWS SDK's sizeable transitive dependency tree with it — something you do not want to manage through hand-wired &lt;code&gt;HintPath&lt;/code&gt; entries. The long-term solution is to convert the legacy project's packages to &lt;code&gt;PackageReference&lt;/code&gt; and isolate all library-facing code in a small SDK-style project — in this case, &lt;code&gt;MvcExample.Infrastructure&lt;/code&gt;. The web project references that project through &lt;code&gt;ProjectReference&lt;/code&gt;, letting MSBuild resolve the full dependency graph while the legacy project never has to know the AWS SDK exists. As a bonus, the conversion also enables &lt;code&gt;dotnet restore&lt;/code&gt;, making clean command-line and CI builds possible without relying on Visual Studio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two operational lessons.&lt;/strong&gt; First, &lt;code&gt;Application_Start&lt;/code&gt; runs eagerly — so an unreachable Secrets Manager at startup meant a crashed app pool, not an error page. Initialization is now handled defensively, surfacing failures as an in-page message at first real use instead. Second, &lt;code&gt;LocalJsonFallbackPath&lt;/code&gt; isn't automatic — if you want the fallback, it has to be wired explicitly at the consumer's configuration call site. It was missing entirely from MvcExample's initial setup, a reminder that it needs to be verified in every new consumer, not assumed from the README.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the panel doesn't show.&lt;/strong&gt; The "how this worked" panel doesn't display a cache-hit indicator, and that's deliberate. Building one would have meant inferring the library's internal cache state from the outside rather than reporting something real — false precision dressed up as telemetry. Better to show nothing than something that looks like data but isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building &amp;amp; Publishing
&lt;/h2&gt;

&lt;p&gt;A .NET Framework 4.8 library in 2026 raises an immediate CI question: what does the pipeline even run on? GitHub Actions' Windows runners are the obvious answer, but LocalStack ships as a Linux container and isn't supported under Windows containers. The &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;library&lt;/a&gt; is built and tested on &lt;code&gt;ubuntu-latest&lt;/code&gt; instead, using Mono to run the .NET Framework test suite. Linux runners also spin up faster, and keeping the pipeline on one OS keeps it simple — the same environment builds the library, runs the tests against LocalStack, and packs the NuGet package.&lt;/p&gt;

&lt;p&gt;There's one hard limit to know about: this works for &lt;em&gt;building&lt;/em&gt; .NET Framework code, not running classic ASP.NET on Linux. Compiling the MVC 5 example app on &lt;code&gt;ubuntu-latest&lt;/code&gt; works fine; hosting it does not — Mono's &lt;code&gt;xsp4&lt;/code&gt; web host is broken and unmaintained. Build on Linux, run on Windows via IIS Express. That boundary is worth knowing before you design a pipeline around either assumption.&lt;/p&gt;

&lt;p&gt;Two principles shaped the rest of the pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No NuGet API keys, anywhere.&lt;/strong&gt; Publishing uses NuGet's Trusted Publishing — the workflow authenticates via OIDC (OpenID Connect), meaning NuGet.org trusts a short-lived token issued by GitHub Actions for this specific repository and workflow, rather than a long-lived API key stored as a repository secret. There is nothing to leak, rotate, or accidentally commit. For a library whose entire purpose is keeping secrets out of the wrong places, publishing it with a static credential would have been a little too ironic.&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%2Fuqwn5kromw4hm0jnbowm.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%2Fuqwn5kromw4hm0jnbowm.png" alt="DGates.AwsSecretsManager listed on NuGet.org, showing the prerelease version, install command, and download stats" width="799" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration tests run against real (local) infrastructure.&lt;/strong&gt; The same &lt;code&gt;docker-compose.yml&lt;/code&gt; and seed script that power local development spin up LocalStack in CI, so the integration tests exercise genuine Secrets Manager API behavior on every push — not mocks. The local-dev investment from earlier paid for itself here: CI didn't need its own separate test infrastructure, because the local story already was one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Three things stand out looking back at this project — none of them about AWS or Secrets Manager specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local development isn't an afterthought.&lt;/strong&gt; The LocalStack setup and the JSON fallback path took real time to build — time that could have gone toward features. It was worth it twice over. During development, the zero-dependency local story meant every design decision could be exercised immediately, in environments ranging from a full Docker setup to a locked-down VM with nothing on it. Then it paid off again in CI: the same &lt;code&gt;docker-compose.yml&lt;/code&gt; and seed script that power local development became the integration test infrastructure, with no separate CI-only setup to build or maintain. Investment in the development experience compounds; it isn't overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for the audience you actually have.&lt;/strong&gt; It would have been easier to build this with the newest patterns available: a modern DI container, &lt;code&gt;IOptions&lt;/code&gt;-style configuration, the latest caching abstractions. The applications that need this library can't use any of that. Every design decision in this post traces back to accepting that constraint instead of fighting it: the DI-agnostic factory exists because the target apps have no container, converting package management is a much smaller step than rewriting the project around an SDK-style project model, and configuration flows through &lt;code&gt;Web.config&lt;/code&gt; because that's where these apps keep it. Meeting legacy applications where they are is what makes incremental modernization possible at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency scales; convenience doesn't.&lt;/strong&gt; Any individual application could hand-roll its own Secrets Manager integration in an afternoon. Taken once, that's perfectly reasonable. Taken a dozen times across a portfolio, it produces a dozen slightly different caching policies, retry behaviors, and configuration conventions, each independently maintained, each evolving in slightly different directions. A shared library means one implementation to test, one set of behaviors to reason about, and one place to fix a bug that would otherwise be a dozen tickets. The value isn't that it saved anyone from writing a JSON deserializer — it's that nobody has to wonder which version of the pattern any given app is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The surprising part of this project wasn't the implementation itself. It was realizing that the hardest problems weren't making it work — they were making it fit. Legacy applications aren't difficult because they're old; they're difficult because they're valuable enough to keep running while the world around them changes.&lt;/p&gt;

&lt;p&gt;That's ultimately what &lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt; aims to provide: not new capabilities, but modern ergonomics for software that's still doing real work.&lt;/p&gt;

&lt;p&gt;If this library helps even a handful of teams modernize legacy applications incrementally instead of rewriting them all at once, it will have done exactly what it was built to do.&lt;/p&gt;

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

&lt;p&gt;The library is currently in beta. Before a stable 1.0 release, I want to focus on two areas: structured logging and deeper documentation of the design decisions covered here. Beyond that are ideas like cache stampede protection, serializer abstraction, and rotation handling — but roadmaps change, so the &lt;a href="https://github.com/dgates82/DGates.AwsSecretsManager/issues" rel="noopener noreferrer"&gt;issue tracker&lt;/a&gt; remains the source of truth for what's actually planned.&lt;/p&gt;

&lt;p&gt;If you're maintaining .NET Framework applications and this problem looks familiar, I'd love to hear how you're solving it — or whether &lt;a href="https://www.nuget.org/packages/DGates.AwsSecretsManager" rel="noopener noreferrer"&gt;&lt;code&gt;DGates.AwsSecretsManager&lt;/code&gt;&lt;/a&gt; helps. Feedback from real legacy codebases is worth more than any roadmap.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aws</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
