<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-AU" xmlns:media="http://search.yahoo.com/mrss/">
  <id>https://david.gardiner.net.au/feed.xml</id>
  <title type="html">David Gardiner</title>
  <updated>2026-04-07T00:21:11.217Z</updated>
  <subtitle>A blog of software development, .NET and other interesting things</subtitle>
  <rights>Copyright 2026 David Gardiner</rights>
  <icon>https://www.gravatar.com/avatar/37edf2567185071646d62ba28b868fab?s=64</icon>
  <logo>https://www.gravatar.com/avatar/37edf2567185071646d62ba28b868fab?s=256</logo>
  <generator uri="https://github.com/flcdrg/astrojs-atom" version="3">astrojs-atom</generator>
  <author>
    <name>David Gardiner</name>
  </author>
  <link href="https://david.gardiner.net.au/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://david.gardiner.net.au/" rel="alternate" type="text/html" hreflang="en-AU"/>
  <category term=".NET"/>
  <category term="Software Development"/>
  <category term="Azure"/>
  <category term="DevOps"/>
  <entry>
    <id>https://david.gardiner.net.au/2026/04/exceptional-unit-tests</id>
    <updated>2026-04-01T09:30:00.000+10:30</updated>
    <title>Exceptional unit tests</title>
    <link href="https://david.gardiner.net.au/2026/04/exceptional-unit-tests" rel="alternate" type="text/html" title="Exceptional unit tests"/>
    <category term=".NET"/>
    <category term="Testing"/>
    <published>2026-04-01T09:30:00.000+10:30</published>
    <summary type="html">Unexpected exceptions being thrown and caught inside application
code, that weren&apos;t obvious until the unit test was run under a debugger.</summary>
    <content type="html">&lt;p&gt;I was working on a .NET application that had a nice suite of unit tests, and pleasingly the tests were all passing.&lt;/p&gt;
&lt;p&gt;While making a change to the code, one of the tests failed (which is the whole point of having tests!). To better understand the reason for the failure I re-ran the test in the Visual Studio debugger.&lt;/p&gt;
&lt;p&gt;I noticed something strange - the system under test was throwing a &lt;code&gt;NullReferenceException&lt;/code&gt;, which was then being caught (and effectively swallowed) by an outer &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; block.&lt;/p&gt;
&lt;p&gt;The surprising thing was that this test should not have been doing that - a pretty straightforward test of some business logic. It was just a coincidence that in this case the exception wasn&apos;t changing the observable behaviour of the code, which is why the test had previously been passing.&lt;/p&gt;
&lt;p&gt;In this case, the underlying cause of the &lt;code&gt;NullReferenceException&lt;/code&gt; turned out to be a missing mocked method on a dependency.&lt;/p&gt;
&lt;p&gt;It did make me wonder though, are there other similar issues hidden elsewhere in the unit tests?&lt;/p&gt;
&lt;p&gt;To find out, I opened up Visual Studio&apos;s &lt;a href=&quot;https://learn.microsoft.com/visualstudio/debugger/managing-exceptions-with-the-debugger?view=visualstudio&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;Exception Settings window&lt;/a&gt; (&lt;strong&gt;Debug&lt;/strong&gt; | &lt;strong&gt;Windows&lt;/strong&gt; | &lt;strong&gt;Exception Settings&lt;/strong&gt;), searched for &apos;NullReferenceException&apos; and ensured that it was set to &apos;Break when thrown&apos;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/exception-settings.BskstRPv_Z1zU8Gz.webp&quot; alt=&quot;Screenshot of Exception Settings window in Visual Studio&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I then ran the entire test suite under the debugger (&lt;strong&gt;Test&lt;/strong&gt; | &lt;strong&gt;Debug All Tests&lt;/strong&gt;) and took note of each time the debugger stopped with a thrown exception. Some of these were other exception types that were expected (and I could turn off the &apos;Break when thrown&apos; on those if they were too noisy).&lt;/p&gt;
&lt;p&gt;I ended up finding a few other tests that had similar issues. There were also some paths in the application code where null handling could be made more robust.&lt;/p&gt;
&lt;p&gt;The tests still pass, but they should now be a bit more reliable in the future for the next developer who is relying on them when making application code changes (which could be me!)&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/exception-settings.BskstRPv.png" width="942" height="498"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/exception-settings.BskstRPv.png" width="942" height="498"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/03/delete-github-action-artifacts</id>
    <updated>2026-03-21T16:00:00.000+10:30</updated>
    <title>Delete old GitHub Actions artifacts with PowerShell</title>
    <link href="https://david.gardiner.net.au/2026/03/delete-github-action-artifacts" rel="alternate" type="text/html" title="Delete old GitHub Actions artifacts with PowerShell"/>
    <category term="GitHub"/>
    <category term="PowerShell"/>
    <published>2026-03-21T16:00:00.000+10:30</published>
    <summary type="html">My GitHub Actions artifact usage was nearing the maximum quota for the month, so I needed a script to delete old artifacts</summary>
    <content type="html">&lt;p&gt;I received an email from GitHub overnight saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You have used 90% of the Actions storage included for the flcdrg account&lt;/p&gt;
&lt;p&gt;Your plan includes 2 GB of Actions storage per month at no extra cost. You have used 90% so far this billing cycle. 1.8 GB used / 2 GB included&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Oh dear, that&apos;s not good. Time for some Spring (or Autumn as it is in Australia) cleaning!&lt;/p&gt;
&lt;p&gt;I found a useful post by &lt;a href=&quot;https://www.eliostruyf.com&quot;&gt;Elio Struyf&lt;/a&gt; - &lt;a href=&quot;https://www.eliostruyf.com/clean-github-actions-artifacts-script/&quot;&gt;Clean up old GitHub Actions artifacts with a script&lt;/a&gt;, which contains a Bash script to delete old artifacts. PowerShell is my preferred scripting language so I first asked Copilot to convert the Bash script to PowerShell.&lt;/p&gt;
&lt;p&gt;I then ran it on some repositories that I new had lots of artifacts, but noticed that the paging was not working quite right, and that it was skipping artifacts if it couldn&apos;t parse the date field.&lt;/p&gt;
&lt;p&gt;I made two changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read all the data in one go using the &lt;code&gt;--paginate --slurp&lt;/code&gt; parameters. This solves the problem that I think was happening when you read a page of results, then deleted them, and then asked the API for the next page, but the counts would now be out due to the deleted items.&lt;/li&gt;
&lt;li&gt;Ensure the date string is parsed using US date format (as it was defaulting to Australian format and then getting confused with dates that didn&apos;t make sense)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&apos;s the final script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;param(
    [Parameter(Position = 0)]
    [string]$Repo,

    [Parameter(Position = 1)]
    [int]$DaysOld = 5
)

if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
    Write-Error &quot;GitHub CLI (gh) is not installed or not available in PATH.&quot;
    exit 1
}

$null = gh auth status *&amp;gt; $null
if ($LASTEXITCODE -ne 0) {
    Write-Host &quot;Please authenticate with the GitHub CLI using &apos;gh auth login&apos;.&quot;
    exit 1
}

if ([string]::IsNullOrWhiteSpace($Repo)) {
    Write-Host &quot;Usage: .\delete-github-action-artifacts.ps1 &amp;lt;owner/repo&amp;gt; [days-old]&quot;
    Write-Host &quot;Example: .\delete-github-action-artifacts.ps1 owner/repo 5&quot;
    exit 1
}

Write-Host &quot;Cleaning up artifacts older than $DaysOld days for repository: $Repo&quot;

$pagesResponse = gh api --paginate --slurp -H &quot;Accept: application/vnd.github+json&quot; &quot;/repos/$Repo/actions/artifacts?per_page=100&quot; | ConvertFrom-Json
$allArtifacts = @()

foreach ($pageResponse in @($pagesResponse)) {
    if ($null -ne $pageResponse.artifacts) {
        $allArtifacts += @($pageResponse.artifacts)
    }
}

if (-not $allArtifacts -or $allArtifacts.Count -eq 0) {
    Write-Host &quot;No artifacts found.&quot;
}
else {
    foreach ($artifact in $allArtifacts) {
        $id = $artifact.id
        $name = $artifact.name
        $createdAt = $artifact.created_at

        if ($null -eq $id -or [string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($createdAt)) {
            $artifactJson = $artifact | ConvertTo-Json -Compress
            Write-Host &quot;Skipping invalid artifact data: $artifactJson&quot;
            continue
        }

        try {
            $createdAtUtc = [DateTimeOffset]::Parse($createdAt, [System.Globalization.CultureInfo]::InvariantCulture).UtcDateTime

            $ageDays = [int][Math]::Floor(([DateTime]::UtcNow - $createdAtUtc).TotalDays)

            if ($ageDays -gt $DaysOld) {
                Write-Host &quot;Deleting artifact: $name (ID: $id, Age: $ageDays days)&quot;
                $null = gh api -X DELETE &quot;/repos/$Repo/actions/artifacts/$id&quot; 2&amp;gt;$null

                if ($LASTEXITCODE -ne 0) {
                    Write-Host &quot;Failed to delete artifact: $name (ID: $id)&quot;
                }
            }
            else {
                Write-Host &quot;Keeping artifact: $name (ID: $id, Age: $ageDays days, Created At: $createdAt)&quot;
            }

        }
        catch {
            Write-Host &quot;Deleting artifact: $name (ID: $id, Created At: $createdAt)&quot;
            $null = gh api -X DELETE &quot;/repos/$Repo/actions/artifacts/$id&quot; 2&amp;gt;$null

            if ($LASTEXITCODE -ne 0) {
                Write-Host &quot;Failed to delete artifact: $name (ID: $id)&quot;
            }

        }

    }
}

Write-Host &quot;Cleanup completed.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I&apos;ve also published it as a GitHub Gist at &lt;a href=&quot;https://gist.github.com/flcdrg/f204fc3f84247fe6247d654c0a673b73&quot;&gt;https://gist.github.com/flcdrg/f204fc3f84247fe6247d654c0a673b73&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Prevention&lt;/h2&gt;
&lt;p&gt;The main cause of accumulating artifacts is by using the &lt;a href=&quot;https://github.com/marketplace/actions/upload-a-build-artifact&quot;&gt;&lt;code&gt;actions/upload-artifact&lt;/code&gt;&lt;/a&gt; action in GitHub Actions workflows. I&apos;ve now updated those actions to include the &lt;a href=&quot;https://github.com/marketplace/actions/upload-a-build-artifact#retention-period&quot;&gt;&lt;code&gt;retention-days&lt;/code&gt; property&lt;/a&gt; so that artifacts are automatically deleted after a few days.&lt;/p&gt;
&lt;p&gt;eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;      - name: Upload wrangler.jsonc
        uses: actions/upload-artifact@v7
        with:
          name: wrangler.jsonc
          path: wrangler.jsonc
          retention-days: 2
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/03/csharp-extension-members</id>
    <updated>2026-03-17T08:00:00.000+10:30</updated>
    <title>Best of C# 14 - extension members</title>
    <link href="https://david.gardiner.net.au/2026/03/csharp-extension-members" rel="alternate" type="text/html" title="Best of C# 14 - extension members"/>
    <category term=".NET"/>
    <published>2026-03-17T08:00:00.000+10:30</published>
    <summary type="html">We&apos;ve had extension methods in C# for a long time, but what are Extension Members?</summary>
    <content type="html">&lt;p&gt;Extension methods were introduced way back in 2007 with &lt;a href=&quot;https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-version-history?WT.mc_id=DOP-MVP-5001655#c-version-30&quot;&gt;C# 3&lt;/a&gt;. But they only allowed you to add methods to an instance type. Extension Members, a new feature of C# 14, finally &apos;extend&apos; this concept to properties and static members too.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/logo_csharp.CqWbN3Rf_Z2mqclD.webp&quot; alt=&quot;C# logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The classic extension method definition looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class MyExtensions
{
    public static int WordCount(this string str) =&amp;gt;
        str.Split([&apos; &apos;, &apos;.&apos;, &apos;?&apos;], StringSplitOptions.RemoveEmptyEntries).Length;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The class needed to be static, and the static method&apos;s first parameter needs the &lt;code&gt;this&lt;/code&gt; modifier.&lt;/p&gt;
&lt;p&gt;That style of extension method hasn&apos;t gone away, and you&apos;re free to keep doing it that way if you like.&lt;/p&gt;
&lt;p&gt;But now we can &lt;a href=&quot;https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14?WT.mc_id=DOP-MVP-5001655#extension-members&quot;&gt;create extension properties or extension methods on types&lt;/a&gt;. To do that there&apos;s a new  &lt;code&gt;extension&lt;/code&gt; keyword that is used to create extension blocks. You can use these for extension properties, and extension methods, and also for both of these for types in addition to instances.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class Enumerable
{
    // Extension block
    extension(string str) // extension members for string instances
    {
        // Extension method:
        public int WordCount() =&amp;gt;
            str.Split([&apos; &apos;, &apos;.&apos;, &apos;?&apos;], StringSplitOptions.RemoveEmptyEntries).Length;

        // Extension property:
        public bool Is80CharsLong =&amp;gt;
            str.Length == 80;
    }    

    extension(string)
    {
        // extension method
        public static string ToTitleCase(string str) =&amp;gt;
            System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str);

        // extension property
        public static string TwoSpaces =&amp;gt; &quot;  &quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You use these extension members like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var sentence = &quot;This is a sentence of words.&quot;;

int wordCount = sentence.WordCount();

var is80CharsLong = sentence.Is80CharsLong;

var title = string.ToTitleCase(&quot;hello world&quot;);

var indent = string.TwoSpaces;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s another useful language tool. Used appropriately it could make your codebase easier to understand and allow you to separate concerns.&lt;/p&gt;
&lt;h2&gt;Possible reasons for not using an &lt;code&gt;extension&lt;/code&gt; block&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;I&apos;ve read that some library authors encountered issues when they migrated their old extension methods to extension blocks and the libraries were targeting older frameworks besides .NET 10. If you need to target older frameworks then test this thoroughly first.&lt;/li&gt;
&lt;li&gt;If all you&apos;re doing is creating extension methods (no properties or type methods), then it&apos;s fine to stick with the old syntax.&lt;/li&gt;
&lt;/ul&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/logo_csharp.CqWbN3Rf.png" width="72" height="72"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/logo_csharp.CqWbN3Rf.png" width="72" height="72"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/03/mystery-files-app</id>
    <updated>2026-03-16T11:00:00.000+10:30</updated>
    <title>Where did this new Files app come from?</title>
    <link href="https://david.gardiner.net.au/2026/03/mystery-files-app" rel="alternate" type="text/html" title="Where did this new Files app come from?"/>
    <category term="Windows 11"/>
    <published>2026-03-16T11:00:00.000+10:30</published>
    <summary type="html">What is this new &apos;Files&apos; app that suddenly appeared on my Windows taskbar, and how
do you make it go away?</summary>
    <content type="html">&lt;p&gt;Recently when starting Windows 11 I noticed that a new icon had appeared on the taskbar. It wasn&apos;t too long after I&apos;d upgraded to 25H2 so my first assumption was it was part of that. The annoying thing was there wasn&apos;t an obvious way to make it go away.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/microsoft-365-companion-files-app.B5t6HI9k_Z1CqVRG.webp&quot; alt=&quot;Screenshot of Windows 11 taskbar with Files application running&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Right-clicking on the icon just gave an option to close the application, but it wasn&apos;t permanent. The next time I signed into Windows then there it was again. Kind of annoying.&lt;/p&gt;
&lt;p&gt;On my computer, I searched in &lt;strong&gt;Windows Settings&lt;/strong&gt; | &lt;strong&gt;Apps&lt;/strong&gt;, but no &apos;Files&apos; application was listed - very odd.&lt;/p&gt;
&lt;p&gt;Searching online for &quot;Windows 11 Files app&quot; brings up heaps of results, but none matched the icon that I was seeing. So I decided to to a bit of detective work to see if I could figure out what this thing was and where it came from. I looked in Task Manager, and sure enough there&apos;s a &lt;code&gt;Files.exe&lt;/code&gt; process running:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/microsoft-365-companion-task-manager.SCkv-gVC_ZAdpUx.webp&quot; alt=&quot;Screenshot of Windows Task Manager showing &apos;Files.exe&apos; process running&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Ah interesting - there&apos;s &quot;M365Companions&quot; in the path. Maybe adding that to my online search keywords might help? Yes, that made all the difference!&lt;/p&gt;
&lt;p&gt;I turned up this &lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-365-apps/companions/overview?WT.mc_id=DOP-MVP-5001655&quot;&gt;Overview of Microsoft 365 companion apps documentation&lt;/a&gt;. That&apos;s more of an IT admin page, but it links to a more end-user friendly &lt;a href=&quot;https://support.microsoft.com/en-au/office/get-started-with-microsoft-365-companions-a27df74a-cc41-4e74-8216-51091dc30194&quot;&gt;Getting started with Microsoft 365 companions page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That second page does say &quot;You can unpin or remove the apps from your taskbar at any time.&quot; which is obviously not correct. Not sure if that&apos;a bug in the software or the documentation.&lt;/p&gt;
&lt;p&gt;So it turns out there&apos;s 3 of these &apos;companion&apos; apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;People companion&lt;/strong&gt;: Find people in the organization (and others they have communicated with), pin close collaborators to accelerate workflows, and quickly learn about people in current meetings without disrupting workflow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Files companion&lt;/strong&gt;: Find your Microsoft 365 files, preview file contents, share files with colleagues, and access recently used documents.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calendar companion&lt;/strong&gt;: Access Microsoft 365 calendar, view upcoming events, join meetings, and search appointments directly from the taskbar.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;m sure someone will find them useful, but for now I just wanted them to go away.&lt;/p&gt;
&lt;p&gt;Armed with that extra information I went back to &lt;strong&gt;Windows Settings&lt;/strong&gt; | &lt;strong&gt;Apps&lt;/strong&gt; and this time searched for &apos;companion&apos;. Sure enough there is a &quot;Microsoft 365 companion apps&quot; app listed.&lt;/p&gt;
&lt;p&gt;Clicking on the &quot;...&quot; menu gives an option to &lt;strong&gt;Uninstall&lt;/strong&gt;, or if you click on &lt;strong&gt;Advanced options&lt;/strong&gt; you see something similar to this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/microsoft-365-companion-advanced.DQfOLick_1aDgez.webp&quot; alt=&quot;Screenshot of Advanced options page for &apos;Microsoft 365 companion apps&apos; app showing &apos;Files&apos; enabled for starting when user logs in&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For now I toggled &lt;strong&gt;Files&lt;/strong&gt; off, so it won&apos;t start automatically the next time I sign in.&lt;/p&gt;
&lt;p&gt;Maybe in the future I might want to make use of these apps, but for now that will do.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/microsoft-365-companion-files-app.B5t6HI9k.png" width="215" height="350"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/microsoft-365-companion-files-app.B5t6HI9k.png" width="215" height="350"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/03/csharp-field-keyword</id>
    <updated>2026-03-10T22:30:00.000+10:30</updated>
    <title>Best of C# 14 - field keyword</title>
    <link href="https://david.gardiner.net.au/2026/03/csharp-field-keyword" rel="alternate" type="text/html" title="Best of C# 14 - field keyword"/>
    <category term=".NET"/>
    <published>2026-03-10T22:30:00.000+10:30</published>
    <summary type="html">The new &apos;field&apos; keyword is in C# 14. When can you use it and when might you not.</summary>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14?WT.mc_id=DOP-MVP-5001655&quot;&gt;C# 14&lt;/a&gt; shipped back in November last year along with .NET 10. I&apos;ve already given a presentation a number of times on my highlights of .NET 10, which included three features of C# 14 that I think are particularly useful.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/logo_csharp.CqWbN3Rf_Z2mqclD.webp&quot; alt=&quot;C# logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The first I&apos;m going to focus on is the new &lt;code&gt;field&lt;/code&gt; keyword.&lt;/p&gt;
&lt;p&gt;The problem this solves is where you&apos;d really like to avoid having to declare a backing field, but prior to C# 14 as soon as you wanted to include any kind of logic in a getter or setter, then you couldn&apos;t use &lt;a href=&quot;https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties?WT.mc_id=DOP-MVP-5001655&quot;&gt;auto properties&lt;/a&gt;, but instead you needed to provide a backing field and implement the getter and setter. eg.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private string _msg;
public string Message
{
    get =&amp;gt; _msg;
    set =&amp;gt; _msg = value ?? throw new ArgumentNullException(nameof(value));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because we&apos;re adding validation to the setter, that forced us to introduce the &lt;code&gt;_msg&lt;/code&gt; field. And if you can be disciplined that&apos;s fine, but the trap is there&apos;s nothing to stop other code in the same class from also referencing that, even if it shouldn&apos;t. The compiler won&apos;t stop you.&lt;/p&gt;
&lt;p&gt;To help with scenarios like this, and to reduce the amount of code you need to write, C# 14 introduces the &lt;code&gt;field&lt;/code&gt; keyword. It&apos;s a way to reference the compiler-generated backing field, but only within the property&apos;s getter and setter. Our code now becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public string Message
{
    get;
    set =&amp;gt; field = value ?? throw new ArgumentNullException(nameof(value));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our line count is one less, plus we&apos;re now not exposing the backing field outside of the property.&lt;/p&gt;
&lt;h2&gt;Under the hood&lt;/h2&gt;
&lt;p&gt;When the C# compiler turns the original code into IL (Intermediate Language), it creates something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.field private string _msg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s how IL represents a regular field.&lt;/p&gt;
&lt;p&gt;And if you search the IL generated by code that uses the &lt;code&gt;field&lt;/code&gt; keyword, you&apos;ll still see a &lt;code&gt;.field&lt;/code&gt; entry, but the name is a bit different:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.field private string &apos;&amp;lt;Message2&amp;gt;k__BackingField&apos;
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )
    .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState)
      = (01 00 00 00 00 00 00 00 )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Message2&amp;gt;k__BackingField&lt;/code&gt; is not a valid C# identifier, so there&apos;s no chance of being able to reference this field by name elsewhere in your C# code. That &lt;code&gt;DebuggerBrowsableAttribute&lt;/code&gt; also means that your debugger will choose to not show the field.&lt;/p&gt;
&lt;p&gt;But because the compiler knows the name it has assigned to the field, it generates the correct IL for you when you use the &lt;code&gt;field&lt;/code&gt; keyword.&lt;/p&gt;
&lt;p&gt;Here&apos;s the IL generated for the property setter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IL_0000: ldarg.0      // this
IL_0001: ldarg.1      // &apos;value&apos;
IL_0002: dup

IL_0003: brtrue.s     IL_0011
IL_0005: pop
IL_0006: ldstr        &quot;value&quot;
IL_000b: newobj       instance void [System.Runtime]System.ArgumentNullException::.ctor(string)
IL_0010: throw
IL_0011: stfld        string Stuff::&apos;&amp;lt;Message&amp;gt;k__BackingField&apos;
IL_0016: ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/dotnet/api/system.reflection.emit.opcodes.stfld?view=net-10.0&amp;amp;WT.mc_id=DOP-MVP-5001655&quot;&gt;&lt;code&gt;stfld&lt;/code&gt;&lt;/a&gt; instruction is saving the current value from the stack into the the compiler-generated backing field.&lt;/p&gt;
&lt;h2&gt;Reasons for not using &lt;code&gt;field&lt;/code&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;If you have legitimate reasons for accessing the backing field elsewhere in the class (such as multiple properties that reference or share the same backing field).&lt;/li&gt;
&lt;li&gt;Thread safety using &lt;code&gt;volatile&lt;/code&gt;, &lt;code&gt;Interlocked&lt;/code&gt; or &lt;code&gt;lock&lt;/code&gt; patterns.&lt;/li&gt;
&lt;li&gt;Lazy initialisation/caching patterns&lt;/li&gt;
&lt;li&gt;Serialization or reflection that makes assumptions about field names&lt;/li&gt;
&lt;/ul&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/logo_csharp.CqWbN3Rf.png" width="72" height="72"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/logo_csharp.CqWbN3Rf.png" width="72" height="72"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/02/azure-postgresql-upgrade</id>
    <updated>2026-02-28T13:00:00.000+10:30</updated>
    <title>Upgrading Azure Database for PostgreSQL flexible server</title>
    <link href="https://david.gardiner.net.au/2026/02/azure-postgresql-upgrade" rel="alternate" type="text/html" title="Upgrading Azure Database for PostgreSQL flexible server"/>
    <category term="Azure"/>
    <category term="Azure Pipelines"/>
    <category term="Terraform"/>
    <published>2026-02-28T13:00:00.000+10:30</published>
    <summary type="html">How to upgrade the PostgreSQL server in Azure, with examples using Terraform, and some workarounds
for known issues you may encounter during the upgrade process.</summary>
    <content type="html">&lt;p&gt;I was working on a project recently that made use of &lt;a href=&quot;https://learn.microsoft.com/azure/postgresql/overview?WT.mc_id=DOP-MVP-5001655&quot;&gt;Azure Database for PostgreSQL flexible server&lt;/a&gt;. The system had been set up a while ago, and so when I was reviewing the resources in the Azure Portal, I noticed a warning banner for the PostreSQL server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Your server version will lose standard Azure support on March 31, 2026. Upgrade now to avoid extended support charges starting April 1, 2026.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/postgresql-upgrade-old-version.BsaaRaF4_2tq5ef.webp&quot; alt=&quot;Screenshot of Azure Portal showing PostgreSQL server with warning about standard support ending 31st March 2026&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Terraform was being used for Infrastructure as Code, and it looked similar to this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;resource &quot;azurerm_postgresql_flexible_server&quot; &quot;server&quot; {
  name                              = &quot;psql-postgresql-apps-australiaeast&quot;
  resource_group_name               = data.azurerm_resource_group.rg.name
  location                          = data.azurerm_resource_group.rg.location
  version                           = &quot;11&quot;
  delegated_subnet_id               = azurerm_subnet.example.id
  private_dns_zone_id               = azurerm_private_dns_zone.example.id
  public_network_access_enabled     = false
  administrator_login               = &quot;psqladmin&quot;
  administrator_password_wo         = ephemeral.random_password.postgresql_password.result
  administrator_password_wo_version = 1
  zone                              = &quot;1&quot;

  storage_mb   = 32768
  storage_tier = &quot;P4&quot;

  sku_name   = &quot;B_Standard_B1ms&quot;
  depends_on = [azurerm_private_dns_zone_virtual_network_link.example]
}
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;As you can see from the code and screenshot above, the PostgreSQL version in use was 11. Doing a bit of research, I found version 11 was &lt;a href=&quot;https://www.postgresql.org/support/versioning/&quot;&gt;first released back in 2018&lt;/a&gt;, and the the final minor update 11.22 was released in 2023.&lt;/p&gt;
&lt;p&gt;Azure provides standard support for PostgreSQL versions (documented at &lt;a href=&quot;https://learn.microsoft.com/azure/postgresql/configure-maintain/concepts-version-policy?WT.mc_id=DOP-MVP-5001655&quot;&gt;Azure Database for PostgreSQL version policy&lt;/a&gt;). There is also the option of paying for &lt;a href=&quot;https://learn.microsoft.com/azure/postgresql/configure-maintain/extended-support?WT.mc_id=DOP-MVP-5001655&quot;&gt;extended support&lt;/a&gt;, though in the case of v11 that only gets you to November this year, so just a few extra months.&lt;/p&gt;
&lt;p&gt;In my case, I wanted to do a test of the upgrade process first, so I restored a backup of the existing server to a new resource. This essentially creates an exact copy of the server at the same version.&lt;/p&gt;
&lt;p&gt;While we are using Infrastructure as Code, I decided to use the Azure Portal to test the upgrade, as I figured if there were any problems, they might be easier to understand, rather than try and interpret weird Terraform/AzureRM errors.&lt;/p&gt;
&lt;p&gt;Following the &lt;a href=&quot;https://learn.microsoft.com/azure/postgresql/configure-maintain/how-to-perform-major-version-upgrade?WT.mc_id=DOP-MVP-5001655&quot;&gt;upgrade documentation&lt;/a&gt;, I clicked on the &lt;strong&gt;Upgrade&lt;/strong&gt; in the Portal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/postgresql-upgrade-portal1.WLqDAiM5_ZscVNA.webp&quot; alt=&quot;Screenshot of Azure Portal upgrade screen&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This initiates a deployment, which depending on how much data you have and the particular SKU you&apos;re running on (eg. how fast the VM you&apos;re using is), this may take quite a while. One time it took over an hour (which was important as that may be longer than the default Terraform lifecycle, and also the pipeline job timeouts).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/postgresql-upgrade-progress.CC7oJ8mA_Z1fvKmn.webp&quot; alt=&quot;Screenshot of Azure Portal showing PostgreSQL resource with upgrade in progress&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If that succeeds, then you should be good to try the real thing with IaC.&lt;/p&gt;
&lt;h2&gt;Upgrading with Terraform&lt;/h2&gt;
&lt;p&gt;To upgrade a major version with Terraform, you need to make a couple of changes to your &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/postgresql_flexible_server&quot;&gt;&lt;code&gt;azurerm_postgresql_flexible_server&lt;/code&gt;&lt;/a&gt; resource:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;version&lt;/code&gt; property should be updated to the desired version&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;create_mode&lt;/code&gt; property should be set to &lt;code&gt;Update&lt;/code&gt; (if it wasn&apos;t specified then the default is &apos;Default&apos;)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;resource &quot;azurerm_postgresql_flexible_server&quot; &quot;server&quot; {
  name                              = &quot;psql-postgresql-apps-australiaeast&quot;
  resource_group_name               = data.azurerm_resource_group.rg.name
  location                          = data.azurerm_resource_group.rg.location
  version                           = &quot;17&quot;
  delegated_subnet_id               = azurerm_subnet.example.id
  private_dns_zone_id               = azurerm_private_dns_zone.example.id
  public_network_access_enabled     = false
  administrator_login               = &quot;psqladmin&quot;
  administrator_password_wo         = ephemeral.random_password.postgresql_password.result
  administrator_password_wo_version = 1
  zone                              = &quot;1&quot;
  create_mode                       = &quot;Update&quot;

  storage_mb   = 32768
  storage_tier = &quot;P4&quot;

  sku_name   = &quot;B_Standard_B1ms&quot;
  depends_on = [azurerm_private_dns_zone_virtual_network_link.example]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The weird thing (which I assume is a side-effect of Terraform state) is that even after you&apos;ve completed the upgrade, you can&apos;t change &lt;code&gt;create_mode&lt;/code&gt; back to &lt;code&gt;Default&lt;/code&gt; - Terraform will throw an error if you try that. Instead you just need to leave it set to &lt;code&gt;Update&lt;/code&gt;, but as long as the &lt;code&gt;version&lt;/code&gt; property doesn&apos;t change then Terraform will leave it at the same version.&lt;/p&gt;
&lt;h3&gt;Adjust your timeouts&lt;/h3&gt;
&lt;p&gt;I was using Azure Pipelines, so I added a &lt;code&gt;timeoutInMinutes&lt;/code&gt; property to the job and set it to 90 minutes. Be aware that there are &lt;a href=&quot;https://learn.microsoft.com/azure/devops/pipelines/process/phases?view=azure-devops&amp;amp;tabs=yaml&amp;amp;WT.mc_id=DOP-MVP-5001655#timeouts&quot;&gt;different default and maximum timeouts&lt;/a&gt; depending on what kind of build agent you use.&lt;/p&gt;
&lt;p&gt;Likewise the Terraform &lt;code&gt;azurerm_postgresql_flexible_server&lt;/code&gt; resource has &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/postgresql_flexible_server#timeouts&quot;&gt;default timeouts&lt;/a&gt;. You may want to specify a &lt;code&gt;timeout&lt;/code&gt; block to extend those values if necessary.&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;p&gt;I hit some compatibility issues with the PostgreSQL instance I was attempting to upgrade. The Portal displayed the following error(s):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The major version upgrade failed precheck. Upgrading shared_preload_libraries library pg_failover_slots from source version 11 to target version 17 is not supported.;
Upgrading shared_preload_libraries library pg_failover_slots from source version 11 to target version 17 is not supported.;
Upgrading shared_preload_libraries library pg_failover_slots from source version 11 to target version 17 is not supported.;
Upgrading shared_preload_libraries library pg_failover_slots from source version 11 to target version 17 is not supported.;
Upgrading shared_preload_libraries library pg_failover_slots from source version 11 to target version 17 is not supported.;
Upgrading with password authentication mode enabled is not allowed from source version MajorVersion11. Please enable SCRAM and reset the passwords prior to retrying the upgrade.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s two issues here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;pg_failover_slots&lt;/code&gt; shared preloaded library is &lt;a href=&quot;https://learn.microsoft.com/answers/questions/5730837/attempt-to-upgrade-azure-database-for-postgresql-f&quot;&gt;not supported for upgrading&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Legacy MD5 passwords are deprecated in newer versions, &lt;a href=&quot;https://techcommunity.microsoft.com/blog/azuredbsupport/azure-postgresql-lesson-learned-6-major-upgrade-blocked-by-password-auth-the-one/4469545&quot;&gt;and &quot;SCRAM&quot; needs to be enabled&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How do we resolve these with Infrastructure as Code? In this case as we&apos;re using Terraform, we need to map/import those settings and then we can modify them. We make use of the &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/postgresql_flexible_server_configuration&quot;&gt;&lt;code&gt;azurerm_postgresql_flexible_server_configuration&lt;/code&gt;&lt;/a&gt; resource for this.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;value&lt;/code&gt; properties should initially match the existing values (eg. Make sure that Terraform thinks they are unchanged). A trick to get the existing values is to run the Terraform in &apos;plan&apos; mode and take note of what values it can see from and then copy those into your code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  to = azurerm_postgresql_flexible_server_configuration.accepted_pasword_auth_method
  id = &quot;${azurerm_resource_group.group.id}/providers/Microsoft.DBforPostgreSQL/flexibleServers/psql-postgresql-apps-australiaeast/configurations/azure.accepted_password_auth_method&quot;
}

resource &quot;azurerm_postgresql_flexible_server_configuration&quot; &quot;accepted_pasword_auth_method&quot; {
  name      = &quot;azure.accepted_password_auth_method&quot;
  server_id = azurerm_postgresql_flexible_server.server.id
  value     = &quot;md5&quot;
}

import {
  to = azurerm_postgresql_flexible_server_configuration.password_encryption
  id = &quot;${azurerm_resource_group.group.id}/providers/Microsoft.DBforPostgreSQL/flexibleServers/psql-postgresql-apps-australiaeast/configurations/password_encryption&quot;
}

resource &quot;azurerm_postgresql_flexible_server_configuration&quot; &quot;password_encryption&quot; {
  name      = &quot;password_encryption&quot;
  server_id = azurerm_postgresql_flexible_server.server.id
  value     = &quot;md5&quot;
}

import {
  to = azurerm_postgresql_flexible_server_configuration.shared_preload_libraries
  id = &quot;${azurerm_resource_group.group.id}/providers/Microsoft.DBforPostgreSQL/flexibleServers/psql-postgresql-apps-australiaeast/configurations/shared_preload_libraries&quot;
}

resource &quot;azurerm_postgresql_flexible_server_configuration&quot; &quot;shared_preload_libraries&quot; {
  name      = &quot;shared_preload_libraries&quot;
  server_id = azurerm_postgresql_flexible_server.server.id
  value     = &quot;anon,auto_explain,pg_cron,pg_failover_slots,pg_hint_plan,pg_partman_bgw,pg_prewarm,pg_stat_statements,pgaudit,pglogical,timescaledb,wal2json&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you&apos;ve got those in place then you can make the changes to remove the upgrade block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource &quot;azurerm_postgresql_flexible_server_configuration&quot; &quot;accepted_pasword_auth_method&quot; {
  name      = &quot;azure.accepted_password_auth_method&quot;
  server_id = azurerm_postgresql_flexible_server.server.id
  value     = &quot;md5,SCRAM-SHA-256&quot;
}

resource &quot;azurerm_postgresql_flexible_server_configuration&quot; &quot;password_encryption&quot; {
  name      = &quot;password_encryption&quot;
  server_id = azurerm_postgresql_flexible_server.server.id
  value     = &quot;SCRAM-SHA-256&quot;
}

resource &quot;azurerm_postgresql_flexible_server_configuration&quot; &quot;shared_preload_libraries&quot; {
  name      = &quot;shared_preload_libraries&quot;
  server_id = azurerm_postgresql_flexible_server.server.id
  value     = &quot;anon,auto_explain,pg_cron,pg_hint_plan,pg_partman_bgw,pg_prewarm,pg_stat_statements,pgaudit,pglogical,timescaledb,wal2json&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will allow any existing MD5 passwords to continue to work, but any new passwords will use the more modern SCRAM-SHA-256.&lt;/p&gt;
&lt;p&gt;For the &lt;code&gt;shared_preload_libraries&lt;/code&gt;, we&apos;ve removed the offending &lt;code&gt;pg_failover_slots&lt;/code&gt; from the list.&lt;/p&gt;
&lt;h2&gt;Tips&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Temporarily upgrade your server SKU to beefier hardware so the upgrade goes faster. If you&apos;re using IaC then make sure you use that to make the change.&lt;/li&gt;
&lt;li&gt;Note that if you change the separate storage performance tier (IOPS), &lt;a href=&quot;https://learn.microsoft.com/azure/virtual-machines/disks-performance-tiers?tabs=azure-cli#restrictions&quot;&gt;you will need to wait 12 hours before downgrading again&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Completion&lt;/h2&gt;
&lt;p&gt;If everything goes to plan, you should end up with your PostgreSQL resource upgraded to the version that you specified. Here&apos;s my resource upgraded to 17.7. &lt;a href=&quot;https://techcommunity.microsoft.com/blog/adforpostgresql/postgresql-18-now-ga-on-azure-postgres-flexible-server/4469802?WT.mc_id=DOP-MVP-5001655&quot;&gt;v18 is actually available&lt;/a&gt; but I wasn&apos;t offered it due to &apos;regional capacity constraints&apos;, which explains why the &apos;Upgrade&apos; button is now disabled.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/postgresql-upgrade-complete.BDSml29o_Z3kvTd.webp&quot; alt=&quot;Screenshot of Azure Portal showing PostgreSQL upgrade complete&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve published source code for a working example of Azure Database for PostgreSQL flexible server with an Azure Container app and using a VNet at &lt;a href=&quot;https://github.com/flcdrg/terraform-azure-postgresql-containerapps&quot;&gt;https://github.com/flcdrg/terraform-azure-postgresql-containerapps&lt;/a&gt;&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/postgresql-logo.BZ7GfDHR.png" width="540" height="557"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/postgresql-logo.BZ7GfDHR.png" width="540" height="557"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/02/ddd-melbourne</id>
    <updated>2026-02-23T17:30:00.000+10:30</updated>
    <title>DDD Melbourne 2026</title>
    <link href="https://david.gardiner.net.au/2026/02/ddd-melbourne" rel="alternate" type="text/html" title="DDD Melbourne 2026"/>
    <category term="Conferences"/>
    <category term="Travel"/>
    <published>2026-02-23T17:30:00.000+10:30</published>
    <summary type="html">David was in Melbourne on the weekend so he could attend the DDD Melbourne 2026 conference. Not only was it good, but he&apos;s written up his notes from the sessions he attended too.</summary>
    <content type="html">&lt;p&gt;On Saturday I attended &lt;a href=&quot;https://dddmelbourne.com/&quot;&gt;DDD Melbourne 2026&lt;/a&gt;. It was great to be back again in Melbourne and attending a DDD conference (instead of organising one!). I got to catch up with some old friends, meet some people for the first time in person and hear some great talks.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_215116344_iOS.CoGQiXLC_ZEGHK6.webp&quot; alt=&quot;David&apos; attendee lanyard&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.sixpivot.com.au/&quot;&gt;SixPivot&lt;/a&gt; supported my travel and attendance, which was really appreciated. I also got to catch up with some of my colleagues, some at the conference and another later on Sunday.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_215247858_iOS.Dg8bCwbF_Z2k8KfU.webp&quot; alt=&quot;Lars Klint welcoming everyone to DDD&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I almost missed the start - turns out the curtains in my hotel room were extra effective at blocking out the light. I&apos;d gone back to sleep a few times, figuring it was still too early and then finally I figured I better check what time it actually was, and it was just after 8am! A mad scramble to get showered and dressed and race out the door. Quickly grabbed breakfast from McDonald&apos;s and then just made it through registration and grabbed a seat for the start. Just as well my hotel was right around the corner from the conference.&lt;/p&gt;
&lt;p&gt;The organising team did a fantastic job this year. The food was good and there was plenty of it - I&apos;d learned from last year that there was no need to queue - they wouldn&apos;t run out!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_041859330_iOS.DaX2mN2c_Z1419dR.webp&quot; alt=&quot;Attendees helping themselves to sausage rolls and scones with jam and cream&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I made handwritten notes for each talk I attended. I&apos;ve transcribed them here as a more legible record. Even I struggle to read my handwriting, so better to attempt that now rather than later! The notes made sense to me at the time, but whether that&apos;s true post event is another question 😃&lt;/p&gt;
&lt;h2&gt;AI: it&apos;s our responsibility - Sreyna Rath&lt;/h2&gt;
&lt;p&gt;Sreyna gave the keynote presentation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_220705825_iOS.roxDAU2a_6OKB4.webp&quot; alt=&quot;Sreyna Rath standing on stage presenting&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI is like a villain developer. A &quot;brilliant jerk&quot; - no values or empathy. No awareness of damage.&lt;/li&gt;
&lt;li&gt;Examples&lt;ul&gt;
&lt;li&gt;Gender bias baked in, racial bias&lt;/li&gt;
&lt;li&gt;Amazon hiring&lt;/li&gt;
&lt;li&gt;US Healthcare - used highest cost as proxy for health needs. Black people excluded more often&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI is a mirror&lt;ul&gt;
&lt;li&gt;Data reflects us&lt;/li&gt;
&lt;li&gt;Brilliant jerk culture&lt;/li&gt;
&lt;li&gt;Scale&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI family tree&lt;ul&gt;
&lt;li&gt;Dark side of the Internet&lt;/li&gt;
&lt;li&gt;Microsoft Tay&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://character.ai/&quot;&gt;Character.AI&lt;/a&gt; - suicide&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Jaimee - built for women &lt;a href=&quot;https://jaimee.ai/&quot;&gt;https://jaimee.ai/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;How do we fix that?&lt;/li&gt;
&lt;li&gt;Compassionate design&lt;ul&gt;
&lt;li&gt;Design for most vulnerable&lt;/li&gt;
&lt;li&gt;Who is most vulnerable?&lt;/li&gt;
&lt;li&gt;What&apos;s the worst thing feature?&lt;/li&gt;
&lt;li&gt;How do we prevent that harm?&lt;/li&gt;
&lt;li&gt;Ask right questions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Persona testing&lt;ul&gt;
&lt;li&gt;Test as different race, gender, age, abilities&lt;/li&gt;
&lt;li&gt;Different to you!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI reviewing AI&lt;ul&gt;
&lt;li&gt;First AI optimised to be helpful&lt;/li&gt;
&lt;li&gt;Second AI reviews the first - optimised for safety&lt;/li&gt;
&lt;li&gt;Eg. Is output sexualised when not requested?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Your &quot;Jedi&quot; toolkit - what kind of Jedi will you be?&lt;/li&gt;
&lt;li&gt;AI doesn&apos;t have values, it has patterns&lt;/li&gt;
&lt;li&gt;The good news: the villain can be reformed&lt;ul&gt;
&lt;li&gt;Jerk =&amp;gt; Jedi&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&quot;May the framework be with you&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_221828400_iOS.DXQzAAy1_Z1o7Ie6.webp&quot; alt=&quot;Slide: The origin story. AI isn&apos;t a crystal ball, it&apos;s a mirror&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_222729452_iOS.TyciaQAm_2vfEzP.webp&quot; alt=&quot;Slide: So how do we reform the villain?&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_223937079_iOS.C7epiWxT_26UPFB.webp&quot; alt=&quot;Slide: Three frameworks recap&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Code is a Conversation: What Are You Really Saying to the Next Developer? - Joel Gallagher&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_231611290_iOS.C97pRzFI_pohPq.webp&quot; alt=&quot;Joel standing behind lectern with title slide behind him&quot; /&gt;\&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code is read more often than written&lt;/li&gt;
&lt;li&gt;Cognitive load&lt;/li&gt;
&lt;li&gt;eg. contrast &quot;The DaVinci Code&quot; (simple, short sentences) vs &quot;A brief history of time&quot; (did anyone ever finish or understand it)&lt;/li&gt;
&lt;li&gt;Naming, structure, comments, commits&lt;ul&gt;
&lt;li&gt;Classes and variables = noun&lt;/li&gt;
&lt;li&gt;Functions = verb + noun&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Watch out for reserved words&lt;/li&gt;
&lt;li&gt;Function size and scope&lt;ul&gt;
&lt;li&gt;Refactor and extract methods&lt;/li&gt;
&lt;li&gt;Reduce depth/complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Comments&lt;ul&gt;
&lt;li&gt;Explain the why, not how&lt;/li&gt;
&lt;li&gt;Don&apos;t comment out code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Commits&lt;/li&gt;
&lt;li&gt;Name, scope, flow, consistency&lt;/li&gt;
&lt;li&gt;Tools&lt;ul&gt;
&lt;li&gt;Consistent standards&lt;/li&gt;
&lt;li&gt;Enforce standards&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Recommended reading&lt;ul&gt;
&lt;li&gt;Clean code&lt;/li&gt;
&lt;li&gt;Pragmatic Programmer (latest edition)&lt;/li&gt;
&lt;li&gt;Make your code more &quot;DaVinci&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_232820876_iOS.DnfK49Pq_2cXIfz.webp&quot; alt=&quot;Slide: Examples of confusing naming&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260220_234358278_iOS.CQpUgH0h_2h69yg.webp&quot; alt=&quot;Slide: Names, Scope, Flow, Consistency&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Managing for Failure - Amy Norris&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_004328729_iOS.Vm98GdJ-_20Qr3c.webp&quot; alt=&quot;Amy presenting&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Failure is good&lt;ul&gt;
&lt;li&gt;Website hacked&lt;/li&gt;
&lt;li&gt;Practice recovery&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;What does failure mean?&lt;ul&gt;
&lt;li&gt;&quot;An emotionally upsetting learning experience&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Why do you want failure?&lt;/li&gt;
&lt;li&gt;Hidden risks of &quot;perfect&quot; teams&lt;ul&gt;
&lt;li&gt;Slow to learn, or averse&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Feel more comfortable failing. What are you bad at&lt;/li&gt;
&lt;li&gt;Example&lt;ul&gt;
&lt;li&gt;Rocket game with faulty controller handle&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Rubber ducking&lt;/li&gt;
&lt;li&gt;Dig deep&lt;/li&gt;
&lt;li&gt;How to encourage teams to be more comfortable with failure&lt;ul&gt;
&lt;li&gt;Share personal failures&lt;/li&gt;
&lt;li&gt;Fire drills&lt;/li&gt;
&lt;li&gt;What not to do&lt;ul&gt;
&lt;li&gt;Punish behaviours you don&apos;t want to see&lt;/li&gt;
&lt;li&gt;Love your cool&lt;/li&gt;
&lt;li&gt;Push people too hard&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;How to keep your boss happy&lt;ul&gt;
&lt;li&gt;Less downtime&lt;/li&gt;
&lt;li&gt;Problems solved faster&lt;/li&gt;
&lt;li&gt;More cohesive team&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Alternatively - do whatever you want until someone yells at you!&lt;/li&gt;
&lt;li&gt;Low risk&lt;ul&gt;
&lt;li&gt;Board games (make sure they understand it isn&apos;t a test)&lt;/li&gt;
&lt;li&gt;Internal projects&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remember people don&apos;t know how to fail&lt;/li&gt;
&lt;li&gt;Helpful rule:&lt;ul&gt;
&lt;li&gt;THIRTY (minutes of no progress)&lt;/li&gt;
&lt;li&gt;FIFTEEN (minutes to do something else)&lt;/li&gt;
&lt;li&gt;FAIL (get fresh eyes)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set failure as the goal. eg. &quot;Get rejected by 10 conferences&quot;&lt;/li&gt;
&lt;li&gt;Over communicate&lt;ul&gt;
&lt;li&gt;Positive feedback&lt;/li&gt;
&lt;li&gt;Negative feedback&lt;/li&gt;
&lt;li&gt;Expectations&lt;/li&gt;
&lt;li&gt;Changes to plans (including why)&lt;/li&gt;
&lt;li&gt;Questions&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Overt positivity&lt;ul&gt;
&lt;li&gt;&quot;Good job me&quot; (without irony)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remember you&apos;re a trash can, not a trash can&apos;t&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_002436463_iOS.Ccr9rJGY_Z1XLKVC.webp&quot; alt=&quot;Slide: Amy&apos;s definition of failure - &amp;quot;An emotionally upsetting learning experience&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_002900113_iOS.BVHuYi1q_7NkwG.webp&quot; alt=&quot;Slide: I want YOU to fail&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_004628197_iOS.Cn7Os5Qa_1FJPyj.webp&quot; alt=&quot;Slide: 4 games that you could play as a team&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_004851896_iOS.FtQnrKd-_Z1HM5CG.webp&quot; alt=&quot;Slide: THIRTY, FIFTEEN, FAIL&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_005755157_iOS.mb7RVTSM_Z3LPqz.webp&quot; alt=&quot;Slide: Remember you&apos;re a trash can, not a trash can&apos;t&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;You Aren&apos;t Always Right: Resolving Technical Disagreements The Easy Way - &lt;a href=&quot;https://github.com/Zopolis4&quot;&gt;Max Downey Twiss&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_011705177_iOS.DYz92vtB_10v8Mi.webp&quot; alt=&quot;Max presenting with overview slide&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why is it important to resolve them?&lt;/li&gt;
&lt;li&gt;How to we resolve them?&lt;ul&gt;
&lt;li&gt;Reduce scope of disagreement&lt;/li&gt;
&lt;li&gt;Compare reasoning&lt;/li&gt;
&lt;li&gt;Bring in another perspective&lt;/li&gt;
&lt;li&gt;Change the circumstances&lt;/li&gt;
&lt;li&gt;Realise you are wrong!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Techniques&lt;ul&gt;
&lt;li&gt;PR examples&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;How not to resolve&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Designed To Fail: Building Resilient Applications - Callum Whyte&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_033041548_iOS.CH-1qZft_zTciP.webp&quot; alt=&quot;Callum with opening slide&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare outage&lt;/li&gt;
&lt;li&gt;CrowdStrike&lt;/li&gt;
&lt;li&gt;Microservices or Monolith&lt;/li&gt;
&lt;li&gt;Defining &quot;healthy&quot;&lt;ul&gt;
&lt;li&gt;200 OK&lt;/li&gt;
&lt;li&gt;Requests/second&lt;/li&gt;
&lt;li&gt;Request performance (better)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Calling an API&lt;ul&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Retry + Backoff&lt;/li&gt;
&lt;li&gt;Circuit breaker&lt;ul&gt;
&lt;li&gt;Opossum&lt;/li&gt;
&lt;li&gt;Polly&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Critical path&lt;ul&gt;
&lt;li&gt;Ecommerce&lt;/li&gt;
&lt;li&gt;Move to event-driven/fire and forget process&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Queues&lt;/li&gt;
&lt;li&gt;Replicas&lt;ul&gt;
&lt;li&gt;Healthcheck&lt;/li&gt;
&lt;li&gt;Not always reliable though&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&quot;Stand-in&quot; platform&lt;ul&gt;
&lt;li&gt;minimal platform running on failover cloud&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OpenTelemetry&lt;/li&gt;
&lt;li&gt;Chaos monkey/studio&lt;ul&gt;
&lt;li&gt;Polly - chaos simulator methods&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_033046845_iOS.CQ7MmpDd_WLiGo.webp&quot; alt=&quot;Callum Whyte presenting&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The Billion Dollar Blindspot: What happens when everyone thinks alike - Amanda Pitcher&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_044712272_iOS.B6MtNGF6_Z1znASp.webp&quot; alt=&quot;Amanda presenting&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Systems thinker vs spacial specialist&lt;/li&gt;
&lt;li&gt;DEIB - Diversity, equity, inclusion, belonging&lt;/li&gt;
&lt;li&gt;Just having policy may not equate to true diversity&lt;/li&gt;
&lt;li&gt;4 dimensions of diversity&lt;ul&gt;
&lt;li&gt;demographic (often covered by compliance/policy)&lt;/li&gt;
&lt;li&gt;cognitive&lt;/li&gt;
&lt;li&gt;functional&lt;/li&gt;
&lt;li&gt;behavioural&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Boeing 737 MAX&lt;ul&gt;
&lt;li&gt;GroupThink&lt;ul&gt;
&lt;li&gt;Confirmation bias risks&lt;/li&gt;
&lt;li&gt;Just because everyone agrees doesn&apos;t mean they are correct&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Diversity sweet spot&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;What can we do?&lt;/li&gt;
&lt;li&gt;Build psychological safety deliberately&lt;ul&gt;
&lt;li&gt;Set the stage&lt;/li&gt;
&lt;li&gt;Invite participation&lt;/li&gt;
&lt;li&gt;Respond productively - react constructively&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Practical tools&lt;/li&gt;
&lt;li&gt;Activity to address GroupThink&lt;ul&gt;
&lt;li&gt;Silent start - 5 mins quiet time to let people capture thoughts&lt;/li&gt;
&lt;li&gt;Non-confrontational assent - dot voting / secret ballot&lt;/li&gt;
&lt;li&gt;Separate generation from evaluation&lt;/li&gt;
&lt;li&gt;Team leader - be a puppet and represent quiet voices&lt;/li&gt;
&lt;li&gt;Time pressure kills&lt;/li&gt;
&lt;li&gt;Balance productivity and quality&lt;/li&gt;
&lt;li&gt;&quot;Yes, and..&quot; instead of &quot;No, but..&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_045032981_iOS.BgBKUe3S_2mT1hi.webp&quot; alt=&quot;Slide: The case for DEIB&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_045205817_iOS.Bscouaj4_Z2nBkmA.webp&quot; alt=&quot;Slide: The four dimensions of Diversity&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_050926641_iOS.BUQ_YAMK_ZhTR11.webp&quot; alt=&quot;Slide: Build psychological safety - deliberately&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_051201607_iOS.BdU7_x89_ZHX65k.webp&quot; alt=&quot;Slide: Practical tools - part 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_052042668_iOS.Df4dkzL2_Z2edktB.webp&quot; alt=&quot;Slide: True diversity isn&apos;t only about who&apos;s in the room. It&apos;s about whose thoughts are voiced and heard&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Hey LEGO robot, Grab me a Coke! - Daniel Fang&lt;/h2&gt;
&lt;p&gt;Daniel was the locknote speaker for the conference. I remember meeting Daniel when he spoke at &lt;a href=&quot;/2024/12/ddd-brisbane&quot;&gt;DDD Brisbane&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_055446379_iOS.CPiPEZhv_Z2df2Gx.webp&quot; alt=&quot;Daniel on stage, with big DDD logo to his right&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Lots of agents to make a Lego robot grab and move a can of coke.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_060204326_iOS.B4Chp_8h_ZBdgzD.webp&quot; alt=&quot;Slide: Screenshot of VS Code with Copilot driving a robot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_062447911_iOS.Csx6Ouif_1PYQQ.webp&quot; alt=&quot;Slide: Lego agent orchestration&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;That&apos;s a wrap&lt;/h2&gt;
&lt;p&gt;What a great team! Thanks again &lt;a href=&quot;https://blog.angelwebdesigns.com.au/about/&quot;&gt;Bron&lt;/a&gt; and everyone.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_064303369_iOS.iDvvsqfe_RJyoV.webp&quot; alt=&quot;All the volunteers and organisers gathered on stage&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Post-DDD&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260221_210144527_iOS.ClphC0f0_Z2qSvnp.webp&quot; alt=&quot;Muesli and fruit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Sunday morning I grabbed some breakfast at a cute little cafe (&lt;a href=&quot;https://linktr.ee/segoviamelbourne&quot;&gt;Segovia&lt;/a&gt;) in an alleyway not far from where I&apos;d been staying. Then I walked to the train station to head out to friends&apos; church in the suburbs. It&apos;s a completely different style of worship to my own church, but that&apos;s half the fun. Went out to lunch with them and it was great catching up.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.thushanfernando.com/&quot;&gt;Thushan&lt;/a&gt; very kindly offered to pick me up and drive me to the airport, which also gave us a chance to chat along the way.&lt;/p&gt;
&lt;p&gt;Flight was a little delayed leaving, but it was good to get home Sunday evening. What a great weekend!&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/20260220_215116344_iOS.CoGQiXLC.jpg" width="454" height="605"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/20260220_215116344_iOS.CoGQiXLC.jpg" width="454" height="605"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/02/a-new-year</id>
    <updated>2026-02-03T12:30:00.000+10:30</updated>
    <title>A new year, and 5 years at SixPivot</title>
    <link href="https://david.gardiner.net.au/2026/02/a-new-year" rel="alternate" type="text/html" title="A new year, and 5 years at SixPivot"/>
    <category term="Animals"/>
    <category term="Cycling"/>
    <category term="Life"/>
    <category term="Work"/>
    <published>2026-02-03T12:30:00.000+10:30</published>
    <summary type="html">Some highlights of 2025, surviving the summer heat, and 5 years at SixPivot.</summary>
    <content type="html">&lt;p&gt;December and January have come and gone. What&apos;s been going on?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260123_235150170_iOS.BwW0bG0e_Z10a4R8.webp&quot; alt=&quot;David wearing sunglasses and Akubra hat and yellow DDD Adelaide t-shirt&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I had 3 weeks off over the Christmas/New Years break, which was much appreciated. We didn&apos;t go away, but it did give me a chance to do a few jobs around the house. We&apos;re renovating our laundry and there was some painting that needed to be done before the new cupboards get installed. The whole process has taken quite a bit longer than we&apos;d hoped but it does feel like the end is in sight.&lt;/p&gt;
&lt;p&gt;The Tour Down Under was on recently in Adelaide. On the second Saturday, the men&apos;s race came quite close to where we live. I met up with our friend Jane and we hung out at top the King of the Mountain stage finish to cheer all the riders on. If you were watching the TV broadcast I&apos;m the yellow blob to the right of the arch, visible for about 0.2 seconds. That bright yellow &apos;DDD Adelaide 2025 Organiser&apos; t-shirt comes in handy!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260125_232629000_iOS.DBN8epnk_1o05Sw.webp&quot; alt=&quot;Still of Tour Down Under TV footage, showing David and Jane on the right&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We&apos;ve already had a number of days over 40°C - Summer is definitely here in Adelaide. On days like that the aim is to keep cool and try and protect our garden and animals from the heat as best we can. Old bedsheets are put to good use! There&apos;s actually a Mandarin tree hiding under there.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260125_221829715_iOS.CQiu2JT7_2rC0xe.webp&quot; alt=&quot;Mandarin tree hiding under some bedsheets&quot; /&gt;&lt;/p&gt;
&lt;p&gt;One other change for the Gardiner family is we&apos;ve had some new additions:&lt;/p&gt;
&lt;p&gt;Vanessa (a 1 year old tortoiseshell-coloured rescue cat) joined us back in May last year. She loves all the girls in the house but just puts up with me. More than once I&apos;ve gone to start work in the morning (or returning after lunch) and discovered there&apos;s an extra &quot;cushion&quot; on my chair. Well I thought it was my chair but I think she has other ideas 😃.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260103_003832468_iOS.exkgOMIu_1BlSdt.webp&quot; alt=&quot;Vanessa the cat, on my work chair&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And in early January our chook collection doubled. We&apos;re now looking after Peckachoo and Egglet, in addition to our two bantams. They belong to my son and daughter in-law, and needed a new home after friends who had been looking after them on their behalf moved to the country.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20260203_020604797_iOS.WbDZhsgC_ZfdTYL.webp&quot; alt=&quot;Two full-sizes chooks&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The new chooks are full-sized, so in comparison to the bantams they seem huge! But they&apos;re settling in well. We&apos;ve kept them apart from the bantams for now. They&apos;re great layers - most days we get an egg from both. I&apos;m hoping it will inspire our bantams (who are not the most reliable in that department) but so far not so much.&lt;/p&gt;
&lt;h2&gt;Adelaide .NET User Group&lt;/h2&gt;
&lt;p&gt;Recently the ADNUG organising team caught up for a meal and we started planning out the year. One minor issue we&apos;re still trying to solve is a venue. Normally we meet at the University of South Australia&apos;s City West campus. But UniSA has just merged with University of Adelaide to become &apos;Adelaide University&apos; and that was causing a delay in us being able to book our regular room. Merging two large complex organisations is tricky at the best of times, so I can only imagine all the internal systems and processes they&apos;re trying to figure out. We&apos;ve ended up with a different venue for February but hopefully we can be back at City West for April.&lt;/p&gt;
&lt;h2&gt;DDD Adelaide&lt;/h2&gt;
&lt;p&gt;I&apos;ve also spent a bit of time editing some of the videos recorded at DDD Adelaide. I&apos;m not giving up my day job just yet, but starting to figure out more about how to use DaVinci Resolve 😃. You can see the recordings that we&apos;ve published so far at &lt;a href=&quot;https://www.youtube.com/@DDDAdelaide&quot;&gt;https://www.youtube.com/@DDDAdelaide&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;5 year work anniversary at SixPivot&lt;/h2&gt;
&lt;p&gt;My first reminder of this was getting some LinkedIn notifications on the weekend. I continue to be grateful for working for a modern, people-focused company. Small enough that I know everyone, but large enough that there is a diverse range of experience and thinking.&lt;/p&gt;
&lt;p&gt;After previously working with a few longer-term clients, I&apos;ve been mixing it up a bit recently with some shorter engagements. I am enjoying the variety.&lt;/p&gt;
&lt;p&gt;I&apos;ve received 500 &lt;a href=&quot;https://handbook.sixpivot.com.au/perks-and-benefits/benefits#perks&quot;&gt;&apos;good vibes&apos; points&lt;/a&gt; for my &apos;workversary&apos;! I&apos;m due to get a new laptop this year, so I might put them towards that (which I may need to given the price of RAM these days!)&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/20260123_235150170_iOS.BwW0bG0e.jpg" width="764" height="1019"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/20260123_235150170_iOS.BwW0bG0e.jpg" width="764" height="1019"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2025/11/ddd-adelaide-2025</id>
    <updated>2025-11-23T18:00:00.000+10:30</updated>
    <title>DDD Adelaide 2025</title>
    <link href="https://david.gardiner.net.au/2025/11/ddd-adelaide-2025" rel="alternate" type="text/html" title="DDD Adelaide 2025"/>
    <category term="Adelaide"/>
    <category term="Conferences"/>
    <category term="Food"/>
    <published>2025-11-23T18:00:00.000+10:30</published>
    <summary type="html">DDD Adelaide is a community-focused software development conference held annually in Adelaide, South Australia. 
How did this year&apos;s event go? Pretty well I think!</summary>
    <content type="html">&lt;p&gt;Yesterday we ran &lt;a href=&quot;https://dddadelaide.com&quot;&gt;DDD Adelaide 2025&lt;/a&gt; - a community-driven conference for everyone involved in software development and related activities in Adelaide.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/ddd-adelaide-2025.DRrPEC-U_1lsops.webp&quot; alt=&quot;DDD Adelaide 2025 logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With an ever so slightly bigger attendance than last year, our venue again was the University of Adelaide. The delicious catering was provided by &lt;a href=&quot;https://www.cargocateringco.com/&quot;&gt;Cargo Catering&lt;/a&gt; and warm refreshments by &lt;a href=&quot;https://b3coffee.com.au&quot;&gt;B3 Coffee&lt;/a&gt; - both who have been with us since 2019.&lt;/p&gt;
&lt;p&gt;I think we&apos;re starting to find our stride with organising the event. Setup on Saturday morning went smoothly and no major dramas to worry about was good. The only thing beyond our control was the weather. It rained most of the day but at least the 3 lecture theatres and the atrium (where sponsors and food and drinks were located) were all relatively close together, so it didn&apos;t put a damper on things 😃&lt;/p&gt;
&lt;p&gt;One new thing this year that we&apos;re trialling is recording some of the presentations. We&apos;ve partnered with &lt;a href=&quot;https://www.txrx.tech/&quot;&gt;TxRx Technologies&lt;/a&gt; on this and hope to be able to publish some of the sessions in the coming weeks.&lt;/p&gt;
&lt;p&gt;Geoffrey Huntley was our keynote speaker. I particularly liked the fact that he came dressed exactly how he appeared in his promo image that we&apos;d used before the conference.&lt;/p&gt;
&lt;p&gt;Being one of the organisers, you don&apos;t always get a chance to sit in all the sessions, but with this year running smoothly I was able to relax a bit and enjoy some really interesting presentations.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ghuntley.com/&quot;&gt;Geoffrey Huntley&apos;s&lt;/a&gt; keynote&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251121_223659356_iOS.3hfyrkFg_eG3Tv.webp&quot; alt=&quot;View of main auditorium from the back. Lots of people seated listing to keynote&quot; /&gt;
&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251121_223706246_iOS.CCT3WzaQ_21ws8d.webp&quot; alt=&quot;Geoffrey presenting keynote, dressed in overalls and wearing a straw hat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Morning tea and lots of discussions happening&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_002059350_iOS.B_loGXSM_Shfv5.webp&quot; alt=&quot;Lots of people milling around the atrium during morning tea&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I was then acting as room host for a couple of sessions, including James Bannan with a really interesting talk about the history of Steganography&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_010928964_iOS.DvJCrEGX_26cFae.webp&quot; alt=&quot;James Bannan presenting on the history of Steganography&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And Ashley Mannix on Industrial strength testing for complex software&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_014022266_iOS.D1nCQBIO_2npt5P.webp&quot; alt=&quot;Ashley Mannix presenting on testing&quot; /&gt;
&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_014113272_iOS.ByXCODMc_ZEp0V1.webp&quot; alt=&quot;Attendees watching Ashley&apos;s talk&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then lunch! I&apos;m not a vegetarian, but I picked that option as there were some spare ones, and it was really nice.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_022435201_iOS.CDpr_IFL_de4Fv.webp&quot; alt=&quot;Lunchtime in the atrium. Lunch bowls are on a table for attendees to grab&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A few more sessions after lunch and then it was time for afternoon tea, and wow that was so yummy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_044221322_iOS.C45zxzqe_25aSpX.webp&quot; alt=&quot;Afternoon tea all laid out ready&quot; /&gt;
&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_045917856_iOS.C16BurtF_1RJMVR.webp&quot; alt=&quot;Attendees enjoying afternoon tea and chatting&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Last session I was back room hosting, and caught Samira talking about the challenges of introducing event-driven architecture.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/20251122_053110825_iOS.ChjaGlqk_t6ey6.webp&quot; alt=&quot;Samira presenting&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The day finished with &lt;a href=&quot;https://www.andrew-best.com/&quot;&gt;Andrew Best&lt;/a&gt; and myself hosting the closing prize draws and thank-yous. Another change we introduced this year was that to be eligible for the prizes you had to have submitted some feedback. Turns out that&apos;s a great incentive for people to give feedback!&lt;/p&gt;
&lt;p&gt;Two bonus highlights for me for the day that I wanted to mention:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Catching up with Nigel Spencer who I worked with &lt;a href=&quot;/2010/01/goodbye-contracting&quot;&gt;way back in 2010&lt;/a&gt; and who is now based in the USA. He timed a trip back to Adelaide to visit family perfectly - or was it a visit to DDD Adelaide and a bit of family catch up on the side? 😃 Random fact: Nigel was a speaker at one of the original CodeCampSA events many years ago that were the predecessor of what is now &apos;DDD Adelaide&apos;.&lt;/li&gt;
&lt;li&gt;Meeting &lt;a href=&quot;https://melissahoughton.dev/&quot;&gt;Melissa Houghton&lt;/a&gt; in person for the first time. I&apos;ve known Melissa for a few years but this is the first time we&apos;ve been in the same place at the same time. She&apos;s been involved with organising &lt;a href=&quot;https://dddmelbourne.com&quot;&gt;DDD Melbourne&lt;/a&gt; and now &lt;a href=&quot;https://dddperth.com&quot;&gt;DDD Perth&lt;/a&gt;, so it was a lovely surprise to discover she&apos;d &apos;popped&apos; over from Perth to checkout the Adelaide event.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the next few weeks the organising team will first (and most importantly!) catch our breath, then catch up to review all the feedback that has been submitted and do our final debrief. And then in the new year we&apos;ll start planning towards DDD Adelaide 2026!&lt;/p&gt;
&lt;p&gt;Thanks to the awesome organising team Andrew, Claire, Harnoor, Isaac, Ryan and Will. What a great team to serve with planning and putting this event on together.&lt;/p&gt;
&lt;p&gt;Extra special thanks to my family for stepping up to volunteer on the day. It&apos;s a very early start and a long day but they were wonderful.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/ddd-adelaide-2025.DRrPEC-U.png" width="650" height="650"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/ddd-adelaide-2025.DRrPEC-U.png" width="650" height="650"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2025/11/aspire-without-dotnet</id>
    <updated>2025-11-17T08:00:00.000+10:30</updated>
    <title>Aspire with Python, React, Rust and Node apps</title>
    <link href="https://david.gardiner.net.au/2025/11/aspire-without-dotnet" rel="alternate" type="text/html" title="Aspire with Python, React, Rust and Node apps"/>
    <category term=".NET"/>
    <category term="Aspire"/>
    <category term="Talks"/>
    <published>2025-11-17T08:00:00.000+10:30</published>
    <summary type="html">Using Aspire to build a distributed application with Python, React, Rust and Node.js components</summary>
    <content type="html">&lt;p&gt;Aspire (formerly .NET Aspire) is a great way to create observable, production-ready distributed apps by defining a code-based model of services, resources, and connections. It simplifies local development and debugging, as well as deployment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/aspire-logo-256.CA6LsmXl_2gR0dV.webp&quot; alt=&quot;Aspire logo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;By their very nature, distributed applications will have at least a few (if not a lot) of components. This presents a challenge both for the local developer experience and for deployment. Aspire seeks to simplify this by allowing you to model the services, resources, and connections in code. With one command you can then not only launch everything locally, but ensure that each service knows how to connect to the other services it needs to function.&lt;/p&gt;
&lt;p&gt;You can use it for both development and deployment, or if you already have an existing deployment process you&apos;re happy with (eg. Infrastructure as Code/deployment pipelines) you can just use Aspire to simply your local development experience.&lt;/p&gt;
&lt;p&gt;Check out my previous post &lt;a href=&quot;/2025/11/aspire&quot;&gt;Introducing Aspire&lt;/a&gt; for an overview of what Aspire is and how it works.&lt;/p&gt;
&lt;p&gt;As part of this year&apos;s .NET Conf virtual conference, I presented a talk &quot;Taking .NET out of .NET Aspire - working with non-.NET applications&quot;, in which I show how despite Aspire being written in .NET, it can integrate with a whole range of other software languages ecosystems. Here&apos;s the recording of that presentation:&lt;/p&gt;



&lt;p&gt;One issue with the talk was that it had to be pre-recorded a couple of weeks beforehand, so it was done using the Aspire 9.5 bits. Now that Aspire 13.0 is out, some of the packages that I used from the Community Toolkit in the demo are no longer necessary as Aspire has improved the integration with Python and Node.js applications.&lt;/p&gt;
&lt;p&gt;The source code for the demo can be found at &lt;a href=&quot;https://github.com/flcdrg/aspire-non-dotnet&quot;&gt;https://github.com/flcdrg/aspire-non-dotnet&lt;/a&gt;. A quick glance and you might think &quot;there&apos;s no .NET or Aspire in this repo&quot; and if you just look at the &lt;code&gt;main&lt;/code&gt; branch then you&apos;d be right. There are separate branches where I incrementally integrate each component into Aspire.&lt;/p&gt;
&lt;p&gt;The scenario I demo is a &apos;Pet supplies&apos; e-commerce website, with a React front-end, Python backend API running with MongoDB. The Python backend also talks to a Rust-based payment gateway service, and as a bonus step right at the end I add a Node.js server app to provide random pet jokes!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/aspire-petstore-web.NuLNUsqZ_Z2ud4JN.webp&quot; alt=&quot;Screenshot of Pet supplies web page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The technology mix is not unheard of, particularly at some larger organisations where you may have different teams working on different features or services and sometimes they are allowed enough autonomy to choose their own technology stack. Whether this is a case of &quot;using the right tool for the job&quot; or &quot;must use the current shiny new thing&quot; isn&apos;t so important. The reality is that for good or other reasons you often find yourself in this situation.&lt;/p&gt;
&lt;h2&gt;MongoDB&lt;/h2&gt;
&lt;p&gt;MongoDB is supported out of the box in Aspire. The original process was to launch this via &lt;code&gt;docker compose&lt;/code&gt;. Aspire actually &lt;a href=&quot;https://aspire.dev/integrations/compute/docker/&quot;&gt;supports compose files too (via the Aspire.Hosting.Docker package)&lt;/a&gt;, but in this case I&apos;m taking advantage of the &lt;a href=&quot;https://aspire.dev/integrations/databases/mongodb/&quot;&gt;&lt;code&gt;Aspire.Hosting.MongoDB&lt;/code&gt;&lt;/a&gt; to configure the MongoDB service, database, and for bonus points, include the Mongo Express management UI. This will still run in a Docker container, but Aspire handles all the configuration for me.&lt;/p&gt;



&lt;pre&gt;&lt;code&gt;var mongo = builder.AddMongoDB(&quot;mongo&quot;)
    .WithDataVolume()
    .WithMongoExpress();

var mongodb = mongo.AddDatabase(&quot;petstore&quot;);
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;code&gt;WithDataVolume()&lt;/code&gt; means that a Docker volume is created to persist the database data between runs.&lt;/p&gt;
&lt;p&gt;The original implementation also has a PowerShell script to populate the database with sample data. I wired up that script so that it gets run automatically when the MongoDB service starts up in Aspire.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var loadData = builder.AddExecutable(&quot;load-data&quot;, &quot;pwsh&quot;, &quot;../mongodb&quot;, &quot;-noprofile&quot;, &quot;./populate.ps1&quot;)
    .WaitFor(mongo)
    .WithArgs(&quot;-connectionString&quot;)
    .WithArgs(new ConnectionStringReference(mongo.Resource, false));
//.WithExplicitStart();
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;The script conveniently already had a parameter defined to pass in a connection string, so I take advantage of that.&lt;/p&gt;
&lt;p&gt;If you&apos;d prefer to just run the script manually (rather than every time you start Aspire) you could uncomment the &lt;code&gt;.WithExplicitStart()&lt;/code&gt; method.&lt;/p&gt;
&lt;h2&gt;Rust&lt;/h2&gt;
&lt;p&gt;I&apos;ve never used the Rust programming language before, but it is becoming increasingly popular, especially where you might previously have used C or C++. Rust uses &lt;a href=&quot;https://doc.rust-lang.org/cargo/&quot;&gt;Cargo&lt;/a&gt; as its package manager and build tool. Support for building and running Rust applications is provided by the &lt;a href=&quot;https://aspire.dev/integrations/frameworks/rust/&quot;&gt;Community Toolkit&apos;s CommunityToolkit.Aspire.Hosting.Rust package&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Configuring the Rust application is quite straightforward with the &lt;code&gt;AddRustApp&lt;/code&gt; method. The application has a default port it listens on but allows that to be overridden via the &lt;code&gt;PAYMENT_API_PORT&lt;/code&gt; environment variable. Aspire will set that to the appropriate port number by the call to &lt;code&gt;WithHttpEndpoint&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var rust = builder.AddRustApp(&quot;rustpaymentapi&quot;, &quot;../RustPaymentApi&quot;, [])
    .WithHttpEndpoint(env: &quot;PAYMENT_API_PORT&quot;);
&lt;/code&gt;&lt;/pre&gt;


&lt;h2&gt;Node.js&lt;/h2&gt;
&lt;p&gt;JavaScript is well known as a front end language, but platforms like Node.js allow you to write server-side applications in JavaScript (and TypeScript) too. Aspire 13.0 introduces improved support for JavaScript apps via a new &lt;a href=&quot;https://www.nuget.org/packages/Aspire.Hosting.JavaScript&quot;&gt;Aspire.Hosting.JavaScript package&lt;/a&gt;. This includes the ability to configure using &lt;code&gt;pnpm&lt;/code&gt; or &lt;code&gt;yarn&lt;/code&gt; package managers (the default being &lt;code&gt;npm&lt;/code&gt;).&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var nodeApp = builder.AddJavaScriptApp(&quot;node-joke-api&quot;, &quot;../NodeApp&quot;, &quot;start&quot;)
    .WithPnpm()
    // If you are using fnm for Node.js version management, you might need to adjust the PATH
    .WithEnvironment(&quot;PATH&quot;, Environment.GetEnvironmentVariable(&quot;PATH&quot;) + &quot;;&quot; + Environment.ExpandEnvironmentVariables(@&quot;%USERPROFILE%\AppData\Roaming\fnm\aliases\default&quot;))
    .WithHttpEndpoint(env: &quot;PORT&quot;)
    .WithOtlpExporter();
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;I use &lt;a href=&quot;https://github.com/Schniz/fnm&quot;&gt;&lt;code&gt;fnm&lt;/code&gt;&lt;/a&gt; (Fast Node Manager) to manage my Node.js versions. This means that the actual &lt;code&gt;node&lt;/code&gt; executable is not in the PATH by default, but rather is added (or updated) dynamically as I change directory via my PowerShell profile script. Because that script isn&apos;t run by Aspire, I explicity append the &lt;code&gt;fnm&lt;/code&gt; default alias directory to the PATH environment variable so that &lt;code&gt;node&lt;/code&gt; can be found.&lt;/p&gt;
&lt;h2&gt;Python&lt;/h2&gt;
&lt;p&gt;Python is an interesting ecosystem. There are a number of different package managers that will influence how you work with it. Aspire has first-class support for Python applications that use &lt;code&gt;pip&lt;/code&gt; via the &lt;code&gt;Aspire.Hosting.Python&lt;/code&gt; NuGet package. (See &lt;a href=&quot;https://aspire.dev/get-started/first-app/?lang=python&quot;&gt;Python apps in Aspire&lt;/a&gt; for more details).&lt;/p&gt;
&lt;p&gt;I recently worked on a client engagement where they were using &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt; with their Python applications. Aspire 13.0 now includes direct support for &lt;code&gt;uv&lt;/code&gt; (via &lt;code&gt;WithUv()&lt;/code&gt;) and &lt;code&gt;uvicorn&lt;/code&gt; (with &lt;code&gt;AddUvicornApp()&lt;/code&gt;):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var pythonApp = builder.AddUvicornApp(&quot;python-api&quot;, &quot;../PythonUv&quot;, &quot;src.api:app&quot;)
    .WithUv()
    .WaitFor(mongo)
    .WaitFor(rust)
    .WaitFor(nodeApp)
    .WithEnvironment(&quot;PYTHONIOENCODING&quot;, &quot;utf-8&quot;)
    .WithEnvironment(&quot;MONGO_CONNECTION_STRING&quot;, new ConnectionStringReference(mongo.Resource, false))
    .WithEnvironment(&quot;PAYMENT_API_BASE_URL&quot;, new EndpointReference(rust.Resource, &quot;http&quot;))
    .WithEnvironment(&quot;NODE_APP_BASE_URL&quot;, ReferenceExpression.Create($&quot;{nodeApp.Resource.GetEndpoint(&quot;http&quot;)}&quot;))
    .WithHttpHealthCheck(&quot;/&quot;)
    .WithExternalHttpEndpoints();
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;We wait for the MongoDB service, the Rust payment service, and the Node.js joke service, so that they have all started before the Python app. We also set up environment variables to pass in the connection strings and endpoints that the Python app needs to connect to those services.&lt;/p&gt;
&lt;h2&gt;A Vite React Frontend web app&lt;/h2&gt;
&lt;p&gt;The same &lt;code&gt;Aspire.Hosting.JavaScript&lt;/code&gt; package that we used to wire up the Node.js application can also be used for the frontend web app. Helpfully, it specifically includes support for Vite apps&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var web = builder.AddViteApp(&quot;web&quot;, &quot;../web-vite-react&quot;)
    .WithPnpm()
    // If you are using fnm for Node.js version management, you might need to adjust the PATH
    .WithEnvironment(&quot;PATH&quot;, Environment.GetEnvironmentVariable(&quot;PATH&quot;) + &quot;;&quot; + Environment.ExpandEnvironmentVariables(@&quot;%USERPROFILE%\AppData\Roaming\fnm\aliases\default&quot;))
    .WaitFor(pythonApp)
    .WithEnvironment(&quot;VITE_API_BASE_URL&quot;, new EndpointReference(pythonApp.Resource, &quot;http&quot;));
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Again, because I use &lt;code&gt;fnm&lt;/code&gt; to manage my Node.js versions, I need to append the &lt;code&gt;fnm&lt;/code&gt; default alias directory to the PATH environment variable so that &lt;code&gt;node&lt;/code&gt; can be found.&lt;/p&gt;
&lt;p&gt;The frontend application only talks to the Python backend API, so we enusre that service has started first, and set up the &lt;code&gt;VITE_API_BASE_URL&lt;/code&gt; environment variable so that the Vite app knows where to find the API.&lt;/p&gt;
&lt;p&gt;And with that in place, we can run the entire distributed application locally with a single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet run --project ./AspireAppHost/AspireAppHost.csproj --launch-profile http
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not all of the applications I&apos;m using here support HTTPS self-signed development certificates, so I stick with running everything over HTTP for now. This is something that Aspire has improved on in 13.0. Obviously in a production deployment you&apos;d want to use HTTPS everywhere. If I didn&apos;t need to set the launch profile, then I could use the Aspire CLI instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aspire run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nice thing about all this is I didn&apos;t need to make any changes any of the front end or back end applications to get them to work with Aspire. All the configuration was done in the Aspire host application. This assumes that your applications have provided a &apos;seam&apos; (eg. environment variables or command line arguments) to allow you to configure things like connection strings, ports, and endpoints.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/aspire-petstore-resources.CkupUbS-_Z1nuOUe.webp&quot; alt=&quot;Screenshot of Aspire dashboard showing resources page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s the final version of AppHost.cs:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var builder = DistributedApplication.CreateBuilder(args);

// MongoDB
var mongo = builder.AddMongoDB(&quot;mongo&quot;)
    .WithDataVolume()
    .WithMongoExpress();

var mongodb = mongo.AddDatabase(&quot;petstore&quot;);

var loadData = builder.AddExecutable(&quot;load-data&quot;, &quot;pwsh&quot;, &quot;../mongodb&quot;, &quot;-noprofile&quot;, &quot;./populate.ps1&quot;)
    .WaitFor(mongo)
    .WithArgs(&quot;-connectionString&quot;)
    .WithArgs(new ConnectionStringReference(mongo.Resource, false));
//.WithExplicitStart();

// Rust service
var rust = builder.AddRustApp(&quot;rustpaymentapi&quot;, &quot;../RustPaymentApi&quot;, [])
    .WithHttpEndpoint(env: &quot;PAYMENT_API_PORT&quot;);

// Node.js App
var nodeApp = builder.AddJavaScriptApp(&quot;node-joke-api&quot;, &quot;../NodeApp&quot;, &quot;start&quot;)
    .WithPnpm()
    // If you are using fnm for Node.js version management, you might need to adjust the PATH
    .WithEnvironment(&quot;PATH&quot;, Environment.GetEnvironmentVariable(&quot;PATH&quot;) + &quot;;&quot; + Environment.ExpandEnvironmentVariables(@&quot;%USERPROFILE%\AppData\Roaming\fnm\aliases\default&quot;))
    .WithHttpEndpoint(env: &quot;PORT&quot;)
    .WithOtlpExporter();

// Python API
var pythonApp = builder.AddUvicornApp(&quot;python-api&quot;, &quot;../PythonUv&quot;, &quot;src.api:app&quot;)
    .WithUv()
    .WaitFor(mongo)
    .WaitFor(rust)
    .WaitFor(nodeApp)
    .WithEnvironment(&quot;PYTHONIOENCODING&quot;, &quot;utf-8&quot;)
    .WithEnvironment(&quot;MONGO_CONNECTION_STRING&quot;, new ConnectionStringReference(mongo.Resource, false))
    .WithEnvironment(&quot;PAYMENT_API_BASE_URL&quot;, new EndpointReference(rust.Resource, &quot;http&quot;))
    .WithEnvironment(&quot;NODE_APP_BASE_URL&quot;, ReferenceExpression.Create($&quot;{nodeApp.Resource.GetEndpoint(&quot;http&quot;)}&quot;))
    .WithHttpHealthCheck(&quot;/&quot;)
    .WithExternalHttpEndpoints();

// Frontend
var web = builder.AddViteApp(&quot;web&quot;, &quot;../web-vite-react&quot;)
    .WithPnpm()
    // If you are using fnm for Node.js version management, you might need to adjust the PATH
    .WithEnvironment(&quot;PATH&quot;, Environment.GetEnvironmentVariable(&quot;PATH&quot;) + &quot;;&quot; + Environment.ExpandEnvironmentVariables(@&quot;%USERPROFILE%\AppData\Roaming\fnm\aliases\default&quot;))
    .WaitFor(pythonApp)
    .WithEnvironment(&quot;VITE_API_BASE_URL&quot;, new EndpointReference(pythonApp.Resource, &quot;http&quot;));

builder.Build().Run();
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;To get the full benefit of Aspire you will want to take a look at adding OpenTelemetry instrumentation. If you already have that in place then there&apos;s probably nothing to change. Aspire will set a bunch of OpenTelemetry-related environment variables for each application. If that&apos;a new thing then you&apos;ll get the double benefit of then being able to take advantage of that telemetry when your applications are deployed to production too!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/aspire-petstore-traces.ZXy-RHuR_Z1wOzaQ.webp&quot; alt=&quot;Screenshot of Aspire dashboard showing traces page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/aspire-petstore-metrics.B07KqI6V_Z1h2lSh.webp&quot; alt=&quot;Screenshot of Aspire dashboard showing metrics page&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Aspire has the potential to greatly simplify and improve the local development experience for distributed applications - potentially removing the need for numerous scripts and manual steps for a developer to get up and running much more quickly. It&apos;s also a tool that can benefit almost every development team, regardless of the technology stack they are using.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/aspire-logo-256.CA6LsmXl.png" width="256" height="256"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/aspire-logo-256.CA6LsmXl.png" width="256" height="256"/>
  </entry>
</feed>
