<?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: Erik-Jan Westendorp</title>
    <description>The latest articles on DEV Community by Erik-Jan Westendorp (@erikjanwestendorp).</description>
    <link>https://dev.to/erikjanwestendorp</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%2F733628%2Fb5c1a9c8-09fd-45fa-8172-ab196e6e8f76.jpeg</url>
      <title>DEV Community: Erik-Jan Westendorp</title>
      <link>https://dev.to/erikjanwestendorp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erikjanwestendorp"/>
    <language>en</language>
    <item>
      <title>Building a Cloudflare Integration for Umbraco Automate</title>
      <dc:creator>Erik-Jan Westendorp</dc:creator>
      <pubDate>Sun, 23 Aug 2026 11:37:09 +0000</pubDate>
      <link>https://dev.to/erikjanwestendorp/building-a-cloudflare-integration-for-umbraco-automate-3f8m</link>
      <guid>https://dev.to/erikjanwestendorp/building-a-cloudflare-integration-for-umbraco-automate-3f8m</guid>
      <description>&lt;p&gt;When I first started experimenting with Umbraco Automate, I wanted to find a real-world use case instead of creating an integration just for the sake of trying the API. It didn't take long to find one. On several Umbraco projects, we use Cloudflare in front of the website. When content changes, there are situations where we want to invalidate the cached version of the affected page.&lt;/p&gt;

&lt;p&gt;Traditionally, I would solve this in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content published
      ↓
Notification handler
      ↓
Determine published URLs
      ↓
Call Cloudflare API
      ↓
Purge cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works. But after looking at Automate, I started wondering:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if the application doesn't need to know anything about Cloudflare at all?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, publishing content could simply trigger an automation, with small reusable actions taking care of the individual steps.&lt;/p&gt;

&lt;p&gt;That idea eventually resulted in three NuGet packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Umbraco.Community.Automate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Umbraco.Community.Automate.Extensions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Umbraco.Community.Automate.Cloudflare&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All currently available as version &lt;code&gt;17.0.0&lt;/code&gt; for Umbraco 17.&lt;/p&gt;

&lt;p&gt;This post describes how I got there, some of the problems I encountered, and what I learned about extending Umbraco Automate along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Starting with the obvious solution
&lt;/h2&gt;

&lt;p&gt;My first idea was simple. Umbraco Automate already provides a &lt;strong&gt;Content Published&lt;/strong&gt; trigger, so I wanted to create a Cloudflare action that could take the published page and purge it from Cloudflare.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Published
      ↓
Purge Cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Cloudflare API itself makes this fairly straightforward.To purge specific URLs you can send a request to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /client/v4/zones/{zoneId}/purge_cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.example.com/some-page/"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So initially I thought the Cloudflare action could simply accept the URL of the published page.&lt;/p&gt;

&lt;p&gt;The Content Published trigger doesn't provide a URL.&lt;/p&gt;

&lt;p&gt;It exposes information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contentKey
contentName
contentTypeKey
contentTypeAlias
cultures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And actually, that makes sense. A URL isn't necessarily a property of the publish event.&lt;/p&gt;




&lt;h1&gt;
  
  
  A content item doesn't necessarily have one URL
&lt;/h1&gt;

&lt;p&gt;This became the first interesting part of the implementation.&lt;/p&gt;

&lt;p&gt;Consider a multilingual Umbraco website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.example.com/
https://www.example.com/en/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now add multiple hostnames, which is perfectly valid in an Umbraco installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.example-a.com/
https://www.example-a.com/en/

https://www.example-b.com/
https://www.example-b.com/en/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a single content item, asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What is the URL?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;is therefore not always the right question.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;What are the published URLs for this content item and culture?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction ended up having quite a large influence on the package design.&lt;/p&gt;




&lt;h1&gt;
  
  
  Separating Umbraco from Cloudflare
&lt;/h1&gt;

&lt;p&gt;My first instinct was to put the URL resolution inside the Cloudflare action.&lt;/p&gt;

&lt;p&gt;The action could receive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Key
Culture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and internally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolve the Umbraco content.&lt;/li&gt;
&lt;li&gt;Determine its URLs.&lt;/li&gt;
&lt;li&gt;Call Cloudflare.&lt;/li&gt;
&lt;li&gt;Purge those URLs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But that didn't feel right.&lt;/p&gt;

&lt;p&gt;Resolving published URLs has nothing to do with Cloudflare.&lt;/p&gt;

&lt;p&gt;The same functionality could be useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;notifying an external API;&lt;/li&gt;
&lt;li&gt;updating a search index;&lt;/li&gt;
&lt;li&gt;sending a webhook;&lt;/li&gt;
&lt;li&gt;invalidating another CDN;&lt;/li&gt;
&lt;li&gt;generating a sitemap;&lt;/li&gt;
&lt;li&gt;or building another Automate workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead I created a generic action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Published Content URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and moved it into a separate package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Umbraco.Community.Automate.Extensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare became its own integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Umbraco.Community.Automate.Cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows Automate to do what it is good at: composing small pieces of functionality.&lt;/p&gt;

&lt;p&gt;The workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Published
      ↓
For Each Culture
      ↓
Get Published Content URLs
      ↓
Purge Cloudflare URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation is probably the design decision I'm happiest with.&lt;/p&gt;




&lt;h1&gt;
  
  
  Building Get Published Content URLs
&lt;/h1&gt;

&lt;p&gt;The generic action receives a content key and culture.&lt;/p&gt;

&lt;p&gt;Its settings are roughly:&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;GetPublishedContentUrlsSettings&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;Guid&lt;/span&gt; &lt;span class="n"&gt;ContentKey&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Culture&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output deliberately contains an array:&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;GetPublishedContentUrlsOutput&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;Guid&lt;/span&gt; &lt;span class="n"&gt;ContentKey&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="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Culture&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Urls&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="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that it's &lt;code&gt;Urls&lt;/code&gt;, not &lt;code&gt;Url&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is important for installations where a culture is available through multiple domains.&lt;/p&gt;

&lt;p&gt;Conceptually the action does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Key + Culture
          ↓
 Umbraco published content
          ↓
 Primary + alternative URLs
          ↓
       string[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output can then be consumed by any other Automate action.&lt;/p&gt;




&lt;h1&gt;
  
  
  Lesson learned: be careful with service lifetimes
&lt;/h1&gt;

&lt;p&gt;While building the action I ran into another useful lesson.&lt;/p&gt;

&lt;p&gt;My first implementation injected &lt;code&gt;IPublishedContentQuery&lt;/code&gt; directly into the action.&lt;/p&gt;

&lt;p&gt;That resulted in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot consume scoped service
'Umbraco.Cms.Core.IPublishedContentQuery'
from singleton
'GetPublishedContentUrlsAction'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automate actions are registered as singletons, while several Umbraco services used for published content are scoped.&lt;/p&gt;

&lt;p&gt;Injecting one directly into a singleton therefore isn't safe.&lt;/p&gt;

&lt;p&gt;Instead, the action needs to work with the appropriate Umbraco context/service scope when resolving published content.&lt;/p&gt;

&lt;p&gt;It's an easy mistake to make when building your first Automate extension because the action itself looks very similar to a normal service.&lt;/p&gt;

&lt;p&gt;The important takeaway:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always consider the lifetime of the services you inject into an Automate action.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Cultures introduced another interesting problem
&lt;/h1&gt;

&lt;p&gt;The Content Published trigger exposes &lt;code&gt;cultures&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not &lt;code&gt;culture&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means a single publish operation can involve multiple cultures.&lt;/p&gt;

&lt;p&gt;The obvious workflow is therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Published
      ↓
For Each
      ↓
Get Published Content URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collection for the loop is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${ trigger.cultures }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And inside the loop the current culture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${ loop.item }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially I accidentally passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${ trigger.cultures }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;directly into the action's &lt;code&gt;Culture&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;That resulted in something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;["es"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;being passed as the culture instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;es
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and consequently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content '&amp;lt;guid&amp;gt;' is not published for culture '["es"]'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For Each:
${ trigger.cultures }

Get Published Content URLs:

Content Key:
${ trigger.contentKey }

Culture:
${ loop.item }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small mistake, but a good example of why understanding the difference between a collection binding and the current loop item matters.&lt;/p&gt;




&lt;h1&gt;
  
  
  Then I wanted to purge multiple URLs at once
&lt;/h1&gt;

&lt;p&gt;Initially the Cloudflare package had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Purge Cloudflare URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with:&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Url&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked perfectly when combined with another &lt;code&gt;For Each&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;Get Published Content URLs
      ↓
For Each URL
      ↓
Purge Cloudflare URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Cloudflare already accepts multiple URLs in one request.&lt;/p&gt;

&lt;p&gt;So why make one HTTP request per URL?&lt;/p&gt;

&lt;p&gt;I changed the action to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Purge Cloudflare URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the Cloudflare client now accepts a collection:&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;Task&lt;/span&gt; &lt;span class="nf"&gt;PurgeUrlsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;apiToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;zoneId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting request can contain all URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.example-a.com/en/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"https://www.example-b.com/en/"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the workflow much nicer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Published Content URLs
      ↓
Purge Cloudflare URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At least, that was the idea.&lt;/p&gt;

&lt;p&gt;And then I learned something interesting about Automate bindings.&lt;/p&gt;




&lt;h1&gt;
  
  
  Binding an array isn't the same as binding a string
&lt;/h1&gt;

&lt;p&gt;My first settings model looked perfectly reasonable:&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;PurgeUrlsSettings&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="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Urls&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="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The previous action already returned:&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;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Urls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so I expected this binding to work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${ previous.urls }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead Automate failed before the action was even executed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to resolve model 'community.cloudflare.purgeUrls'
to type PurgeUrlsSettings.

The JSON value could not be converted to System.String[].
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was confusing at first because the source value really was a &lt;code&gt;string[]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important detail is how action settings are stored.&lt;/p&gt;

&lt;p&gt;The binding itself is initially represented as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${ previous.urls }"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a string expression.&lt;/p&gt;

&lt;p&gt;If the property is already declared as &lt;code&gt;string[]&lt;/code&gt;, JSON deserialization needs to happen before that expression can resolve to its collection value.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"${ previous.urls }"
        ↓
Deserialize as string[]
        ↓
💥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Looking at For Each provided the answer
&lt;/h1&gt;

&lt;p&gt;The interesting thing was that this already worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Published Content URLs
      ↓
For Each

Collection:
${ previous.urls }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So Automate clearly &lt;em&gt;could&lt;/em&gt; consume my array.&lt;/p&gt;

&lt;p&gt;That led me into the Automate source code.&lt;/p&gt;

&lt;p&gt;The built-in &lt;code&gt;For Each&lt;/code&gt; doesn't define its collection setting as an array.&lt;/p&gt;

&lt;p&gt;Instead, conceptually it does this:&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setting contains the &lt;strong&gt;binding expression&lt;/strong&gt;, not the resolved collection.&lt;/p&gt;

&lt;p&gt;The expression is evaluated later at runtime.&lt;/p&gt;

&lt;p&gt;That same approach works nicely for the Cloudflare action.&lt;/p&gt;

&lt;p&gt;The setting can remain:&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;PurgeUrlsSettings&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;Urls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the configured value remains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${ previous.urls }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, Automate's &lt;code&gt;BindingEvaluator&lt;/code&gt; can evaluate the raw expression:&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="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_bindingEvaluator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EvaluateRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BindingData&lt;/span&gt;
        &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action can then turn that resolved value into the collection it needs.&lt;/p&gt;

&lt;p&gt;This was probably the most interesting technical lesson from building the package.&lt;/p&gt;

&lt;p&gt;Sometimes the type of a &lt;strong&gt;configuration property&lt;/strong&gt; shouldn't represent the type of the eventual runtime value.&lt;/p&gt;

&lt;p&gt;In this case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Setting
string expression
      ↓
BindingEvaluator
      ↓
runtime collection
      ↓
Cloudflare API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is a better model.&lt;/p&gt;




&lt;h1&gt;
  
  
  Normalizing the URLs
&lt;/h1&gt;

&lt;p&gt;Before sending anything to Cloudflare, the action normalizes the collection:&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;urls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it verifies that something remains:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failed&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"At least one URL is required."&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;and validates the URLs:&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;invalidUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&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;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UriKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Absolute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&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;invalidUrl&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failed&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;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invalidUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' is not a valid absolute URL."&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;Only after that does the action call Cloudflare.&lt;/p&gt;




&lt;h1&gt;
  
  
  Creating a reusable Cloudflare connection
&lt;/h1&gt;

&lt;p&gt;I didn't want API tokens and zone IDs to be configured on every action.&lt;/p&gt;

&lt;p&gt;Automate has a connection concept for exactly this purpose, so the Cloudflare package provides its own connection type.&lt;/p&gt;

&lt;p&gt;The connection contains information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account ID
Zone ID
API Token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API token can also be validated against Cloudflare.&lt;/p&gt;

&lt;p&gt;Cloudflare provides an endpoint for checking whether a token is valid and active, which makes the connection test much more useful than simply checking whether a value was entered.&lt;/p&gt;

&lt;p&gt;A successful response looks roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives users feedback while configuring the connection instead of waiting for their first automation run to fail.&lt;/p&gt;




&lt;h1&gt;
  
  
  Keep API tokens scoped
&lt;/h1&gt;

&lt;p&gt;For the Cloudflare API token, I recommend granting only the permissions required by the workflow.&lt;/p&gt;

&lt;p&gt;For cache purging, don't use a token with unnecessary account-wide permissions.&lt;/p&gt;

&lt;p&gt;The connection should know:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Token
Zone ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the action should only perform the operation it was designed for.&lt;/p&gt;

&lt;p&gt;It's a small detail, but integrations like these make security boundaries very visible.&lt;/p&gt;




&lt;h1&gt;
  
  
  The resulting packages
&lt;/h1&gt;

&lt;p&gt;The experiment eventually became three packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Umbraco.Community.Automate
&lt;/h2&gt;

&lt;p&gt;The convenience package.&lt;/p&gt;

&lt;p&gt;Installing it brings in the complete collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Umbraco.Community.Automate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Umbraco.Community.Automate.Extensions
&lt;/h2&gt;

&lt;p&gt;Generic Automate building blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Umbraco.Community.Automate.Extensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Currently including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get Published Content URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Umbraco.Community.Automate.Cloudflare
&lt;/h2&gt;

&lt;p&gt;The Cloudflare integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Umbraco.Community.Automate.Cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Currently providing the Cloudflare connection and cache purge functionality.&lt;/p&gt;

&lt;p&gt;The packages are versioned alongside their supported Umbraco major version, so the first stable release is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for Umbraco 17.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I like this approach
&lt;/h1&gt;

&lt;p&gt;The part I like most isn't actually the Cloudflare API integration.&lt;/p&gt;

&lt;p&gt;Calling an HTTP API isn't particularly complicated.&lt;/p&gt;

&lt;p&gt;The interesting part is the separation of responsibilities.&lt;/p&gt;

&lt;p&gt;Instead of writing:&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;ContentPublishedHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Find content&lt;/span&gt;
    &lt;span class="c1"&gt;// Determine cultures&lt;/span&gt;
    &lt;span class="c1"&gt;// Determine domains&lt;/span&gt;
    &lt;span class="c1"&gt;// Resolve URLs&lt;/span&gt;
    &lt;span class="c1"&gt;// Read Cloudflare configuration&lt;/span&gt;
    &lt;span class="c1"&gt;// Call Cloudflare&lt;/span&gt;
    &lt;span class="c1"&gt;// Handle failures&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we can create reusable building blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Published
      ↓
For Each Culture
      ↓
Get Published Content URLs
      ↓
Purge Cloudflare URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And tomorrow somebody could build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Published
      ↓
Get Published Content URLs
      ↓
Notify external service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Content Published
      ↓
Get Published Content URLs
      ↓
Update search index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without changing &lt;code&gt;Get Published Content URLs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's where I think Automate becomes really interesting.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I learned
&lt;/h1&gt;

&lt;p&gt;A few things stood out while building these packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Actions should do one thing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Get Published Content URLs&lt;/code&gt; shouldn't know Cloudflare exists.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;Purge Cloudflare URLs&lt;/code&gt; shouldn't need to know anything about Umbraco content.&lt;/p&gt;

&lt;p&gt;The workflow connects them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model collections explicitly
&lt;/h3&gt;

&lt;p&gt;A published content item can have multiple cultures, and one culture can have multiple URLs.&lt;/p&gt;

&lt;p&gt;Designing around collections from the start avoids assumptions that only work for simple websites.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bindings are runtime values
&lt;/h3&gt;

&lt;p&gt;A binding expression such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${ previous.urls }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is stored as a string but may resolve to something completely different at runtime.&lt;/p&gt;

&lt;p&gt;Looking at how Automate's own &lt;code&gt;For Each&lt;/code&gt; implementation handles this was extremely useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch your DI lifetimes
&lt;/h3&gt;

&lt;p&gt;Automate actions and Umbraco scoped services don't necessarily share the same lifetime.&lt;/p&gt;

&lt;p&gt;Be careful when injecting services such as published-content APIs directly into actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composition beats coupling
&lt;/h3&gt;

&lt;p&gt;The Cloudflare use case resulted in a generic action that turned out to be useful completely independently of Cloudflare.&lt;/p&gt;

&lt;p&gt;That's a good sign that the responsibility belongs in its own extension.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's next?
&lt;/h1&gt;

&lt;p&gt;There are plenty of directions this could go.&lt;/p&gt;

&lt;p&gt;Cloudflare supports more than purging individual URLs, so possible future actions include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Purge by cache tag
Purge by prefix
Purge by hostname
Purge everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I don't want one giant "Cloudflare action" with every possible option.&lt;/p&gt;

&lt;p&gt;I'd rather keep each operation explicit and let Automate compose them.&lt;/p&gt;

&lt;p&gt;The generic Extensions package also opens the door to other reusable Umbraco actions that aren't tied to a specific integration.&lt;/p&gt;

&lt;p&gt;And that's probably what I find most interesting about Automate: once you start thinking in small triggers, actions and outputs, you quickly start seeing automation opportunities everywhere.&lt;/p&gt;




&lt;h1&gt;
  
  
  Try it
&lt;/h1&gt;

&lt;p&gt;The packages are available on NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Umbraco.Community.Automate
Umbraco.Community.Automate.Extensions
Umbraco.Community.Automate.Cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source code, examples and issues are available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/erikjanwestendorp/Umbraco.Community.Automate" rel="noopener noreferrer"&gt;https://github.com/erikjanwestendorp/Umbraco.Community.Automate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is open source, so feedback, ideas, issues and pull requests are very welcome.&lt;/p&gt;

&lt;p&gt;If you're experimenting with Umbraco Automate as well, I'd love to hear what kind of integrations or reusable actions you're building.&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>umbracoautomate</category>
      <category>dotnet</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>Handling Azure Key Vault Secret Naming with Umbraco UIBuilder License Configuration</title>
      <dc:creator>Erik-Jan Westendorp</dc:creator>
      <pubDate>Wed, 19 Mar 2025 08:58:39 +0000</pubDate>
      <link>https://dev.to/erikjanwestendorp/handling-azure-key-vault-secret-naming-with-umbraco-uibuilder-license-configuration-ega</link>
      <guid>https://dev.to/erikjanwestendorp/handling-azure-key-vault-secret-naming-with-umbraco-uibuilder-license-configuration-ega</guid>
      <description>&lt;h2&gt;
  
  
  The Issue: Key Vault Naming Constraints
&lt;/h2&gt;

&lt;p&gt;When configuring the Umbraco UIBuilder license using Azure Key Vault, you might encounter a frustrating issue: Key Vault secret names cannot contain dots ("."), while Umbraco UIBuilder uses dots to structure its configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Umbraco"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Licenses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Products"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Umbraco.UIBuilder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_LICENSE_KEY"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, Azure Key Vault enforces strict naming rules, allowing only &lt;strong&gt;alphanumeric&lt;/strong&gt; characters and &lt;strong&gt;dashes&lt;/strong&gt; ("-") in secret names. Additionally, the &lt;code&gt;Azure.Extensions.AspNetCore.Configuration.Secrets&lt;/code&gt; package automatically converts double dashes (--) to colons (:), making it even trickier when trying to map hierarchical configurations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this a problem?
&lt;/h2&gt;

&lt;p&gt;Since the Umbraco UIBuilder package uses dot-separated keys in its configuration, using &lt;code&gt;Umbraco.UIBuilder&lt;/code&gt; as a secret name isn't possible in Key Vault. Instead, we need a way to transform secret names from Key Vault into a structure that fits the expected configuration format for Umbraco UIBuilder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Implementing a Custom KeyVaultSecretManager
&lt;/h2&gt;

&lt;p&gt;To resolve this issue, we can implement a custom KeyVaultSecretManager to control how secret names are mapped from Azure Key Vault to the expected configuration structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Store the Secret in Key Vault&lt;br&gt;
Since dots are not allowed, we store the secret in Key Vault using a different naming convention, for example:&lt;/p&gt;

&lt;p&gt;Instead of &lt;code&gt;Umbraco--Licenses--Products--Umbraco.UIBuilder&lt;/code&gt;, store it as:&lt;br&gt;
&lt;code&gt;Umbraco--Licenses--Products--Umbraco-UIBuilder&lt;/code&gt; (using a single dash - instead of a dot .).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Create a Custom KeyVaultSecretManager&lt;br&gt;
Now, we need to create a custom &lt;code&gt;KeyVaultSecretManager&lt;/code&gt; that replaces dashes (-) with dots (.) when reading the Umbraco UIBuilder secret from Azure Key Vault.&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;Azure.Extensions.AspNetCore.Configuration.Secrets&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;Azure.Security.KeyVault.Secrets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;My.Namespace&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;class&lt;/span&gt; &lt;span class="nc"&gt;CustomSecretManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KeyVaultSecretManager&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;override&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KeyVaultSecret&lt;/span&gt; &lt;span class="n"&gt;secret&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;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetKey&lt;/span&gt;&lt;span class="p"&gt;(&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Umbraco:Licenses:Products:Umbraco-UIBuilder"&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Register the Custom KeyVaultSecretManager&lt;br&gt;
To integrate Key Vault with your Umbraco application, follow the official &lt;a href="https://docs.umbraco.com/umbraco-cms/extending/key-vault" rel="noopener noreferrer"&gt;Umbraco documentation&lt;/a&gt;. However, you’ll need to modify the &lt;code&gt;ConfigureKeyVault&lt;/code&gt; method to use the &lt;code&gt;CustomKeyVaultSecretManager&lt;/code&gt; instead of the default configuration.&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;Azure.Identity&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;Azure.Security.KeyVault.Secrets&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;Azure.Extensions.AspNetCore.Configuration.Secrets&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;Microsoft.Extensions.Configuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;My.Namespace&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebApplicationBuilderExtensions&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;static&lt;/span&gt; &lt;span class="n"&gt;WebApplicationBuilder&lt;/span&gt; &lt;span class="nf"&gt;ConfigureKeyVault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;WebApplicationBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&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;keyVaultEndpoint&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;"AzureKeyVaultEndpoint"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyVaultEndpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyVaultEndpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UriKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Absolute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;validUri&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAzureKeyVault&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;SecretClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validUri&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;DefaultAzureCredential&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;CustomKeyVaultSecretManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Use the custom manager&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workaround allows us to stay aligned with Umbraco's recommended approach while bypassing Key Vault's naming restrictions, ensuring Umbraco UIBuilder can still retrieve its configuration correctly&lt;/p&gt;

</description>
      <category>umbraco</category>
      <category>uibuilder</category>
      <category>keyvault</category>
      <category>configuration</category>
    </item>
  </channel>
</rss>
