<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.eke.li/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.eke.li/" rel="alternate" type="text/html" /><updated>2026-03-06T13:47:44+00:00</updated><id>https://www.eke.li/feed.xml</id><title type="html">Thoughts and stuff</title><subtitle>A collection of thoughts and stuff, primarily about programming and technology.</subtitle><entry><title type="html">Where is my disk space? Docker ate it.</title><link href="https://www.eke.li/docker/2026/03/06/where-is-my-disk-space-docker.html" rel="alternate" type="text/html" title="Where is my disk space? Docker ate it." /><published>2026-03-06T11:25:00+00:00</published><updated>2026-03-06T11:25:00+00:00</updated><id>https://www.eke.li/docker/2026/03/06/where-is-my-disk-space-docker</id><content type="html" xml:base="https://www.eke.li/docker/2026/03/06/where-is-my-disk-space-docker.html"><![CDATA[<p>I use a lot of devcontainers. They’re great - reproducible environments, no polluting my host machine, easy to share with colleagues. But they come with a cost that sneaks up on you: disk space.</p>

<p>I recently found myself with only 59GB free on a 1TB drive. That’s not great. I had a suspicion about where it all went, I’ve been here before.</p>

<h2 id="finding-the-culprit">Finding the culprit</h2>

<p>As I said - I’ve been through this before. Last time I found that Docker’s build cache had ballooned to 284GB. Just the build cache. I cleaned that up and compacted the virtual hard disks and got a decent chunk back. But here I was again, running low.</p>

<p>A quick check:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker system <span class="nb">df</span>
</code></pre></div></div>
<p>My results:</p>

<table>
  <thead>
    <tr>
      <th>TYPE</th>
      <th>TOTAL</th>
      <th>ACTIVE</th>
      <th>SIZE</th>
      <th>RECLAIMABLE</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Images</td>
      <td>34</td>
      <td>34</td>
      <td>93.43GB</td>
      <td>14.44GB (15%)</td>
    </tr>
    <tr>
      <td>Containers</td>
      <td>40</td>
      <td>0</td>
      <td>53.28GB</td>
      <td>53.28GB (100%)</td>
    </tr>
    <tr>
      <td>Local Volumes</td>
      <td>48</td>
      <td>48</td>
      <td>140.4GB</td>
      <td>0B (0%)</td>
    </tr>
    <tr>
      <td>Build Cache</td>
      <td>699</td>
      <td>0</td>
      <td>0B</td>
      <td>0B</td>
    </tr>
  </tbody>
</table>

<p>93GB in images, 53GB in container layers, 140GB in volumes. That’s kind of a lot. But the build cache was already at 0B this time - I had pruned that previously.</p>

<h2 id="the-containers">The containers</h2>

<p>40 containers, none running. Most are stopped devcontainers from various projects I work on. Each one accumulates writable data over time - installed packages, build artifacts, caches. Some had several gigabytes of writable data.</p>

<p>I listed them with their last-used dates and image names to figure out which ones I still needed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker ps <span class="nt">-a</span> <span class="se">\</span>
    <span class="nt">--format</span> <span class="s1">''</span> <span class="se">\</span>
  | <span class="k">while </span><span class="nb">read </span>name<span class="p">;</span> <span class="k">do
    </span>docker inspect <span class="s2">"</span><span class="nv">$name</span><span class="s2">"</span> <span class="se">\</span>
    | jq <span class="nt">-r</span> <span class="s1">'.[0] |
      ([.State.FinishedAt,
        .State.StartedAt]
       | max
       | split("T")[0])
      + "|" +
      (.Created
       | split("T")[0])
      + "|" + "'</span><span class="s2">"</span><span class="nv">$name</span><span class="s2">"</span><span class="s1">'"
      + "|" +
      .Config.Image'</span>
  <span class="k">done</span> | <span class="nb">sort</span>
</code></pre></div></div>

<p>Devcontainer images are named <code class="language-plaintext highlighter-rouge">vsc-[projectname]-[hash]</code>, so it’s easy to tell what’s what. I had containers from 8 months ago that I’d completely forgotten about.</p>

<p>I went through and removed the ones I didn’t need anymore. Important: don’t just blindly delete everything. If you use devcontainers, your stopped containers hold state you might want. I kept anything I’d used in the last couple of weeks and anything for active projects.</p>

<h2 id="shared-volumes">Shared volumes</h2>

<p>Something I learned during this cleanup: many devcontainers share volumes. Things like <code class="language-plaintext highlighter-rouge">shell-history</code>, <code class="language-plaintext highlighter-rouge">minikube-config</code>, and the <code class="language-plaintext highlighter-rouge">vscode</code> volume itself are often shared across multiple containers. This is actually probably bugs in how we’ve set up our devcontainers with named volumes to have things survive rebuilds (hint: make the project-name part of the volume-name).</p>

<p>This does mean that if you have three iterations of a python project as separate containers, they might all share the same shell history and config volumes. Deleting the older containers won’t lose that data - the volumes persist as long as at least one container still references them.</p>

<p>You can check what volumes each container uses:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker ps <span class="nt">-a</span> <span class="se">\</span>
    <span class="nt">--format</span> <span class="s1">''</span> <span class="se">\</span>
  | <span class="k">while </span><span class="nb">read </span>c<span class="p">;</span> <span class="k">do
    </span><span class="nb">echo</span> <span class="s2">"=== </span><span class="nv">$c</span><span class="s2"> ==="</span>
    docker inspect <span class="s2">"</span><span class="nv">$c</span><span class="s2">"</span> <span class="se">\</span>
    | jq <span class="nt">-r</span> <span class="s1">'
      .[0].Mounts[] |
      "    \(.Type)
        \(.Name // "-")
        \(.Destination)"'</span>
    <span class="nb">echo
  </span><span class="k">done</span>
</code></pre></div></div>

<h2 id="pruning-carefully">Pruning carefully</h2>

<p>After removing old containers, I pruned in stages:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># images not referenced by containers</span>
docker image prune <span class="nt">-a</span> <span class="nt">-f</span>

<span class="c"># volumes not referenced by containers</span>
docker volume prune <span class="nt">-f</span>

<span class="c"># Remove build cache</span>
<span class="c"># do this LAST, it tends to re-appear</span>
docker builder prune <span class="nt">-a</span> <span class="nt">-f</span>
</code></pre></div></div>

<p>One thing that surprised me: build cache keeps re-appearing. After pruning images, suddenly there’s build cache again. Is this the build-cache from the image that’s being kept aroudn? I don’t know.  I ended up running the builder prune multiple times. Run <code class="language-plaintext highlighter-rouge">docker system df</code> between rounds to see if more has surfaced.</p>

<p>After all the pruning I’d freed about 144GB inside Docker. Images went from 93GB to 53GB, containers from 53GB to 24GB, volumes from 140GB to 119GB, and build cache gave up about 52GB across several rounds.</p>

<h2 id="the-vhdx-file-you-dont-know-about">The vhdx file you don’t know about</h2>

<p>Here’s the thing that tripped me up last time, and that I only discovered properly this time: Docker on Windows uses multiple virtual hard disk files, and you need to compact all of them.</p>

<p>I knew about these two:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">C:\Users\...\AppData\Local\Docker\wsl\main\ext4.vhdx</code> - the Docker engine</li>
  <li><code class="language-plaintext highlighter-rouge">C:\Users\...\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu_...\LocalState\ext4.vhdx</code> - Ubuntu WSL</li>
</ul>

<p>But there was a third one:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">C:\Users\...\AppData\Local\Docker\wsl\disk\docker_data.vhdx</code> - <strong>the actual Docker data</strong></li>
</ul>

<p>That third file was 555GB. Yes, really. The <code class="language-plaintext highlighter-rouge">main/ext4.vhdx</code> that I’d been diligently compacting was tiny in comparison. All the container layers, images, volumes - it all lived in <code class="language-plaintext highlighter-rouge">docker_data.vhdx</code>.</p>

<p>The names and locations of the virtual hard-drives may vary by setup. Find all of them with:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find /c/Users/&lt;YourUsername&gt;/AppData <span class="se">\</span>
  <span class="nt">-name</span> <span class="s2">"*.vhdx"</span>
</code></pre></div></div>

<h2 id="compacting-the-vhdx-files">Compacting the vhdx files</h2>

<p>The virtual hard disks grow as Docker writes data, but they do not automagically shrink. Even after you delete data inside Docker, the vhdx file stays the same size on your Windows drive. You have to compact them manually.</p>

<p>Here’s the process, and the order <em>matters</em>:</p>

<h3 id="1-kill-docker-desktop-first">1. Kill Docker Desktop first</h3>

<p><code class="language-plaintext highlighter-rouge">docker desktop stop</code> tended to hang forever on my machine. If it works for you - great! If it doesn’t, instead:</p>

<ol>
  <li>Open Task Manager, find Docker Desktop, end the process tree</li>
  <li>Then run <code class="language-plaintext highlighter-rouge">wsl --shutdown</code></li>
</ol>

<p>The order matters. Docker Desktop keeps a file handle on the vhdx files even after WSL shuts down. If you don’t kill Docker Desktop first, <code class="language-plaintext highlighter-rouge">diskpart</code> won’t be able to open the files. Then again, that <em>may</em> only have been for my never-ending docker desktop process. Not sure.</p>

<h3 id="2-compact-in-diskpart">2. Compact in diskpart</h3>

<p>Open <code class="language-plaintext highlighter-rouge">diskpart.exe</code> (it needs admin rights, and will open a new terminal): For each of the virtual hard-drive -files you’ve found do:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>select vdisk file="path_to_the_vhdx"
compact vdisk
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">docker_data.vhdx</code> took the longest - mine was at 555GB and took a while (time for lunch). If it gets stuck partway, leave it for a while, if it doesn’t recover - <code class="language-plaintext highlighter-rouge">Ctrl+C</code> to cancel that (you can also restart the compaction if you want).</p>

<h3 id="3-start-back-up">3. Start back up</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wsl
docker desktop start
</code></pre></div></div>

<p>Verify everything is still there with <code class="language-plaintext highlighter-rouge">docker system df</code>.</p>

<h2 id="was-it-worth-it">Was it worth it?</h2>

<p>In my case I recovered about 380GB of space on my C-drive. Pretty good, and definitely necessary when I was down to just over 10GB before I started.</p>

<p>Without compaction all the space freed inside Docker will be reused by Docker instead of it allocating new blocks and growing the vhdx further, so this will buy you time before you need to do it again or buy a bigger drive. With compaction you get the space back on your drive, but it will be used up again as you use Docker.</p>

<p>If you have the time to clean up and compact it, it’s worth it.</p>

<h2 id="the-checklist">The checklist</h2>

<p>For my future self, and for anyone else running lots of devcontainers on Windows:</p>

<ol>
  <li>Run <code class="language-plaintext highlighter-rouge">docker system df</code> to see where the space is</li>
  <li>List containers by last used date, remove ones you don’t need</li>
  <li>Check for shared volumes before deleting containers</li>
  <li><code class="language-plaintext highlighter-rouge">docker image prune -a -f</code></li>
  <li><code class="language-plaintext highlighter-rouge">docker volume prune -f</code></li>
  <li><code class="language-plaintext highlighter-rouge">docker builder prune -a -f</code> (run this last, and maybe twice)</li>
  <li>Kill Docker Desktop process tree, then <code class="language-plaintext highlighter-rouge">wsl --shutdown</code></li>
  <li>Find <strong>all</strong> vhdx files - especially <code class="language-plaintext highlighter-rouge">docker_data.vhdx</code></li>
  <li>Compact all of them in <code class="language-plaintext highlighter-rouge">diskpart.exe</code></li>
  <li>Start WSL and Docker Desktop back up</li>
  <li>Verify with <code class="language-plaintext highlighter-rouge">docker system df</code></li>
</ol>

<p>Docker’s disk usage is hidden behind virtual hard disks that only grow and never shrink. And the biggest one might not be the one you’d expect, go looking through your disk for others!</p>]]></content><author><name>Tomas Ekeli</name></author><category term="docker" /><category term="docker" /><category term="devcontainers" /><category term="disk-space" /><category term="windows" /><category term="wsl" /><summary type="html"><![CDATA[How to find and reclaim hundreds of gigabytes that Docker Desktop silently consumes on Windows, and the vhdx file you probably don't know about.]]></summary></entry><entry><title type="html">Running the devcontainer CLI without installing it</title><link href="https://www.eke.li/docker/devcontainers/devops/vscode/2026/02/08/running-devcontainer-cli-without-installing-it.html" rel="alternate" type="text/html" title="Running the devcontainer CLI without installing it" /><published>2026-02-08T15:55:48+00:00</published><updated>2026-02-08T15:55:48+00:00</updated><id>https://www.eke.li/docker/devcontainers/devops/vscode/2026/02/08/running-devcontainer-cli-without-installing-it</id><content type="html" xml:base="https://www.eke.li/docker/devcontainers/devops/vscode/2026/02/08/running-devcontainer-cli-without-installing-it.html"><![CDATA[<p>I like devcontainers. I really like that they remove the “it works on my machine” problem, and I also really <em>really</em> like that they keep my machine clean. They do this by freeing me from installing all kinds of developer tools on my machine. I just run devcontainers, and whatever strange toolset is needed for the particular project or stack I’m working on just works. Gone are the days of having strange versions of node, python, java, rust, etc. installed on my machine. I just run devcontainers, and everything works.</p>

<p>Ironic then, that the devcontainer CLI tool needs Node, NPM, Python and C/C++ (in particular versions). Yes, <a href="https://github.com/devcontainers/cli#npm-install">I’m not kidding</a>. This is a travesty, and should not be allowed to stand! It is, in fact, registered as <a href="https://github.com/devcontainers/cli/issues/63">an issue</a>, which has been open since summer 2022. That is creeping up on 4 years ago, at the time of writing. Safe to say - it’s not going to be fixed.</p>

<p>Personally, I don’t really care all that much - I use devcontainers through VS Code and that handles all interactions with them for me. But, it is wrong, dammit. Just plain wrong!</p>

<p>There should be only one dependency for devcontainers, and that is Docker! Well, Docker and Linux, but that’s a given. And a computer. And the internet. Oh, gods, we’re depending on the internet again. Well, let’s get this exercise in futility on the road:</p>

<h2 id="the-devcontainer-cli">The Devcontainer CLI</h2>

<p>Well, how do we get the devcontainer CLI without installing it? Like any programmer I’m a fan of recursion, so immediately I thought - we run it in a devcontainer! Silly, right? Without a devcontainer CLI we cannot run a devcontainer. True, but we can step down one level and run it in a docker container. In fact, one of the common devcontainers already contain most of what we need to run the devcontainer CLI: <code class="language-plaintext highlighter-rouge">mcr.microsoft.com/vscode/devcontainers/typescript-node</code>.</p>

<p>Let’s just try it out - and see if we can run the devcontainer CLI from there:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span>docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\</span>
    <span class="nt">--entrypoint</span> /bin/bash <span class="se">\</span>
    mcr.microsoft.com/vscode/devcontainers/typescript-node
root ➜ / <span class="nv">$ </span>npm <span class="nb">install</span> <span class="nt">-g</span> @devcontainers/cli
<span class="c"># installation logging...</span>
root ➜ / <span class="nv">$ </span>devcontainer <span class="nt">--version</span>
0.83.0
</code></pre></div></div>

<p>Good, that worked! We can indeed install the devcontainer CLI in the container and run it. Now, let’s make a docker image out of this, so we don’t have to install the devcontainer CLI every time, so we make this Dockerfile:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> mcr.microsoft.com/vscode/devcontainers/typescript-node</span>
<span class="k">RUN </span>npm <span class="nb">install</span> <span class="nt">-g</span> @devcontainers/cli
<span class="k">ENTRYPOINT</span><span class="s"> ["devcontainer"]</span>
</code></pre></div></div>

<p>And build it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span>docker build <span class="nt">-t</span> devcontainer-cli <span class="nb">.</span>
</code></pre></div></div>

<p>Great - now we have a docker image with the devcontainer CLI installed. Let’s run it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span>docker run <span class="nt">--rm</span> <span class="nt">-it</span> devcontainer-cli <span class="nt">--version</span>
0.83.0
</code></pre></div></div>

<p>That’s cool, but we want to run it as if it was a tool on our host machine (without installing), so we need to give the running container access to our host’s Docker daemon and file-system. To make it easier to call, we also put it inside a bash-script instead of typing it out every time (we <em>are</em> lazy, after all). Here’s the script (called <code class="language-plaintext highlighter-rouge">dc.sh</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\ </span> <span class="c"># run, but remove when done</span>
    <span class="nt">-v</span> /var/run/docker.sock:/var/run/docker.sock <span class="se">\ </span><span class="c"># give access host docker</span>
    <span class="nt">-v</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span>:<span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span> <span class="se">\ </span><span class="c"># access current directory (and subdirectories)</span>
    <span class="nt">-w</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span> <span class="se">\ </span><span class="c"># set working directory</span>
    devcontainer-cli <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span> <span class="c"># pass all arguments to container</span>
</code></pre></div></div>

<p>This script mounts the docker socket, so the container can talk to the host’s Docker daemon. It also mounts the current directory (and subdirectories) into the container, and sets the working directory to the current directory. Finally, it passes all arguments to the <code class="language-plaintext highlighter-rouge">devcontainer</code> command inside the container. With it we should be able to run the devcontainer CLI as if it was installed on our host machine, without actually installing it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span><span class="nb">chmod</span> +x dc.sh
host <span class="nv">$ </span>./dc.sh <span class="nt">--version</span>
0.83.0
</code></pre></div></div>

<p>Excellent!</p>

<h2 id="one-small-snag">One small snag</h2>

<p>Very cool so far, let’s try it out:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span><span class="nb">mkdir </span>test-devcontainer
host <span class="nv">$ </span><span class="nb">cd </span>test-devcontainer
host <span class="nv">$ </span>../dc.sh up
<span class="c"># unhappy logs about not being able to find a devcontainer.json file</span>
</code></pre></div></div>

<p>Of course! We need to tell the devcontainer CLI what do start!</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span><span class="nb">mkdir</span> .devcontainer
host <span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'{
    "name": "Test Devcontainer",
    "image": "mcr.microsoft.com/devcontainers/base:ubuntu"
}'</span> <span class="o">&gt;</span> .devcontainer/devcontainer.json
host <span class="nv">$ </span>../dc.sh up
<span class="o">[</span>4 ms] @devcontainers/cli 0.83.0. Node.js v24.13.0. linux 6.6.87.2-microsoft-standard-WSL2 x64.
Error: spawn docker ENOENT
    at ChildProcess._handle.onexit <span class="o">(</span>node:internal/child_process:286:19<span class="o">)</span>
    at onErrorNT <span class="o">(</span>node:internal/child_process:484:16<span class="o">)</span>
    at process.processTicksAndRejections <span class="o">(</span>node:internal/process/task_queues:89:21<span class="o">)</span>
<span class="o">{</span><span class="s2">"outcome"</span>:<span class="s2">"error"</span>,<span class="s2">"message"</span>:<span class="s2">"spawn docker ENOENT"</span>,<span class="s2">"description"</span>:<span class="s2">"An error occurred setting up the container."</span><span class="o">}</span>
</code></pre></div></div>

<p>And this is where we hit a snag. Turns out the container we based off of doesn’t have Docker installed, so it can’t talk to the Docker daemon. Easily fixed - we just modify our Dockerfile to install Docker:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> mcr.microsoft.com/vscode/devcontainers/typescript-node</span>

<span class="c"># Install docker and clean up after</span>
<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> docker.io <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> /var/lib/apt/lists/<span class="k">*</span>

<span class="c"># Install devcontainer CLI</span>
<span class="k">RUN </span>npm <span class="nb">install</span> <span class="nt">-g</span> @devcontainers/cli

<span class="k">ENTRYPOINT</span><span class="s"> ["devcontainer"]</span>
</code></pre></div></div>
<p>Now we build the image again, and try to run it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host <span class="nv">$ </span>docker build <span class="nt">-t</span> devcontainer-cli <span class="nb">.</span>
host <span class="nv">$ </span>../dc.sh up
<span class="o">[</span>2 ms] @devcontainers/cli 0.83.0. Node.js v24.13.0. linux 6.6.87.2-microsoft-standard-WSL2 x64.
<span class="o">[</span>+] Building 0.8s <span class="o">(</span>6/6<span class="o">)</span> FINISHED
<span class="c"># chatty logs omitted..</span>
Container started
<span class="o">{</span><span class="s2">"outcome"</span>:<span class="s2">"success"</span>,<span class="s2">"containerId"</span>:<span class="s2">"c55f3e53963bcc08f8e6484e79334d219b47757d9ed3daf05841e487e516fc3f"</span>,<span class="s2">"remoteUser"</span>:<span class="s2">"vscode"</span>,<span class="s2">"remoteWorkspaceFolder"</span>:<span class="s2">"/workspaces/setup"</span><span class="o">}</span>
host <span class="nv">$ </span>docker ps
<span class="c"># list of containers running - one of which should have the containerId from above</span>
</code></pre></div></div>

<p>And there we have it! The devcontainer CLI is running, and it can talk to the Docker daemon on our host machine, and it can see our files. We can run all devcontainer CLI commands as if it was installed on our host machine, without actually installing it.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This was a fun little exercise showing how we can use containers to run tools without installing them on our host machine. The devcontainer CLI is just one example, but this approach can be used for any tool that can run in a container. We just need to make sure the container has access to the necessary resources (like the Docker daemon and the file system) to do its job.</p>

<p>Personally I haven’t really used the devcontainer CLI outside of VS Code, but it’s good to know that we can run it without installing it, and that we can even run it in a container if we want to.</p>

<p>For developers who want to use devcontainers without VS Code, maybe directly from the command line this could be helpful. Or for CI/CD pipelines that want to use devcontainers without installing the CLI on the build agents. There must be dozens of you out there!</p>

<p>For me it just scratches the itch of the devcontainer CLI (a tool that I use to avoid installing things on my host machine) requiring me to install things on my host machine.</p>

<p>Until next time!</p>

<h2 id="addendum-docker-socket-permissions-on-linux">Addendum: Docker socket permissions on Linux</h2>

<p>If you’re trying this on a pure Linux host (as opposed to Docker Desktop on macOS or Windows), you might get a permission-problem when the container tries to talk to the Docker socket. This is because the socket is owned by the <code class="language-plaintext highlighter-rouge">docker</code> group on your host, and the user inside the container isn’t a member of that group.</p>

<p>The fix is to tell Docker to add the host’s docker group to the container process. Update <code class="language-plaintext highlighter-rouge">dc.sh</code> to:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
docker run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\</span>
    <span class="nt">-v</span> /var/run/docker.sock:/var/run/docker.sock <span class="se">\</span>
    <span class="nt">-v</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span>:<span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-w</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">--group-add</span> <span class="si">$(</span><span class="nb">stat</span> <span class="nt">-c</span> <span class="s1">'%g'</span> /var/run/docker.sock<span class="si">)</span> <span class="se">\</span>
    devcontainer-cli <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">stat -c '%g'</code> command gets the group-ID of the socket file, and <code class="language-plaintext highlighter-rouge">--group-add</code> grants that group to the container’s process. Now the container should have access to write to the socket without running as root or changing permissions on the host.</p>]]></content><author><name>Tomas Ekeli</name></author><category term="docker" /><category term="devcontainers" /><category term="devops" /><category term="vscode" /><category term="devops" /><category term="docker" /><category term="development" /><category term="devcontainers" /><category term="vscode" /><summary type="html"><![CDATA[How to run the devcontainer CLI without installing it on your host machine, using only Docker.]]></summary></entry><entry><title type="html">Failing to replace Docker with Podman for Dev Containers</title><link href="https://www.eke.li/docker/2026/01/18/failing-with-podman.html" rel="alternate" type="text/html" title="Failing to replace Docker with Podman for Dev Containers" /><published>2026-01-18T09:00:00+00:00</published><updated>2026-01-18T09:00:00+00:00</updated><id>https://www.eke.li/docker/2026/01/18/failing-with-podman</id><content type="html" xml:base="https://www.eke.li/docker/2026/01/18/failing-with-podman.html"><![CDATA[<p><img src="/assets/img/2026-01-18-failing-with-podman.webp" alt="Purple seals on a &quot;bliss&quot; background. One of them has a captain's hat and a cup with two handles and a Kubernetes logo. The other has a small cog." /></p>

<p>I spent my Sunday trying to replace Docker Desktop with Podman. It didn’t work out. Here’s what happened, what I learned, and why I’m back on Docker Desktop.</p>

<h2 id="the-motivation">The motivation</h2>

<p>Docker Desktop’s licensing changed a while back, and while it’s not expensive, I was curious about alternatives. Podman keeps coming up as the obvious choice: it’s open source, daemonless, rootless by default, and claims Docker CLI compatibility. My C: drive was also running extremely low on space, and I figured a fresh start might help with that out too.</p>

<h2 id="what-worked">What worked</h2>

<p>Installation was straightforward:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nx">RedHat.Podman</span><span class="w">
</span><span class="n">podman</span><span class="w"> </span><span class="nx">machine</span><span class="w"> </span><span class="nx">init</span><span class="w">
</span><span class="n">podman</span><span class="w"> </span><span class="nx">machine</span><span class="w"> </span><span class="nx">start</span><span class="w">
</span></code></pre></div></div>

<p>Basic container operations work fine. The CLI is compatible enough that you can alias <code class="language-plaintext highlighter-rouge">docker</code> to <code class="language-plaintext highlighter-rouge">podman</code> and most commands just work:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-Alias</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">docker</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">podman</span><span class="w">
</span><span class="n">docker</span><span class="w"> </span><span class="nx">run</span><span class="w"> </span><span class="nx">hello-world</span><span class="w">  </span><span class="c"># works</span><span class="w">
</span><span class="n">docker</span><span class="w"> </span><span class="nx">ps</span><span class="w">               </span><span class="c"># works</span><span class="w">
</span></code></pre></div></div>

<p>I moved the Podman machine to my E: drive to save space on C:, which was easy enough with WSL’s export/import:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">--export</span><span class="w"> </span><span class="nx">podman-machine-default</span><span class="w"> </span><span class="nx">E:\podman\backup.tar</span><span class="w">
</span><span class="n">wsl</span><span class="w"> </span><span class="nt">--unregister</span><span class="w"> </span><span class="nx">podman-machine-default</span><span class="w">
</span><span class="n">wsl</span><span class="w"> </span><span class="nt">--import</span><span class="w"> </span><span class="nx">podman-machine-default</span><span class="w"> </span><span class="nx">E:\podman\machine</span><span class="w"> </span><span class="nx">E:\podman\backup.tar</span><span class="w">
</span></code></pre></div></div>

<p>Podman Desktop exists too, if you want a nice GUI similar to Docker Desktop.</p>

<h2 id="hitting-a-wall">Hitting a wall</h2>

<p>I use devcontainers <strong>a lot</strong>. They’re how I keep my development environments reproducible and contained. I wrote about them <a href="/2023/03/devcontainers/">back in 2023</a>, and they’ve become central to how I work.</p>

<p>When I tried to open a project in VS Code and launch the devcontainer, things quickly fell apart.</p>

<p>The first problem: VS Code’s Dev Containers extension runs inside my Ubuntu WSL distro, where it looks for a Docker socket at <code class="language-plaintext highlighter-rouge">/var/run/docker.sock</code>. But Podman runs in its own separate WSL distro (<code class="language-plaintext highlighter-rouge">podman-machine-default</code>), exposing its API through a Windows named pipe. These two can’t talk to each other directly.</p>

<h2 id="tying-things-together">Tying things together</h2>

<p>I spent hours setting up a relay between the Windows named pipe and a Unix socket in Ubuntu WSL. This involved:</p>

<ul>
  <li>Installing <code class="language-plaintext highlighter-rouge">npiperelay</code> on Windows to bridge named pipes to stdin/stdout</li>
  <li>Installing <code class="language-plaintext highlighter-rouge">socat</code> in Ubuntu WSL to listen on a Unix socket</li>
  <li>Writing a script to wire them together</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>socat UNIX-LISTEN:/var/run/docker.sock,fork,mode<span class="o">=</span>666 <span class="se">\</span>
    EXEC:<span class="s2">"npiperelay.exe -ep //./pipe/podman-machine-default"</span>,nofork
</code></pre></div></div>

<p>After some debugging (<code class="language-plaintext highlighter-rouge">npiperelay</code> wasn’t in PATH when running under sudo, the <code class="language-plaintext highlighter-rouge">socat</code> process was messing with terminal settings, etc.), the relay actually worked. <code class="language-plaintext highlighter-rouge">docker ps</code> from Ubuntu WSL talked to Podman successfully.</p>

<p>But the devcontainer builds still failed.</p>

<h2 id="unsurmountable-filesystem-issues">Unsurmountable filesystem issues</h2>

<p>The devcontainer build got further, but then failed with an error about an invalid symlink in a devcontainer feature. I worked around that by removing the offending feature. Then it failed because the workspace folder was empty.</p>

<p>The bind mount: the thing that maps my actual project folder (in the Ubuntu WSL distro filesystem) into the container: wasn’t working. The container was running, the mount was configured, but the folder was empty inside the container.</p>

<p>This is a fundamental problem: <strong>Podman’s WSL distro cannot see the filesystem of my Ubuntu WSL distro.</strong></p>

<p>Docker Desktop solves this transparently. It has special filesystem sharing between its VM and all WSL distros. Podman doesn’t have that. This is good for security, but bad for my workflow. Its WSL distro is isolated.</p>

<p>My options at this point were:</p>

<ol>
  <li>Move all my code to a Windows drive (C: or E:): that’s <em>slow</em>, especially for git operations from within a Linux container</li>
  <li>Clone repos inside the Podman machine directly: then I lose all my tooling, git credentials, SSH keys, and dotfiles</li>
  <li>Install Podman natively inside Ubuntu WSL instead of using the Windows Podman: this is its own can of worms and might not work seamlessly with VS Code either</li>
  <li>Go back to Docker Desktop</li>
</ol>

<h2 id="the-trade-offs">The trade-offs</h2>

<p>Podman’s security-first approach is genuinely good. Rootless by default, no daemon, better isolation. But this security isolates the running container from the filesystem I need, and that broke my workflow.</p>

<p>There’s also value in using the standard approach. When you use mainstream tooling:</p>
<ul>
  <li>Sources like Stack Overflow are easier to find</li>
  <li>Blog tutorials apply</li>
  <li>Colleagues can help</li>
  <li>VS Code extensions are tested against it</li>
</ul>

<p>This is a general point: Every deviation from standard adds friction. I can absorb a Sunday of troubleshooting. But if I recommended this to my teams of developers, that’s many work-days lost, plus ongoing support burden, plus “it works on my machine” debugging when setups drift. One of the major points of using containers is to reduce such friction, not add to it.</p>

<p>The real cost of Docker Desktop licensing: $5/user/month or whatever it is now: suddenly looks cheap compared to developer time.</p>

<h2 id="back-to-docker-desktop">Back to Docker Desktop</h2>

<p>I cleaned up Podman:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">podman</span><span class="w"> </span><span class="nx">machine</span><span class="w"> </span><span class="nx">stop</span><span class="w">
</span><span class="n">podman</span><span class="w"> </span><span class="nx">machine</span><span class="w"> </span><span class="nx">rm</span><span class="w">
</span><span class="n">winget</span><span class="w"> </span><span class="nx">uninstall</span><span class="w"> </span><span class="nx">RedHat.Podman</span><span class="w">
</span><span class="n">wsl</span><span class="w"> </span><span class="nt">--unregister</span><span class="w"> </span><span class="nx">podman-machine-default</span><span class="w">
</span></code></pre></div></div>

<p>And reinstalled Docker Desktop:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nx">Docker.DockerDesktop</span><span class="w">
</span></code></pre></div></div>

<p>One thing I made sure to do: move Docker’s data to E: immediately. In Docker Desktop’s settings, under Resources → Advanced → Disk image location, you can point it at another drive. No more filling up C:.</p>

<p>The devcontainer built and started on the first try. My files were there. Everything just worked.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Podman is a good tool. It works well for many things. Its security model is genuinely better than Docker’s. But for using Dev Containers on Windows with WSL, it fails.</p>

<p>For my workflow: devcontainers with code living in WSL’s native filesystem: Docker Desktop is still the pragmatic choice. The “magic” it does behind the scenes to make filesystem sharing work across WSL distros is exactly what I need.</p>

<p>Maybe Podman will solve this eventually. Maybe there’s a configuration I missed. But after a full Sunday of trying, I’m back where I started, with a bit more appreciation for what Docker Desktop actually does.</p>

<p>Sometimes the boring, mainstream choice is the right one.</p>

<p>And, I know - why do I still use Windows for my host OS?</p>]]></content><author><name>Tomas Ekeli</name></author><category term="docker" /><category term="docker" /><category term="podman" /><category term="wsl" /><category term="development" /><category term="devcontainers" /><summary type="html"><![CDATA[I tried replacing Docker Desktop with Podman on Windows. It didn't work out. Here's what happened, what I learned, and why I'm back on Docker Desktop.]]></summary></entry><entry><title type="html">Implementing RFC 9457: Problem Details for HTTP APIs in ASP.NET</title><link href="https://www.eke.li/dotnet/2025/09/26/problem-details.html" rel="alternate" type="text/html" title="Implementing RFC 9457: Problem Details for HTTP APIs in ASP.NET" /><published>2025-09-26T15:29:00+00:00</published><updated>2025-09-26T15:29:00+00:00</updated><id>https://www.eke.li/dotnet/2025/09/26/problem-details</id><content type="html" xml:base="https://www.eke.li/dotnet/2025/09/26/problem-details.html"><![CDATA[<p><img src="/assets/img/2025-09-26-problem-details.webp" alt="A computer monitor displaying an error message with a sad face emoji. Text overlay reads Implementing RFC 9457: Problem Details for HTTP APIs in ASP.NET." /></p>

<h2 id="http-status-codes-are-not-enough">HTTP Status codes are not enough</h2>

<p>When we implement HTTP APIs we can deal with errors in many ways. The HTTP protocol has a set of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">status codes</a> that we can use to indicate what went wrong, and we should certainly use those!</p>

<p>Aside: there’s a special place reserved for developers who use <code class="language-plaintext highlighter-rouge">200 OK</code> for everything, including errors. Don’t be that developer. No, it is not OK. Never. Stop it!</p>

<table>
  <thead>
    <tr>
      <th>Status Code family</th>
      <th>Meaning</th>
      <th>Informally</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1xx</td>
      <td>Informational</td>
      <td>Go on…</td>
    </tr>
    <tr>
      <td>2xx</td>
      <td>Success</td>
      <td>Yay!</td>
    </tr>
    <tr>
      <td>3xx</td>
      <td>Redirection</td>
      <td>You’re in the wrong place</td>
    </tr>
    <tr>
      <td>4xx</td>
      <td>Client errors</td>
      <td>Your mistake</td>
    </tr>
    <tr>
      <td>5xx</td>
      <td>Server errors</td>
      <td>My mistake</td>
    </tr>
  </tbody>
</table>

<p>The most basic was is to return a 200 if everything is OK, and a 500 if something went wrong. This is, however, not very informative. A 500 means “Internal Server Error”, which is not very helpful to the client. It does not tell the client what went wrong, or how to fix it.</p>

<p>It is slightly better to use more specific status codes. The simple act of dividing errors into 4xx (client errors) and 5xx (server errors) is a good start. Using <code class="language-plaintext highlighter-rouge">400 Bad Request</code> for invalid input, <code class="language-plaintext highlighter-rouge">401 Unauthorized</code> for missing or invalid authentication, <code class="language-plaintext highlighter-rouge">403 Forbidden</code> for lack of permissions, <code class="language-plaintext highlighter-rouge">404 Not Found</code> for missing resources, and so on, is even better.</p>

<p>There are a lot of status codes to choose from, but even so you’re likely to find yourself with a case that doesn’t quite fit - or you would like to be more specific about what went wrong.</p>

<p>When a client submits a form with validation-errors, for example. It is good to return a <code class="language-plaintext highlighter-rouge">400 Bad Request</code>, but it would be even better to tell the client which fields were invalid, and why.</p>

<h2 id="problem-details-to-the-rescue">Problem Details to the rescue</h2>

<p><a href="https://www.rfc-editor.org/rfc/rfc9457.html">RFC 9457: Problem Details for HTTP APIs</a> defines a standard way to return more information about errors in HTTP APIs. The RFC defines a JSON format for the response body that can be used to provide more information about the error. It replaces the older <a href="https://www.rfc-editor.org/rfc/rfc7807.html">RFC 7807</a> which defined a similar format, but was less flexible. These RFCs were published in July 2023 and March 2016 respectively. That’s right, this has been around for nearly 10 years at the time of writing!</p>

<p>These RFCs don’t change the status codes, you still return them as normal, but they talk about what you should include in the response body to provide more information about the error.</p>

<p>Sure, you <em>can</em> just return an empty body, or come up with your own format, but using a standard format has many advantages:</p>
<ul>
  <li>Clients can be written to understand the standard format, and can handle errors in a consistent way.</li>
  <li>It is easier to document your API, as you can refer to the standard format.</li>
  <li>It is easier to test your API, as you can use standard tools to generate and validate requests and responses.</li>
  <li>It is much easier to integrate with other systems, as they can understand the standard format.</li>
  <li><strong>You don’t have to invent your own format.</strong>
    <ul>
      <li>And these RFCs have already considered all the things your format would forget</li>
      <li>Seriously - you’re not that special. Use the standard.</li>
    </ul>
  </li>
</ul>

<h3 id="the-problem-details-format">The Problem Details format</h3>

<p>The Problem Details format is a JSON object that looks like this (example from the spec):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com/probs/out-of-credit"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"You do not have enough credit."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">403</span><span class="p">,</span><span class="w">
  </span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Your current balance is 30, but that costs 50."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/account/12345/msgs/abc"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"balance"</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="p">,</span><span class="w">
  </span><span class="nl">"accounts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"/account/12345"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"/account/67890"</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This includes <code class="language-plaintext highlighter-rouge">type</code>, <code class="language-plaintext highlighter-rouge">title</code>, <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">detail</code> and <code class="language-plaintext highlighter-rouge">instance</code> properties, which are defined in the RFC. It also includes additional properties, like <code class="language-plaintext highlighter-rouge">balance</code> and <code class="language-plaintext highlighter-rouge">accounts</code>, which are not defined in the RFC, but can be used to provide more information about the error specific to the case.</p>

<p>A common extension to the Problem Details format is to include a list of errors, (e.g. validation-errors for multiple fields) like this:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://tools.ietf.org/html/rfc9110#section-15.5.1"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"One or more validation errors occurred."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The submitted form was not valid."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
  </span><span class="nl">"errors"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Language"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"The Language field is required."</span><span class="p">,</span><span class="w">
      </span><span class="s2">"'Language' must not be empty."</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"Tags"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"The Tags field is required."</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"CreatedBy"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"The CreatedBy field is required."</span><span class="p">,</span><span class="w">
      </span><span class="s2">"'Created By' must not be empty."</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"Reference"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="s2">"The Reference field is required."</span><span class="p">,</span><span class="w">
      </span><span class="s2">"'Reference' must not be empty."</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"traceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"00-84c9d447edf8d4bcba82187356336e76-ca92c022b7da8472-01"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>With this information developers using you API can more easily understand what went wrong, and how to fix it. They can also use the errors to decorate the user-interface with errors, helping the user fix their input.</p>

<p>Note that this particular style of errors is not defined in the RFC, and I’ve seen many different variations of this. Since it is not defined in the RFC, you can should work with your clients to agree on a format that works for you.</p>

<h2 id="implementing-problem-details-in-aspnet">Implementing Problem Details in ASP.NET</h2>

<p>ASP.NET has built-in support for Problem Details, so you can use it out of the box. You can return a <code class="language-plaintext highlighter-rouge">ProblemDetails</code> object from your controller actions, and ASP.NET will automatically serialize it to JSON and set the appropriate status code.</p>

<p>You have to add the <code class="language-plaintext highlighter-rouge">Microsoft.AspNetCore.Mvc.ProblemDetails</code> package to your project.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Microsoft.AspNetCore.Mvc.ProblemDetails
</code></pre></div></div>

<p>This is pretty easy to do - you just add the following code in your application startup (usually in <code class="language-plaintext highlighter-rouge">Program.cs</code>):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddProblemDetails</span><span class="p">();</span>
</code></pre></div></div>

<p>This makes any problems with Asp.NET <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling-api?view=aspnetcore-9.0&amp;tabs=minimal-apis">automatically return Problem Details responses</a>, and also any error responses that do not have a body. With this a <code class="language-plaintext highlighter-rouge">404 Not Found</code> will return a Problem Details response with the appropriate status code and a default message.</p>

<p>But, that middleware does not know what actually went wrong, and cannot provide any additional information. For that you have to do it yourself.</p>

<p>I like <a href="https://www.milanjovanovic.dev/asp-net-core-web-api-error-handling-best-practices/">Milan Jovanović’s approach</a>, but I have an alternative approach that I prefer.</p>

<h3 id="the-problemdetailer">The ProblemDetailer</h3>

<p>What I want is for my controller to be able to return a <code class="language-plaintext highlighter-rouge">ProblemDetails</code> object with the appropriate status code and additional information, without having to write a lot of boilerplate code.</p>

<p>Here’s a small example of how I want a controller action to look (you can do this with minimal APIs as well):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.AspNetCore.Mvc</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">OneOf</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">OneOf.Types</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">ProblemDetailsExample</span><span class="p">;</span>

<span class="p">[</span><span class="n">ApiController</span><span class="p">]</span>
<span class="p">[</span><span class="nf">Route</span><span class="p">(</span><span class="s">"api/example"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">ExampleController</span><span class="p">(</span>
    <span class="n">IProblemDetailer</span> <span class="n">_problems</span><span class="p">,</span>
    <span class="n">IExampleHandler</span> <span class="n">_handler</span>
<span class="p">)</span> <span class="p">:</span> <span class="n">ControllerBase</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">HttpGet</span><span class="p">(</span><span class="s">"{id:guid}"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">IActionResult</span><span class="p">&gt;</span> <span class="nf">GetExample</span><span class="p">(</span>
        <span class="n">Guid</span> <span class="n">id</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">ct</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">maybeExample</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_handler</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>

        <span class="k">return</span> <span class="n">maybeExample</span><span class="p">.</span><span class="nf">Match</span><span class="p">(</span>
            <span class="n">example</span> <span class="p">=&gt;</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">example</span><span class="p">),</span>
            <span class="n">notFound</span> <span class="p">=&gt;</span> <span class="n">_problems</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">(</span>
                <span class="n">detail</span><span class="p">:</span> <span class="s">"Example not found"</span><span class="p">,</span>
                <span class="n">errors</span><span class="p">:</span> <span class="k">new</span><span class="p">()</span>
                <span class="p">{</span>
                    <span class="p">[</span><span class="s">"id"</span><span class="p">]</span> <span class="p">=</span> <span class="n">id</span><span class="p">.</span><span class="nf">ToString</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">),</span>
            <span class="n">error</span> <span class="p">=&gt;</span> <span class="n">_problems</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span>
                <span class="n">detail</span><span class="p">:</span> <span class="s">"Failed to get example"</span><span class="p">,</span>
                <span class="n">errors</span><span class="p">:</span> <span class="k">new</span><span class="p">()</span>
                <span class="p">{</span>
                    <span class="p">[</span><span class="s">"message"</span><span class="p">]</span> <span class="p">=</span> <span class="n">error</span><span class="p">.</span><span class="n">Value</span><span class="p">,</span>
                    <span class="p">[</span><span class="s">"id"</span><span class="p">]</span> <span class="p">=</span> <span class="n">id</span><span class="p">.</span><span class="nf">ToString</span><span class="p">()</span>
                <span class="p">}</span>
            <span class="p">)</span>
        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">Example</span><span class="p">(</span>
    <span class="n">Guid</span> <span class="n">Id</span>
<span class="p">);</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">IExampleHandler</span>
<span class="p">{</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">OneOf</span><span class="p">&lt;</span><span class="n">Example</span><span class="p">,</span> <span class="n">NotFound</span><span class="p">,</span> <span class="n">Error</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;</span> <span class="nf">Get</span><span class="p">(</span>
        <span class="n">Guid</span> <span class="n">id</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">ct</span>
    <span class="p">);</span>
<span class="p">}</span>

<span class="c1">// just a silly example showing returning different results</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ExampleHandler</span> <span class="p">:</span> <span class="n">IExampleHandler</span>
<span class="p">{</span>
    <span class="k">static</span> <span class="k">readonly</span> <span class="n">Guid</span> <span class="n">_illegal</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span>
        <span class="s">"00000000-0000-0000-0000-000000000001"</span>
    <span class="p">);</span>

    <span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">OneOf</span><span class="p">&lt;</span><span class="n">Example</span><span class="p">,</span> <span class="n">NotFound</span><span class="p">,</span> <span class="n">Error</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;</span> <span class="nf">Get</span><span class="p">(</span>
        <span class="n">Guid</span> <span class="n">id</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">ct</span>
    <span class="p">)</span> <span class="p">=&gt;</span>
        <span class="n">Task</span><span class="p">.</span><span class="n">FromResult</span><span class="p">&lt;</span><span class="n">OneOf</span><span class="p">&lt;</span><span class="n">Example</span><span class="p">,</span> <span class="n">NotFound</span><span class="p">,</span> <span class="n">Error</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;(</span>
            <span class="n">id</span> <span class="k">switch</span>
            <span class="p">{</span>
                <span class="kt">var</span> <span class="n">empty</span> <span class="n">when</span> <span class="n">empty</span> <span class="p">==</span> <span class="n">Guid</span><span class="p">.</span><span class="n">Empty</span> <span class="p">=&gt;</span>
                    <span class="k">new</span> <span class="nf">NotFound</span><span class="p">(),</span>
                <span class="kt">var</span> <span class="n">special</span> <span class="n">when</span> <span class="n">special</span> <span class="p">==</span> <span class="n">_illegal</span> <span class="p">=&gt;</span>
                    <span class="k">new</span> <span class="n">Error</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;(</span>
                        <span class="s">"That ID is forbidden"</span>
                    <span class="p">),</span>
                <span class="n">_</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="nf">Example</span><span class="p">(</span><span class="n">id</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I am using <a href="https://github.com/mcintyre321/OneOf">OneOf</a> that I have <a href="https://www.eke.li/2023/04/oneof-with-benchmarks/">written of earlier</a> to represent the possible outcomes of the <code class="language-plaintext highlighter-rouge">GetExample</code> method. It can return an <code class="language-plaintext highlighter-rouge">Example</code>, a <code class="language-plaintext highlighter-rouge">NotFound</code>, or an <code class="language-plaintext highlighter-rouge">Error&lt;string&gt;</code>. I find this very useful for representing the possible outcomes of a method, and it works well with the Problem Details approach.</p>

<p>I feel this works well together and ends up with pretty clean code.</p>

<p>Here’s my <code class="language-plaintext highlighter-rouge">IProblemDetailer</code> interface:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.AspNetCore.Mvc</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">ProblemDetailsExample</span><span class="p">;</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">IProblemDetailer</span>
<span class="p">{</span>
    <span class="n">BadRequestObjectResult</span> <span class="nf">BadRequest</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Bad Request"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The request is invalid."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>

    <span class="n">NotFoundObjectResult</span> <span class="nf">NotFound</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Not Found"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The requested resource was not found."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>

    <span class="n">ConflictObjectResult</span> <span class="nf">Conflict</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Conflict"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The request could not be completed due to a conflict."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>

    <span class="n">ObjectResult</span> <span class="nf">Error</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Error"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"An unexpected error occurred."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">int</span> <span class="n">statusCode</span> <span class="p">=</span> <span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status500InternalServerError</span>
    <span class="p">);</span>

    <span class="n">UnprocessableEntityObjectResult</span> <span class="nf">UnprocessableEntity</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Unprocessable Entity"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The request was well-formed but could not be processed."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>

    <span class="n">ObjectResult</span> <span class="nf">TooManyRequests</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Too Many Requests"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"Unable to process the request due to rate limiting."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">retryAfter</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>

    <span class="n">ObjectResult</span> <span class="nf">Gone</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Gone"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The requested resource is no longer available."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>

    <span class="n">ObjectResult</span> <span class="nf">MethodNotAllowed</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Method Not Allowed"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The HTTP method is not allowed for the requested resource."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see the caller can include a title, detail, errors, and instance, or nothing at all, and get a reasonable default response.</p>

<p>The implementation of the <code class="language-plaintext highlighter-rouge">IProblemDetailer</code> interface is straightforward, and populates the type based on the status code. The implementation is a bit repetitive to read, but is included here for reference:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">using</span> <span class="nn">System.Collections.Frozen</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Diagnostics</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.AspNetCore.Mvc</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">static</span> <span class="n">Microsoft</span><span class="p">.</span><span class="n">AspNetCore</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="n">StatusCodes</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">ProblemDetailsExample</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ProblemDetailer</span><span class="p">(</span>
    <span class="n">IHttpContextAccessor</span> <span class="n">_httpContextAccessor</span>
<span class="p">)</span> <span class="p">:</span> <span class="n">IProblemDetailer</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">BadRequestObjectResult</span> <span class="nf">BadRequest</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Bad Request"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The request is invalid."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>

        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status400BadRequest</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">NotFoundObjectResult</span> <span class="nf">NotFound</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Not Found"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The requested resource was not found."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>

        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status404NotFound</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">ConflictObjectResult</span> <span class="nf">Conflict</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Conflict"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The request could not be completed due to a conflict."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status409Conflict</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">ObjectResult</span> <span class="nf">Error</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Error"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"An unexpected error occurred."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">int</span> <span class="n">statusCode</span> <span class="p">=</span> <span class="n">Status500InternalServerError</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span><span class="n">statusCode</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="p">{</span>
            <span class="n">StatusCode</span> <span class="p">=</span> <span class="n">statusCode</span>
        <span class="p">};</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">UnprocessableEntityObjectResult</span> <span class="nf">UnprocessableEntity</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Unprocessable Entity"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The request was well-formed but could not be processed."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status422UnprocessableEntity</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">ObjectResult</span> <span class="nf">TooManyRequests</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Too Many Requests"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"Unable to process the request due to rate limiting."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">retryAfter</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">retryAfter</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">ext</span><span class="p">[</span><span class="s">"retryAfter"</span><span class="p">]</span> <span class="p">=</span> <span class="n">retryAfter</span><span class="p">;</span>
            <span class="n">_httpContextAccessor</span>
                <span class="p">.</span><span class="n">HttpContext</span>
                <span class="p">?.</span><span class="n">Response</span>
                <span class="p">.</span><span class="n">Headers</span>
                <span class="p">.</span><span class="nf">Append</span><span class="p">(</span>
                    <span class="s">"Retry-After"</span><span class="p">,</span>
                    <span class="n">retryAfter</span>
                <span class="p">);</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status429TooManyRequests</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="p">{</span>
            <span class="n">StatusCode</span> <span class="p">=</span> <span class="n">Status429TooManyRequests</span>
        <span class="p">};</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">ObjectResult</span> <span class="nf">Gone</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Gone"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The requested resource is no longer available."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status410Gone</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="p">{</span>
            <span class="n">StatusCode</span> <span class="p">=</span> <span class="n">Status410Gone</span>
        <span class="p">};</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">ObjectResult</span> <span class="nf">MethodNotAllowed</span><span class="p">(</span>
        <span class="kt">string</span> <span class="n">title</span> <span class="p">=</span> <span class="s">"Method Not Allowed"</span><span class="p">,</span>
        <span class="kt">string</span> <span class="n">detail</span> <span class="p">=</span> <span class="s">"The HTTP method is not allowed for the requested resource."</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">instance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">inst</span><span class="p">,</span> <span class="n">ext</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetContext</span><span class="p">(</span>
            <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">,</span>
            <span class="n">errors</span><span class="p">,</span>
            <span class="n">instance</span>
        <span class="p">);</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="n">type</span><span class="p">)</span> <span class="p">=</span> <span class="nf">GetType</span><span class="p">(</span>
            <span class="n">Status405MethodNotAllowed</span>
        <span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">ProblemDetails</span>
            <span class="p">{</span>
                <span class="n">Title</span> <span class="p">=</span> <span class="n">title</span><span class="p">,</span>
                <span class="n">Detail</span> <span class="p">=</span> <span class="n">detail</span><span class="p">,</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">code</span><span class="p">,</span>
                <span class="n">Type</span> <span class="p">=</span> <span class="n">type</span><span class="p">,</span>
                <span class="n">Instance</span> <span class="p">=</span> <span class="n">inst</span><span class="p">,</span>
                <span class="n">Extensions</span> <span class="p">=</span> <span class="n">ext</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="p">{</span>
            <span class="n">StatusCode</span> <span class="p">=</span> <span class="n">Status405MethodNotAllowed</span>
        <span class="p">};</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">string</span><span class="p">)</span> <span class="nf">GetType</span><span class="p">(</span><span class="kt">int</span> <span class="n">statusCode</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">StatusLinks</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="n">statusCode</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">type</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="p">(</span><span class="n">statusCode</span><span class="p">,</span> <span class="n">type</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="p">(</span>
            <span class="n">Status500InternalServerError</span><span class="p">,</span>
            <span class="n">StatusLinks</span><span class="p">[</span><span class="n">Status500InternalServerError</span><span class="p">]</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">object</span><span class="p">?&gt;)</span> <span class="nf">GetContext</span><span class="p">(</span>
        <span class="n">HttpContext</span><span class="p">?</span> <span class="n">context</span><span class="p">,</span>
        <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;?</span> <span class="n">errors</span><span class="p">,</span>
        <span class="kt">string</span><span class="p">?</span> <span class="n">possibleInstance</span> <span class="p">=</span> <span class="k">null</span>
    <span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">instance</span> <span class="p">=</span> <span class="n">possibleInstance</span>
            <span class="p">??</span> <span class="n">context</span><span class="p">?.</span><span class="n">Request</span><span class="p">.</span><span class="n">Path</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">traceId</span> <span class="p">=</span> <span class="n">Activity</span><span class="p">.</span><span class="n">Current</span><span class="p">?.</span><span class="n">Id</span>
            <span class="p">??</span> <span class="n">context</span><span class="p">?.</span><span class="n">TraceIdentifier</span><span class="p">;</span>

        <span class="k">return</span> <span class="p">(</span>
            <span class="n">instance</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">,</span>
            <span class="k">new</span><span class="p">()</span>
            <span class="p">{</span>
                <span class="p">[</span><span class="s">"traceId"</span><span class="p">]</span> <span class="p">=</span> <span class="n">traceId</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">,</span>
                <span class="p">[</span><span class="s">"errors"</span><span class="p">]</span> <span class="p">=</span> <span class="n">errors</span> <span class="p">??</span> <span class="p">[]</span>
            <span class="p">}</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="k">static</span> <span class="k">readonly</span> <span class="n">FrozenDictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span> <span class="n">StatusLinks</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;(</span><span class="m">24</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="n">Status400BadRequest</span><span class="p">]</span>           <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status401Unauthorized</span><span class="p">]</span>         <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7235#section-3.1"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status402PaymentRequired</span><span class="p">]</span>      <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.2"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status403Forbidden</span><span class="p">]</span>            <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status404NotFound</span><span class="p">]</span>             <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status405MethodNotAllowed</span><span class="p">]</span>     <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.5"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status406NotAcceptable</span><span class="p">]</span>        <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.6"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status407ProxyAuthenticationRequired</span><span class="p">]</span> <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7235#section-3.2"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status408RequestTimeout</span><span class="p">]</span>       <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.7"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status409Conflict</span><span class="p">]</span>             <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status410Gone</span><span class="p">]</span>                 <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.9"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status411LengthRequired</span><span class="p">]</span>       <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.10"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status412PreconditionFailed</span><span class="p">]</span>   <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7232#section-4.2"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status413PayloadTooLarge</span><span class="p">]</span>      <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.11"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status414UriTooLong</span><span class="p">]</span>           <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.12"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status415UnsupportedMediaType</span><span class="p">]</span> <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.13"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status416RangeNotSatisfiable</span><span class="p">]</span>  <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7233#section-4.4"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status417ExpectationFailed</span><span class="p">]</span>    <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.14"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status418ImATeapot</span><span class="p">]</span>            <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7168#section-2.3.3"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status422UnprocessableEntity</span><span class="p">]</span>  <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc4918#section-11.2"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status429TooManyRequests</span><span class="p">]</span>      <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc6585#section-4"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status500InternalServerError</span><span class="p">]</span>  <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status501NotImplemented</span><span class="p">]</span>       <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.2"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status502BadGateway</span><span class="p">]</span>           <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.3"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status503ServiceUnavailable</span><span class="p">]</span>   <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.4"</span><span class="p">,</span>
        <span class="p">[</span><span class="n">Status504GatewayTimeout</span><span class="p">]</span>       <span class="p">=</span> <span class="s">"https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.5"</span><span class="p">,</span>
    <span class="p">}.</span><span class="nf">ToFrozenDictionary</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I hope this helps you implement Problem Details in your ASP.NET applications!</p>]]></content><author><name>Tomas Ekeli</name></author><category term="dotnet" /><category term="dotnet" /><category term="aspnet" /><category term="development" /><category term="http" /><category term="standards" /><category term="rfc" /><category term="api" /><summary type="html"><![CDATA[Tell your clients what went wrong, not just that something went wrong. With RFC 9457 and ASP.NET it's easy to do.]]></summary></entry><entry><title type="html">Testing an AuthenticationHandler in ASP.NET</title><link href="https://www.eke.li/dotnet/2025/09/26/testing-authenticationhandler.html" rel="alternate" type="text/html" title="Testing an AuthenticationHandler in ASP.NET" /><published>2025-09-26T11:41:00+00:00</published><updated>2025-09-26T11:41:00+00:00</updated><id>https://www.eke.li/dotnet/2025/09/26/testing-authenticationhandler</id><content type="html" xml:base="https://www.eke.li/dotnet/2025/09/26/testing-authenticationhandler.html"><![CDATA[<p><img src="/assets/img/2025-09-26-testing-authenticationhandler.webp" alt="A computer monitor displaying colourful lines labeled AuthenticationHandler, with a magnifying glass showing a question mark in front and a shield with a checkmark. Text overlay reads Testing an AuthenticationHandler in ASP.NET." /></p>

<h2 id="our-authenticationhandler">Our AuthenticationHandler</h2>

<p>Let us say we have an <code class="language-plaintext highlighter-rouge">AuthenticationHandler</code> that looks like this to handle authentication. Here is a silly example randomly allowing or denying authentication:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">RandomAuthenticationHandler</span>
<span class="p">:</span> <span class="n">AuthenticationHandler</span><span class="p">&lt;</span><span class="n">AuthenticationSchemeOptions</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">SchemaName</span> <span class="p">=</span> <span class="s">"Random"</span><span class="p">;</span>

    <span class="k">readonly</span> <span class="kt">bool</span> <span class="n">_enabled</span><span class="p">;</span>
    <span class="k">readonly</span> <span class="kt">double</span> <span class="n">_chance</span><span class="p">;</span>
    <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">RandomAuthenticationHandler</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">RandomAuthenticationHandler</span><span class="p">(</span>
        <span class="n">IOptionsMonitor</span><span class="p">&lt;</span><span class="n">AuthenticationSchemeOptions</span><span class="p">&gt;</span> <span class="n">options</span><span class="p">,</span>
        <span class="n">ILoggerFactory</span> <span class="n">loggerFactory</span><span class="p">,</span>
        <span class="n">UrlEncoder</span> <span class="n">encoder</span><span class="p">,</span>
        <span class="n">IOptions</span><span class="p">&lt;</span><span class="n">RandomSettings</span><span class="p">&gt;</span> <span class="n">randomSettings</span>
        <span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span>
            <span class="n">options</span><span class="p">,</span>
            <span class="n">loggerFactory</span><span class="p">,</span>
            <span class="n">encoder</span>
        <span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span> <span class="p">=</span> <span class="n">loggerFactory</span>
            <span class="p">.</span><span class="n">CreateLogger</span><span class="p">&lt;</span><span class="n">RandomAuthenticationHandler</span><span class="p">&gt;();</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">randomSettings</span><span class="p">?.</span><span class="n">Value</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">LogMissingConfiguration</span><span class="p">(</span><span class="n">_logger</span><span class="p">);</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span>
                <span class="s">"Application misconfigured: No RandomSettings"</span>
            <span class="p">)</span>
        <span class="p">}</span>
        <span class="n">_randomSettings</span> <span class="p">=</span> <span class="n">randomSettings</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">AuthenticateResult</span><span class="p">&gt;</span> <span class="nf">HandleAuthenticateAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(!</span><span class="n">_randomSettings</span><span class="p">.</span><span class="n">Enabled</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">LogDisabled</span><span class="p">(</span><span class="n">_logger</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span>
                <span class="n">AuthenticateResult</span><span class="p">.</span><span class="nf">NoResult</span><span class="p">()</span>
            <span class="p">);</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">Random</span><span class="p">.</span><span class="n">Shared</span><span class="p">.</span><span class="nf">NextDouble</span><span class="p">()</span> <span class="p">&lt;</span> <span class="n">_randomSettings</span><span class="p">.</span><span class="n">Chance</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">LogRandomlyFailing</span><span class="p">(</span><span class="n">_logger</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span>
                <span class="n">AuthenticateResult</span><span class="p">.</span><span class="nf">Fail</span><span class="p">(</span><span class="s">"Not authenticated"</span><span class="p">)</span>
            <span class="p">);</span>
        <span class="p">}</span>

        <span class="nf">LogRandomlySucceeding</span><span class="p">(</span><span class="n">_logger</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span>
            <span class="n">AuthenticateResult</span><span class="p">.</span><span class="nf">Success</span><span class="p">(</span>
                <span class="k">new</span> <span class="nf">AuthenticationTicket</span><span class="p">(</span>
                    <span class="n">principal</span><span class="p">:</span> <span class="k">new</span> <span class="nf">ClaimsPrincipal</span><span class="p">(</span>
                        <span class="k">new</span> <span class="nf">ClaimsIdentity</span><span class="p">(</span>
                            <span class="n">authenticationType</span><span class="p">:</span> <span class="n">SchemaName</span><span class="p">,</span>
                            <span class="n">claims</span><span class="p">:</span>
                            <span class="p">[</span>
                                <span class="k">new</span> <span class="nf">Claim</span><span class="p">(</span>
                                    <span class="n">ClaimTypes</span><span class="p">.</span><span class="n">NameIdentifier</span><span class="p">,</span>
                                    <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">().</span><span class="nf">ToString</span><span class="p">()</span>
                                <span class="p">),</span>
                                <span class="k">new</span> <span class="nf">Claim</span><span class="p">(</span>
                                    <span class="n">ClaimTypes</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
                                    <span class="s">"Random User"</span>
                                <span class="p">),</span>
                            <span class="p">]</span>
                        <span class="p">)</span>
                    <span class="p">),</span>
                    <span class="n">authenticationScheme</span><span class="p">:</span> <span class="n">SchemaName</span>
                <span class="p">)</span>
            <span class="p">)</span>
        <span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// logging omitted for brevity</span>
<span class="p">}</span>

<span class="cm">/**
 * Bind this in your IoC setup
 **/</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">RandomSettings</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">Enabled</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">Range</span><span class="p">(</span><span class="m">0.0</span><span class="p">,</span> <span class="m">1.0</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">double</span> <span class="n">Chance</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>How would we test such a thing? Set aside that this is a silly example, and randomness it inherently hard to test. How do we set up the <code class="language-plaintext highlighter-rouge">AuthenticationHandler</code> in a test?</p>

<h2 id="setting-up-the-test">Setting up the test</h2>

<p>We need to instantiate our <code class="language-plaintext highlighter-rouge">RandomAuthenticationHandler</code> in a test. We can do that fine.</p>

<p>In the following example I’m using <a href="https://nsubstitute.github.io/">NSubstitute</a> for mocking, and <a href="https://xunit.net/">xUnit</a> for the test framework. The logging is set up in a base class called <code class="language-plaintext highlighter-rouge">Test_with_logs</code> that I use in many tests, you can read about that in <a href="https://www.eke.li/testing/2023/12/22/testing-your-logging.html">this post</a>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Given_a_RandomAuthenticationHandler</span>
<span class="p">:</span> <span class="n">Test_with_logs</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="n">IOptionsMonitor</span><span class="p">&lt;</span><span class="n">AuthenticationSchemeOptions</span><span class="p">&gt;</span> <span class="n">_options_monitor</span><span class="p">;</span>
    <span class="k">protected</span> <span class="n">IOptions</span><span class="p">&lt;</span><span class="n">RandomSettings</span><span class="p">&gt;</span> <span class="n">_random_settings</span><span class="p">;</span>
    <span class="k">protected</span> <span class="n">UrlEncoder</span> <span class="n">_url_encoder</span><span class="p">;</span>

    <span class="k">protected</span> <span class="n">RandomAuthenticationHandler</span> <span class="n">_handler</span><span class="p">;</span>

    <span class="k">protected</span> <span class="nf">Given_a_RandomAuthenticationHandler</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">_options_monitor</span> <span class="p">=</span> <span class="n">Substitute</span>
            <span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">IOptionsMonitor</span><span class="p">&lt;</span><span class="n">AuthenticationSchemeOptions</span><span class="p">&gt;&gt;();</span>

        <span class="n">_options_monitor</span>
            <span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">Arg</span><span class="p">.</span><span class="n">Any</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;())</span>
            <span class="p">.</span><span class="nf">Returns</span><span class="p">(</span><span class="k">new</span> <span class="nf">AuthOptions</span><span class="p">());</span>

        <span class="n">_url_encoder</span> <span class="p">=</span> <span class="n">Substitute</span><span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">UrlEncoder</span><span class="p">&gt;();</span>

        <span class="n">_random_settings</span> <span class="p">=</span> <span class="n">Options</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">RandomSettings</span>
            <span class="p">{</span>
                <span class="n">Enabled</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
                <span class="n">Chance</span> <span class="p">=</span> <span class="m">0.2</span>
            <span class="p">}</span>
        <span class="p">);</span>

        <span class="n">_handler</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">RandomAuthenticationHandler</span><span class="p">(</span>
            <span class="n">_options_monitor</span><span class="p">,</span>
            <span class="n">TestLoggerFactory</span><span class="p">,</span>
            <span class="n">_url_encoder</span><span class="p">,</span>
            <span class="n">_random_settings</span>
        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">When_authenticating_randomly</span>
<span class="p">:</span> <span class="n">Given_a_RandomAuthenticationHandler</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">It_should_not_crash</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">_random_authentication_handler</span><span class="p">.</span><span class="nf">AuthenticateAsync</span><span class="p">();</span>

        <span class="k">true</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">BeTrue</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Our simple test fails, since the <code class="language-plaintext highlighter-rouge">AuthenticationHandler</code> must be initialised before use. We can do that by calling the <code class="language-plaintext highlighter-rouge">InitializeAsync</code> -method. This is the thing that aspnet calls before our <code class="language-plaintext highlighter-rouge">HandleAuthenticationAsync</code> method is called. Let’s add that to our test base-class (the <code class="language-plaintext highlighter-rouge">Given</code> -class):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">public</span> <span class="k">class</span> <span class="nc">Given_a_RandomAuthenticationHandler</span>
<span class="p">:</span> <span class="n">Test_with_logs</span>
<span class="p">{</span>
    <span class="c1">// ...previously shown code omitted for brevity...</span>

    <span class="k">protected</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">Initialize_with</span><span class="p">(</span><span class="n">HttpContext</span> <span class="n">context</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">_handler</span><span class="p">.</span><span class="nf">InitializeAsync</span><span class="p">(</span>
            <span class="k">new</span> <span class="nf">AuthenticationScheme</span><span class="p">(</span>
                <span class="n">RandomAuthenticationHandler</span><span class="p">.</span><span class="n">SchemaName</span><span class="p">,</span>
                <span class="k">null</span><span class="p">,</span>
                <span class="k">typeof</span><span class="p">(</span><span class="n">RandomAuthenticationHandler</span><span class="p">)</span>
            <span class="p">),</span>
            <span class="n">context</span>
        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we can call that in our test, and pass in a <code class="language-plaintext highlighter-rouge">HttpContext</code> that we set up for the test. Let’s do that:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">public</span> <span class="k">class</span> <span class="nc">When_authenticating_randomly</span>
<span class="p">:</span> <span class="n">Given_a_RandomAuthenticationHandler</span>
<span class="p">{</span>
    <span class="k">readonly</span> <span class="n">DefaultHttpContext</span> <span class="n">_context</span> <span class="p">=</span> <span class="k">new</span><span class="p">();</span>

    <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">It_should_return_a_result</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="nf">Initialize_with</span><span class="p">(</span><span class="n">_context</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_handler</span><span class="p">.</span><span class="nf">AuthenticateAsync</span><span class="p">();</span>

        <span class="n">result</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">NotBeNull</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">It_should_log_something</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="nf">Initialize_with</span><span class="p">(</span><span class="n">_context</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">_handler</span><span class="p">.</span><span class="nf">AuthenticateAsync</span><span class="p">();</span>

        <span class="n">Logs</span>
            <span class="p">.</span><span class="nf">Should</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">NotBeEmpty</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="c1">// more tests here...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And there you have it! Now you can manipulate the <code class="language-plaintext highlighter-rouge">HttpContext</code> as you like before you initialize, and test your <code class="language-plaintext highlighter-rouge">AuthenticationHandler</code> in isolation.</p>

<p>Exercise for the reader: write a test wherein the <code class="language-plaintext highlighter-rouge">_random_settings.Value</code> is null verifying that the <code class="language-plaintext highlighter-rouge">InvalidOperationException</code> is thrown.</p>

<p>Happy testing!</p>]]></content><author><name>Tomas Ekeli</name></author><category term="dotnet" /><category term="csharp" /><category term="development" /><category term="dotnet" /><category term="aspnet" /><summary type="html"><![CDATA[How to set up an AuthenticationHandler for testing]]></summary></entry><entry><title type="html">Comparing records with collections in C#</title><link href="https://www.eke.li/dotnet/2023/12/25/comparing-records-with-collections.html" rel="alternate" type="text/html" title="Comparing records with collections in C#" /><published>2023-12-25T22:30:00+00:00</published><updated>2023-12-25T22:30:00+00:00</updated><id>https://www.eke.li/dotnet/2023/12/25/comparing-records-with-collections</id><content type="html" xml:base="https://www.eke.li/dotnet/2023/12/25/comparing-records-with-collections.html"><![CDATA[<p><img src="/assets/img/2023-12-25-comparing-records-with-collections.webp" alt="Two silhouettes with a glowing connection between them representing equal C# records." /></p>

<p><strong>2023-12-29: There’s a update in the end of this post</strong></p>

<h2 id="use-records-for-value-semantics">Use records for value semantics</h2>

<p>Records are a great thing in C#. They are immutable, and give you “<a href="https://en.wikipedia.org/wiki/Value_semantics">value semantics</a>” for comparison. This means that two records with the same values are considered equal, even if they are different instances. This saves you a lot of time and troublesome code!</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">record</span> <span class="nf">Person</span><span class="p">(</span>
    <span class="kt">string</span> <span class="n">FirstName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">LastName</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">person1</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Person</span><span class="p">(</span><span class="s">"Tomas"</span><span class="p">,</span> <span class="s">"Ekeli"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">person2</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Person</span><span class="p">(</span><span class="s">"Tomas"</span><span class="p">,</span> <span class="s">"Ekeli"</span><span class="p">);</span>

<span class="c1">// will print "true"</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">person1</span> <span class="p">==</span> <span class="n">person2</span><span class="p">);</span>
</code></pre></div></div>

<p>There are some caveats to this, but they are not the focus of this post. You can read more about them <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#value-equality">here</a>.</p>

<h2 id="the-problem-with-collections">The problem with collections</h2>

<p>Wonderful, but it all falls down if you have a collection of some sort as a property in your record. Then the comparison will only compare the references of the collections, not the contents. This is because the default implementation of <code class="language-plaintext highlighter-rouge">Equals</code> and <code class="language-plaintext highlighter-rouge">GetHashCode</code> for collections only compares the references.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">record</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="kt">string</span> <span class="n">FirstName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">LastName</span><span class="p">,</span>
    <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">Nicknames</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">person1</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="s">"Tomas"</span><span class="p">,</span>
    <span class="s">"Ekeli"</span><span class="p">,</span>
    <span class="p">[</span> <span class="s">"Tommy"</span> <span class="p">]</span>
<span class="p">);</span>

<span class="kt">var</span> <span class="n">person2</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="s">"Tomas"</span><span class="p">,</span>
    <span class="s">"Ekeli"</span><span class="p">,</span>
    <span class="p">[</span> <span class="s">"Tommy"</span> <span class="p">]</span>
<span class="p">);</span>

<span class="c1">// will print "false"</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">person1</span> <span class="p">==</span> <span class="n">person2</span><span class="p">);</span>

<span class="c1">// referencing person1's nicknames</span>
<span class="kt">var</span> <span class="n">person3</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="s">"Tomas"</span><span class="p">,</span>
    <span class="s">"Ekeli"</span><span class="p">,</span>
    <span class="n">person1</span><span class="p">.</span><span class="n">Nicknames</span>
<span class="p">);</span>

<span class="c1">// will print "true"</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">person1</span> <span class="p">==</span> <span class="n">person3</span><span class="p">);</span>
</code></pre></div></div>

<p>This is (in my experience) usually not what I want. When I use a record I am almost always using it for its value semantics. I want to compare the contents of the collections, not the references. So how do we do that?</p>

<h2 id="solution-nemesisessentials">Solution: Nemesis.Essentials</h2>

<p>We could implement our own collections that override <code class="language-plaintext highlighter-rouge">Equals</code> and <code class="language-plaintext highlighter-rouge">GetHashCode</code> to compare the contents. But that is a lot of work, and it is easy to get wrong, and will be a chore to maintain.</p>

<p>But, there <em>is</em> such a library already out there that we can just use, it is called <a href="https://github.com/nemesissoft/Nemesis.Essentials">Nemesis.Essentials</a> and you can install it from NuGet. It is MIT licensed, so you can use it in your projects without any worries about licensing.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dotnet</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">package</span><span class="w"> </span><span class="nx">Nemesis.Essentials</span><span class="w">
</span></code></pre></div></div>

<p>You change your record to use the <code class="language-plaintext highlighter-rouge">ValueCollection&lt;T&gt;</code> from the library instead of <code class="language-plaintext highlighter-rouge">List&lt;T&gt;</code>. Then you get the value semantics you want. With the <a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#collection-expressions">new collection expressions in C# 12</a>, it is even easier to use.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">using</span> <span class="nn">Nemesis.Essentials.Design</span><span class="p">;</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="kt">string</span> <span class="n">FirstName</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">LastName</span><span class="p">,</span>
    <span class="n">ValueCollection</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">Nicknames</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">person1</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="s">"Tomas"</span><span class="p">,</span>
    <span class="s">"Ekeli"</span><span class="p">,</span>
    <span class="k">new</span><span class="p">([</span> <span class="s">"Tommy"</span> <span class="p">])</span>
<span class="p">);</span>

<span class="kt">var</span> <span class="n">person2</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PersonWithNickname</span><span class="p">(</span>
    <span class="s">"Tomas"</span><span class="p">,</span>
    <span class="s">"Ekeli"</span><span class="p">,</span>
    <span class="k">new</span><span class="p">([</span> <span class="s">"Tommy"</span> <span class="p">])</span>
<span class="p">);</span>

</code></pre></div></div>

<p>There are a lot of other things in that library as well, but this one seemed very helpful. I hope you find it useful as well!</p>

<h2 id="update-2023-12-29-danger-caveats">Update 2023-12-29 Danger! Caveats!</h2>

<p>As has been pointed out to me by <a href="https://hachyderm.io/@skolima">Leszek Ciesielski</a> - there are some caveats to <a href="https://plud.re/notes/9nt0ow2gvup4nb33">this approach</a>. While it <em>does</em> provide the equality semantics I am after, it comes with consequences.</p>

<p><code class="language-plaintext highlighter-rouge">ValueCollection</code> inherits <a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.objectmodel.collection-1">System.Collections.ObjectModel.Collection</a>, a <em>mutable</em> data-structure.</p>

<p>This is not what we want in a record, as their immutability is why they can have value semantics in the first place. And, since the collection can be part of how the record is compared and hashed - this means that the record’s hash-code can change. This is a problem. It can also affect serialization -performance, which is bad.</p>

<p>Whether or not these problems are deal-breakers for you depends on your use-case. Do you need to serialize and deserialize your records? Do you need to use them as keys in a dictionary? Will you never mutate the collection? If the answer to any of these questions is “yes”, then you should probably <em>not</em> use this approach.</p>

<p>I still think that the library is useful, but it is not a silver bullet. If anyone ever finds one of those fabled silver bullets - please tell me! I will probably use the Nemesis -library in some cases, but not in others.</p>

<p>An alternative solution is to use <a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablelist-1">System.Collections.Immutable.ImmutableList</a> instead of <code class="language-plaintext highlighter-rouge">ValueCollection&lt;T&gt;</code>. This is an immutable data-structure, and it has the equality semantics we want. But, it is not a drop-in replacement for <code class="language-plaintext highlighter-rouge">List&lt;T&gt;</code>, so you will have to change your code to use it. It also has some performance implications, so you need to consider those carefully before you use it.</p>

<p>There is also the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen">System.Collections.Frozen</a> -namespace in dotnet8, which spends more time when constructing the collection, but is immutable from then on and offers <a href="https://davecallan.com/dotnet-8-frozendictionary-benchmarks/">better performance after creation</a>. They only have Dictionaries and Sets, though - no Lists.</p>]]></content><author><name>Tomas Ekeli</name></author><category term="dotnet" /><category term="csharp" /><category term="development" /><category term="dotnet" /><category term="records" /><category term="collections" /><summary type="html"><![CDATA[Records have surprising equality-semantics when they contain collections. Here is a simple way to fix it.]]></summary></entry><entry><title type="html">Testing your logging in C#</title><link href="https://www.eke.li/testing/2023/12/22/testing-your-logging.html" rel="alternate" type="text/html" title="Testing your logging in C#" /><published>2023-12-22T15:30:00+00:00</published><updated>2023-12-22T15:30:00+00:00</updated><id>https://www.eke.li/testing/2023/12/22/testing-your-logging</id><content type="html" xml:base="https://www.eke.li/testing/2023/12/22/testing-your-logging.html"><![CDATA[<p><img src="/assets/img/2023-12-22-testing-your-logging.webp" alt="Computer screen displaying C# code with a magnifying glass highlighting logging functions and a checklist of passed test cases." /></p>

<p>When you write your code a sometimes disregarded part of the functionality is what gets logged and when. As you write code that needs to log to some specific level, or where logs are an important part of the functionality, you should also test that you are logging correctly.</p>

<h2 id="verifying-logging-can-be-tricky">Verifying logging can be tricky</h2>

<p>The problem is that logging is often done through static methods or extension methods in C#, and these are hard to mock. Your actual code might look something like this, logging with the <a href="https://www.nuget.org/packages/Microsoft.Extensions.Logging/"><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Logging</code></a> -library:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.Extensions.Logging</span><span class="p">;</span>
<span class="cm">/* ... other usings to get IService
 and Result */</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">Thing</span><span class="p">(</span>
  <span class="n">ILogger</span> <span class="n">_logger</span><span class="p">,</span>
  <span class="n">IService</span> <span class="n">_dependency</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Result</span><span class="p">&gt;</span> <span class="nf">DoTheThing</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
      <span class="s">"Doing something"</span><span class="p">);</span>
    <span class="k">try</span>
    <span class="p">{</span>
      <span class="kt">var</span> <span class="n">something</span> <span class="p">=</span> <span class="k">await</span>
        <span class="n">_dependency</span><span class="p">.</span><span class="nf">DoSomething</span><span class="p">();</span>
      <span class="k">return</span> <span class="n">Result</span>
        <span class="p">.</span><span class="nf">Success</span><span class="p">(</span><span class="n">something</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="n">_logger</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span>
        <span class="n">ex</span><span class="p">,</span>
        <span class="s">"Something went wrong"</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="n">Failure</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you want to have a test that verifies that the logging is done correctly, you need to mock the logger. This is not hard, you just mock the <code class="language-plaintext highlighter-rouge">ILogger</code> -interface, but the <code class="language-plaintext highlighter-rouge">LogInformation</code> and <code class="language-plaintext highlighter-rouge">LogError</code> -methods are <em>extension methods</em>, and you can’t mock extension methods. You can’t mock static methods either, so what do you do?</p>

<h2 id="mocking-and-extension-methods">Mocking and extension methods</h2>

<p>You can find lots of suggestions on the internet to mock out the methods on the actual <code class="language-plaintext highlighter-rouge">ILogger</code> -interface that the extension methods call, but <em>that is not a good idea</em>. Mocking like that means you are tying your tests to the <em>inner details</em> of the extension methods. You should not need to know how the extension methods work to test your code! Depending on inner workings leaves you open to breaking changes in the extension methods, and it makes your tests harder to read.</p>

<p>Instead - take a step back and think about what you’re actually trying to test. You don’t care about how the message is written, you just want to test that the correct log message is written, and that it is written at the correct log level. What you really need is the log-output in some way.</p>

<h2 id="melt">MELT</h2>
<p>This is just what <a href="https://github.com/alefranz/MELT">MELT</a> gives you! MELT is a library that lets you work with the standard <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Logging</code> -library, but lets you run your tests and capture the logged output. Then you verify that the output (which is what you actually care about) is correct. This frees you up from knowing <em>how</em> the logging actually happens under-the-hood, and you can assert on the output instead. <a href="https://alessio.franceschelli.me/posts/dotnet/how-to-test-logging-when-using-microsoft-extensions-logging/">Here’s a great post on it</a>.</p>

<p>To install it, add Microsoft.Extensions.Logging (if not already there) and the <a href="https://www.nuget.org/packages/MELT/">MELT NuGet package</a> to your test project:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dotnet</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">package</span><span class="w"> </span><span class="nx">Microsoft.Extensions.Logging</span><span class="w">
</span><span class="n">dotnet</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">package</span><span class="w"> </span><span class="nx">MELT</span><span class="w">
</span></code></pre></div></div>

<p>In this is an example <a href="https://github.com/xunit/xunit">XUnit</a> test that verifies that the correct log message is written, with <a href="https://nsubstitute.github.io/">NSubstitute</a> and <a href="https://docs.shouldly.org/">Shouldly</a> for mocking and assertions:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">MELT</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Extensions.Logging</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">NSubstitute</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">NSubstitute.ExceptionExtensions</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Shouldly</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Xunit</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ThingLogsAsExpected</span>
<span class="p">{</span>
  <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">LogsCorrectMessage</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="c1">// Arrange</span>
    <span class="kt">var</span> <span class="n">factory</span> <span class="p">=</span> <span class="n">TestLoggerFactory</span>
      <span class="p">.</span><span class="nf">Create</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">logger</span> <span class="p">=</span> <span class="n">factory</span>
      <span class="p">.</span><span class="n">CreateLogger</span><span class="p">&lt;</span><span class="n">Thing</span><span class="p">&gt;();</span>

    <span class="kt">var</span> <span class="n">dependency</span> <span class="p">=</span> <span class="n">Substitute</span>
      <span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">IService</span><span class="p">&gt;();</span>
    <span class="n">dependency</span>
      <span class="p">.</span><span class="nf">DoSomething</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">Throws</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">"Boom!"</span><span class="p">)</span>
      <span class="p">);</span>

    <span class="kt">var</span> <span class="n">system_under_test</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Thing</span><span class="p">(</span>
      <span class="n">logger</span><span class="p">,</span>
      <span class="n">dependency</span><span class="p">);</span>

    <span class="c1">// Act</span>
    <span class="k">await</span> <span class="n">system_under_test</span>
      <span class="p">.</span><span class="nf">DoTheThing</span><span class="p">();</span>

    <span class="c1">// Assert</span>
    <span class="n">factory</span>
      <span class="p">.</span><span class="n">Sink</span>
      <span class="p">.</span><span class="n">LogEntries</span>
      <span class="p">.</span><span class="nf">ShouldContain</span><span class="p">(</span><span class="n">entry</span> <span class="p">=&gt;</span>
        <span class="n">entry</span><span class="p">.</span><span class="n">LogLevel</span> <span class="p">==</span>
          <span class="n">LogLevel</span><span class="p">.</span><span class="n">Error</span>
        <span class="p">&amp;&amp;</span> <span class="n">entry</span><span class="p">.</span><span class="n">Exception</span> <span class="p">!=</span> <span class="k">null</span>
        <span class="p">&amp;&amp;</span> <span class="n">entry</span><span class="p">.</span><span class="n">Exception</span><span class="p">.</span><span class="n">Message</span>
          <span class="p">==</span> <span class="s">"Boom!"</span>
        <span class="p">&amp;&amp;</span> <span class="n">entry</span><span class="p">.</span><span class="n">Message</span> <span class="p">==</span>
          <span class="s">"Something went wrong"</span>
      <span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="a-handy-base-class-for-your-tests">A handy base class for your tests</h2>
<p>The access to the <code class="language-plaintext highlighter-rouge">TestLoggerFactory</code> and its <code class="language-plaintext highlighter-rouge">Sink</code> -property is what gives you access to the logged output. Personally I find this direct use a bit too verbose - so I usually create a super-class for my tests that let me access it in a more convenient way:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">MELT</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Extensions.Logging</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">NSubstitute</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">NSubstitute.ExceptionExtensions</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Shouldly</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Xunit</span><span class="p">;</span>

<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">TestsWithLogging</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="n">ITestLoggerFactory</span> <span class="n">Factory</span> <span class="p">=</span>
     <span class="n">TestLoggerFactory</span><span class="p">.</span><span class="nf">Create</span><span class="p">();</span>

  <span class="k">protected</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">LogEntry</span><span class="p">&gt;</span> <span class="n">Logs</span> <span class="p">=&gt;</span>
    <span class="n">Factory</span><span class="p">.</span><span class="n">Sink</span><span class="p">.</span><span class="n">LogEntries</span><span class="p">;</span>

  <span class="k">public</span> <span class="nf">TestsWithLogging</span><span class="p">()</span> <span class="p">=&gt;</span>
    <span class="n">Factory</span><span class="p">.</span><span class="n">Sink</span><span class="p">.</span><span class="nf">Clear</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">WhenTheDependencyThrows</span>
  <span class="p">:</span> <span class="n">TestsWithLogging</span>
<span class="p">{</span>
  <span class="k">readonly</span> <span class="n">IService</span> <span class="n">_dependency</span><span class="p">;</span>
  <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">Thing</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>
  <span class="k">readonly</span> <span class="n">Thing</span> <span class="n">_system_under_test</span><span class="p">;</span>
  <span class="k">readonly</span> <span class="n">Result</span> <span class="n">_result</span><span class="p">;</span>

  <span class="k">public</span> <span class="nf">WhenTheDependencyThrows</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="c1">// Arrange</span>
    <span class="n">_dependency</span> <span class="p">=</span> <span class="n">Substitute</span>
      <span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">IService</span><span class="p">&gt;();</span>
    <span class="n">_logger</span> <span class="p">=</span> <span class="n">Factory</span>
      <span class="p">.</span><span class="n">CreateLogger</span><span class="p">&lt;</span><span class="n">Thing</span><span class="p">&gt;();</span>

    <span class="n">_system_under_test</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Thing</span><span class="p">(</span>
      <span class="n">_logger</span><span class="p">,</span>
      <span class="n">_dependency</span><span class="p">);</span>

    <span class="n">_dependency</span>
      <span class="p">.</span><span class="nf">DoSomething</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">Throws</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">"Boom!"</span><span class="p">)</span>
      <span class="p">);</span>

    <span class="c1">// Act</span>
    <span class="n">_result</span> <span class="p">=</span> <span class="n">_system_under_test</span>
      <span class="p">.</span><span class="nf">DoTheThing</span><span class="p">()</span>
      <span class="p">.</span><span class="n">Result</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// each Fact is an assertion</span>
  <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">LogsError</span><span class="p">()</span> <span class="p">=&gt;</span>
    <span class="n">Logs</span>
      <span class="p">.</span><span class="nf">ShouldContain</span><span class="p">(</span><span class="n">logEntry</span> <span class="p">=&gt;</span>
        <span class="n">logEntry</span><span class="p">.</span><span class="n">LogLevel</span> <span class="p">==</span>
          <span class="n">LogLevel</span><span class="p">.</span><span class="n">Error</span>
        <span class="p">&amp;&amp;</span> <span class="n">logEntry</span><span class="p">.</span><span class="n">Exception</span> <span class="p">!=</span>
          <span class="k">null</span>
        <span class="p">&amp;&amp;</span> <span class="n">logEntry</span>
          <span class="p">.</span><span class="n">Exception</span>
          <span class="p">.</span><span class="n">Message</span> <span class="p">==</span> <span class="s">"Boom!"</span>
        <span class="p">&amp;&amp;</span> <span class="n">logEntry</span><span class="p">.</span><span class="n">Message</span> <span class="p">==</span>
          <span class="s">"Something went wrong"</span>
      <span class="p">);</span>

  <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">LogsInformation</span><span class="p">()</span> <span class="p">=&gt;</span>
    <span class="n">Logs</span>
      <span class="p">.</span><span class="nf">ShouldContain</span><span class="p">(</span><span class="n">logEntry</span> <span class="p">=&gt;</span>
        <span class="n">logEntry</span><span class="p">.</span><span class="n">LogLevel</span> <span class="p">==</span>
          <span class="n">LogLevel</span><span class="p">.</span><span class="n">Information</span>
        <span class="p">&amp;&amp;</span> <span class="n">logEntry</span><span class="p">.</span><span class="n">Message</span> <span class="p">==</span>
          <span class="s">"Doing something"</span>
      <span class="p">);</span>

  <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">LogsTwoMessages</span><span class="p">()</span> <span class="p">=&gt;</span>
    <span class="n">Logs</span>
      <span class="p">.</span><span class="nf">Count</span><span class="p">()</span>
      <span class="p">.</span><span class="nf">ShouldBe</span><span class="p">(</span><span class="m">2</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This way I can access the <code class="language-plaintext highlighter-rouge">Logs</code> -property directly in the test, and verify that it contains what I expect. By clearing the logs before each test each run gets their own log and the tests won’t affect each other.</p>

<p>This way of writing tests with the assertion and the act in the constructor is a bit unusual, but I find it very convenient. It makes the tests very readable, and it makes it easy to add or remove assertions. As you can see here I’ve added two more assertions, and the test still reads well.</p>

<p>Some readers may not agree with my rather aggressive column-limit. I don’t actually break my code at 43 characters in real life, but I do try to keep it under 80 characters. I do it here to keep the code within the screen for readers on mobile devices (there have been complaints).</p>

<p>If you’re using <a href="https://nunit.org/">NUnit</a> you can do the same thing with a <code class="language-plaintext highlighter-rouge">OneTimeSetUp</code> -method, and if you’re using MSTest you can do the same thing with a <code class="language-plaintext highlighter-rouge">[TestInitialize]</code> -attribute. I prefer XUnit, as each Fact is inherently separate, but you can do this with any test framework. Just be careful with any statics.</p>

<h2 id="conclusion">Conclusion</h2>
<p>When you want to test that you are logging correctly it can be tricky - as extension methods and statics are hard to mock. By using MELT you can test that the correct log message is written, and that it is written at the correct log level. This frees you up from knowing how the logging actually happens under-the-hood, and you can specify just what you are interested in.</p>

<p>You may not need or want to test your logging, but if you do - MELT is a great way to do it!</p>

<h2 id="ps-performance-and-memory-when-logging">P.S.: Performance and memory when logging</h2>

<p>The example <code class="language-plaintext highlighter-rouge">Thing</code> here uses the extension-methods from the <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Logging</code> -library directly in the code. This works, and is often what we do when we are not massively concerned with performance.</p>

<p>If you are you should <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator">generate log-methods</a> using the <code class="language-plaintext highlighter-rouge">LoggerMessage</code> -attribute. This will generate a static method that you can call instead of the extension methods on <code class="language-plaintext highlighter-rouge">ILogger</code>. It is much faster and uses less memory (particularly when you log values).</p>

<p>Note that for this to work your class must be <code class="language-plaintext highlighter-rouge">partial</code> (to allow the generated code to “take over” for the log-methods). Example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">Thing</span><span class="p">(</span>
  <span class="n">ILogger</span> <span class="n">_logger</span><span class="p">,</span>
  <span class="n">IService</span> <span class="n">_dependency</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Result</span><span class="p">&gt;</span> <span class="nf">DoTheThing</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="nf">Entered</span><span class="p">(</span><span class="n">_logger</span><span class="p">);</span>
    <span class="k">try</span>
    <span class="p">{</span>
      <span class="kt">var</span> <span class="n">something</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_dependency</span>
        <span class="p">.</span><span class="nf">DoSomething</span><span class="p">();</span>
      <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="nf">Success</span><span class="p">(</span><span class="n">something</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="nf">Failed</span><span class="p">(</span><span class="n">_logger</span><span class="p">,</span> <span class="n">ex</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="n">Failure</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="nf">LoggerMessage</span><span class="p">(</span>
    <span class="n">EventId</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span>
    <span class="n">Level</span> <span class="p">=</span> <span class="n">LogLevel</span><span class="p">.</span><span class="n">Information</span><span class="p">,</span>
    <span class="n">Message</span> <span class="p">=</span> <span class="s">"Doing something"</span><span class="p">)]</span>
  <span class="k">public</span> <span class="k">static</span> <span class="k">partial</span> <span class="k">void</span> <span class="nf">Entered</span><span class="p">(</span>
    <span class="n">ILogger</span> <span class="n">logger</span><span class="p">);</span>

  <span class="p">[</span><span class="nf">LoggerMessage</span><span class="p">(</span>
    <span class="n">EventId</span> <span class="p">=</span> <span class="m">2</span><span class="p">,</span>
    <span class="n">Level</span> <span class="p">=</span> <span class="n">LogLevel</span><span class="p">.</span><span class="n">Error</span><span class="p">,</span>
    <span class="n">Message</span> <span class="p">=</span> <span class="s">"Something went wrong"</span><span class="p">)]</span>
  <span class="k">public</span> <span class="k">static</span> <span class="k">partial</span> <span class="k">void</span> <span class="nf">Failed</span><span class="p">(</span>
    <span class="n">ILogger</span> <span class="n">logger</span><span class="p">,</span>
    <span class="n">Exception</span> <span class="n">ex</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Hmm, maybe I’ve finally found a real use for <code class="language-plaintext highlighter-rouge">#region</code>? In fact you may pull those partial methods out to a different file, perhaps called <code class="language-plaintext highlighter-rouge">Thing.Logging.cs</code> or something like that. That way you can keep the code that does the actual work separate from the logging code. Personally I prefer to have it all in one file.</p>]]></content><author><name>Tomas Ekeli</name></author><category term="testing" /><category term="csharp" /><category term="tdd" /><category term="testing" /><category term="logging" /><category term="mocking" /><summary type="html"><![CDATA[When you want to test that you are logging correctly it can be tricky - as extension methods and statics are hard to mock. Here is a way to do it.]]></summary></entry><entry><title type="html">A new home in the Fediverse</title><link href="https://www.eke.li/fediverse/2023/12/17/a-new-home-in-the-fediverse.html" rel="alternate" type="text/html" title="A new home in the Fediverse" /><published>2023-12-17T20:00:00+00:00</published><updated>2023-12-17T20:00:00+00:00</updated><id>https://www.eke.li/fediverse/2023/12/17/a-new-home-in-the-fediverse</id><content type="html" xml:base="https://www.eke.li/fediverse/2023/12/17/a-new-home-in-the-fediverse.html"><![CDATA[<p>My social-media path has brought me from Facebook, Google+, Laconi.ca (remember that?), Twitter, Mastodon and Pixelfed to my own server in the Fediverse.</p>

<p><img src="/assets/img/2023-12-17-a-new-home-in-the-fediverse.webp" alt="illustration of a vibrant Fediverse community rising like a phoenix with interconnected nodes representing various platforms, set against a starry cyberspace background, symbolizing a welcoming and interconnected online world" /></p>

<h2 id="some-history">Some history</h2>

<p>I used to post a lot on Twitter, when that still was a good place to be. I met lots of interesting people, learnt and taught, helped and received help. I have never been, nor wanted to be, any kind leader or influencer, but I felt I was part of a community. I was a part of something bigger than myself. I was a part of the Internet.</p>

<p>Then Twitter started to change. Maybe it did not change, maybe I just started to notice it more.</p>

<p>Still, Twitter was where the people were - it was where I needed to be to keep interacting with so many good people. I was caught by the network effect, and did not want to leave.</p>

<p>I made small forays into the <a href="https://en.wikipedia.org/wiki/Fediverse">Fediverse</a>, but kept coming back to Twitter. Maybe it was just inertia, maybe it was <a href="https://en.wikipedia.org/wiki/Metcalfe%27s_law">Metcalfe’s Law</a>, but I stayed. Then, some years ago, I decided to try out something else.</p>

<p>I did not delete my Twitter account yet, but I made a <a href="https://snabelen.no/explore">Mastodon</a> account, and I made a <a href="https://pixelfed.de/web/explore">Pixelfed</a> account. I did not use them much. I had few followers and followed few people. It felt quiet and lonely, I did not feel like I was part of a community.</p>

<p>Slowly I built up a group of people to follow. It takes a little effort, but it’s not complex. Here, without further ado, is the way to build yourself an interesting timeline on the Fediverse:</p>

<ol>
  <li>Find someone interesting. This is the first and somewhat hard thing to do. I found people by looking at mastodon servers that seemed interesting and following some likely people there. I also found people by looking at the “local timeline” on the server I joined.</li>
  <li>Find out who the interesting person is following. This is the easy part. Just click on the “following” link on their profile.</li>
  <li>Follow some of those people.</li>
  <li>Look at your timeline and interact with people you find interesting.</li>
  <li>Mute or block assholes. There are going to be assholes, but you don’t have to listen to them. Don’t be offended if people think you’re an asshole and blocks you. It’s fine.</li>
  <li>Repeat.</li>
</ol>

<p>Don’t worry about whether people follow you - just post things that interest you and be a nice person. If you seem interesting, people will follow you. If you do not, they won’t. It’s fine.</p>

<p>Nobody gets a prize for having followers, that’s <em>not</em> what the Fediverse is about. This is probably my favourite thing about the Fediverse - it does not play by the “eternal growth at any cost” rules of commercial social media. It’s not about the numbers, it’s about the people.</p>

<p>I had interesting interactions on the Fediverse, and Twitter, at the same time. It was a little awkward, maintaining two “presences”, but it kind of worked.</p>

<p>Things really changed, however, when the current owner of Twitter cum X bought the company and started changing things. I’d had enough, and wanted no part in supporting what that company was turning into, and I no longer needed to be there to interact with people. I left.</p>

<h2 id="the-fediverse">The Fediverse</h2>

<p>There are many <a href="https://scribe.rip/@VirtualAdept/a-friendly-introduction-to-the-fediverse-5b4ef3f8ed0e">explanations</a> on the internet about what the Fediverse is, and how it works. I will not repeat them here, but I will give a short summary:</p>

<p>When you’re using “traditional” (commercial) social media, you are using a service that is owned by a company. You sign up to Facebook and get an account in their system. Then you tell them who you want to connect with (that’s in their system), and they give you a timeline and ways of communicating with those people.</p>

<p>With the Fediverse it’s slightly different. You still use a service (let’s say <a href="https://mastodon.social">Mastodon Social</a>), which <em>may</em> be controlled by a company but usually is a group of friends or a community. You sign up to that service, and get an account in their system. Then you tell them who you want to connect with that’s on the Fediverse, and they give you a timeline and ways of communicating with those people.</p>

<p>The important difference is that the people you can connect with on the Fediverse are not just people on the service you’ve signed up to (Mastodon Social in our example). You can connect with anyone on any number of other services that participate in the Fediverse.</p>

<p>There are many kinds of services in the Fediverse, that provide many different services. Micro-blogging (like Twitter) is the most common, but there are also services for photo sharing (like Instagram), video sharing (like YouTube), and many others.</p>

<p>You’re probably going to set up an account on a <a href="https://joinmastodon.org">Mastodon</a> -server first. Mastodon is a type of server, and there are many different instances of that server (like Mastodon Social). Just like <a href="https://wordpress.org">WordPress</a> is a kind of server, and many people and organisations run their own Wordpress -sites using that kind of server.</p>

<p>The magic comes from all of these different kinds of servers communicating with each other. A user on one server can communicate with a user on a different server (just that is kind of cool), even if the two servers are of totally different kinds! This is possible because, whilst the servers are different, they all speak the same language. They all use the same <a href="https://www.w3.org/TR/activitypub/">protocol</a> to communicate with each other. This is what makes the Fediverse so powerful.</p>

<h3 id="an-example">An example</h3>

<p>I have an account on a server called <a href="https://plud.re">plud.re</a>. My username there is @tomasekeli, so my full address is <a href="https://plud.re/@tomasekeli">@tomasekeli@plud.re</a>. My friends over on Mastodon Social can enter that address in their search bar, and find me, follow me and interact with me. And plud.re is a totally different kind of server from Mastodon! If Facebook, X and, TikTok and Instagram did this you would be able to follow TikTok users from Instagram and share their videos with your friends on Facebook from your X timeline!</p>

<h2 id="my-own-server">My own server</h2>

<p>Another neat part of the Fediverse is that you are not expected to stay on one server forever. You can migrate to a new home on the internet that you like better. I have done this several times: I started on mastodon.social (most people do) before I went to mastodon.tech. That server was closed down, for personal reasons of the owner and maintainer, and I moved on to snabelen.no, which is a Norwegian Mastodon server. And now, this weekend, I moved over to plud.re - which is my own server.</p>

<p>Yes, I am now running my own server, and am in control of what that server does and how it behaves. This means that I can set it up how I want it to be, but it also means that I am no longer using resources on a server that other people pay for. I did donate to the upkeep of the other servers I’ve been on, but now I pay my own way.</p>

<p>This server is <em>not</em> a Mastodon server, but runs something called <a href="https://joinfirefish.org">Firefish</a>. It has a very different user-interface than Mastodon, and has some other features that I quite like.</p>

<p>It is also a very small server with only one user (me), and an admin-bot. This is important to me, as I don’t think I’d make a good moderator of others’ behaviour and speech. If I do let others join plud.re it will be restricted to people I trust and know, personally.</p>

<p>And, that’s just it! The Fediverse lets me to do this - I can run my own server and decide on the policies of that server. If I notice that certain other servers contain a lot of assholes (there are neo-nazi servers out there) I can tell my server not to communicate with those servers. I can do this and remain a part of the community in the Fediverse. All the people who followed me had their “follow” automatically moved to my new server, and we can still interact! I am still a part of the Internet. Feels good, man.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I think the Fediverse is a great place to be, and I would like you to join in! I have had enough of free-loading on huge companies that make money by being the funnel i must talk through. These companies are not good companies, and they do not have good intentions - they cannot have, as they are beholden to their shareholders. I want to be a part of the community of interesting people that is the Fediverse. A virtual place where the people are the important thing, not the “engagement” or how many you can reach. And, I want you to join me there.</p>

<p>So, go to <a href="https://joinmastodon.org">joinmastodon.org</a> and find a server that looks interesting! It’s not massively important to pick the right one, but I’d recommend something other than mastodon.social. Join up and find me - just search for <code class="language-plaintext highlighter-rouge">@tomasekeli@plud.re</code> and I’ll follow you back and we can have a conversation.</p>

<p>Find your new home in the Fediverse, and I’ll meet you there!</p>]]></content><author><name>Tomas Ekeli</name></author><category term="fediverse" /><category term="fediverse" /><category term="small-web" /><category term="internet" /><category term="self-hosting" /><summary type="html"><![CDATA[I have my own Fediverse server now, and I think you should join the Fediverse.]]></summary></entry><entry><title type="html">Setting Up a New Windows PC</title><link href="https://www.eke.li/productivity/2023/12/11/setting-up-a-new-pc.html" rel="alternate" type="text/html" title="Setting Up a New Windows PC" /><published>2023-12-11T12:00:00+00:00</published><updated>2023-12-11T12:00:00+00:00</updated><id>https://www.eke.li/productivity/2023/12/11/setting-up-a-new-pc</id><content type="html" xml:base="https://www.eke.li/productivity/2023/12/11/setting-up-a-new-pc.html"><![CDATA[<p><img src="/assets/img/2023-12-11-setting-up-a-new-windows-pc-my-personal-workflow.webp" alt="A new computer setup with various applications" /></p>

<p>Setting up a new Windows PC can be quite the task, but it doesn’t have to be daunting. Here I’m sharing my own setup routine, covering everything from the must-have utilities to customizing browsers and setting up development environments. If you’re curious about how a seasoned developer and tech enthusiast approaches this task, or looking for ideas to optimize your own setup, this guide’s for you.</p>

<h2 id="starting-with-the-browser-firefox">Starting with the Browser: Firefox</h2>

<h3 id="the-gateway-to-the-web-firefox">The Gateway to the Web: Firefox</h3>
<p>My setup journey begins with Firefox, my preferred internet browser. It’s known for its speed, privacy, and customization options. Other options are out there, but I try to stay away from Google and Microsoft’s browsers. And, since they started going into the crypto bullshit I no longer want to touch Brave. For me, it should be some sort of a Firefox. There are other variants of Firefox out there, but I prefer the original on Windows.</p>

<p>There are two ways to install <a href="https://getfirefox.com">Firefox</a></p>

<ul>
  <li>Use the command line for a swift installation: <code class="language-plaintext highlighter-rouge">winget install firefox.firefox</code>.</li>
  <li>Or, use Edge to download it from <a href="https://getfirefox.com">https://getfirefox.com</a> then set it as the default browser without importing old data.</li>
</ul>

<h3 id="essential-utilities">Essential Utilities</h3>

<p>After setting up the browser, I focus on essential utilities to streamline my workflow. The first things I need to even be able to install other things and connect to my data are:</p>

<ul>
  <li><strong><a href="https://7-zip.org/">7-Zip</a></strong>: A file archiver known for its high compression ratio. Install command: <code class="language-plaintext highlighter-rouge">winget install 7zip.7zip</code>.</li>
  <li><strong><a href="https://hluk.github.io/CopyQ/">CopyQ</a></strong>: An advanced clipboard manager that stores more than the default number of clipboard entries. I do not want to compute without something like this. Install command: <code class="language-plaintext highlighter-rouge">winget install hluk.copyq</code>.</li>
  <li><strong><a href="https://keepassxc.org/">KeePassXC</a></strong>: A secure password manager to keep all credentials safe. Its database syncs across various cloud services for accessibility. Installation and setup involve:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">winget install keepassxcteam.keepassxc</code>.</li>
      <li>Open my database-file with it - that’s stored on several cloud providers and on my phone. This database is encrypted with the one password I must remember - it is a pretty long and gnarly one.</li>
      <li>Custom settings for startup behavior and browser integration.
        <ul>
          <li>Activate auto-launch on startup, minimised to tray</li>
          <li>Minimise-on-exit (don’t close)</li>
          <li>Hide to system-tray when minimised</li>
          <li>Activate browser integration
            <ul>
              <li>For firefox, install the <a href="https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/">KeePassXC-Browser</a>.</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong><a href="https://joplinapp.org/">Joplin</a></strong>: A note-taking application that synchronizes with cloud storage for easy access to notes. Installation and setup involve syncing with the cloud storage for note retrieval, but all my notes are then available on all my devices, encrypted and safe - so the cloud-provider cannot read them. I use Joplin extensively, and find it to be a great tool.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">winget install joplin.joplin</code></li>
    </ul>
  </li>
</ul>

<h2 id="customizing-firefox">Customizing Firefox</h2>

<p>Firefox isn’t just a browser for me; it’s a customized tool that enhances my browsing experience.</p>

<ul>
  <li><strong>Sync</strong>: I use the sync feature with my email for seamless access across devices.</li>
  <li><strong>Extensions</strong>: I use add-ons to give Firefox new capabilities, and redirect it to privacy-friendly alternatives. A few of the most important extensions are:
    <ul>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/">KeePassXc-Browser</a> makes logging into the myriad of sites easy and secure. I don’t think I remember any site passwords other than my master password anymore, and neither should you.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/">FirefoxPWA</a> lets me run web-apps as native applications with Firefox.
        <ul>
          <li><code class="language-plaintext highlighter-rouge">winget install -e Microsoft.VcRedist.2015+.x64</code></li>
          <li><code class="language-plaintext highlighter-rouge">winget install mozillafirefoxpwa</code>.</li>
        </ul>
      </li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/libredirect/">LibRedirect</a> lets me redirect mainstream sites to privacy-friendly alternatives. I use it to redirect YouTube to my own Invidious instance, Google Maps to OpenStreetMap, etc.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/noscript/">NoScript</a>: I have sites I trust, for everything else I want to decide if I want to run scripts or not. This is a bit of a hassle, but it’s worth it, removes all cookie-popups and sign-up pop-overs.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/">uBlock Origin</a>: I use this to block ads and trackers. I am against being sold by ad-networks, and it’s getting downright dangerous to browse the web without an ad-blocker.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/privacy-badger17/">Privacy Badger</a>: I use this to block trackers and fingerprinting. Worth it.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/">Joplin Web Clipper</a>: I use this to save web-pages to my Joplin notes. Don’t do it a lot, but sometimes it’s a time-saver.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/darkreader/">Dark Reader</a>: I use this to make sites dark. I like dark sites, and this is a great way to get most pages to not blind me.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/omnivore/">Omnivore</a>: It’s like Pocket or ReadItLater, but it’s open-source and I can host it myself. I use it to save articles for later reading. I use this a lot, for when I find interesting things I don’t have time to read right now.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/user-agent-string-switcher/">User-Agent Switcher and Manager</a>: I use this to change my user-agent, when bad web-devs (there, I said it) block Firefox by the user-agent. I don’t like it, but it’s a fact of life (bad devs exist).</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/modify-header-value/">Modify Header Value</a>: I use this to change my request headers. A bit of a developer-tool for when I use APIs that require a specific header. I don’t use it a lot, but it’s nice to have.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/reader-view/">Open in Reader View</a>: Reader view is a wonderful feature that strips away all the cruft and lets me read the article without distractions. This add-on lets me open links in reader view with a right-click.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/right-click-borescope/">Right-Click Borescope</a>: This is a true gem - it lets you find the images you clicked on even if they are hidden under layers of divs. I never have to go into the dev-tools to find the image I want to save again.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/add-custom-search-engine/">Add custom search engine</a>: Makes it real easy to add new search-engines to Firefox. I use this to add my own Searx-instance as a search-engine, and others. That so many people just use Google is sad, I don’t even think it’s a very good search engine anymore. I use Searx, but there are lots of others out there.</li>
      <li><a href="https://addons.mozilla.org/en-US/firefox/addon/simplelogin/">SimpleLogin</a>: I use this to create disposable email-addresses for sites that require an email-address. I don’t want to give my email-address to every site out there, and I don’t want to use my work-email for personal stuff. This is a great way to get around that. With the integrated password manager I don’t even need to remember the account I set up for a site, and since I’m a paying customer of ProtonMail I get SimpleLogin with my account there.</li>
    </ul>
  </li>
  <li><strong>Search Engine</strong>: Adding a privacy-focused search engine as the default one in Firefox enhances my browsing privacy. I have my own instance of <a href="https://en.wikipedia.org/wiki/Searx">Searx</a> that I run for me and my family. So, I set that as my default search engine in Firefox.</li>
</ul>

<h2 id="cloud-services-for-seamless-sync">Cloud Services for Seamless Sync</h2>

<p>Cloud storage is integral to accessing files and databases on any device.</p>

<ul>
  <li><strong><a href="https://nextcloud.com/">Nextcloud</a></strong>: A secure cloud storage solution that I use to access files and the password database. It’s also where my personal calendar and address-book lives. You can use this for a lot of things, and you can host it yourself if you want. Install command for the client-software: <code class="language-plaintext highlighter-rouge">winget install nextcloud.nextclouddesktop</code>.</li>
  <li><strong><a href="https://proton.me/">Proton Services</a></strong>: For secure email, file-storage and VPN services, I use and pay for Proton. There are other good providers out there, but this is among the better ones. Installation can be done through winget commands or directly from their website.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">winget install proton.protondrive</code> - like Dropbox, but secure.</li>
      <li><code class="language-plaintext highlighter-rouge">winget install protontechnologies.protonmailbridge</code> - a Bridge that lets Thunderbird connect to ProtonMail through a small, local IMAP-server.</li>
      <li><code class="language-plaintext highlighter-rouge">winget install protontechnologies.protonvpn</code> - a VPN-service that I use when I’m on public networks, or when I want to access content that is blocked in my country.</li>
    </ul>
  </li>
</ul>

<h2 id="email-and-calendar-management-thunderbird">Email and Calendar Management: Thunderbird</h2>

<p>Thunderbird is my go-to for managing emails and calendars. I don’t absolutely adore it, although it’s gotten much better, but it’s far better than Outlook.</p>

<ul>
  <li>Install <a href="https://www.thunderbird.net/">Thunderbird</a> using <code class="language-plaintext highlighter-rouge">winget install mozilla.thunderbird</code>.</li>
  <li>Add-ons like Dark Reader and calendar integrations enhance functionality.</li>
  <li>As my various places of work have tended to use Microsoft Exchange, and that has a spotty record of supporting IMAP and CALDav, I have used <a href="https://davmail.sourceforge.io/">DavMail</a> to connect to Exchange. But, in recent years I have switched over to the (paid) extension <a href="https://www.beonex.com/owl/">Owl for Exchange</a>. It’s extremely easy to set up and works flawlessly. I highly recommend it! Well worth the money.</li>
  <li>I have my personal calendar on my NextCloud-instance, and I use the <a href="https://www.thunderbird.net/en-US/calendar/">Lightning</a> extension to connect to it. Works great, and I can share my calendar with my family. I tried using <a href="https://protonmail.com/blog/protoncalendar-beta-announcement/">Proton Calendar</a> for a while, but it doesn’t support anything but their web-interface and their own apps. I want to use my own calendar-app, so I switched back to my NextCloud-calendar.</li>
</ul>

<h2 id="development-vscode">Development: VSCode</h2>

<p>As a developer, an efficient coding environment is crucial. I use VSCode for my development needs. People tell me I should use VSCodium, to get rid of the telemetry. I agree, but I’m not willing to give up the extensions.</p>

<ul>
  <li><strong><a href="https://code.visualstudio.com/">VSCode</a></strong>: A versatile code editor with extensions for various programming languages. Install command: <code class="language-plaintext highlighter-rouge">winget install microsoft.visualstudiocode</code>. I really should make a post on the extensions I use, but that’s for another day.</li>
  <li><strong><a href="https://github.com/tonsky/FiraCode">Fira Code</a></strong>: A monospaced font with programming ligatures for a better coding experience. Install command: <code class="language-plaintext highlighter-rouge">winget install nerdfonts.firacode</code>.</li>
  <li>These days I do all my development in Docker-containers - so I keep my computer (the host-machine) as clean as possible. I don’t install any SDKs, or development tools on it, and never have conflicts between them anymore.</li>
</ul>

<h2 id="command-line-efficiency-git-and-powershell">Command-Line Efficiency: Git and PowerShell</h2>

<p>For version control and scripting, I rely on Git and PowerShell on the Windows -machine.</p>

<ul>
  <li><strong><a href="https://gitforwindows.org/">Git</a></strong>: Essential for version control, installed using <code class="language-plaintext highlighter-rouge">winget install git.git</code>.
    <ul>
      <li>When I still did development on my host-machine, I used <a href="https://gitextensions.github.io/">GitExtensions</a> to get a nice GUI for Git. If you’re developing directly on Windows I would recommend it: <code class="language-plaintext highlighter-rouge">winget install gitextensions.gitextensions</code>.</li>
    </ul>
  </li>
  <li><strong><a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4">PowerShell</a></strong>: Enhanced with Oh My Posh and other modules for a powerful scripting experience. Customized with a <a href="https://gist.github.com/TomasEkeli/19d029631b2f8d75e15547872409f6ae">profile script</a> and an <a href="https://gist.github.com/TomasEkeli/e06c29e4300596a9f99bf3f2b81ce728">oh-my-posh configuration-file</a> for a personalized touch.
    <ul>
      <li>Also needs a font that supports the glyphs used by oh-my-posh, I use <a href="https://github.com/ryanoasis/nerd-fonts/releases/download/v2.1.0/CascadiaCode.zip">CascadyiaCove NF</a> for this. Remember to change the WinTerm default font to this one, or the shell will look garbled.</li>
      <li>This is based on and slightly modified from the setup shared by Scott Hanselman in his <a href="https://www.hanselman.com/blog/my-ultimate-powershell-prompt-with-oh-my-posh-and-the-windows-terminal">blog post</a>.</li>
    </ul>
  </li>
</ul>

<h2 id="integrating-linux-windows-subsystem-for-linux-wsl">Integrating Linux: Windows Subsystem for Linux (WSL)</h2>

<p>Bringing Linux into Windows adds versatility to my setup. I do all my development, and much of my other work from Linux under windows. For many things it’s just a nicer experience.</p>

<ul>
  <li>Activate Hyper-V and install a preferred Linux distro with WSL for Linux environment support on Windows.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">wsl --install</code> - this will install the latest Ubuntu LTS, and set it up for you.</li>
    </ul>
  </li>
</ul>

<h2 id="final-touches-office-docker-and-communication-tools">Final Touches: Office, Docker, and Communication Tools</h2>

<p>To round up the setup:</p>

<ul>
  <li><strong><a href="https://www.libreoffice.org/">LibreOffice</a></strong>: For handling office documents, installed via <code class="language-plaintext highlighter-rouge">winget install libreoffice</code>. I know most people prefer Microsoft Office, but I don’t.</li>
  <li><strong><a href="https://www.docker.com/products/docker-desktop/">Docker Desktop</a></strong>: Essential for containerization projects. I keep meaning to try out alternatives like <a href="https://podman.io/">PodMan</a> or <a href="https://rancherdesktop.io/">Rancher</a>, but Docker just keeps on working for me. Requires WSL to be installed and activated. Install command: <code class="language-plaintext highlighter-rouge">winget install docker.dockerdesktop</code></li>
  <li><strong><a href="https://remarkable.com">ReMarkable</a></strong>: For integration with my paper-tablet. I use it for note-taking and sketching. I have a ReMarkable 2, and I love it. I use it for all my note-taking, and for sketching out ideas, and reading. Installed via <code class="language-plaintext highlighter-rouge">winget install remarkable.remarkable</code></li>
  <li>Communication tools for staying connected with teams and communities.
    <ul>
      <li><strong><a href="https://teams.microsoft.com/">Teams</a></strong>: For work-related communication. Not very good for chat, but excellent for video-meetings. Installed via <code class="language-plaintext highlighter-rouge">winget install microsoft.teams</code>.</li>
      <li><strong><a href="https://slack.com/">Slack</a></strong>: For work and community-related communication. Much better than Teams for chats and channels as well as informal video-meetings, but not nearly as good for “real meetings”. Installed via <code class="language-plaintext highlighter-rouge">winget install slacktechnologies.slack</code>.</li>
      <li><strong><a href="https://discord.com/">Discord</a></strong> For more community-related communication. Installed via <code class="language-plaintext highlighter-rouge">winget install discord.discord</code>.</li>
      <li><strong><a href="https://element.io/">Element</a></strong>: For community-related communication over the <a href="https://spec.matrix.org/latest/">Matrix protocol</a>. There are lots of other cool clients for Matrix, but I like Element. I wish more people would use Matrix. Installed via <code class="language-plaintext highlighter-rouge">winget install element.element</code>.</li>
    </ul>
  </li>
</ul>

<p>By following these steps, I ensure that my new Windows PC is not just a machine, but a personalized workspace that aligns with my workflow and preferences. It’s all about creating a space where productivity is effortless and secure.</p>

<p>Got your own Windows setup that you’re proud of, or some cool tips to share? I’m keen to hear about how others tackle their tech setups. There are no comments enabled here, but feel free to reach out and connect with me on the social platforms listed in the footer. Maybe send me a link to your own post describing how you set up a new PC? Let’s swap stories and maybe pick up some new tricks along the way!</p>]]></content><author><name>Tomas Ekeli</name></author><category term="productivity" /><category term="windows" /><category term="workflow" /><category term="productivity" /><summary type="html"><![CDATA[Setting up a new computer can be a bit of a chore, this is my list of things I always set up on a new Windows PC.]]></summary></entry><entry><title type="html">From big ideas to small wins</title><link href="https://www.eke.li/2023/12/go-small/" rel="alternate" type="text/html" title="From big ideas to small wins" /><published>2023-12-10T11:00:00+00:00</published><updated>2023-12-10T11:00:00+00:00</updated><id>https://www.eke.li/2023/12/from-big-ideas-to-small-wins</id><content type="html" xml:base="https://www.eke.li/2023/12/go-small/"><![CDATA[<p><img src="/assets/img/2023-12-10-from-big-ideas-to-small-wins.webp" alt="A diverse group of people gathering around a computer discussing ideas and creating something" /></p>

<p>From big ideas to small wins - a story about a hackathon.</p>

<h2 id="hackathing">Hackathing</h2>

<p>We had a hackathon this last week, working on a real challenge that’s been bugging us for a while. I was part of a team of five programmers. We spent the first day of two talking about the problem, exploring it and coming up with possible approaches. We knew we’d have a short time to make a presentation, and if possible a demo, so we had to be careful not to overreach. We had to go small.</p>

<p>We came up with four different approaches. I won’t go into the details of the problem or solutions, but our approaches differed in complexity and scope.</p>

<p>First off we had an approach that boiled down to creating a new “box” that would attend to the challenge. It would solve the problem, and it had the potential to be a very powerful tool. But it would take time to build, and would lead to a new thing in our landscape that we would need to connect, support, maintain, document and monitor. It was, however, a pretty sexy solution - so it had a lot going for it.</p>

<p>Our second approach was extremely minimal. It amounted to adding some new things to a database, and changing around some things. It played with our existing tools, but it also risked introducing bugs in our current offering. It would mean taking advantage of some capabilities in our existing stack that we haven’t yet used, but it would also mean that we’d have to be careful not to break anything. It was a very small solution, but it was also a bit risky. Being such a small solution it would also not be very impressive in a demo, it was “not the stuff of legends”.</p>

<p>Our third approach leaned heavily on using some very hyped technology. It would let us do some very cool things that we cannot do today, and would probably give us a cool point from the marketing people. It was not technology we were familiar with, though, so we couldn’t be specific on how long it would take to implement. It was also a bit risky, since we didn’t know how well it would work in our environment.</p>

<p>Our fourth approach was a bit of a wild-card - implementing a fresh new algorithm we came up with to solve our need. The algorithm was conceptually simple, but implementing it in a way that was fast enough and actually solved our problem might get tricky. We also weren’t sure that it would solve all edge-cases, so we’d have to dig deeper into it and test it thoroughly. It was a bit risky, but it was also a very small solution. It’s main advantage was that it would require no new components added and could be a pure software solution.</p>

<p>With all these, and keeping in mind that we wanted a demo that really impressed our co-workers, we decided to go for the first solution. It was unlikely that we’d have a fully working box, but we felt we could create a prototype that proved the concept, and it could even lead to an entirely new product. It did not hurt that it was the brainchild of the most senior developer on the team! We were excited about it, and left for the day feeling good about our choice.</p>

<h2 id="unexpected-challenges">Unexpected challenges</h2>

<p>We met on the second and final day, expecting to start creating a small new service with a very limited and specific scope. We had a plan, and we were ready to go.</p>

<p>As they say, life happens. And this Friday had a lot of life happening - particularly with some of our customers and a gnarly database-issue that needed our best people’s attention. Our group was down from five to three, and our small-scoped service suddenly seemed like a very big task. Then we got the message that our demo had been brought forward from 15:00 to 13:00. What’s a few hours? Well, for us it was about a third of our remaining time. We had to rethink our approach.</p>

<p>We put our heads together and looked at our options. We could concede defeat and throw in the towel, we could crunch for a few hours and see what we got to, or we could go for one of the other approaches we had discussed. With such a tight deadline we decided to drop our plans for a cool new service, and go for the second approach, the one that was a little bit risky and not very sexy. We had to go small.</p>

<p>We spent some time working on presentation and story, as our demo was going to be mainly a presentation. We had a few slides, we had a some code and we had a demo. The code-changes were so small that they fit on a single slide. We included a sequence-diagram to explain where the new code would have to fit into our current flow. We even identified a way to make this change with a minimal impact - it would change nothing about the existing tables in our database and include just one new line of code in our server. Our solution was no longer risky.</p>

<p>We had a story that we could tell, and we had a solution that we could show. It was a very short story, and a small change, but it had an impact that solved our challenge. We had to go small, but we had something to show for it.</p>

<h2 id="the-demo">The demo</h2>

<p>Our team of three were two remote people and just one in the offices. And, our demo was up against people present in the office. The other two of us were cheering on remotely, and active in chat, but not our presence was small. We presented our solution with a Miro-board presentation enriched with animations by our remote members moving things around on cue. Delightfully low-tech and home-spun, we thought, at least we’d have a chance at the “best presentation” award.</p>

<p>In the end we actually won the “best solution” -group by popular vote. The voting was anonymous and no reasons were given, but we got an incredible 60% of the votes. This was quite surprising to us, as we had not expected to win anything. We had to go small, but we had something to show for it, and it resonated.</p>

<p>And our hopes of winning “best presentation” - nope. We got smashed in that vote, and came in last. We had to go small, and we had something to show for it, but our presentation obviously did not win the crowd over.</p>

<h2 id="learning">Learning</h2>

<p>As I sit and think about it - I think we should have gone for the small solution from the start. Anything that has a minimal impact, but solves a real need is inherently preferable to something that is bigger and more involved that solves that same need. We should have gone small, but we did not until we were forced to. Losing much of our team and time brought the need into a clearer focus - and we got to deliver on something focused and small.</p>

<p>I hope I am wise enough to remember this lesson the next time I am faced with a challenge. I hope I am wise enough to go small.</p>]]></content><author><name>Tomas Ekeli</name></author><category term="learning" /><category term="learning" /><category term="hackathon" /><category term="needs" /><category term="small" /><summary type="html"><![CDATA[From big ideas to small wins - a story about a hackathon.]]></summary></entry></feed>