<?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: Prune</title>
    <description>The latest articles on DEV Community by Prune (@y_ustsinovich).</description>
    <link>https://dev.to/y_ustsinovich</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1225947%2Fd7483d9a-5d4e-4cd7-926e-c93794c06a55.webp</url>
      <title>DEV Community: Prune</title>
      <link>https://dev.to/y_ustsinovich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/y_ustsinovich"/>
    <language>en</language>
    <item>
      <title>The .NET Options pattern: a practical field guide</title>
      <dc:creator>Prune</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:26:47 +0000</pubDate>
      <link>https://dev.to/y_ustsinovich/the-net-options-pattern-a-practical-field-guide-35on</link>
      <guid>https://dev.to/y_ustsinovich/the-net-options-pattern-a-practical-field-guide-35on</guid>
      <description>&lt;p&gt;Every ASP.NET Core service binds configuration. Far fewer get the lifetime, the validation, and the reload story right. Here is the whole surface in one page, with the traps marked.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt; = singleton, bound once, no reload, no named options.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; = scoped, rebound per request, reloads, cannot go in a singleton.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt; = singleton, live value + &lt;code&gt;OnChange&lt;/code&gt;, works everywhere.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;ValidateOnStart()&lt;/code&gt; or your validation runs late, inside a request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OnChange&lt;/code&gt; fires multiple times per file save - debounce and dispose it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern in one screen
&lt;/h2&gt;

&lt;p&gt;Bind a section, validate it, and register it - all through &lt;code&gt;OptionsBuilder&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SmtpOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Section&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Smtp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&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;Host&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;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;65535&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;int&lt;/span&gt; &lt;span class="n"&gt;Port&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;init&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="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;UseStartTls&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;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;Timeout&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;init&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;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&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;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="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseStartTls&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"StartTLS on port 25 is almost never right."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole happy path. Everything below is about the decisions this snippet hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three interfaces, one table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;DI lifetime&lt;/th&gt;
&lt;th&gt;Sees reload&lt;/th&gt;
&lt;th&gt;Named options&lt;/th&gt;
&lt;th&gt;Inject into singleton&lt;/th&gt;
&lt;th&gt;Cost per resolve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Singleton&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Bound once, then a field read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scoped&lt;/td&gt;
&lt;td&gt;Yes, per request&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;.Get(name)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Re-binds + re-validates each scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Singleton&lt;/td&gt;
&lt;td&gt;Yes, live&lt;/td&gt;
&lt;td&gt;Yes, &lt;code&gt;.Get(name)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Cached; recomputed only on change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mechanical differences drive every recommendation. &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; re-binds the section and re-runs validation on every scope, so a heavy graph (large collections, decryption, remote lookups in a validator) is paid per request. &lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt; caches in &lt;code&gt;IOptionsMonitorCache&amp;lt;T&amp;gt;&lt;/code&gt; and only rebuilds when a change token fires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing, concretely
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A typed settings object read inside a controller or handler&lt;/strong&gt; → &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt;. One consistent value for the request, reload between requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A singleton client, background service, or anything in &lt;code&gt;Program.cs&lt;/code&gt; wiring&lt;/strong&gt; → &lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt;. It is the only reloadable one you can put there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A value that genuinely never changes at runtime&lt;/strong&gt; (feature-shaped constants, framework glue) → &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt;. Cheapest, and honest about intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Named options
&lt;/h2&gt;

&lt;p&gt;Register the same type more than once under different names, then resolve by name:&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;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="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Smtp:Primary"&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="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"backup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Smtp:Backup"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOptionsMonitor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SmtpOptions&lt;/span&gt; &lt;span class="n"&gt;Primary&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SmtpOptions&lt;/span&gt; &lt;span class="n"&gt;Backup&lt;/span&gt;  &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"backup"&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Trap:&lt;/strong&gt; &lt;code&gt;IOptions&amp;lt;T&amp;gt;.Value&lt;/code&gt; is hard-wired to &lt;code&gt;Options.DefaultName&lt;/code&gt; (the empty string). Inject &lt;code&gt;IOptions&amp;lt;SmtpOptions&amp;gt;&lt;/code&gt; against the setup above and you get an unconfigured object with no error. Named options exist only on &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Validation, done properly
&lt;/h2&gt;

&lt;p&gt;Three layers, roughly in order of power:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.ValidateDataAnnotations()&lt;/code&gt; - attributes on the options type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.Validate(predicate, message)&lt;/code&gt; - quick cross-field rules inline.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IValidateOptions&amp;lt;T&amp;gt;&lt;/code&gt; - a real class when rules get involved or need dependencies.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SmtpOptionsValidator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IValidateOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ValidateOptionsResult&lt;/span&gt; &lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SmtpOptions&lt;/span&gt; &lt;span class="n"&gt;o&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;failures&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&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;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;failures&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;"Smtp:Timeout must be at least 1 second."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;failures&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;"Smtp:Host looks like a dev value."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ValidateOptionsResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidateOptionsResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="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="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IValidateOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;SmtpOptionsValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The line people forget: ValidateOnStart
&lt;/h3&gt;

&lt;p&gt;Without it, none of the above runs until the first read of &lt;code&gt;.Value&lt;/code&gt; / &lt;code&gt;.CurrentValue&lt;/code&gt; / &lt;code&gt;.Get()&lt;/code&gt;. In a web app that is usually mid-request, so a bad deploy looks like a 500 to a user instead of a failed boot in your pipeline.&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;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="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// .NET 6+&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="n"&gt;AddOptionsWithValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateDataAnnotations&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// .NET 8+ shorthand&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Trimming / AOT:&lt;/strong&gt; annotate a partial validator with &lt;code&gt;[OptionsValidator]&lt;/code&gt; (.NET 8+) and the source generator writes the &lt;code&gt;Validate&lt;/code&gt; body for you - no reflection, no &lt;code&gt;ValidateDataAnnotations&lt;/code&gt; runtime cost.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Configure, PostConfigure, and stacking
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;Configure&amp;lt;T&amp;gt;&lt;/code&gt; call for a given name is additive and runs in registration order. &lt;code&gt;PostConfigure&amp;lt;T&amp;gt;&lt;/code&gt; runs after all of them - the place to enforce an invariant or apply an override that must win:&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;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="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Smtp"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// 1&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="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 2&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="n"&gt;PostConfigure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;   &lt;span class="c1"&gt;// last word, regardless of file&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotchas grab-bag
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Captive dependency.&lt;/strong&gt; &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; (scoped) in a singleton constructor throws under the default scope validation. In middleware, put it on the &lt;code&gt;InvokeAsync&lt;/code&gt; parameters; in a hosted service, inject &lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt; and read &lt;code&gt;CurrentValue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OnChange&lt;/code&gt; double-fires.&lt;/strong&gt; One save, several FS events. Debounce (e.g. drop events within ~250 ms) and keep the returned &lt;code&gt;IDisposable&lt;/code&gt; - unregister it in &lt;code&gt;Dispose&lt;/code&gt; so the callback dies with its owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CurrentValue&lt;/code&gt; in a hot loop.&lt;/strong&gt; It is a property that may allocate on rebuild. Read it once into a local at the top of the operation instead of per iteration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bad reload can take everything down.&lt;/strong&gt; If a reloaded value fails validation, &lt;code&gt;CurrentValue&lt;/code&gt; throws &lt;code&gt;OptionsValidationException&lt;/code&gt; on every access until the file is fixed. Reload is not free of blast radius.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes ConfigMaps.&lt;/strong&gt; They mount as a swapped &lt;code&gt;..data&lt;/code&gt; symlink, and the physical-file watcher frequently misses that. Test reload in-cluster; fall back to a rollout restart if it does not fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown keys are silent.&lt;/strong&gt; A misspelled key in &lt;code&gt;appsettings.json&lt;/code&gt; binds to nothing and says nothing. On .NET 8+ pass &lt;code&gt;o =&amp;gt; o.ErrorOnUnknownConfiguration = true&lt;/code&gt; to &lt;code&gt;Bind&lt;/code&gt; to catch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GetSection().Get&amp;lt;T&amp;gt;()&lt;/code&gt; is not the Options pattern.&lt;/strong&gt; It is a one-shot manual bind: no DI, no reload, no validation. Fine for values you need &lt;em&gt;before&lt;/em&gt; the container exists; wrong for everything after.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing options
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IOptions&amp;lt;T&amp;gt;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;SmtpOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"smtp.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;587&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;sut&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// IOptionsMonitor&amp;lt;T&amp;gt; - tiny fake, no package needed&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StaticMonitor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IOptionsMonitor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;CurrentValue&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt; &lt;span class="nf"&gt;OnChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NullDisposable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Default to &lt;code&gt;IOptionsMonitor&amp;lt;T&amp;gt;&lt;/code&gt; in infra code, &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; in request code, &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt; for static settings.&lt;/li&gt;
&lt;li&gt;Always chain &lt;code&gt;ValidateOnStart()&lt;/code&gt; (or &lt;code&gt;AddOptionsWithValidateOnStart&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Never inject &lt;code&gt;IOptionsSnapshot&amp;lt;T&amp;gt;&lt;/code&gt; into a singleton or a middleware constructor.&lt;/li&gt;
&lt;li&gt;Debounce and dispose every &lt;code&gt;OnChange&lt;/code&gt; registration.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;.Get(name)&lt;/code&gt; for named options; &lt;code&gt;IOptions&amp;lt;T&amp;gt;&lt;/code&gt; can't see them.&lt;/li&gt;
&lt;li&gt;Reserve &lt;code&gt;configuration.Get&amp;lt;T&amp;gt;()&lt;/code&gt; for pre-container bootstrap only.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Five registries, one prompt: Polish accounting firm's AML/KYC checks - KRS, CEIDG, CRBR, and sanctions screening - in one prompt.</title>
      <dc:creator>Prune</dc:creator>
      <pubDate>Thu, 27 Aug 2026 20:00:00 +0000</pubDate>
      <link>https://dev.to/y_ustsinovich/five-registries-one-prompt-polish-accounting-firms-amlkyc-checks-krs-ceidg-crbr-and-20kk</link>
      <guid>https://dev.to/y_ustsinovich/five-registries-one-prompt-polish-accounting-firms-amlkyc-checks-krs-ceidg-crbr-and-20kk</guid>
      <description>&lt;p&gt;&lt;em&gt;How an AI assistant can check a client against Poland's company register, VAT list, beneficial-owner registry, and sanctions lists - and hand you a ready-to-file compliance document before your coffee gets cold.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;registries and lists to check by hand for every new client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;~60–90 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;what a thorough manual check of one company usually takes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;prompt to an AI assistant to get a finished risk card&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  An obligation nobody enjoys
&lt;/h2&gt;

&lt;p&gt;Under Polish AML law, an accounting firm is an "obligated institution" - the same legal category as banks and notaries, just a lot smaller - regardless of whether it serves a one-person business or a company with foreign shareholders.&lt;/p&gt;

&lt;p&gt;In practice that means mandatory customer due diligence before you can even start working with a client: identify the entity, establish its beneficial owner, screen it against sanctions and PEP lists, and produce a documented risk assessment.&lt;/p&gt;

&lt;p&gt;Done by hand, that looks like: one tab for the court register (KRS), a second for the VAT payer list, a third for the beneficial-owners registry - which happens to sit behind a CAPTCHA — a fourth for some sanctions list, and a fifth: a blank document where all of it has to be stitched into something coherent. At ten new clients a month, that's realistically a full day of work that is neither bookkeeping nor advisory — it's just copying data between&lt;br&gt;
systems.&lt;/p&gt;

&lt;p&gt;And if the regulator's audit turns up a missing or incomplete file, the Act allows administrative fines of up to &lt;strong&gt;EUR 1,000,000&lt;/strong&gt;. In practice, small firms typically see something closer to &lt;strong&gt;PLN 15,000–25,000&lt;/strong&gt; (roughly EUR 3,500–6,000) - plus something that stings more than the fine itself: the firm's name published in the public register of violations.&lt;/p&gt;
&lt;h2&gt;
  
  
  One conversation instead of five tabs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Ustingit/Ustin.Integrations.AML.Poland.Mcp" rel="noopener noreferrer"&gt;AML Poland MCP&lt;/a&gt; connects an AI assistant — Claude, for instance — directly to the registries you already have to use. Instead of clicking through pages, you just ask.&lt;/p&gt;

&lt;p&gt;Under the hood, it's an implementation of &lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt;, the open standard that lets a language model reach for real data instead of guessing. This particular server gives an assistant four concrete tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;verify_company_basic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registry status (court register or sole-trader register), legal form, address, board members, VAT payer status, and reported bank accounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fetch_crbr_beneficiaries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Beneficial owners from Poland's Central Register of Beneficial Owners — name, citizenship, and the nature of their control.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;screen_sanctions_and_pep&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Screens a person against EU/UN/OFAC sanctions lists and the Polish national list, plus PEP (politically exposed person) status.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate_aml_risk_card&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ties all of the above together, computes the risk level and required procedure (standard or enhanced diligence), and generates the finished document — Markdown or PDF.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a conversation, it looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Check client NIP 7740001454 for AML and generate a risk card.

Checking the court register, the VAT list, the beneficial-owners
registry, and sanctions lists. Here's the summary:

  Name              ORLEN SPÓŁKA AKCYJNA
  Registry status   Active
  VAT status        Active payer
  Beneficial owners manual check required (CAPTCHA)

  Risk level        Medium
  Procedure         Standard
  Document          risk_card.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not a made-up example - it's the exact result the server returns for a real NIP, checked live against the public registries while this tool was being tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens under the hood
&lt;/h2&gt;

&lt;p&gt;No database of its own, no middleman that could go stale. Every query goes straight to the official source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accounting firm
      │
      ▼
AI assistant  (Claude + MCP)
      │
      ▼
KRS / CEIDG · VAT White List · CRBR (UBO) · Sanctions &amp;amp; PEP
      │
      ▼
Risk card  (PDF / Markdown)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four tools reach into five official sources — the Ministry of Justice (the KRS company register), the Ministry of Finance (the VAT White List and the CRBR beneficial-owners registry), Poland's Ministry of Interior (its national sanctions list), and the &lt;a href="https://www.opensanctions.org/" rel="noopener noreferrer"&gt;OpenSanctions&lt;/a&gt; aggregator (EU/UN/OFAC sanctions plus global PEP data) - and a risk engine combines the results into one assessment, following the Act's own thresholds: any sanctions or PEP hit automatically raises the risk to high and triggers enhanced due diligence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this tool does &lt;em&gt;not&lt;/em&gt; pretend to do
&lt;/h2&gt;

&lt;p&gt;Public registries have real limitations - and we'd rather the tool say so plainly than quietly skip a step.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CRBR&lt;/strong&gt; — The public beneficial-owners search is protected by an invisible Google CAPTCHA. This tool &lt;strong&gt;does not attempt to bypass it&lt;/strong&gt; - instead it clearly reports that a given entity needs manual verification, with a link to the official search UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KRS&lt;/strong&gt; — The public company-register API masks board members' personal data (e.g. &lt;code&gt;J*****&lt;/code&gt;). Full identification still has to rest on the representative's ID document anyway
— the tool flags this itself instead of falsely reporting "all clear."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CEIDG&lt;/strong&gt; — The sole-trader register requires its own API token (free after registration). Without it, company checks work normally and the sole-trader check is simply skipped — with a clear note explaining why.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A finished document, not raw data
&lt;/h2&gt;

&lt;p&gt;Every check ends in a &lt;strong&gt;Client AML Risk Assessment Card&lt;/strong&gt; - the exact document a firm has to keep on file anyway, just already written.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown&lt;/strong&gt; - to paste into an internal system, edit further, or feed into your own automation. Plain text, no locked-in format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF&lt;/strong&gt; - to sign, print, and send to the client. Full Polish diacritics render correctly - even inside the bank-account table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The document defaults to Polish, since that's the language a Polish firm's records are kept in, but every tool takes a language parameter - switch it to English with a single word if a client or auditor needs that. The server itself stores nothing: no client database, no query history beyond your own environment. The five-year retention duty stays exactly where it should: with the firm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An independent accounting firm&lt;/strong&gt; - install it once in Claude Desktop or Claude Code, and from then on every new client is one prompt, not thirty minutes of clicking through registries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An accounting-software vendor&lt;/strong&gt; - the code is open (MIT license) and embeddable as an integration inside a platform like Comarch Optima, inFakt, or wFirma - an AML module as a built-in feature, not one more browser tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Five registries. One prompt. The rest is coffee.
&lt;/h2&gt;

&lt;p&gt;The source is fully open on GitHub — browse it, run it locally, or self-host it so client data never leaves your own infrastructure except for calls to the official registries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Ustingit/Ustin.Integrations.AML.Poland.Mcp" rel="noopener noreferrer"&gt;→ View the repository on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MIT License · Python + FastMCP · data from KRS, CEIDG, the Finance Ministry's White List, CRBR, OpenSanctions, and the MSWiA list.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AML Poland MCP is an open tool for accounting firms. It does not replace the judgment of a designated AML/compliance officer.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>poland</category>
    </item>
  </channel>
</rss>
