<?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: Matthew Thomas</title>
    <description>The latest articles on DEV Community by Matthew Thomas (@mthomas564).</description>
    <link>https://dev.to/mthomas564</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%2F3277849%2F580c37aa-d242-4d65-a031-031938286ec6.jpeg</url>
      <title>DEV Community: Matthew Thomas</title>
      <link>https://dev.to/mthomas564</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mthomas564"/>
    <language>en</language>
    <item>
      <title>PHP2Shell and the Case for Public Sites That Don't Run Code</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:55:57 +0000</pubDate>
      <link>https://dev.to/mthomas564/php2shell-and-the-case-for-public-sites-that-dont-run-code-318j</link>
      <guid>https://dev.to/mthomas564/php2shell-and-the-case-for-public-sites-that-dont-run-code-318j</guid>
      <description>&lt;p&gt;Last Wednesday, government agencies and companies around the world took websites offline rather than leave them exposed. The cause was PHP2Shell, a pair of critical bugs in PHP itself, and it's a good moment to ask a question I think more people should be asking: why is a public website running code at all?&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The two vulnerabilities, CVE-2026-109618 and CVE-2026-109619, both scored 9.2 out of 10. Both can lead to an unauthenticated attacker running code on a server that handles PHP requests. PHP shipped fixes on 21 October (8.5.12, 8.4.27, 8.3.36 and 8.2.35), after details leaked ahead of the planned disclosure. Amazon then told defenders to look for &lt;a href="https://techcrunch.com/2026/10/24/php-ocalypse-how-two-critical-web-bugs-almost-broke-the-internet/" rel="noopener noreferrer"&gt;exploitation as early as 17 October&lt;/a&gt;, four days before the patches existed.&lt;/p&gt;

&lt;p&gt;The picture has been mixed. On 22 October, Sansec said mass exploitation looked less likely than first feared, because the public exploits didn't work against a default PHP install and needed specific knowledge of the target. Since then, TechCrunch has reported that Trail of Bits had working exploits by late Friday and that Cloudflare's CEO has seen a huge number of exploit attempts, though there were still no signs of mass exploitation as of 24 October. "Not mass compromise yet" isn't a comfortable place to be when the bug is in the language runtime itself. The UK's NCSC &lt;a href="https://www.ncsc.gov.uk/news/critical-security-issue-affecting-php" rel="noopener noreferrer"&gt;called it critical&lt;/a&gt;, said all PHP versions are affected, and told organisations that can't patch or mitigate quickly to consider taking the system offline until they can.&lt;/p&gt;

&lt;p&gt;That's the part that stuck with me. Normally the advice is to keep your plugins updated and use strong passwords. Here, &lt;a href="https://securitylabs.datadoghq.com/articles/php2shell-critical-vulnerabilities-in-php/" rel="noopener noreferrer"&gt;Datadog's analysis&lt;/a&gt; found application-level authentication can't block an attack, and the config workarounds that were circulating don't fully protect you. You could have a perfectly maintained site and still be exposed until your host patched. And if you're on an older branch, there may not be an official fix at all: PHP's own project has no fixed release for 8.1 or older, only source patches for 7.4, 8.0 and 8.1, so you're relying on a distribution or vendor backport, or an upgrade you may not have planned for.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't really a PHP problem
&lt;/h2&gt;

&lt;p&gt;I don't want this to turn into PHP bashing. Log4Shell was Java. The PHP-CGI bug from 2024 was a different flaw in the same ecosystem. Every language has its bad week.&lt;/p&gt;

&lt;p&gt;The real issue is the model. On a traditional dynamic site, every single page view runs code on a server you're responsible for. A request comes in, the server executes something, queries a database, and builds the page. That's attack surface on every visit, and it's there even when the site is a brochure that changes twice a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a static site does differently
&lt;/h2&gt;

&lt;p&gt;A static site builds its pages in advance. A visitor asks for a page and gets a file from a CDN. There's no PHP, no database and no admin login sitting on the public domain.&lt;/p&gt;

&lt;p&gt;So when a bug like PHP2Shell lands, there's nothing for it to land on. No runtime is processing requests, so there's nothing to inject code into. &lt;em&gt;Does that mean it's secure?&lt;/em&gt; Not by magic, but the whole category of "attacker makes the server run something" mostly goes away.&lt;/p&gt;

&lt;p&gt;This is where a headless CMS fits in nicely. Editors write content in a separate tool, and when someone publishes, the site is rebuilt and deployed. I'd reach for Sanity, but the idea doesn't depend on it. Contentful, Storyblok or a headless WordPress would all work the same way. What matters is that the editing side and the public side are separate. With a build like that, the thing visitors hit isn't connected to the CMS or a database at all, so there's no path from the public site back to anything valuable.&lt;/p&gt;

&lt;p&gt;A build isn't the only way to use a headless CMS though. With Sanity, for example, querying the API directly is just as standard, and its docs recommend the cached, read-only API CDN for fetching content from a browser. That's still a very different shape from PHP2Shell. The page itself is still static files, and the request for content goes to a read-only endpoint on a managed service rather than a runtime on a server you look after, so there's much less there to attack. The trade-off is a dependency at request time, and the uncached API is the one that always gives you the freshest content, so the cached one trades a little freshness for speed. And if you fetch content from a server-rendered app instead of the browser, you're running a server again, which puts it back on your patch list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it bulletproof? No
&lt;/h2&gt;

&lt;p&gt;Static sites aren't immune, and I'd be doing you a disservice if I said they were.&lt;/p&gt;

&lt;p&gt;The build pipeline still runs Node and a pile of npm dependencies, so supply chain risk is real. Anything dynamic, like a contact form or search, needs a service somewhere, and that service is code. The hosting platform runs software too.&lt;/p&gt;

&lt;p&gt;Headless WordPress is the awkward one here. It still runs PHP, so PHP2Shell applies to the WordPress side exactly as it does to any other site, and it still needs patching. What you gain is that it's no longer the public face. If the site is built statically, only your editors and the build need to reach it, so it can sit behind a login or an IP allowlist instead of being open to every visitor and every scanner. That's a real improvement, but it isn't the same as not running PHP at all.&lt;/p&gt;

&lt;p&gt;The difference is where the risk sits and who deals with it. The build runs on your terms, isn't exposed to the internet, and can be locked down. The hosting layer is patched by a team whose full-time job is exactly that. And the part you do own is small enough to actually reason about.&lt;/p&gt;

&lt;p&gt;There's also an honest "when this is overkill" in reverse: if you're building something with user logins, user-generated content or a big shop, you need a dynamic backend, and that's fine. Most business websites aren't that. They're a handful of pages, a blog and a form, and they've been running a full server-side stack out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;Patching quickly is still the right call when something like this drops, and anyone running PHP should have done it by now. But I'd rather run a site that doesn't need an urgent patch than one that's very good at getting them.&lt;/p&gt;

&lt;p&gt;If your public site only needs to show content, it shouldn't have to execute anything to do it.&lt;/p&gt;

</description>
      <category>security</category>
      <category>staticsites</category>
      <category>php</category>
      <category>hosting</category>
    </item>
    <item>
      <title>Moving CI to GitHub but Keeping CD in Azure DevOps</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:02:07 +0000</pubDate>
      <link>https://dev.to/mthomas564/moving-ci-to-github-but-keeping-cd-in-azure-devops-1m0h</link>
      <guid>https://dev.to/mthomas564/moving-ci-to-github-but-keeping-cd-in-azure-devops-1m0h</guid>
      <description>&lt;p&gt;Microsoft's new &lt;a href="https://devblogs.microsoft.com/devops/enterprise-live-migrations-public-preview/" rel="noopener noreferrer"&gt;Enterprise Live Migrations (ELM)&lt;/a&gt; tool (currently in public preview) makes moving repos from Azure DevOps (ADO) to GitHub Enterprise (GHE) easy, and it can even rewire your existing pipelines, as long as they stay in ADO. But is that the way to go? Do your pipelines work how you want them to after the move?&lt;br&gt;
In our case, no. We wanted to standardise on GHE's repo features, so we went hybrid: CI in GHE, CD in ADO. A hybrid approach was suggested to us by the GitHub team and is well known in the community, but everyone draws the line differently.&lt;br&gt;
What most of the online guidance doesn't cover is data residency. ELM only supports GHE.com, and there's no UK region, which changes what a hybrid setup looks like for UK regulated environments.&lt;/p&gt;

&lt;p&gt;This is my take on a potential implementation, with the decisions that we made and the trade-offs we had to accept. This isn't meant to be a one size fits all solution, but it may help you think about your own migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's moving to GitHub Actions
&lt;/h2&gt;

&lt;p&gt;We chose to move our CI over to GitHub Actions for a few reasons, primarily to standardise our CI and to get closer to the code.&lt;/p&gt;

&lt;p&gt;Putting it closer to the code makes it easier for developers to see the results of their changes, without having to jump between platforms.&lt;br&gt;
It also makes it easier to manage the CI builds across our projects. Reusable workflows are built for this. They mean that we can have a simple CI.yaml in each repo, which in turn calls out to a shared workflow with the actual build steps.&lt;br&gt;
This simplifies it for developers as they don't need to worry about the details of the workflow. It also makes it easier from a platform perspective, we can enforce certain steps centrally, and making changes is done in one place and affects all repos.&lt;/p&gt;

&lt;p&gt;Make sure you version your shared workflows. We have chosen to start with a v1 tag. While we are iterating on the current workflows, we move that tag to the latest commit to make changes, and create a minor version tag as well to track changes.&lt;br&gt;
Our CI builds reference the v1 tag, so they will always use the latest version of the shared workflow. When we make a breaking change, we will create a v2 tag, and update the CI builds to reference that instead.&lt;br&gt;
Doing it this way allows us to update them all at once, but have control for when we need to make a breaking change which can then be rolled out incrementally.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why not deploy from GitHub too?
&lt;/h2&gt;

&lt;p&gt;Ideally you want to have your CI and CD in the same place, but there are some constraints that affected us and our ability to do that.&lt;br&gt;
The primary one, data residency. We have our own &amp;lt;name&amp;gt;.ghe.com instance, which is hosted in Europe as that is the closest region available.&lt;br&gt;
Our resources are all in UK Azure regions, and we want to be able to deploy privately. GitHub Enterprise has VNet integration for runners, simple right? Wrong, UK South and UK West are not &lt;a href="https://docs.github.com/en/enterprise-cloud@latest/admin/data-residency/network-details-for-ghecom#supported-regions-for-azure-private-networking" rel="noopener noreferrer"&gt;supported regions&lt;/a&gt; for VNet integration, so we cannot deploy privately from GitHub Actions.&lt;/p&gt;

&lt;p&gt;We could instead use the self-hosted runners and deploy them inside our network boundary, but that would mean we would have to manage the runners ourselves, and we would lose the benefits of using managed runners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Azure DevOps and Managed DevOps Pools?
&lt;/h2&gt;

&lt;p&gt;Firstly, why ADO? We already use it, and we've made the decision that we will keep it for boards and use the GitHub -&amp;gt; Azure Boards integration.&lt;br&gt;
ADO Boards are a much more mature product, especially for teams and organisations that want strict work planning processes.&lt;/p&gt;

&lt;p&gt;But the big reason to keep CD in ADO is that we can use &lt;a href="https://learn.microsoft.com/en-us/azure/devops/managed-devops-pools/architecture-overview" rel="noopener noreferrer"&gt;Managed DevOps Pools (MDPs)&lt;/a&gt;. MDPs are a resource that you deploy into your own Azure subscription, and the agents are injected into your network through a delegated subnet. They are a managed set of runners that can be used by any ADO pipeline. MDPs give us the private network without the maintenance.&lt;/p&gt;

&lt;p&gt;Why not ADO self-hosted agents? Well, the reason is the same as on the GitHub side, self-hosted vs managed. They are a great option, but you have to patch, scale and look after them yourself, and that's the work MDPs take off our plate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the hand-off works
&lt;/h2&gt;

&lt;p&gt;The workflow that we settled on is to do CI and create release artifacts in GHE, and then do the deployment stage in ADO.&lt;br&gt;
The hand-off between the two is done through an immutable release in GHE. The release is created by a CD workflow in GHE, and publishes the artifacts to GHE releases. We then trigger our ADO pipeline to pull this release and use it for deployment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famqde67en8g79hbr0ird.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famqde67en8g79hbr0ird.png" alt="ADO-GHE Flow" width="800" height="968"&gt;&lt;/a&gt;&lt;br&gt;
This is a simplified example rather than our full end to end process, but it shows the key hand-offs between the two systems.&lt;/p&gt;

&lt;p&gt;To get from GHE to ADO, we use the &lt;a href="https://github.com/Azure/pipelines" rel="noopener noreferrer"&gt;Azure/pipelines&lt;/a&gt; action, which triggers an Azure Pipelines run straight from a GitHub Actions workflow. It's as simple as giving it your project URL and pipeline name, but you will need a PAT for it to authenticate. I'll come back to that in the trade-offs section.&lt;br&gt;
Once triggered, our ADO pipeline uses the GHE API to pull the release artifacts. For ease, I then publish those artifacts on the ADO side as a pipeline artifact, this way they are there for easy reuse, especially between environments.&lt;br&gt;
A note on the ADO side: the built-in task for downloading a GitHub release doesn't support a GHE.com service connection, so I had to fall back to a custom script that calls the GHE API directly. It works, but it's code we now own and maintain, and it needs a credential passed in. Not ideal, but at the time of writing it was the only option I found. If the built-in task gains support for GHE.com, I'd switch back in a heartbeat.&lt;/p&gt;

&lt;p&gt;Now, there are a few things to consider when it comes to the hand-off between the two systems. The first is making sure that the release is immutable. GHE releases support this, but it isn't on by default, so you need to enable release immutability, either per repo or as an org-wide policy. It only applies to releases created after you turn it on.&lt;/p&gt;

&lt;p&gt;Secondly, what is the trigger point, not the technical how, the business why. Some of this will depend on your stakeholders and the business requirement. Equally, you must make sure that the right build gets to the right environments, for example you may choose to push to dev every time a PR is completed into main so that the development team can test it. But then when do you push onto test for stakeholders to see it?&lt;br&gt;
Technically there are a few ways to handle the outcome of this decision, firstly it can be workflow dispatch (a manual trigger), no automation and no help for the development teams. Next, you can use tags, the team create a tag on a commit and this is then the GHE action trigger for the process to start. Finally, you can use a release branch, either long or short-lived, and merging into this branch is the start of the process. The important thing is to make sure that the right people are involved in the decision and that it is documented.&lt;br&gt;
There are advantages and disadvantages to each of these approaches, and it is important to make sure these are all considered and that everyone is aligned before you make the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the immutable release matters
&lt;/h2&gt;

&lt;p&gt;Immutability in releases is a primary concern when designing any release process, and something I've talked about in &lt;a href="https://matthewthomas.cloud/posts/immutable-deployments/" rel="noopener noreferrer"&gt;my post on immutable deployments&lt;/a&gt;.&lt;br&gt;
The key point is that you want to always be able to be sure of what was built is what is deployed, and that if you roll back you have the exact same artifact to redeploy. This is especially important when you have a hand-off between two systems, as it is easy for the wrong artifact to be deployed if you don't have a clear contract between the two systems.&lt;/p&gt;

&lt;p&gt;In our implementation we know that what ADO is pulling is exactly what GHE built and published, and therefore the state it was in, tests passing etc, is the state that will be deployed.&lt;br&gt;
If we did not approach it with this design, then ADO would be rebuilding the code and creating a new artifact, which could be different to what was built in GHE. How different is a very open-ended question, and it could be as simple as a dependency being updated, or a test being fixed, or a build step being changed. The point is that you don't know what has changed, and therefore you don't know what is being deployed. This is a risk that we are not willing to take.&lt;/p&gt;

&lt;p&gt;When it comes to traceability, especially in an incident, we can track back from our production deployment to the exact release and commit that was deployed. This is a key requirement for us, and hopefully everyone doing this! You need to be sure of what was deployed so you can track what changed and why.&lt;br&gt;
A great help on this, compared to our previous position, is the built-in release note generation in GitHub releases. It automatically generates the release notes based on the gap between the two commits and any PRs that have been merged. This helps for operations but also is a great help for stakeholder management.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs
&lt;/h2&gt;

&lt;p&gt;With every solution, there are always some trade-offs and this is no different.&lt;br&gt;
The main trade-off is that we have two systems to manage, however in our position, this would have been the case whether we chose to split deployment or not due to the use of Azure Boards.&lt;br&gt;
As part of the two systems, there are two sets of auth to manage. Especially with these kinds of hybrid setups as all of the auth between the systems is done through PATs, which are a pain to manage.&lt;br&gt;
PATs expire, have to be stored somewhere safe, and are normally tied to the user who created them. To avoid pinning them to a single person, we are choosing to use a service account with shared ownership, so nothing breaks when someone moves on. We have an ADO PAT stored in GHE so it can trigger the pipeline, and a GHE PAT stored in ADO so it can pull the release from the GHE API. That's still two long-lived secrets to scope tightly and rotate before they expire, which matters a lot in a regulated environment.&lt;br&gt;
You also have teams having to look in two systems to see the state of an application. In my opinion, this leads really heavily into a discussion of who owns what parts of the process. Personally I think that the CI process should be a developer-owned process, and the CD process should be an operations-owned process. This doesn't mean that as the DevOps side we can't implement CI, creating reusable workflows and templates for example, but the outcome of that action is a developer problem.&lt;br&gt;
Having said that, traceability is better with immutable releases, but it does mean that you have to manage the release history in two systems. This is a trade-off that we are willing to make, as we feel that the benefits of having immutable releases outweigh the cost of managing the release history in two systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this changes
&lt;/h2&gt;

&lt;p&gt;This solution probably isn't the end-game for us, and anyone else in this situation.&lt;br&gt;
If, and hopefully when, GitHub's private networking reaches UK regions, we will 100% be looking to collapse our deployments back into GitHub Actions and have one platform.&lt;/p&gt;

&lt;p&gt;When this does hopefully happen, the move should be small and can be staged, primarily because of the enforcement of immutable releases. The contract between our build and deployment is that artifact, so as long as that remains we can repoint either side of the process.&lt;/p&gt;

&lt;p&gt;Personally the migration from ADO to GHE has been a great experience, it's still on-going and is going to be for a while. I'm learning a ton and solving some really interesting challenges, both technical and business.&lt;/p&gt;

</description>
      <category>githubenterprise</category>
      <category>azuredevops</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Write Access for Everyone, CODEOWNERS for Real Control</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/write-access-for-everyone-codeowners-for-real-control-5b8</link>
      <guid>https://dev.to/mthomas564/write-access-for-everyone-codeowners-for-real-control-5b8</guid>
      <description>&lt;p&gt;How do you stop the wrong people editing files they shouldn't be touching? Traditionally I've leant on a couple of things: limiting project access and branch protection rules. Both are good tools, and both are things I've used plenty in Azure DevOps, and even in TFS back in the day! But now moving to GitHub Enterprise that approach falls over, not because the concepts don't exist but because doing it that way would mean a huge admin overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why restricting write access is the wrong fix
&lt;/h2&gt;

&lt;p&gt;The instinct is that the less people that can access the project the less risk. To be honest, it is the right instinct but working in GitHub that approach doesn't hold up. GitHub doesn't have projects as a grouping of repositories, just an organisation with all the repositories flat within that. So if you wanted to support the same model you are forced to manage access to n number of repositories individually. Your users won't have access to do that; it's going to fall to a small group of admins who used to manage say 35 projects, now having to manage 181 repositories individually. Those are some real numbers of a planned migration to GHE, and that excludes the archived repositories that are going to sit in an archive organisation!&lt;/p&gt;

&lt;p&gt;You can see from those numbers this won't scale: every user has to be configured on every repository as part of the migration, and then every new repository needs the same treatment. It is also hard to fit within the Enterprise Team structure that I have suggested before as you'd need to make more small groups in Entra and sync them or create lots of teams in GHE to manage it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give (almost) everyone write access
&lt;/h2&gt;

&lt;p&gt;So, now time for the solution... give everyone write access to everything in the organisation. Okay so that sounds scary, but it is the right thing to do.&lt;/p&gt;

&lt;p&gt;Firstly this ties into the base permission levels baked into the organisations. This immediately reduces some overhead by not having to manage access to every repository individually, and uses the base permission level to control what the default access is for new repositories. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cwxde59op1aioovbk0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0cwxde59op1aioovbk0s.png" alt="Setting base permissions for a team on an org, redacted" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trade-off for this is of course that write access without a gate is dangerous, so the gate has to be real before you do this. That gate is branch protection and CODEOWNERS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branch protection is the actual gate
&lt;/h2&gt;

&lt;p&gt;Now it's time to set up the gate. Branch protection is the mechanism to stop bad changes landing in main, not who can push or make changes. The ruleset is similar to the kind of things you would have set up in Azure DevOps with a few differences but the principle is the same.&lt;/p&gt;

&lt;p&gt;The way I've done this is to set up a repository ruleset at the organisation level, think back to the idea of reducing admin overhead, do it once and apply to all repositories. This is a great way to reduce admin overhead and enforce consistency across all repositories.&lt;/p&gt;

&lt;p&gt;What have I done in the ruleset? There are a few things that I have set up, and by no means have I used every option. Firstly, it's a branch ruleset, with a filter to then select which branches. In my case that is the 'main', 'release' and the default branch in case somehow a different name has been used.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bt8vfntfuz0tmye9nwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bt8vfntfuz0tmye9nwm.png" alt="Branch ruleset and its targets" width="800" height="699"&gt;&lt;/a&gt;Before I get into the rules themselves, the other thing I've enabled is the bypass list. It's set just to organisation admins, and it's there for a breakglass scenario where the ruleset is preventing a merge that needs to happen. This is a safety valve, but it should be used with caution and only in exceptional circumstances. The process for this should be documented and include an audit trail, for example a service ticket.&lt;/p&gt;

&lt;p&gt;Then we get into the actual rules I've applied:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict creations - don't let users create branches that match the filter, this is to stop people creating a branch called 'main' and bypassing the ruleset&lt;/li&gt;
&lt;li&gt;Restrict deletions - don't let users delete branches that match the filter, this is to stop people deleting the main branch and bypassing the ruleset&lt;/li&gt;
&lt;li&gt;Require pull request reviews before merging - this is the main gate, it stops people pushing directly to the branch and forces them to go through a PR. Most of the options here are up to you, but for this approach make sure 'Require review from Code Owners' is ticked.&lt;/li&gt;
&lt;li&gt;Require status checks to pass - make sure that the build is green before allowing a merge, I've set this to look for a check called 'build' and that gets setup in each migrated repository as part of the migration process.&lt;/li&gt;
&lt;li&gt;Block force pushes - don't let people force push to the branch, this is to stop people bypassing the ruleset. &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F554htr9ie3c2tglu6wif.png" alt="The rules within the ruleset" width="800" height="692"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a lot of settings that can be configured here, and I haven't gone through them all so if you want to learn about them all have a look here: &lt;a href="https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets" rel="noopener noreferrer"&gt;GitHub Docs - About rulesets&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CODEOWNERS: who has to say yes
&lt;/h2&gt;

&lt;p&gt;I mentioned that you need to tick 'Require review from Code Owners' in the branch protection ruleset, but what does that actually mean? Code Owners is a way of documenting who is responsible for what within the repository. The file is called CODEOWNERS and is placed in the .github folder in the root of the repository.&lt;/p&gt;

&lt;p&gt;The file is a simple text file with paths and names of users or teams that are responsible for those paths. When a PR is raised that touches any of the areas, the users are automatically added as reviewers on the PR and must approve the PR before it can be merged. This makes sure that the right people are reviewing changes to the right areas of the codebase. Here's an example, obviously redacted, code owners file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Default owner for anything not matched below&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@jbloggs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@asmith&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# Infrastructure and pipeline files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/infra/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@jbloggs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/.github/workflows/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@jbloggs&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# API project&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/src/Api/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@csmith&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@dpatel&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# Shared libraries need sign-off from a specific senior dev&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/src/Shared/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@asmith&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;# Docs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/docs/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@ewhite&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@csmith&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see from the example that the default owner is set to two users, and then specific paths have specific owners. Using it this way means it can't be forgotten, and it can't be skipped as it's documented and wired into the branch protection ruleset.&lt;/p&gt;

&lt;p&gt;Here you can see two users have been added, and the small shield icon shows they are code owners. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6u43aenyu6x1i710pe1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6u43aenyu6x1i710pe1.png" alt="PR showing auto-requested review from CODEOWNERS, redacted" width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha / lesson learned
&lt;/h2&gt;

&lt;p&gt;It all sounds great, but it can also be a bit fiddly to get right. I've had to consider a few things to make sure it's going to work as expected.&lt;/p&gt;

&lt;p&gt;Firstly, make sure the CODEOWNERS file exists, and is in the right path. Helpfully if you're editing this in the GitHub web UI, it will tell you if the file is valid, but only after you have committed, which has caught me out a few times. One thing worth flagging: I tried using our enterprise teams (the ones synced from Entra) in CODEOWNERS, and it doesn't work. GitHub requires the team to have direct write access to the repository, and enterprise teams don't grant that on their own the way org teams do. So for CODEOWNERS specifically, you're stuck using individual usernames or org-level teams, not enterprise teams.&lt;/p&gt;

&lt;p&gt;The other thing that I'm currently working through with this, is making sure it actually fits all the teams and projects. As I said, 181 repositories to migrate, and some are going to be more complex than others. The blanket rules might not fit all of them and may need a tweak, yet to find out! The code owners approach should fit everyone fine, but the branch protection ruleset means you have to have a solid SDLC which is shared across all the teams and that they are all working in the same way. If they aren't, then the ruleset will be a pain to work with and will need to be tweaked for each repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Am I trying to stop people doing what they want to do? No, I'm trying to stop people doing what they shouldn't be doing. The branch protection ruleset and CODEOWNERS file are the actual gate, not who has write access to the repository. It's a clean way to manage access and permissions whilst reducing the admin overhead, and it also enforces a consistent SDLC across all the teams and projects.&lt;/p&gt;

&lt;p&gt;Will it work for everyone? I'd like to think yes, but the answer is probably no. Every organisation is different, and teams all like to work in slightly different ways, regardless of an SDLC.&lt;/p&gt;

</description>
      <category>githubenterprise</category>
      <category>devops</category>
      <category>governance</category>
    </item>
    <item>
      <title>Azure Config as Code</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Wed, 29 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/azure-config-as-code-2o67</link>
      <guid>https://dev.to/mthomas564/azure-config-as-code-2o67</guid>
      <description>&lt;p&gt;If you've read any of my other posts, you'll know that I am a fan of Azure App Configuration. I've specifically spoken about its use for Feature Flag management in my &lt;a href="https://dev.to/posts/feature-flags-what-and-why"&gt;feature flag what and why&lt;/a&gt; and &lt;a href="https://dev.to/posts/tbd-feature-flagging"&gt;TBD feature flagging&lt;/a&gt; posts.&lt;/p&gt;

&lt;p&gt;It is a great tool for managing both configuration and feature flags, and something that I have been working with development teams on recently to implement. Lately we've been discussing using it as the main store for an application, including tenant information that will be used with FinBuckle, and this raised some questions about how to properly manage it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problems
&lt;/h2&gt;

&lt;p&gt;Firstly, when managing it, you have to go into the Azure Portal, go to the resource and then manage the values directly. This is okay for a small number of values or if you have simple values. As mentioned, we want to use it for the main store for an application, and we therefore want to be able to store complex values just as JSON objects. On top of this, there is no real way to version within it, yes, you can take snapshots and add tags, but it's not really version control.&lt;/p&gt;

&lt;p&gt;Secondly, there is no way to control the network access, it's open or closed, not ideal when you want to have it private but also be able to manage it.&lt;/p&gt;

&lt;p&gt;These points make it feel not very enterprise-ready, whilst a brilliant tool. We also use API Management, and therefore use &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/example-scenario/devops/automated-api-deployments-apiops" rel="noopener noreferrer"&gt;APIOps&lt;/a&gt; and this is where the inspiration came from...&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;I want to make managing App Configuration as easy as possible whilst supporting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex Values&lt;/li&gt;
&lt;li&gt;Version Control and Rollback&lt;/li&gt;
&lt;li&gt;Network Access Control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and making it enterprise-ready.&lt;/p&gt;

&lt;p&gt;To do this, I created &lt;a href="https://github.com/MThomas564/azure-config-as-code" rel="noopener noreferrer"&gt;Azure Config as Code&lt;/a&gt;. This is a template that can be cloned into your own environment to help manage App Config in a code style.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does it do?
&lt;/h3&gt;

&lt;p&gt;It covers a few elements to enable you to properly manage your App Config:&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuration as Code
&lt;/h4&gt;

&lt;p&gt;Store your application configuration in version-controlled JSON files, which can then support complex values. A file is created for each environment, allowing you to separately manage each environment, as they will have different values.&lt;/p&gt;

&lt;p&gt;The files are stored within a schema I have created which supports the following types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;String&lt;/li&gt;
&lt;li&gt;JSON&lt;/li&gt;
&lt;li&gt;JSON Array&lt;/li&gt;
&lt;li&gt;Feature Flag&lt;/li&gt;
&lt;li&gt;Key Vault&lt;/li&gt;
&lt;li&gt;Default plain text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Feature Flag and Key Vault types are specific to support the import of Feature Flags, and Key Vault references, meaning all aspects can be stored in Git.&lt;/p&gt;

&lt;h4&gt;
  
  
  Flattening Complex Structures
&lt;/h4&gt;

&lt;p&gt;From the configuration as code, it then is flattened into a KVSet format which can then be imported into App Config. As part of this, it can automatically flatten complex structures such as JSON objects and arrays to allow them to be stored in App Config safely. However, as they are stored as a complex object in Git, you can edit them properly and manage them. This takes something 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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Finbuckle:MultiTenant:Stores:ConfigurationStore:Tenants"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&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;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"910eb0bf-6a57-454b-8458-f3e299591684"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"910eb0bf-6a57-454b-8458-f3e299591684"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Keys"&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;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TestKey1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"53d9262b-a83a-4f31-b4bc-213187a59eb2"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b987448b-5418-4ef8-8a71-f55ea16cf156"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b987448b-5418-4ef8-8a71-f55ea16cf156"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Keys"&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;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TestKey2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"Value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8327d419-f013-490e-8466-8fb6f0a5a15e"&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;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&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;and converts it to the following KVSet object with serialised JSON Values:&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;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Finbuckle:MultiTenant:Stores:ConfigurationStore:Tenants"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;910eb0bf-6a57-454b-8458-f3e299591684&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Identifier&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;910eb0bf-6a57-454b-8458-f3e299591684&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Test1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Keys&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;TestKey1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Value&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;53d9262b-a83a-4f31-b4bc-213187a59eb2&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}]},{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Id&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b987448b-5418-4ef8-8a71-f55ea16cf156&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Identifier&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;b987448b-5418-4ef8-8a71-f55ea16cf156&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Test2&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Keys&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;TestKey2&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Value&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;8327d419-f013-490e-8466-8fb6f0a5a15e&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;"content_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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;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;h4&gt;
  
  
  Version Control and Rollback
&lt;/h4&gt;

&lt;p&gt;The files are all stored in Git, so naturally you can use Git to version control them and track the changes, as well as support pull requests for proposed changes.&lt;/p&gt;

&lt;p&gt;On top of this, the pipeline templates are set up to create an immutable rollback point by publishing the flattened KVSets as build artifacts. These can then be retained and used to rollback to previous versions as needed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Network Access Control
&lt;/h4&gt;

&lt;p&gt;Now, to say that this template fixes this is a bit of a stretch. Instead, what it does is setup the framework to allow you to implement this using your own DevOps Agents. Realistically, if you are working in a private environment, you will have, or want to have, private DevOps Agents within your network. This will allow them to access the App Config instances over private networking. The templates are also set in such a way to allow you to use a different pool for each stage/environment, and therefore allow you to have different access levels for each.&lt;/p&gt;

&lt;h4&gt;
  
  
  Dry Run and Approval
&lt;/h4&gt;

&lt;p&gt;The pipelines also include a step to carry out a dry run import of the configuration. This then returns a list of what changes are going to be made, allowing a manual check before pushing. To further enhance this, a manual approval step is included to allow appropriate approvers to be notified and be required to approve the changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;If this is something that you are interested in using, you can head over to the &lt;a href="https://github.com/MThomas564/azure-config-as-code" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt; and clone the repository.&lt;/p&gt;

&lt;p&gt;From here you will need to update the pipeline variables to suit your environment. Use the dev.json file in the config folder as a guide for creating your own config files.&lt;/p&gt;

&lt;p&gt;Import the pipelines into your DevOps environment, and you should be good to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This is a template that I have created to help the management of App Configuration in a code style. It doesn't replace the Azure Portal, but it does make it a more enterprise-ready solution that can be managed using CI/CD.&lt;/p&gt;

&lt;p&gt;I have some additional features that I plan to add to this over the coming weeks, so stay tuned! If there is something specific that you'd like to see it do, feel free to raise an issue on the GitHub repo.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>appconfiguration</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Feature Flags in Trunk-Based Development</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Tue, 14 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/feature-flags-in-trunk-based-development-81p</link>
      <guid>https://dev.to/mthomas564/feature-flags-in-trunk-based-development-81p</guid>
      <description>&lt;p&gt;Firstly, what is Trunk-Based Development (TBD)? The idea behind TBD is to have one main branch that multiple developers commit lots of smaller changes to. This main branch is the 'trunk' of the codebase. Developers should be committing frequently, and the main trunk should always be deployable.&lt;/p&gt;

&lt;p&gt;Feature flags, as discussed &lt;a href="https://dev.to/posts/feature-flags-what-and-why"&gt;here&lt;/a&gt;, are flags that allow functionality and features to be turned on and off without deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do we need them specifically in TBD?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decoupled deployments
&lt;/h3&gt;

&lt;p&gt;Adding flags for a feature means that we can deploy the code decoupled from a release. This means we could deploy bug fixes to production, but without enabling that new feature that might still be undergoing testing.&lt;/p&gt;

&lt;p&gt;Why do we need this? Back to that trunk, we have one main branch that should always be deployable. This is especially important when a system goes into an operations stage; we need to be able to address functionality that isn't working as intended, but also be able to release new features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive delivery
&lt;/h3&gt;

&lt;p&gt;Progressive delivery allows a feature to be rolled out, but not to everyone. Think A/B testing or canary releases: we let a subset of users see a new feature or change behind a flag. A challenge of releases is that you test as much as you can, but real end users always find something different. Be it they use it slightly differently, on a different device, etc.&lt;/p&gt;

&lt;p&gt;Using canary or A/B lets the change be tested with real users in a real environment, but without it being available to all users at once. This way, it gets tested with a subset, and you are able to get targeted feedback. Plus, if there is an issue, you don't affect the whole user base.&lt;/p&gt;

&lt;h3&gt;
  
  
  Improved rollback
&lt;/h3&gt;

&lt;p&gt;When a new feature is rolled out, there is always a possibility that something goes wrong. It could be a small bug, or it could flat out not work. Without flags, the only way to roll back would be to redeploy the previous version, and depending on the deployment and hosting strategy, this could be a long process causing even more downtime.&lt;/p&gt;

&lt;p&gt;Now, with flags, we can 'roll back' by just turning off the flag. The flag goes off, the feature is disabled, and the system is back operational. This is an incredibly fast way to mitigate issues, which is great for the operations piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenges
&lt;/h2&gt;

&lt;p&gt;With any technique there are always challenges, and feature flags are no different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Too many or stale flags
&lt;/h3&gt;

&lt;p&gt;Creating too many flags can lead to a messy codebase and creates a constant loop of technical debt. With that, the more flags that get added, the higher the chance of stale flags—ones that are created and never used or tidied up.&lt;/p&gt;

&lt;p&gt;To try and avoid this, clear naming conventions and lifecycle policies are important. Ensure that flags have an owner who is responsible for their lifecycle. As part of this, as the feature is completed and fully rolled out, the flag should be removed. Now, sometimes this might not be the way; depending on the feature, you might want to keep the flag to be able to turn it off in case of issues, but this should be a conscious decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code complexity
&lt;/h3&gt;

&lt;p&gt;Implementing feature flags is a relatively simple piece of work, especially if you use a prebuilt library or service. However, the addition of flags creates additional complexity regardless, as you have to manage the different branches of the code.&lt;/p&gt;

&lt;p&gt;Where possible, you should use flags to encapsulate a complete feature, and avoid using them throughout the business logic. This way, the code remains as clean as possible but can still be toggled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing complexity
&lt;/h3&gt;

&lt;p&gt;As we said above, adding more flags creates more branches through the code—more routes that the application flow can take. This means that testing needs to become more complex to support this. We still need to be able to test the application as a whole, including the flags.&lt;/p&gt;

&lt;p&gt;This can be mitigated by limiting the scope of flags, ideally one flag per feature. Doing so helps to limit the complexity, as there are only two states for the feature: on or off. If there are multiple flags that interact for a feature, the testing must cover each of the combinations, which can quickly grow out of hand.&lt;/p&gt;

&lt;p&gt;Automated testing is also key to help with this; tests can be run frequently and help to check each of the possible paths without a developer needing to manually test each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Governance and ownership
&lt;/h3&gt;

&lt;p&gt;As mentioned above, having clear ownership of flags is important. This includes the lifecycle of the flag, but also its usage.&lt;/p&gt;

&lt;p&gt;A flag should have a clear owner to ensure that someone is responsible for it. This helps to avoid the situation where a flag gets created and then forgotten about because it's just used and then that's that. The owner is also responsible for checking that the flag is actually used, and if not, to remove it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visibility and tooling
&lt;/h3&gt;

&lt;p&gt;As I've discussed in a previous blog, there are a number of different ways to implement feature flags. Depending on what you use, the visibility of your flags will differ.&lt;/p&gt;

&lt;p&gt;Using a feature flag service like Azure App Configuration will give you a single place to see your flags and their status. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn55uaouhxxc3l8ge5pem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn55uaouhxxc3l8ge5pem.png" alt="Azure App Configuration Feature Flags"&gt;&lt;/a&gt;Here you can see the flags and whether they are enabled or not, giving a quick, easy way to see what features are enabled in your application.&lt;/p&gt;

&lt;p&gt;As someone working in a Microsoft house, I'm always going to be biased towards App Configuration, but other services like LaunchDarkly will offer the same visibility in their own way.&lt;/p&gt;

&lt;p&gt;However, as I have discussed before, you can implement flags just using app settings or environment variables. This is okay for quick development or small projects, but you will not have a simple, nice way to create this visibility. You would need to create your own tooling or dashboards to monitor this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended practices
&lt;/h2&gt;

&lt;p&gt;There are a few things that should be considered when implementing flags, especially in a TBD environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat flags as short-lived; they should not become a permanent piece of configuration or logic.&lt;/li&gt;
&lt;li&gt;Add flag management to the definition of done for a feature. Once fully complete, the flags should be cleaned up if possible.&lt;/li&gt;
&lt;li&gt;Implement flagging 'properly' using an appropriate tool, not by rolling your own in-app settings or similar.&lt;/li&gt;
&lt;li&gt;Automate where possible.

&lt;ul&gt;
&lt;li&gt;Create a custom table to track flag usage, when they were created, by whom, and their current status.&lt;/li&gt;
&lt;li&gt;Use Application Insights or similar to track flag usage in the application; this can help identify stale flags or ones that are not being used.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example implementation
&lt;/h2&gt;

&lt;p&gt;Here’s an example of how a new feature can be implemented using trunk-based development and feature flags.&lt;/p&gt;

&lt;p&gt;The code has been committed directly to the main branch, as expected in TBD. However, because the feature isn’t yet finished or fully tested, it’s hidden behind a flag. This allows the code to be continuously deployed without affecting end users, while ensuring the rest of the team can keep working on other features or bug fixes.&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderModel&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_featureManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabledAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NewCheckoutFlow"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;NewCheckoutService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;LegacyCheckoutService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;In this example, the legacy checkout flow continues to operate until the team is ready to switch over to the new one. The feature can be tested safely by toggling the flag on and verifying the new path. Keeping the legacy code available means the system can quickly fall back if an issue is discovered. Once testing is complete and the business is confident in the new flow, the flag can be removed and the new implementation becomes the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Trunk-based development is a great, simple way to manage a codebase and help developers work together effectively. It does, however, introduce challenges around deployments and ensuring that other fixes or features are not blocked by one another. Properly implementing feature flags can help to mitigate this challenge and keep the code clean, safe, and decoupled from deployments.&lt;/p&gt;

&lt;p&gt;Using both in combination should be a key part of any software development lifecycle to ensure that a team can deliver value effectively and efficiently.&lt;/p&gt;

&lt;p&gt;I’ve seen feature flags thrown in at the last minute before to get something released while hiding another problem. Did it work? Yes — but it also created a mess that was hard to clean up later. Designing your SDLC with feature flags in mind from the start will leave you in a far better place, without needing sticky plasters over deeper issues.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>trunkbaseddevelopmen</category>
      <category>featureflags</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Automating Astro Builds and Deployments</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Fri, 03 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/automating-astro-builds-and-deployments-5alj</link>
      <guid>https://dev.to/mthomas564/automating-astro-builds-and-deployments-5alj</guid>
      <description>&lt;p&gt;I decided on &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fastro.build%2F" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fastro.build%2F" alt="Astro" width="" height=""&gt;&lt;/a&gt; as the best fit for my personal blog; it's simple to use and author with, as it is written in MDX. Primarily, it creates a static site that can be published, making it lightweight to host and fast.&lt;/p&gt;

&lt;p&gt;Recently, I moved all of my DNS management to Cloudflare and wanted to make further use of it, having invested time in setting it up. This led me to discover Cloudflare Workers, which are a free hosting option included in the free plan.&lt;/p&gt;

&lt;p&gt;Now, what problem was I trying to solve?&lt;/p&gt;

&lt;p&gt;I want to be able to write my blogs whenever I wish, which sometimes means more than one in a day, one a week, or even one a month. However, I want to automate the publishing of them, as I could if I had used something like WordPress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What solution did I come up with?
&lt;/h2&gt;

&lt;p&gt;A key element was that the solution had to be free, or at least as inexpensive as possible. I also wanted to avoid hosting it myself in my homelab, as that would then become a 'production' service. I came up with the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write blog posts in MDX and push to GitHub with the publish datetime set in the future in the metadata.&lt;/li&gt;
&lt;li&gt;Run a daily GitHub Action to commit to the site's repository.&lt;/li&gt;
&lt;li&gt;Cloudflare Worker watches for new commits on GitHub and updates the site.&lt;/li&gt;
&lt;li&gt;Zapier monitors the site's RSS feed and pushes new posts into &lt;a href="https://buffer.com/" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; to publish to LinkedIn.&lt;/li&gt;
&lt;li&gt;Check and update posts in Buffer and allow them to auto-publish to LinkedIn.&lt;/li&gt;
&lt;li&gt;Share on LinkedIn.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;p&gt;The first step is to write the blog posts in MDX and push them to GitHub. There are required fields in the metadata for the blog files, including 'pubDatetime'.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Automating Astro Builds and Deployments&lt;/span&gt;
&lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;automating-astro&lt;/span&gt;
&lt;span class="na"&gt;pubDatetime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2025-10-03T00:00:00&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A guide on how I have automated the build and deployment of my Astro site using GitHub Actions, Zapier and Cloudflare.&lt;/span&gt; 
&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;astro&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;automation&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cloudflare&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;heroImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../../assets/images/AstroAutomation.png&lt;/span&gt;
&lt;span class="na"&gt;ogImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../../assets/images/AstroAutomation.png&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;p&gt;Next, there is the GitHub Action that runs daily to add a commit to the site, which Cloudflare looks for. This could potentially be done more efficiently, but for now it works as I need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Daily Auto Rebuild&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt; &lt;span class="c1"&gt;# 8 AM UK time (7 AM UTC)&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trigger-rebuild&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create empty commit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "GitHub Actions"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "actions@github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git commit --allow-empty -m "chore: trigger rebuild"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for site to build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt; &lt;span class="c1"&gt;# Wait 5 minutes (300 seconds)&lt;/span&gt;


      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extract latest post URL from RSS&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_post_url&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;RSS_URL="https://matthewthomas.cloud/rss.xml"&lt;/span&gt;
          &lt;span class="s"&gt;POST_URL=$(curl -s "$RSS_URL" | awk 'BEGIN{RS="&amp;lt;item&amp;gt;"} NR==2 {match($0, /&amp;lt;link&amp;gt;([^&amp;lt;]+)&amp;lt;\/link&amp;gt;/, a); print a[1]}' )&lt;/span&gt;
          &lt;span class="s"&gt;echo "Latest post URL: $POST_URL"&lt;/span&gt;
          &lt;span class="s"&gt;echo "post_url=$POST_URL" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ping LinkedIn Post Inspector&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -s "https://www.linkedin.com/post-inspector/inspect?url=${{ steps.get_post_url.outputs.post_url }}"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see this adds a daily chore commit. Additionally I have added a step to put the blog post URL through the LinkedIn Post Inspector. This step might be redundant, but I found that sometimes LinkedIn would not pick up the OG image. As you can see, this adds a daily chore commit. Additionally, I have added a step to put the blog post URL through the LinkedIn Post Inspector. This step might be redundant, but I found that sometimes LinkedIn would not pick up the OG image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;p&gt;The Cloudflare Worker connected to the repository is looking for new commits to then trigger a new build.&lt;/p&gt;

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

&lt;p&gt;When it sees a new commit, it will pull the latest code and then build the site using the following commands:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx astro build

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler deploy

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;p&gt;Next, I have a simple Zapier workflow that monitors my RSS feed every 15 minutes to look for new posts. When found, it automatically adds them to Buffer. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ni7aa74139bfm9y6a2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ni7aa74139bfm9y6a2l.png" alt="Zapier Workflow" width="362" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;p&gt;The final automation step is in Buffer, a useful social media management tool. Now, do I really need to use one? Probably not; I was kind of forced to by wanting to automate the LinkedIn posting. I tried to do it via Zapier directly, but the quality of the post created was very poor, unless I paid for subscriptions.&lt;/p&gt;

&lt;p&gt;I found Buffer to be a good compromise. I can automate posts through this workflow, then manage the posts in Buffer to make any changes or updates before they are published. I can also use it to schedule any other posts I may want to share outside of blog posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6
&lt;/h3&gt;

&lt;p&gt;Lastly, the post gets shared on LinkedIn. This is something I have only started doing with this blog, but I want to share my work and thoughts more widely.&lt;/p&gt;

&lt;p&gt;This is taking some learning, especially with how the LinkedIn algorithm works, but I am starting to get a feel for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall, I am really happy with how this setup works for me. I couldn't find any existing solutions online that worked for me, so creating my own was the best option.&lt;/p&gt;

&lt;p&gt;It could probably be simplified in some places, but it hits the key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free or very low cost&lt;/li&gt;
&lt;li&gt;No hosting required&lt;/li&gt;
&lt;li&gt;Fully automated&lt;/li&gt;
&lt;li&gt;Easy to write and publish blogs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd be interested to hear if this works for anyone else, or if you have any suggestions as to how to improve it.&lt;/p&gt;

</description>
      <category>development</category>
      <category>astro</category>
      <category>automation</category>
      <category>github</category>
    </item>
    <item>
      <title>Sharing Code Across .NET Projects with csproj Includes: An Azure Functions Example</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Thu, 28 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/sharing-code-across-net-projects-with-csproj-includes-an-azure-functions-example-3ln6</link>
      <guid>https://dev.to/mthomas564/sharing-code-across-net-projects-with-csproj-includes-an-azure-functions-example-3ln6</guid>
      <description>&lt;p&gt;The ability to reuse code without duplication is always the aim of any developer following the DRY principle, &lt;em&gt;Don't Repeat Yourself&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Normally as developers we turn these things into a nugget package and install the library that way allowing us to reuse the code. This is great in a lot of circumstances but not always.&lt;/p&gt;

&lt;p&gt;We can do that using compilation links within the csproj file to configure a project to compile files from a different project, well specifically a different path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Compile&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"../Shared/Functions/Function1.cs"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above snippet is how to include a file from another location to be compiled within our application.&lt;/p&gt;

&lt;h2&gt;
  
  
  My use case
&lt;/h2&gt;

&lt;p&gt;Recently I have been working with a development team building an API for a service that will have both and Web UI and a Mobile App calling the API. Due to the two different client types we have implemented two different authentication options for the API which is being provided using policy in APIM. One is app to app, and one is a B2C route for the end users of the mobile app. This has been working great, until we got a requirement for both the web and the mobile app to use the same endpoint of the API. Due to the way the policy works this cannot be implemented without duplicating the endpoint within the same API.&lt;/p&gt;

&lt;p&gt;To do this we chose to split the API into two projects that can then have their own APIM policies enforcing different authentication schemes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;//File: Functions.B2C.csproj
&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Compile&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"../Functions/SendEmailFunction.cs"&lt;/span&gt; &lt;span class="na"&gt;Link=&lt;/span&gt;&lt;span class="s"&gt;"Functions\SendEmailFunction.cs&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;/ItemGroup

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above example you can see that within our new B2C function, we are including a function from the original Function App.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link Property
&lt;/h2&gt;

&lt;p&gt;The additional property added 'Link' allows us to set the symbolic link location for how it displays within Visual Studio. This is not required but is preferred for cleanliness when you are looking at the project within an IDE.&lt;/p&gt;

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

&lt;p&gt;As you can see above the project within Visual Studio now shows our function, even though from a different project, and it's clearly identifiable as a link from the icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final implementation
&lt;/h2&gt;

&lt;p&gt;The final implementation that we settled on was to even further split this out, ending up with the following projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Functions.common

&lt;ul&gt;
&lt;li&gt;Contains all of the shared code for the functions including the actual function definitions&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Functions.API

&lt;ul&gt;
&lt;li&gt;Has no direct code, using compile links to include a subset of functions&lt;/li&gt;
&lt;li&gt;Function a&lt;/li&gt;
&lt;li&gt;Function b&lt;/li&gt;
&lt;li&gt;Function c&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Functions.B2C

&lt;ul&gt;
&lt;li&gt;Has no direct code, using compile links to include a subset of functions&lt;/li&gt;
&lt;li&gt;Function c&lt;/li&gt;
&lt;li&gt;Function d&lt;/li&gt;
&lt;li&gt;Function e&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;As you can see we are now able to share functions between the two hosting applications without the need to duplicate actual code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Developers should always aim to create DRY solutions where at all possible. This functionality helps with creating this especially in more speciliased or specific use cases which are not suited to creating an installable package. Packages are great for instances when the code will be shared to other solutions but in a case were the code is being used within the same solution this often creates more complication than is really required.&lt;/p&gt;

&lt;p&gt;Above we have been able to create a shared set of functions and then deploy two different Function Apps using their own specified subset of the functions creating two individual APIs without having to repeat ourselves.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>development</category>
      <category>codesharing</category>
      <category>bestpractices</category>
    </item>
    <item>
      <title>Feature Flags - What and Why</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Tue, 19 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/feature-flags-what-and-why-314e</link>
      <guid>https://dev.to/mthomas564/feature-flags-what-and-why-314e</guid>
      <description>&lt;h2&gt;
  
  
  What are feature flags? Why should you use them?
&lt;/h2&gt;

&lt;p&gt;Firstly, what are they? Feature flags are a method of being able to enable and disable features in an application without having to redeploy your application. They can be used for simple features like displaying content or not, or they can be used for more complex features such as enabling or disabling an AI help chat.&lt;/p&gt;

&lt;p&gt;Secondly, why should you use them? The main reason is being able to disable a feature without having to redeploy your application, which would potentially cause downtime. This could be needing to disable it because it doesn't work, or because you weren't ready to release it. This last part is incredibly beneficial in my opinion. If you are working in an agile way you want to be completing PRs as soon as they are done, and that means code is into the main branch. But what if that feature isn't fully ready or tested, well without feature flags you'd be stuck not able to release from that branch. Using flagging you can just disable that feature and keep up a steady release cycle.&lt;/p&gt;

&lt;p&gt;For example, I recently implemented feature flagging in a Blazor project I was helping with. We needed to be able to publish to all of our environments, critically production, but in production we could only show a holding page for the time being. This was previously implemented in a separate branch and the deployment pipeline run from that branch. Instead, we now have a simple feature flag that changes what pages are rendered to enable this control.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to implement feature flags
&lt;/h2&gt;

&lt;p&gt;There are several ways to implement feature flags, from simple boolean checks with config values to using a third-party service. The way you implement depends on a number of things including the team, infrastructure, and the budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple Boolean Checks
&lt;/h2&gt;

&lt;p&gt;The simplest way to implement feature flags is to use a boolean check in your code. This can be done by using a configuration file or environment variable to store the flag state.&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;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"FeatureFlags:NewFeature"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Execute new feature code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Execute old feature code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty simple, right? And this can be repeated anywhere, especially if you use dependency injection to inject the configuration into the rest of the codebase. It does have its drawbacks though. Firstly, you would have to have access to the hosting environment to change the configuration value, for example access to the Azure App Service hosting it. Secondly, if you are using Azure App Service, these values are held as environment variables, and changing them causes the application to restart. This means we lose out on the benefit of not having to cause any downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure App Configuration
&lt;/h2&gt;

&lt;p&gt;Okay, so as an Azure Architect, I am obviously going to have a bias to Azure options and therefore have to mention App Configuration. This is a really well-priced service within Azure which can be used with a number of different SDKs and application types. One of the key benefits is that it can watch for config changes and update without having to restart the application.&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="nf"&gt;AddAzureAppConfiguration&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;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureAppConfiguration&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;hostingContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&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;var&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAzureAppConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Connect&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="s"&gt;"ConnectionStrings:AppConfig"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseFeatureFlags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureRefresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refreshOptions&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;refreshOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FeatureFlags:Sentinel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refreshAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                 &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetCacheExpiration&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Add feature management&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddFeatureManagement&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A really powerful tool which has a built-in feature flag system which can be simply enabled. You can then add a feature gate to the code, for example in a controller:&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;Microsoft.AspNetCore.Mvc&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.FeatureManagement&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;MyApi.Controllers&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/[controller]"&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;BetaController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IFeatureManager&lt;/span&gt; &lt;span class="n"&gt;_featureManager&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BetaController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFeatureManager&lt;/span&gt; &lt;span class="n"&gt;featureManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_featureManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;featureManager&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;HttpGet&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_featureManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabledAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CoolNewFeature"&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Beta feature is ENABLED"&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="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Beta feature is DISABLED"&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;As you can see in the example, the get method will only respond with an OK if the feature is enabled in App Configuration.&lt;/p&gt;

&lt;p&gt;Using Azure App Configuration is perfect if you are already using Azure services. It allows you to make these configuration changes without having to restart the application. It also allows your platform team to limit the access that people need to be able to change these. Instead of a user having to have contributor rights on the whole App Service, they can just be given rights on the App Configuration. Yes, there is still a risk of malicious activity this way, but the blast radius is smaller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third Party Services
&lt;/h2&gt;

&lt;p&gt;There are several third-party services that can be used to manage and implement feature flags. Using a more platform-agnostic service can be beneficial, especially if you are multi-cloud, or still hosting on-premises. These services would allow you to use the same management pane and implementation regardless of the hosting environment. They do come with additional costs which would need to be factored into any pricing for a project.&lt;/p&gt;

&lt;p&gt;One popular option is &lt;a href="https://launchdarkly.com/" rel="noopener noreferrer"&gt;LaunchDarkly&lt;/a&gt;. I won't go into detail about how to implement this as it's well documented on their site, for example for .NET &lt;a href="https://docs.launchdarkly.com/sdk/server-side/dotnet" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Something I do like about LaunchDarkly is the really clean and simple UI that is provided for managing the flags. I can see this as being great for being able to delegate the control of the flags to a wider team, such as product owners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Whilst I have not been in a position personally to use a service such as LaunchDarkly, I can definitely see the benefits of using an independent service like this. You get all of the flexibility but with a wider range of supported environments. Now, that isn't to say you couldn't, for example, use Azure App Configuration with an application hosted on-premises, but would you be paying for Azure if you are hosting on-premises?&lt;/p&gt;

&lt;p&gt;Implementing feature flags is a must-have in my opinion for any project, especially one working in an agile way. No matter how you choose to implement them, they will be beneficial to the application. Yes, there is some additional work upfront to set it up, and you have to make sure they are implemented for each feature needing one, but the benefits far outweigh the costs.&lt;/p&gt;

</description>
      <category>featureflags</category>
      <category>softwaredevelopment</category>
      <category>devops</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Troubleshooting Entra External Identities B2C OTP Emails</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Tue, 12 Aug 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/troubleshooting-entra-external-identities-b2c-otp-emails-1mca</link>
      <guid>https://dev.to/mthomas564/troubleshooting-entra-external-identities-b2c-otp-emails-1mca</guid>
      <description>&lt;p&gt;Microsoft have released a new feature to the Identity Platform to allow you to configure &lt;a href="https://learn.microsoft.com/en-us/entra/identity-platform/custom-extension-email-otp-get-started?tabs=azure-communication-services%2Cazure-portal" rel="noopener noreferrer"&gt;Configure a custom email provider for one-time passcode events&lt;/a&gt; within a custom authentication extension for an external tenant.&lt;/p&gt;

&lt;p&gt;At the time when we started to implement this feature, it was still in preview, meaning we had no blogs like this to help. We ended up having to raise a support ticket with Microsoft to assist in getting this resolved.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is not a full guide on setting this up. If you would like one, the Microsoft documentation above does explain all of the steps.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Our setup for this is a workforce tenant with Front Door, APIM and the API, and then an external tenant which is set up with the custom authentication extension. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fken5fnm3vk4q07syv2f5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fken5fnm3vk4q07syv2f5.png" alt="Setup Diagram" width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was The Problem?
&lt;/h2&gt;

&lt;p&gt;The error message that we received was the following when trying to test the user sign-in/sign-up flow: &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhc2oyevumo6smktzjs6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhc2oyevumo6smktzjs6g.png" alt="Error message" width="462" height="374"&gt;&lt;/a&gt;&lt;code&gt;There was an issue looking up your account. Tap Next to try again.&lt;/code&gt;Honestly, one of the most misleading error messages that left us chasing our tails for quite some time. This error message can and does occur whether the account already existed or not, so nothing to do with not being able to look up the account.&lt;/p&gt;

&lt;p&gt;Several hours of checking all of the configurations in B2C later, I decided to move on and check the other side, the Workforce Tenant.&lt;/p&gt;

&lt;p&gt;From the diagram above, the first thing to check is Azure Front Door (AFD) and the Web Application Firewall (WAF) that is configured alongside that. Let's have a look at the logs: &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38m44wlqlix9ve4an5hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38m44wlqlix9ve4an5hx.png" alt="WAF Logs" width="498" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our setup, the WAF has an IP Restriction rule on it to only allow traffic from certain IP addresses, mainly our corporate VPN. This is to help control the access to the development and test environments. The WAF logs are showing that the request is being blocked by the IP filtering rule. Entra has a lot of IPs that the traffic could originate from, so to get around this I updated the rule to not block if the request matched our OTP endpoint.&lt;/p&gt;

&lt;p&gt;From here, another test showed that the request was now being allowed through, but we were still getting the same error message. The next step in our chain was to check API Management (APIM). Looking at the logs, I could see that the API was not returning any errors, but it was not returning the expected response either.&lt;/p&gt;

&lt;p&gt;Digging into this further, I started looking at the code itself, and found the culprit. The code, whilst a completely valid function, was not returning the expected response that Entra needed. The code was just returning a standard 200 OK for success.&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;ResponseObject&lt;/span&gt; &lt;span class="n"&gt;responseData&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;ResponseObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"microsoft.graph.OnOtpSendResponseData"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;responseData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Actions&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="n"&gt;ResponseAction&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;new&lt;/span&gt; &lt;span class="nf"&gt;ResponseAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"microsoft.graph.OtpSend.continueWithDefaultBehavior"&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkObjectResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the key part from the documentation above that had been missed. It has to return the exact object and actions that the B2C flow expects.&lt;/p&gt;

&lt;p&gt;Fixing that with the development team and a quick redeploy, and we were able to get the flow working as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hopefully, this will help anyone else who is trying to set up the custom email provider for one-time passcode events in Entra B2C. If you do have similar issues, I would recommend checking all the steps in your chain one at a time. If you have WAF rules, for example, check the logs or put the WAF in detection mode so you can see the requests and work from there. Double-check the code against the documentation, and make sure to return the correct object and actions that Entra expects.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>entra</category>
      <category>b2c</category>
      <category>troubleshooting</category>
    </item>
    <item>
      <title>.NET API Versioning in Azure API Management</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/net-api-versioning-in-azure-api-management-83n</link>
      <guid>https://dev.to/mthomas564/net-api-versioning-in-azure-api-management-83n</guid>
      <description>&lt;p&gt;When writing an API in .NET, it's common to be able to have multiple versions of your API. This functionality is built into .NET, allowing you to specify different versions of your API endpoints. Azure API Management (APIM) is a great tool to create an API hub, especially when it comes to managing multiple versions of your API.&lt;/p&gt;

&lt;p&gt;However, when you want to set this up you can run into some issues, especially if you want your API to have versioning in the URL. Whilst you can handle the versioning in the URL, a header or query parameters, in the URL is my preferred option.&lt;/p&gt;

&lt;p&gt;When this is set up in both the API and APIM you can end up with some URLs like the following:&lt;code&gt;https://apim.azure-api.net/api/v1/v1/value&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is pretty poor really, but why does it happen?&lt;/p&gt;

&lt;p&gt;Firstly, the OpenAPI file that has been generated by the API has the version in the path as you would expect as that is how we are managing the version selection. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkphnn0le8zdfnurv6hbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkphnn0le8zdfnurv6hbm.png" alt="alt text" width="325" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then we have the version in APIM, which is required when you enable versioning on the API settings. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxf1x15hsekmnog79hqsw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxf1x15hsekmnog79hqsw.png" alt="alt text" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How can this be fixed?&lt;/p&gt;

&lt;h2&gt;
  
  
  Update the API project
&lt;/h2&gt;

&lt;p&gt;To start with we need to make some changes in our .NET project. For reference, the project I am using to demonstrate is .NET 9 and uses the following packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" /&amp;gt;
&amp;lt;PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" /&amp;gt;
&amp;lt;PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" /&amp;gt;
&amp;lt;PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="9.0.7"&amp;gt;
    &amp;lt;IncludeAssets&amp;gt;runtime; build; native; contentfiles; analyzers; buildtransitive&amp;lt;/IncludeAssets&amp;gt;
    &amp;lt;PrivateAssets&amp;gt;all&amp;lt;/PrivateAssets&amp;gt;
&amp;lt;/PackageReference&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have shared this project &lt;a href="https://github.com/MThomas564/APIVersionTest" rel="noopener noreferrer"&gt;here on Github&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 - Set up the Program.cs to have API Versioning
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddApiVersioning(options =&amp;gt;
{
    options.DefaultApiVersion = new ApiVersion(1);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
})
.AddMvc()
.AddApiExplorer(options =&amp;gt;
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = false;
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key thing to note here is &lt;code&gt;options.SubstituteApiVersionInUrl = false&lt;/code&gt;. This prevents the version being set in the OpenAPI file, leaving it like this: &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk93dmz9627qqzvk1ecp6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk93dmz9627qqzvk1ecp6.png" alt="alt text" width="338" height="92"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 - Create an OpenApiDocumentTransformer
&lt;/h3&gt;

&lt;p&gt;A document transformer can be used to customise the output which OpenAPI generates, allowing both the removal of properties but also the addition of custom ones if needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class OpenAPIPathTransform : IOpenApiDocumentTransformer
{
    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
    {
        var newPaths = new OpenApiPaths();
        foreach (var path in document.Paths)
        {
            // Remove '/api/v{version}' from the path
            var newPath = Regex.Replace(path.Key, @"/api/v\{version\}", "", RegexOptions.IgnoreCase);
            var pathItem = path.Value;

            // Remove 'version' path parameter from all operations if present
            foreach (var operation in pathItem.Operations.Values)
            {
                if (operation.Parameters != null)
                {
                    operation.Parameters = operation.Parameters
                        .Where(p =&amp;gt; !(p.In == ParameterLocation.Path &amp;amp;&amp;amp; p.Name == "version"))
                        .ToList();
                }
            }

            newPaths.Add(newPath, pathItem);
        }

        document.Paths = newPaths;

        // Set the OpenAPI info.version property based on the document name
        if (!string.IsNullOrEmpty(context.DocumentName))
        {
            document.Info.Version = context.DocumentName;
        }
        return Task.CompletedTask;
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has a few parts to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove the API version from the path.&lt;/li&gt;
&lt;li&gt;Remove the version parameter from the operations, otherwise the spec is invalid.&lt;/li&gt;
&lt;li&gt;Set the version property to have the version from the document name that we will set in the Program.cs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3 - Register the OpenAPI Generation and document transformer in the Program.cs
&lt;/h3&gt;

&lt;p&gt;Next we need to register the fact that we want to generate OpenAPI files for the API, and the versions these are for. With that we register the document transformer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddOpenApi("v1", options =&amp;gt;
{
    options.AddDocumentTransformer&amp;lt;OpenAPIPathTransform&amp;gt;();
});
builder.Services.AddOpenApi("v2", options =&amp;gt;
{
    options.AddDocumentTransformer&amp;lt;OpenAPIPathTransform&amp;gt;();
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we set our two versions, and register the document transformer so it updates the spec file to have the properties we want, and, importantly, the path.&lt;/p&gt;

&lt;p&gt;Now our API spec file looks like this:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Update APIM
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I am not covering the full steps of setting up an API in APIM here, just the steps important to what I am demonstrating.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 - Create a new API in APIM
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6pibcamue98qhscwygq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6pibcamue98qhscwygq.png" alt="alt text" width="765" height="749"&gt;&lt;/a&gt;In this example I am creating from an OpenAPI spec file which I have locally created by the project.&lt;/p&gt;

&lt;p&gt;The key things are the settings for this project is the &lt;code&gt;API URL suffix&lt;/code&gt; which is set to &lt;code&gt;api&lt;/code&gt;. And then the enabling of versioning, with the correct identifier and the scheme of path.&lt;/p&gt;

&lt;p&gt;When we look at the test API for one of our endpoints we see a request URL of &lt;code&gt;https://apim.azure-api.net/api/v1/Value&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Perfect, right? Not quite...&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 - Update the policy
&lt;/h3&gt;

&lt;p&gt;Whilst the request here looks right, the &lt;code&gt;/api/v1&lt;/code&gt; only really relates to the routing within APIM itself. We need to update the backend call to include this too so we get to the right endpoints, and the right version.&lt;/p&gt;

&lt;p&gt;This is the inbound portion of my APIM policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;inbound&amp;gt;
    &amp;lt;base /&amp;gt;
    &amp;lt;set-backend-service backend-id="ap-apimtest" /&amp;gt;
    &amp;lt;rewrite-uri template="@{ return "/api/v1/" + context.Operation.UrlTemplate; }" /&amp;gt;
&amp;lt;/inbound&amp;gt;
    ```



This firstly sets the backend to be pointing to my API backend. And then it rewrites the URL that is used to call the backend to include `/api/v1` as we need. Then it appends the operation we are calling so we get the right thing. 

## That's it!
There are quite a few steps to doing it this way, and there might be some other ways of doing it.
That being said, I have not found any good pieces of documentation for how to do it, specifically for this kind of setup with the versioning in the URL.

Please reach out if you have other ways to do it!


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Function App Vs Web App for APIs</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Tue, 08 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/function-app-vs-web-app-for-apis-4ffb</link>
      <guid>https://dev.to/mthomas564/function-app-vs-web-app-for-apis-4ffb</guid>
      <description>&lt;p&gt;When it comes to hosting an API in Azure, there are a couple of options. The two main ones are &lt;strong&gt;Azure Function Apps&lt;/strong&gt; and &lt;strong&gt;Azure Web Apps&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When I've investigated comparing the two online before, the results often lead to the conclusion that Function Apps are the way forward; however, I don't think that this is always the case.&lt;/p&gt;

&lt;p&gt;If you want to run a lightweight service that is event-driven, which importantly can include HTTP requests, then yes, a Function App might be a good choice. But what if you want to run a fully featured RESTful API? Then a Function App might not be the best choice.&lt;/p&gt;

&lt;p&gt;I'm currently looking into this with some colleagues around what is the best approach for a new .NET API project. The project is essentially an internal search index which will, for the new iteration, likely be using a SQL database backend.&lt;/p&gt;

&lt;p&gt;Here are its requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Be cost-effective.&lt;/li&gt;
&lt;li&gt;Be hosted in Azure.&lt;/li&gt;
&lt;li&gt;Be written in .NET.&lt;/li&gt;
&lt;li&gt;Serve the information over an HTTP endpoint to allow a front end to consume it.&lt;/li&gt;
&lt;li&gt;Be able to have IP restrictions, and it will be presented through Azure API Management.&lt;/li&gt;
&lt;li&gt;Have private connectivity to the SQL database.&lt;/li&gt;
&lt;li&gt;Be scalable.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Azure Function Apps are great for lightweight, event-driven tasks and can be cost-effective on a consumption plan.&lt;/li&gt;
&lt;li&gt;Function Apps lack native controller support, making full REST APIs harder to build and maintain.&lt;/li&gt;
&lt;li&gt;Swagger and model binding support in Function Apps is limited and requires workarounds.&lt;/li&gt;
&lt;li&gt;Azure Web Apps support standard MVC controllers, middleware, and seamless Swagger integration.&lt;/li&gt;
&lt;li&gt;Web Apps offer better local development, debugging, and native authentication features.&lt;/li&gt;
&lt;li&gt;Web Apps provide predictable scaling and flexible networking, suitable for complex APIs.&lt;/li&gt;
&lt;li&gt;For fully featured, scalable APIs, Web Apps are generally the better choice; Function Apps suit smaller, event-driven workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So why not just use a Function App?
&lt;/h2&gt;

&lt;p&gt;Okay, so to start with, yes, a Function App can be a lot cheaper, &lt;strong&gt;IF&lt;/strong&gt; you can run on a consumption plan. &lt;em&gt;I don't plan on explaining all of the different SKUs here, but essentially, with a consumption plan you just pay for the execution time you use.&lt;/em&gt; This is a big IF that really depends on the use case and the environment it will be used in.&lt;/p&gt;

&lt;p&gt;During the discussion about a new project, we were having this exact debate: 'Would a consumption plan Function App be a good choice?'. Looking at the metrics of the current system and the general traffic, it was looking good. Flurries of requests during the working hours but not hundreds of requests, and then not used out of hours. &lt;em&gt;Great, so consumption plan?_Well... no. We have to think about what third-party health checks may be used. In this instance, the organisation uses Better Stack which will hit the service every 3 minutes. This means the service will be running nearly all the time—so are we really going to see any benefit of the consumption plan? _No, not really.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, we need to think about the networking side. We needed to have private networking options, including VNET integration, and the original consumption plan did not support this. Luckily, Microsoft have added the new flex consumption plan so that solves that problem.&lt;/p&gt;

&lt;p&gt;We also need to consider the scaling options. Function Apps have event-driven scaling, so if you have a sudden spike in traffic, it can respond quickly to handle the load. Additionally, with the new flex consumption plan you can have always-ready instances to help eliminate the cold starts.&lt;/p&gt;

&lt;p&gt;So Function Apps are starting to sound the better route, so what are the issues with what we're wanting to do?&lt;/p&gt;

&lt;p&gt;Firstly, Functions.&lt;/p&gt;

&lt;p&gt;Even when set as HTTP triggers, they are not a 'normal' API endpoint and therefore we don't get a standard Swagger file generated from the API. There is an old package to add this support but it is in maintenance mode and isn't being updated. There is a version that can be used, but the latest V2.0 is only available as pre-release and therefore may not be usable depending on the environment. As Swagger uses Newtonsoft.Json as opposed to the standard function packages which is System.Text.Json. To enable Swagger, both Newtonsoft.Json and System.Text.Json would have to be added, leading to duplication of attributes on properties.&lt;/p&gt;

&lt;p&gt;Secondly, and arguably a bigger issue, is the lack of the ControllerBase. This prevents you being able to set up controllers within your API. On top of that, the Function middleware does not handle the FromBody attribute—if used, you will simply get a null body. You obviously can develop a workaround and create your own packages to add the required support. This is what my colleagues have done to get the functionality by creating a FunctionBase, JsonBodyAttribute, and a JsonBodyExceptionMiddleware.&lt;/p&gt;

&lt;p&gt;Lastly, without the ability to have proper controllers, you're left with having to have ever-growing function files to have each endpoint. Where each endpoint that is required for our API becomes its own function. This is overly cumbersome and difficult to manage. We would also end up with a lot of repeated code for the set-up of each function, meaning it does not follow the DRY principle—one of the core principles of software development.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Honestly, can anyone say that this is a great idea long term?&lt;/em&gt; Personally, I don't think it is. You are creating a heavy reliance on these packages that you as an organisation or team are maintaining. Increasing your maintenance overhead and likely creating technical debt, that all developers know deep down will never have the time to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Well, what about a Web App instead? (my preferred option)
&lt;/h2&gt;

&lt;p&gt;We can get the same networking options with VNET integration and private endpoints—as long as we use the right App Service Plan.&lt;/p&gt;

&lt;p&gt;We also get the scaling options, metric-driven rather than event-driven. They do work as we would need them to, but it is generally slower to scale than event-driven. From personal experience it works fine—yes, not as fast, but it is still quick enough. Tip: You'll likely want to set the rules slightly lower to scale potentially earlier than required, but it helps avoid the potential delay.&lt;/p&gt;

&lt;p&gt;My favourite benefit is the ability to develop the API in a more standard, and arguably more maintainable way. This allows us to develop an arguably more appropriate RESTful API. We can use standard controllers to handle requests, which also enables automatic Swagger generation. We can follow the best practices, using attributes and middleware as well as being able to follow standard MVC patterns. &lt;em&gt;Great—no workarounds required!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Local development is easier for developing a controller-based API. We get proper native support within IDEs such as Visual Studio or Rider. We can run the API locally with functionality like hot reload and without the need for workarounds for Function dependencies.&lt;/p&gt;

&lt;p&gt;We also have the added flexibility to easily deploy the system to another hosting provider, be that on-premises or another cloud provider. Debugging is also easier and more intuitive as we can get more detailed stack traces and logging.&lt;/p&gt;

&lt;p&gt;Whilst we can build middleware for hosting on a Function App, a lot of this is ready to go with a standard web API. Things such as exception handling and properly bubbling up errors for an application layer are easier to handle.&lt;/p&gt;

&lt;p&gt;We have better ability to add authentication and authorisation to the API. For example, we can use the native &lt;code&gt;[authorize]&lt;/code&gt; attribute to protect endpoints. We can't do this on a Function App—we could have used &lt;code&gt;AuthorizationLevel&lt;/code&gt; but this does not give us the same functionality as on a web app. In our web app we have native support for authentication such as OAuth2, OpenID Connect and JWT Bearer tokens. This includes support for multiple authentication providers, primarily for us the use of Azure Entra.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what does the cost really look like?
&lt;/h2&gt;

&lt;p&gt;Any of the costs suggested are based on the basic settings on the pricing calculator and are correct as of the day of writing. Based on a Windows-hosted application, premium functionality and currently ignoring the new flex consumption plan we have...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Function App: EP1 SKU - £122.89&lt;/li&gt;
&lt;li&gt;App Service: P0V3 SKU - £88.50&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Okay, so on the face of it the App Service is cheaper, right? Then you've got to think about the flex consumption plan. The pricing here is not yet on the calculator, and is honestly a bit harder to calculate. Firstly: "Flex consumption plan pricing includes a monthly free grant of 250,000 executions and 100,000 GB-s of resource consumption per month per subscription in pay-as-you-go on-demand pricing across all function apps in that subscription." &lt;a href="https://azure.microsoft.com/en-gb/pricing/details/functions/" rel="noopener noreferrer"&gt;Azure Functions Pricing&lt;/a&gt;So you'd get a reasonable amount of free hosting; you do, however, have to take into account the storage account needed for hosting the function as this is not included. You would also need to consider that if you have any always-ready instances these will use up the free quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose?
&lt;/h2&gt;

&lt;p&gt;You can probably tell my preference, but when it comes to choosing between the two options you have to consider a few things and, realistically, a choice and standard should be set across either a development team or the organisation. It is not a good idea to have a mix of the two within a team—but I'll talk about that another day.&lt;/p&gt;

&lt;p&gt;Nine times out of ten, if you are building a fully fledged API then going for a Web App and a standard RESTful .NET API is the way to go. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Well-structured and maintainable code.&lt;/li&gt;
&lt;li&gt;Great implementation of controllers with middleware and Swagger support.&lt;/li&gt;
&lt;li&gt;Simpler local development workflow.&lt;/li&gt;
&lt;li&gt;More portable and flexible codebase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Function Apps do have their place; they are great for lightweight, event-driven implementations. This could include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timer or event-triggered tasks.&lt;/li&gt;
&lt;li&gt;Background processing tasks.&lt;/li&gt;
&lt;li&gt;Webhooks or lightweight API endpoints.&lt;/li&gt;
&lt;li&gt;Offloading asynchronous tasks from a main application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So back to the debate: 'Can we build entire APIs in a Function App?' Yes. Should we? Realistically, no. If your team or organisation has a current standard of Function Apps for APIs then it is likely worth a discussion like we are having.&lt;/p&gt;

&lt;p&gt;A possible suggestion of how to choose from a biased viewpoint: Your mileage may vary, and every use case can be different. I know I would always recommend a Web App for a full API, but you should always consider the specific requirements of the project. Function Apps may be lightweight and may seem 'cheaper', but if you are looking to build a fully featured API or trying to future-proof, then a Web App is likely the better choice.&lt;/p&gt;

&lt;p&gt;I'd be interested to hear your thoughts, agree or disagree? Feel free to reach out on LinkedIn and let me know!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Versioned .NET APIs with Swagger UI</title>
      <dc:creator>Matthew Thomas</dc:creator>
      <pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/mthomas564/versioned-net-apis-with-swagger-ui-2m09</link>
      <guid>https://dev.to/mthomas564/versioned-net-apis-with-swagger-ui-2m09</guid>
      <description>&lt;p&gt;Creating a follow up to my blog on &lt;a href="https://dev.to/blog/dotnet-api-versioning-apim"&gt;.NET API Versioning in Azure API Management&lt;/a&gt;, I wanted to cover how to set up Swagger UI for versioned APIs in .NET.&lt;/p&gt;

&lt;p&gt;When developing an API and working locally, being able to have an interface to interact with the API is incredibly useful. There are quite a few options that can be used, but &lt;a href="https://swagger.io/tools/swagger-ui/" rel="noopener noreferrer"&gt;Swagger UI&lt;/a&gt; is one of the most popular.&lt;/p&gt;

&lt;p&gt;If you set up versioning on the API without trimming and editing paths, then it's very likely to just work as it should. However, if you've followed the steps in my previous post, you've got customised OpenAPI spec files which don't really follow the standard Swagger expectations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Swagger UI with Custom Versioning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 - Install Swagger UI
&lt;/h3&gt;

&lt;p&gt;The first thing to do is to install Swagger UI in the project. The default way to do this would be to install the whole Swashbuckle package, however as we are using the Microsoft.AspNetCore.OpenApi packages, having both prevents the spec files being generated.&lt;/p&gt;

&lt;p&gt;Instead install just the Swagger UI Package:&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 Microsoft.AspNetCore.OpenApi.SwaggerUI

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 - Enable Swagger UI in Program.cs
&lt;/h3&gt;

&lt;p&gt;Now we have installed the package, it needs to configured in the &lt;code&gt;Program.cs&lt;/code&gt; file:&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;app&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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapOpenApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwaggerUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwaggerEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/openapi/v1.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"API Version 1"&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;SwaggerEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/openapi/v2.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"API Version 2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoutePrefix&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="c1"&gt;// Serve Swagger at the app's root&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 first thing to note here is making sure this is wrapped in the &lt;code&gt;if (app.Environment.IsDevelopment())&lt;/code&gt; block. This is to make sure we don't expose the Swagger UI or OpenAPI specs when the application is running in production.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;app.MapOpenApi()&lt;/code&gt; call is used to map the OpenAPI endpoints, making sure they are available for the Swagger UI to consume.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;app.UseSwaggerUI()&lt;/code&gt; call is where we configure the Swagger UI. The &lt;code&gt;options.SwaggerEndpoint()&lt;/code&gt; method is used to specify the OpenAPI spec files that the Swagger UI will use. In this case, it is specified twice as there are two versions of the API, for each new version a new line will need to be added.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;options.RoutePrefix = string.Empty;&lt;/code&gt; line is used to serve the Swagger UI at the root of the application, this is also often set to &lt;code&gt;/swagger&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 - Test Swagger UI
&lt;/h3&gt;

&lt;p&gt;Now that we've enabled Swagger UI, we should be able to run the application and see our versions.&lt;/p&gt;

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

&lt;p&gt;Perfect, the versions are there as we expected.&lt;/p&gt;

&lt;p&gt;The problem is that when we try to test the API, we get a 404 because the UI is trying to access the API at the wrong path. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjs802vurzrfnjbg09p7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxjs802vurzrfnjbg09p7.png" alt="alt text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 - Update Swagger UI to Use Correct Paths
&lt;/h3&gt;

&lt;p&gt;To fix this, we need to update our document transformer to set the API paths correctly for the server paths. This then enables the Swagger UI to have the correct server path and be able to call the API.&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;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Servers&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OpenApiServer&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="s"&gt;$"/api/&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;DocumentName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This clears the servers set by default and overrides them with corrected ones to fit the version paths. This is done based on the document name, which in the example project is the version, e.g. &lt;code&gt;v1&lt;/code&gt; or &lt;code&gt;v2&lt;/code&gt;. To see the full file, check out the &lt;a href="https://github.com/MThomas564/APIVersionTest/blob/main/OpenAPIPathTransform.cs" rel="noopener noreferrer"&gt;OpenAPIPathTransform.cs&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 - Test the Swagger UI Again
&lt;/h3&gt;

&lt;p&gt;Now that we've updated the Swagger UI to use the correct paths, we can test it again. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9fhr0ya9oqru6h1e702.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9fhr0ya9oqru6h1e702.png" alt="alt text"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;As simple as that, we have a functional Swagger UI that supports our versioned API.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>development</category>
      <category>api</category>
      <category>apim</category>
    </item>
  </channel>
</rss>
