<?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-06-09T06:10:26.611Z</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/06/macbookpro</id>
    <updated>2026-06-09T15:00:00.000+09:30</updated>
    <title>I bought a MacBook Pro!</title>
    <link href="https://david.gardiner.net.au/2026/06/macbookpro" rel="alternate" type="text/html" title="I bought a MacBook Pro!"/>
    <category term="Hardware"/>
    <category term="macOS"/>
    <category term="Work"/>
    <published>2026-06-09T15:00:00.000+09:30</published>
    <summary type="html">The decision has been made. David is broadening his hardware horizons with the selection of an Apple MacBook Pro as his next laptop.</summary>
    <content type="html">&lt;p&gt;So last time I shared how I was trying to figure out &lt;a href=&quot;/2026/05/which-laptop&quot;&gt;what my next laptop would be&lt;/a&gt;. Well I made a decision.. and it is a bit of a surprising one, but yes. I now have an Apple MacBook Pro!&lt;/p&gt;
&lt;p&gt;I remember the first time I attended Microsoft&apos;s &lt;a href=&quot;/2015/11/north-america-trip-mvp-summit-2015&quot;&gt;MVP Summit&lt;/a&gt; in Redmond back in 2015, being really surprised at the number of MVPs and Microsoft staff that were using MacBooks. I just assumed being a Microsoft event everyone would be using Windows, but apparently not. That made a lasting impression.&lt;/p&gt;
&lt;p&gt;More recently I&apos;ve read with interest how a few of my SixPivot colleagues have been using MacBooks for work. Some were long-time Mac users, others more recent adopters. One good piece of advice was to make sure you still had a good Windows laptop available. We work with a diverse range of clients - some provide their own laptops (often with preconfigured VPN/security software), some allow you to use your preferred device, and some might need it to be a Windows machine. My old Dell is still in good shape, so I have that covered.&lt;/p&gt;
&lt;p&gt;After weighing up all the options I wrote about in the last post, I ended up picking the MacBook.&lt;/p&gt;
&lt;p&gt;The specs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;16-inch MacBook Pro in Space Black with M5 Pro chip&lt;/li&gt;
&lt;li&gt;18-core CPU, 20-core GPU, 16-core Neural Engine&lt;/li&gt;
&lt;li&gt;64GB unified memory&lt;/li&gt;
&lt;li&gt;2TB SSD storage&lt;/li&gt;
&lt;li&gt;Standard display&lt;/li&gt;
&lt;li&gt;US English Backlit Magic Keyboard with Touch ID&lt;/li&gt;
&lt;li&gt;Three Thunderbolt 5 ports, MagSafe 3 port, 3.5mm headphone jack, HDMI port, SDXC card slot&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/macbook-with-box.aih314YC_2GN1M.webp&quot; alt=&quot;New MacBook Pro laptop sitting on the box it came in&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Physically it&apos;s not dissimilar to other laptops I&apos;ve used. One obvious thing is the different arrangement of modifier keys on the keyboard. (More about that later).&lt;/p&gt;
&lt;p&gt;The getting started process was pretty straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirm language and region&lt;/li&gt;
&lt;li&gt;Optionally transfer data from another device (I chose to set up as new)&lt;/li&gt;
&lt;li&gt;Accessibility preferences&lt;/li&gt;
&lt;li&gt;Data and privacy preferences&lt;/li&gt;
&lt;li&gt;Creating a local Mac account&lt;/li&gt;
&lt;li&gt;Optionally then sign in with Apple Account (which I did)&lt;/li&gt;
&lt;li&gt;Confirm Mac settings&lt;/li&gt;
&lt;li&gt;Siri and AI&lt;/li&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;li&gt;Touch ID&lt;/li&gt;
&lt;li&gt;Apple Pay&lt;/li&gt;
&lt;li&gt;And then you&apos;re about done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A friendly welcome screen and we&apos;re up and running. Sometimes change is a good thing. You can challenge your habits and assumptions and sometimes find better ways of doing things.&lt;/p&gt;
&lt;h2&gt;Nice things&lt;/h2&gt;
&lt;p&gt;Everyone raves about battery life, especially with the newer ARM-based MacBooks. For the most part I&apos;m seeing this to be true.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://support.apple.com/en-au/guide/app-store/fird2c7092da/mac&quot;&gt;integration with iPhone apps&lt;/a&gt; is nifty. I wrote a few years back &lt;a href=&quot;/2011/02/comparison-of-public-weather-forecasts&quot;&gt;how dodgy a lot of the weather data for Australia is&lt;/a&gt;. So it&apos;s nice that through this feature I can have the Australian Bureau of Meteorology&apos;s iOS app sitting on my desktop as a &lt;a href=&quot;https://support.apple.com/en-au/guide/mac-help/mchl52be5da5/mac&quot;&gt;widget&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/desktop-gadgets.rpQC1f3S_ZT77Fa.webp&quot; alt=&quot;Screenshot of macOS desktop showing calendar and Adelaide weather gadgets&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That integration also includes Messages, Phone and FaceTime. Messages is the one I&apos;ve noticed the most, as they pop up as notifications in macOS in addition to on my iPhone. As I normally have my phone on silent, the &apos;ding&apos; from macOS surprised me at first. Being able to copy/paste screenshots into Messages from macOS is a nice bonus. Yes, you can do that on iPhone, but it is much easier viewing a 600 page PDF on a big screen that you want to copy a diagram out of.&lt;/p&gt;
&lt;h2&gt;New and different things&lt;/h2&gt;
&lt;p&gt;macOS is Unix-based. Or according to Wikipedia: A &quot;proprietary Unix operating system, derived from OPENSTEP for Mach and FreeBSD&quot;.&lt;/p&gt;
&lt;p&gt;As an experienced iPhone user, there are some areas of the user interface that are quite familiar. System Settings is a good example.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/macos-system-settings.LSeT9782_Z8Kvep.webp&quot; alt=&quot;Screenshot of macOS System Settings&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I suspect many Mac users may have never even opened a shell and are quite happy sticking with the GUI interface. macOS comes with &apos;Terminal&apos; as the default terminal program, and &lt;a href=&quot;https://zsh.sourceforge.io/&quot;&gt;zsh&lt;/a&gt; as the default shell.&lt;/p&gt;
&lt;p&gt;zsh is an interesting choice for macOS - it&apos;s apparently &lt;a href=&quot;https://sourceforge.net/p/zsh/code/commit_browser&quot;&gt;still hosted on SourceForge&lt;/a&gt; (there&apos;s a blast from the past!), but from what I&apos;ve seen online, the reason Apple chose it was due to the software license it uses.&lt;/p&gt;
&lt;p&gt;I could switch to another shell (including PowerShell) but for now I want to immerse myself in all things &apos;Mac&apos; so becoming more acquainted with zsh is the way to go.&lt;/p&gt;
&lt;p&gt;I have installed &lt;a href=&quot;https://ghostty.org/&quot;&gt;Ghostty&lt;/a&gt; on the recommendation of &lt;a href=&quot;https://blog.gertjvr.com/&quot;&gt;Gert&lt;/a&gt; and have been using that in preference to Terminal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/ghostty.DLMn2rUY_Z1Y7uvJ.webp&quot; alt=&quot;Ghostty screenshot&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Swiping to switch applications&lt;/h3&gt;
&lt;p&gt;I&apos;ve been impressed when I&apos;ve seen other MacBook users somehow swishing left and right between fullscreen applications. I accidentally swiped with three fingers on the trackpad and discovered that was how you do that! &lt;kbd&gt;Command&lt;/kbd&gt;-&lt;kbd&gt;Tab&lt;/kbd&gt; is the keyboard equivalent (and similar to Windows Alt-&lt;kbd&gt;Tab&lt;/kbd&gt;)
&lt;/p&gt;&lt;h3&gt;Menus&lt;/h3&gt;
&lt;p&gt;This has caught me out a few times, and I was vaguely aware of this but had kind of forgotten. macOS menus always live at the top of the screen. Sometimes if the application you&apos;re using is fullscreen then the menu will disappear unless you mouse up to the top.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/finder-menu.D6UM4Ym4_Z1l8IVi.webp&quot; alt=&quot;screenshot of menus&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Start menu&lt;/h3&gt;
&lt;p&gt;It&apos;s called the Dock, it lives at the bottom, and like the application menus hides itself unless you mouse down the bottom of the screen.&lt;/p&gt;
&lt;p&gt;I do keep hitting the &lt;kbd&gt;Command&lt;/kbd&gt; key thinking it&apos;s the &lt;kbd&gt;Windows&lt;/kbd&gt; key and it will bring up the dock (or Apps list). Turns out I should be using &lt;kbd&gt;Fn&lt;/kbd&gt;-&lt;kbd&gt;A&lt;/kbd&gt; and &lt;kbd&gt;Fn&lt;/kbd&gt;-&lt;kbd&gt;Shift&lt;/kbd&gt;-&lt;kbd&gt;A&lt;/kbd&gt; respectively.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/dock.a4IM3S1-_Z1KrVou.webp&quot; alt=&quot;screenshot of dock&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Keyboard&lt;/h3&gt;
&lt;p&gt;The keyboard is fine. The feel is quite comparable to my Dell, which I&apos;ve had no complaints with. As I mentioned above, the modifier keys are slightly different. There&apos;s a &lt;kbd&gt;Control&lt;/kbd&gt; key but it is used differently to Windows. All the familiar shortcuts that you&apos;d use &lt;kbd&gt;Ctrl&lt;/kbd&gt; on Windows, you actually use the &lt;kbd&gt;Command&lt;/kbd&gt; key (with the funny squiggle &apos;⌘&apos;).&lt;/p&gt;
&lt;p&gt;So close, but so different:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/windows-modifier-keys.De9A2oJr_SUasB.webp&quot; alt=&quot;Windows keyboard showing modifier keys&quot; /&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/mac-modifier-keys.DSil0LSd_cu6Xd.webp&quot; alt=&quot;MacBook Pro keyboard showing modifier keys&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And no &lt;kbd&gt;Delete&lt;/kbd&gt; key! How bizarre. Well technically there is a key that Apple refers to as &apos;Delete&apos;, but it has the ⌫ icon and acts like what I think of as &lt;kbd&gt;Backspace&lt;/kbd&gt; (in that it deletes the character to the left and moves left). If you use &lt;kbd&gt;fn&lt;/kbd&gt;-&lt;kbd&gt;Delete&lt;/kbd&gt; then that does the equivalent of a normal delete key (Apple refer to this as &apos;forward delete&apos;), or you may be able to use &lt;kbd&gt;Control&lt;/kbd&gt;-&lt;kbd&gt;D&lt;/kbd&gt;.&lt;/p&gt;
&lt;p&gt;I need to memorise the &lt;a href=&quot;https://support.apple.com/en-us/102650&quot;&gt;Mac keyboard shortcuts&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;I also regularly use an external keyboard. That creates a conundrum. When connected to my MacBook, do I:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the modifier keys on the external keyboard like they physically mirror the ones on the MacBook Pro keyboard, or&lt;/li&gt;
&lt;li&gt;Change the mapping of the external keyboard so that I use it in same way as when I&apos;m connected to a Windows machine.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I guess the question is which muscle memory is a stronger trigger - does the &quot;feel&quot; of the external keyboard have me fall into thinking &quot;Windows&quot; modifiers, or does looking at the MacBook Pro screen help transition my brain&apos;s process to work in &quot;Apple&quot; mode?&lt;/p&gt;
&lt;h2&gt;Equivalent things&lt;/h2&gt;
&lt;p&gt;As &lt;a href=&quot;https://chocolatey.org/&quot;&gt;Chocolatey&lt;/a&gt; is for Windows, &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt; is a package manager for macOS (and also Linux). Where possible I&apos;ve been using &lt;code&gt;brew&lt;/code&gt; to install software.&lt;/p&gt;
&lt;p&gt;It&apos;s nice to find familiar applications are available on both platforms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.firefox.com&quot;&gt;Firefox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Visual Studio Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/en-au/microsoft-365&quot;&gt;Microsoft Office&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bitwarden.com/&quot;&gt;Bitwarden&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://slack.com/intl/en-au/&quot;&gt;Slack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.scootersoftware.com/&quot;&gt;Beyond Compare&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Missing things&lt;/h2&gt;
&lt;p&gt;A few Windows favourites that I will need to find alternatives to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://paint.net/&quot;&gt;Paint.NET&lt;/a&gt; (I&apos;m trying &lt;a href=&quot;https://www.gimp.org/&quot;&gt;GIMP&lt;/a&gt; but it feels overly complex for what I need)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tortoisegit.org/&quot;&gt;TortoiseGit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Weird things&lt;/h2&gt;
&lt;p&gt;I&apos;m quite used to Windows Hello being able to sign me in by looking at me on my Windows laptop. Likewise on my iPhone using Face ID. So it is really odd that this is missing on the MacBook. I&apos;ve found myself a few times smiling sweetly at my sleeping laptop, only to realise that it was oblivious to my attentions. There is Touch ID at least.&lt;/p&gt;
&lt;p&gt;I started to install the &lt;a href=&quot;https://www.synology.com/en-global/dsm/packages/ActiveBackup&quot;&gt;Synology Active Backup for Business agent&lt;/a&gt;, but there&apos;s some extra steps that sounded tricky. That may deserve a separate blog post if I get it working.&lt;/p&gt;
&lt;h2&gt;Limited support for Dell docks&lt;/h2&gt;
&lt;p&gt;I have a &lt;a href=&quot;/2021/02/new-laptop&quot;&gt;Dell WD19TB Dock&lt;/a&gt; that I usually run two of my three monitors through. I was a bit surprised to discover that the MacBook couldn&apos;t see the second external monitor connected to the dock - it was just mirroring the same content on both. I found this documentation on &lt;a href=&quot;https://www.dell.com/support/kbdoc/en-au/000124312/dell-thunderbolt-dock-wd19tb-and-apple-usb-c-hosts&quot;&gt;Dell Thunderbolt Docks and Apple USB-C Hosts&lt;/a&gt; which explains what is possible.&lt;/p&gt;
&lt;p&gt;It turns out macOS doesn&apos;t support &quot;&lt;a href=&quot;https://www.intel.com/content/www/us/en/support/articles/000059105/graphics.html&quot;&gt;Multi-Stream Transport&lt;/a&gt;&quot;. And if you&apos;re using DisplayPort and DisplayPort/HDMI (which I was), then mirroring is what happens. The workaround is to make sure one of the monitors is instead connected via USB-C. None of my monitors have USB-C output, so I bought one of these &lt;a href=&quot;https://www.amazon.com.au/gp/product/B0DJSHRWFT?th=1&amp;amp;linkCode=ll2&amp;amp;tag=flcdrg07-22&amp;amp;linkId=fcb96e0b880ec7f1655eae1dbe21b454&amp;amp;ref_=as_li_ss_tl&quot;&gt;USB-C to HDMI cables&lt;/a&gt;, and sure enough I then had two distinct external monitors.&lt;/p&gt;
&lt;p&gt;While the dock can technically host 3 monitors, it doesn&apos;t look like that will work at all with macOS. I usually run the 3rd monitor through a separate hub (as my Dell laptop can&apos;t run 3 through the dock at high resolution), so I can do the same with the MacBook.&lt;/p&gt;
&lt;p&gt;According to the &apos;M5 Pro chip&apos; section of &lt;a href=&quot;https://support.apple.com/en-us/101571#macbook-pro-m5-pro&quot;&gt;How many displays can be connected to MacBook Pro&lt;/a&gt;, 3 external displays is the hard limit.&lt;/p&gt;
&lt;h2&gt;SixPivot laptop allowance&lt;/h2&gt;
&lt;p&gt;One of the things I realised when I was researching laptops was that, even though the &lt;a href=&quot;https://handbook.sixpivot.com.au/perks-and-benefits/laptop-allowance#how-the-laptop-scheme-works&quot;&gt;laptop allowance at SixPivot&lt;/a&gt; is very generous, because of the crazy hardware price hikes, you don&apos;t get as much as you used to. I&apos;m pleased to see that just in the last few days they&apos;ve not only increased the allowance, but also provided a second option if you are happy to wait 3 years for a hardware refresh instead of the normal 2. In two (or three) years time that will be something to consider.&lt;/p&gt;
&lt;h2&gt;Onwards&lt;/h2&gt;
&lt;p&gt;It&apos;s been a pretty smooth start. I&apos;m looking forward to learning more about what this machine can do 😀&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/macbook-with-box.aih314YC.jpg" width="857" height="643"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/macbook-with-box.aih314YC.jpg" width="857" height="643"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/05/which-laptop</id>
    <updated>2026-05-29T09:00:00.000+09:30</updated>
    <title>New laptop options in 2026</title>
    <link href="https://david.gardiner.net.au/2026/05/which-laptop" rel="alternate" type="text/html" title="New laptop options in 2026"/>
    <category term="Hardware"/>
    <published>2026-05-29T09:00:00.000+09:30</published>
    <summary type="html">It&apos;s hardware refresh time. Which laptops are in the running for my next main development machine?</summary>
    <content type="html">&lt;p&gt;It&apos;s been 3 years since my l&lt;a href=&quot;/2023/04/new-laptop&quot;&gt;ast laptop purchase&lt;/a&gt;, and it&apos;s technically a year overdue (as &lt;a href=&quot;https://www.sixpivot.com.au&quot;&gt;SixPivot&lt;/a&gt; allows for a hardware refresh every two years). My Dell has been chugging along pretty well after a few stability issues in the first 12 months.&lt;/p&gt;
&lt;p&gt;Usually I&apos;d like to bump things up incrementally with each new device. A bit more RAM, faster CPU or larger storage. But then AI happened, and component prices for those things in particular have skyrocketed! I quickly realised that while I may end up getting a newer generation CPU, and probably an onboard NPU (neural processing unit), unless I was willing to pay much more then the other specs would likely stay the same.&lt;/p&gt;
&lt;p&gt;We do get a &lt;a href=&quot;https://handbook.sixpivot.com.au/perks-and-benefits/laptop-allowance&quot;&gt;generous laptop allowance at SixPivot&lt;/a&gt;, but this time around it will likely need quite a bit of supplementing thanks to those price rises. That is a bit disappointing, but there&apos;s nothing I can do about it - it is what it is.&lt;/p&gt;
&lt;p&gt;And so I decided to look at machines with 64GB RAM and 2TB storage. So what should I get for a replacement?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Width (mm)&lt;/th&gt;
&lt;th&gt;Depth (mm)&lt;/th&gt;
&lt;th&gt;Height (mm)&lt;/th&gt;
&lt;th&gt;Display size&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;RAM (GB)&lt;/th&gt;
&lt;th&gt;Storage (TB)&lt;/th&gt;
&lt;th&gt;Cost (AUD)&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Existing Dell&lt;/td&gt;
&lt;td&gt;344&lt;/td&gt;
&lt;td&gt;230&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;15.5&quot;&lt;/td&gt;
&lt;td&gt;3456x2160&lt;/td&gt;
&lt;td&gt;i9&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$5,468&lt;/td&gt;
&lt;td&gt;Apr 2023&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;/2023/04/new-laptop&quot;&gt;XPS 9530&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework 16&lt;/td&gt;
&lt;td&gt;356&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;td&gt;17.9&lt;/td&gt;
&lt;td&gt;16&quot;&lt;/td&gt;
&lt;td&gt;2560x1600&lt;/td&gt;
&lt;td&gt;AMD Ryzen AI 7 350&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$5,613&lt;/td&gt;
&lt;td&gt;6-Apr&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://frame.work/au/en/products/laptop16-diy-amd-ai300/configuration/new&quot;&gt;https://frame.work/au/en/products/laptop16-diy-amd-ai300/configuration/new&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework 13&lt;/td&gt;
&lt;td&gt;296&lt;/td&gt;
&lt;td&gt;228&lt;/td&gt;
&lt;td&gt;15.8&lt;/td&gt;
&lt;td&gt;13.5&quot;&lt;/td&gt;
&lt;td&gt;2256x1504&lt;/td&gt;
&lt;td&gt;AMD Ryzen AI 7 350&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$4,472&lt;/td&gt;
&lt;td&gt;6-Apr&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://frame.work/au/en/products/laptop13-diy-amd-ai300/configuration/new&quot;&gt;https://frame.work/au/en/products/laptop13-diy-amd-ai300/configuration/new&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MBP 14&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;td&gt;221&lt;/td&gt;
&lt;td&gt;15.5&lt;/td&gt;
&lt;td&gt;14.2&quot;&lt;/td&gt;
&lt;td&gt;3024x1964&lt;/td&gt;
&lt;td&gt;M5 Pro (18 Core)&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$5,399&lt;/td&gt;
&lt;td&gt;26-Apr&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.apple.com/au/shop/buy-mac/macbook-pro/14-inch-space-black-standard-display-apple-m5-pro-chip-18-core-cpu-20-core-gpu-64gb-memory-2tb-storage&quot;&gt;https://www.apple.com/au/shop/buy-mac/macbook-pro/14-inch-space-black-standard-display-apple-m5-pro-chip-18-core-cpu-20-core-gpu-64gb-memory-2tb-storage&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPB 16&lt;/td&gt;
&lt;td&gt;355&lt;/td&gt;
&lt;td&gt;248&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;td&gt;16.2&quot;&lt;/td&gt;
&lt;td&gt;3456x2234&lt;/td&gt;
&lt;td&gt;M5 Max&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$6,999&lt;/td&gt;
&lt;td&gt;28-May&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.jbhifi.com.au/products/apple-macbook-pro-16-inch-with-m5-max-chip-2tb-48gb-space-black&quot;&gt;https://www.jbhifi.com.au/products/apple-macbook-pro-16-inch-with-m5-max-chip-2tb-48gb-space-black&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MBP 16&lt;/td&gt;
&lt;td&gt;355&lt;/td&gt;
&lt;td&gt;248&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;td&gt;16.2&quot;&lt;/td&gt;
&lt;td&gt;3456x2234&lt;/td&gt;
&lt;td&gt;M5 Pro (18 Core)&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$5,799&lt;/td&gt;
&lt;td&gt;25-Apr&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.apple.com/au/shop/buy-mac/macbook-pro/16-inch-silver-standard-display-apple-m5-pro-chip-18-core-cpu-20-core-gpu-64gb-memory-2tb-storage&quot;&gt;https://www.apple.com/au/shop/buy-mac/macbook-pro/16-inch-silver-standard-display-apple-m5-pro-chip-18-core-cpu-20-core-gpu-64gb-memory-2tb-storage&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dell XPS 16&lt;/td&gt;
&lt;td&gt;352&lt;/td&gt;
&lt;td&gt;237&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;16&quot;&lt;/td&gt;
&lt;td&gt;3.2K&lt;/td&gt;
&lt;td&gt;Series 3 Intel® Core™ Ultra X7 358H (16 cores, up to 4.8 GHz)&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;$6,603&lt;/td&gt;
&lt;td&gt;7-May&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.dell.com/en-au/shop/dell-laptops/xps-16-laptop-2026/spd/xps-da16260-laptop/cda16260cto02mau#customization-anchor&quot;&gt;https://www.dell.com/en-au/shop/dell-laptops/xps-16-laptop-2026/spd/xps-da16260-laptop/cda16260cto02mau#customization-anchor&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;I&apos;ve given Dell a good run, but I was really frustrated with the build quality of the most recent laptop. While it eventually stabilised, it required more than one service technician to come and replace a faulty component. So I was really open to other options.&lt;/p&gt;
&lt;p&gt;Framework laptops had really caught my eye. I really liked how &quot;composable&quot; they are, and being able to swap in and out different modules. That would probably have been my first choice except for one minor issue - they only offer a 1 year warranty in Australia, and as this will be a work laptop it needs to be fully supported for at least 2 years. So sadly, as good as they looked, they were out of the race.&lt;/p&gt;
&lt;p&gt;On that note, I should mention that the prices above don&apos;t include extended support. Depending on the brand and model, 3 years support can range from $450-650. Not something to overlook when crunching the numbers. This laptop will be a work tool, and if there&apos;s a problem it needs to be fixed ASAP to minimise any downtime.&lt;/p&gt;
&lt;p&gt;I&apos;m not a complete stranger to Apple hardware. I&apos;ve been a happy iPhone user since giving up on Windows Phone, and even before that I owned an Apple Newton MessagePad. But I&apos;ve never had an Apple desktop or laptop. I&apos;ve noticed some of my SixPivot colleagues have been using MacBooks, so I was intrigued. Could that be an option?&lt;/p&gt;
&lt;p&gt;And can I really write off Dell? To be fair I did take a look at their latest XPS laptop (now that they&apos;ve decided to switch back the &apos;XPS&apos; brand again!)&lt;/p&gt;
&lt;p&gt;Time to weigh up the pros and cons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stick with a Windows laptop&lt;/li&gt;
&lt;li&gt;Brave stepping out into the Apple macOS ecosystem&lt;/li&gt;
&lt;li&gt;Maybe I should look at another laptop vendor (not Lenovo, their keyboard layout is really irritating!)&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/05/mastodon-apps</id>
    <updated>2026-05-22T00:10:00.000+09:30</updated>
    <title>16 Mastodon client apps for iOS</title>
    <link href="https://david.gardiner.net.au/2026/05/mastodon-apps" rel="alternate" type="text/html" title="16 Mastodon client apps for iOS"/>
    <category term="iOS"/>
    <published>2026-05-22T00:10:00.000+09:30</published>
    <summary type="html">A review of 16 Mastodon iOS apps, with 6 making the cut for further consideration</summary>
    <content type="html">&lt;p&gt;I&apos;ve been part of the &lt;a href=&quot;/2022/11/hello-mastodon&quot;&gt;Mastodon Fediverse since 2022&lt;/a&gt;, and I&apos;m always on the lookout for a decent iOS app to use on my phone. I originally started with the stock Mastodon app, and then tried running &lt;a href=&quot;https://github.com/elk-zone/elk&quot;&gt;Elk&lt;/a&gt; as a progressive web app on the phone. Both were unreliable, and so more recently I&apos;ve been using Manfred (see below).&lt;/p&gt;
&lt;p&gt;But I wondered what else was out there and decided to try installing as many Mastodon apps as I could to find ones I might stick with.&lt;/p&gt;
&lt;p&gt;To even be considered for evaluation, an app must have been updated in the last 6 months.&lt;/p&gt;
&lt;p&gt;Other features I&apos;m interested in&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Free (or at least some free trial period)&lt;/li&gt;
&lt;li&gt;Can have in-app purchases for extra features, but basic functionality (like posting) should be included.&lt;/li&gt;
&lt;li&gt;I lean more towards the apps that have a clean UI and focus on the content/feed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I ended up trying out 16 apps, and found 6 that are worth spending a bit more time with.&lt;/p&gt;
&lt;p&gt;As much as possible, all screenshots were taken at the same moment in time for the feed to allow comparison of how they display identical content.&lt;/p&gt;
&lt;p&gt;Screenshots are from my iPhone 16 Pro running iOS 26.5&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/ice-cubes-for-mastodon/id6444915884&quot;&gt;Ice Cubes for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Seems pretty good. Looks nice and works well. I like how it makes full use of the display.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/02-ice-cubes.CBJsEqLD_EIVEF.webp&quot; alt=&quot;screenshot of Ice Cubes&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/metatext/id1523996615&quot;&gt;MetaText&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Also a decent app. Also in consideration.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/03-metatext.DQ-wpd-G_2viUBY.webp&quot; alt=&quot;screenshot of MetaText&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/crowfi-for-mastodon/id6756513753&quot;&gt;Crowfi for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Another reasonable one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/05-crowfi.BTTKhVKv_6TEdn.webp&quot; alt=&quot;screenshot of Crowfi&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/iphanpy-for-mastodon/id6755365082&quot;&gt;iPhanpy for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;And another one&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/06-iphanpy.Bb7mkStn_Z1ygcmK.webp&quot; alt=&quot;screenshot of iPhanpy&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/mona-7-for-mastodon/id6755672518&quot;&gt;Mona 7 for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;And another one!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/07-mona.idcPVOyO_ZKinSS.webp&quot; alt=&quot;screenshot of &quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/trunks-for-mastodon/id6444749479&quot;&gt;Trunks for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;And even another one. Nice preview feature in the post editor.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/08-trunks.CDoGoEdy_Z2aFSPN.webp&quot; alt=&quot;screenshot of Trunks&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/ivory-for-mastodon-by-tapbots/id6444602274&quot;&gt;Ivory&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A nice app, but while it is free in the app store, you need to pay after a 7 day trial. $AU25/year seems a bit much for just a social media client.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/01-ivory.B-_ctYP-_Z1H8y2D.webp&quot; alt=&quot;screenshot of Ivory&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/feather-for-mastodon/id6446263061&quot;&gt;feather for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most apps are dark mode but this one has a bright white background by default.&lt;/p&gt;
&lt;p&gt;I tried posting with a image, but it had a problem with photo permissions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/09-feather.D4er538F_Z2hjfX9.webp&quot; alt=&quot;screenshot of feather&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/manfred-for-mastodon/id1667817813&quot;&gt;Manfred for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve been using this app for a few months now. It looked promising, but continues to have a few glitches with scrolling and a few other rough edges like the post editor. Time to leave this one for now. Might come back in the future to see how it is going.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/16-manfred.D_Hhxtdz_Zuhq4e.webp&quot; alt=&quot;screenshot of Manfred&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/openvibe-bluesky-mastodon/id1666230916&quot;&gt;Openvibe&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve actually had this app installed for a long time, and used it to cross post to both Mastodon and Bluesky (as it supports as well as Tumblr and Threads). Sometimes it is good, other times it will fail to post to one or both services.&lt;/p&gt;
&lt;p&gt;I gather they&apos;ve just updated their authentication for Bluesky, so maybe that will help.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/15-openvibe.Py-0JXV2_Acqd1.webp&quot; alt=&quot;screenshot of Openvibe&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/mastodon/id1571998974&quot;&gt;Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The original app. Most of the time it was fine, but then earlier this year it got buggy and wouldn&apos;t load new posts, so I switched to Manfred.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/10-mastodon.DTad6uNC_2vfgNg.webp&quot; alt=&quot;screenshot of Mastodon&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/tootle-for-mastodon/id1236013466&quot;&gt;Tootle for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No idea what this app is doing. I can&apos;t get it to sign in. Tried entering my server &lt;code&gt;mastodon.online&lt;/code&gt; but it doesn&apos;t like it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/11-tootle.kResXTJR_Z6LhtL.webp&quot; alt=&quot;screenshot of Tootle&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/pinkleap-bluesky-mastodon/id6740792786&quot;&gt;Pinkleap&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Also does Bluesky. I found the app a bit buggy. Sometimes it got stuck on the splash screen. Doesn&apos;t seem to want to refresh the feed properly.&lt;/p&gt;
&lt;p&gt;Not real keen on the large list of profile photos across the top.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/12-pinkleap.BNlYh9Ba_Z2qU3Yr.webp&quot; alt=&quot;screenshot of &quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/dawn-for-mastodon-by-nightfox/id1668645019&quot;&gt;DAWN for Mastodon by Nightfox&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Reminds me of the old Twitter app. I&apos;ve moved on.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/13-dawn.DzDeZ9CH_Z2vEfbQ.webp&quot; alt=&quot;screenshot of DAWN&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/pheme-for-mastodon/id6751176171&quot;&gt;Pheme for Mastodon&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;m confused. That is not my feed. Not sure what this app is doing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/14-pheme.BSViOzts_Z2w5OKT.webp&quot; alt=&quot;screenshot of &quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://apps.apple.com/us/app/indigo-for-bluesky-mastodon/id6763755310&quot;&gt;Indigo&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Looks nice, Indigo also supports Bluesky, bu you can&apos;t post for free. Nope.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/04-indigo.G-eAbYfg_ZoBLf3.webp&quot; alt=&quot;screenshot of Indigo&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;So that&apos;s narrowed down the field to 6 contenders. I&apos;ll keep using those for a little longer and see where I land.&lt;/p&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/mastodon-logo.CGj-59jp.png" width="313" height="81"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/mastodon-logo.CGj-59jp.png" width="313" height="81"/>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/05/google-search-indexing</id>
    <updated>2026-05-10T17:00:00.000+09:30</updated>
    <title>Google, what happened!!?</title>
    <link href="https://david.gardiner.net.au/2026/05/google-search-indexing" rel="alternate" type="text/html" title="Google, what happened!!?"/>
    <category term="Blogging"/>
    <category term="WWW"/>
    <published>2026-05-10T17:00:00.000+09:30</published>
    <summary type="html">When I discovered that Google has stopped indexing pages on my blog.</summary>
    <content type="html">&lt;p&gt;I was working again with Aspire this week and hit another issue launching the solution under Visual Studio. It felt familiar, but I wasn&apos;t sure. A quick search online threw up some results, but nothing great.&lt;/p&gt;
&lt;p&gt;Did I make a note of the error in OneNote? No.&lt;/p&gt;
&lt;p&gt;What about my blog? Umm.. yes! And super embarrassing, it was &lt;a href=&quot;/2026/2026-04-10-vs-debugging-fatal-error.md&quot;&gt;my most recent post&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;So yay! I applied the workaround and was unblocked. But that got me thinking.. surely my post should have been somewhere in the search results by now?&lt;/p&gt;
&lt;p&gt;And that&apos;s how I discovered that starting in mid-April Google suddenly decided to stop indexing pages on my site!&lt;/p&gt;
&lt;p&gt;Here&apos;s the summary from &lt;a href=&quot;https://search.google.com/search-console&quot;&gt;Google Search Console&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/search-console-pages-indexed.CY_yX2PA_Z2nGuPB.webp&quot; alt=&quot;Screenshot from Google Search Console showing just 6 pages indexed&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Only 6 pages currently being indexed? What on earth?&lt;/p&gt;
&lt;p&gt;Scrolling further down that page and it breaks down why pages aren&apos;t being indexed:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/search-console-why-not-indexed.Dprfn7nc_ZAO59c.webp&quot; alt=&quot;Screenshot from Google Search Console with &apos;Why pages aren’t indexed&apos; table&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Pretty much all of my actual blog posts are under the &apos;Crawled - currently not indexed&apos; category.&lt;/p&gt;
&lt;p&gt;So for some reason (and they don&apos;t really say why) Google knows about the pages, but has not included them in the index, so that&apos;s why my search didn&apos;t show up my blog post.&lt;/p&gt;
&lt;p&gt;So what changed in April?&lt;/p&gt;
&lt;p&gt;I looked back at the Git history of my blog repo. There were some changes I merged late March. The only thing that was slightly interesting was I did upgrade from Astro 5 to 6 during that time.&lt;/p&gt;
&lt;p&gt;Looking at the source of the homepage, I noticed something curious:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; href=&quot;/favicon.ico&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;
    &amp;lt;meta name=&quot;generator&quot; content=&quot;Astro v6.3.1&quot;&amp;gt;
    &amp;lt;link rel=&quot;canonical&quot; href=&quot;https://david.gardiner.net.au/index&quot;&amp;gt;
    &amp;lt;link rel=&quot;sitemap&quot; href=&quot;/sitemap-index.xml&quot;&amp;gt;
    &amp;lt;title&amp;gt;David Gardiner&amp;lt;/title&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &apos;canonical&apos; line looks wrong! The &lt;code&gt;href&lt;/code&gt; should be set to &lt;code&gt;https://david.gardiner.net.au/&lt;/code&gt;, but it has a &lt;code&gt;/index&lt;/code&gt; tacked on the end. I checked the canonical values for other pages, and they were all fine. So just the home page is wrong.&lt;/p&gt;
&lt;p&gt;Out of interest, I switched back to the revision of the repo before that change and rebuilt the website at that point in time. Sure enough it was correct back then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;link rel=&quot;icon&quot; href=&quot;/favicon.ico&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&amp;gt;
    &amp;lt;meta name=&quot;generator&quot; content=&quot;Astro v5.18.1&quot;&amp;gt;
    &amp;lt;link rel=&quot;canonical&quot; href=&quot;https://david.gardiner.net.au/&quot;&amp;gt;
    &amp;lt;link rel=&quot;sitemap&quot; href=&quot;/sitemap-index.xml&quot;&amp;gt;
    &amp;lt;title&amp;gt;David Gardiner&amp;lt;/title&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code that created the canonical URL looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;link
      rel=&quot;canonical&quot;
      href={new URL(Astro.url.pathname.replace(&quot;.html&quot;, &quot;&quot;), Astro.site)}
    /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So it appears the functionality has changed in Astro 6 slightly. I created a new function that returns the correct value, including for the home page:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;export function getCanonicalUrl(astroUrl: URL, astroSite: URL | undefined): string | URL | null | undefined {
  const pathname = astroUrl.pathname.replace(&quot;.html&quot;, &quot;&quot;);
  if (pathname === &quot;/index&quot;) {
    return new URL(&quot;/&quot;, astroSite);
  }
  return new URL(pathname, astroSite);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My blog site is rendered statically, and as a way of ensuring that no surprising changes to the generated HTML happen from component updates, I previously created a couple of snapshot tests using Verify CLI. In particular these check the contents of a specific blog post and also the RSS feed.&lt;/p&gt;
&lt;p&gt;But I realised I didn&apos;t have one in place for the home page. While that wouldn&apos;t work well for my main blog repository (which is private), as the home page content would change every time I published a new blog post, that isn&apos;t true for the public repo where I maintain the blog engine logic. There I have just a few old blog posts that i use for testing.&lt;/p&gt;
&lt;p&gt;I&apos;ve &lt;a href=&quot;https://github.com/flcdrg/astro-blog-engine/blob/main/.github/workflows/main.yml#L136-L139&quot;&gt;added that into the pipeline now&lt;/a&gt;, so that in the future if there are any unexpected changes from upgrades, the snapshot test will allow me to review them and decide if they are acceptable or not.&lt;/p&gt;
&lt;p&gt;Now whether this is the actual cause of Google taking a dislike to me, I have no idea. But it&apos;s the most obvious thing I can see so far. Time will tell if I get back in Google&apos;s good books or not.&lt;/p&gt;
&lt;p&gt;On the plus side, I did check with Bing&apos;s &lt;a href=&quot;https://www.bing.com/webmasters&quot;&gt;Webmaster tools&lt;/a&gt;, and that all looks fine (and Bing is returning results for my site), so at least it isn&apos;t everyone who doesn&apos;t like me.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>https://david.gardiner.net.au/2026/04/vs-debugging-fatal-error</id>
    <updated>2026-04-10T12:00:00.000+09:30</updated>
    <title>Fatal error 0x8013132d debugging .NET 10 applications in Visual Studio</title>
    <link href="https://david.gardiner.net.au/2026/04/vs-debugging-fatal-error" rel="alternate" type="text/html" title="Fatal error 0x8013132d debugging .NET 10 applications in Visual Studio"/>
    <category term=".NET"/>
    <category term="Aspire"/>
    <category term="Visual Studio"/>
    <published>2026-04-10T12:00:00.000+09:30</published>
    <summary type="html">A workaround for a fatal error while trying to debug a .NET 10 application in Visual Studio</summary>
    <content type="html">&lt;p&gt;Trying to debug an &lt;a href=&quot;https://aspire.dev&quot;&gt;Aspire&lt;/a&gt; .NET 10 application in Visual Studio today and hitting this error:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://david.gardiner.net.au/_astro/visual-studio-fatal-error.B1dWqQi6_Zzh0OH.webp&quot; alt=&quot;Visual Studio error dialog&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A fatal error has occurred and debugging needs to be terminated. For more details, please see the Microsoft Help and Support web site. HRESULT=0x8013132d. ErrorCode=0x0.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A bit of searching online turned up &lt;a href=&quot;https://developercommunity.microsoft.com/t/11022347&quot;&gt;this bug report&lt;/a&gt; on the Microsoft Developer Community site.&lt;/p&gt;
&lt;p&gt;That was eventually forwarded to the &lt;a href=&quot;https://github.com/dotnet/runtime/issues/124913&quot;&gt;.NET runtime&lt;/a&gt; repo on GitHub where it was identified as relating to a &lt;a href=&quot;https://learn.microsoft.com/dotnet/core/compatibility/interop/9.0/cet-support?WT.mc_id=DOP-MVP-5001655&quot;&gt;breaking change introduced in .NET 9&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This change improves the security of .NET applications, but in this case is stopping me from being able to debug.&lt;/p&gt;
&lt;p&gt;You can opt out of this new behaviour by adding the following property to your csproj:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CETCompat&amp;gt;false&amp;lt;/CETCompat&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given in my case the problem only happens when I&apos;m debugging, then it would be preferable to leave this new feature on by default, so I&apos;ve added a condition to the property like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;CETCompat Condition=&quot;&apos;$(Configuration)&apos; == &apos;Debug&apos;&quot;&amp;gt;false&amp;lt;/CETCompat&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my Aspire AppHost csproj file, the &lt;code&gt;PropertyGroup&lt;/code&gt; element looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;OutputType&amp;gt;Exe&amp;lt;/OutputType&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net10.0&amp;lt;/TargetFramework&amp;gt;
    &amp;lt;ImplicitUsings&amp;gt;enable&amp;lt;/ImplicitUsings&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
    &amp;lt;UserSecretsId&amp;gt;00000000-0000-0000-0000-000000000000&amp;lt;/UserSecretsId&amp;gt;
    &amp;lt;CETCompat Condition=&quot;&apos;$(Configuration)&apos; == &apos;Debug&apos;&quot;&amp;gt;false&amp;lt;/CETCompat&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content>
    <media:thumbnail url="https://david.gardiner.net.au/_astro/visual-studio-2026.VSY9MLYp.png" width="256" height="256"/>
    <media:content medium="image" url="https://david.gardiner.net.au/_astro/visual-studio-2026.VSY9MLYp.png" width="256" height="256"/>
  </entry>
  <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>
</feed>
