<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Western Devs</title>
  
  <link href="/feed.xml" rel="self" type="application/atom+xml"/>
  <link href="https://westerndevs.com" rel="alternate" type="application/atom+xml"/>
  
  <updated>2026-03-22T17:42:31.818Z</updated>
  <id>https://westerndevs.com/</id>
  
  <author>
    <name>Western Devs</name>
	<uri>https://westerndevs.com</uri>
    <email>info@westerndevs.com</email>
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title type="html">Expensive Hosting for Small to Medium Projects</title>
    <link href="https://westerndevs.com/_/hosting-costs/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/hosting-costs/</id>
    <published>2026-01-15T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.818Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>If you happened to read my previous post about <a href="https://blog.simontimms.com/2025/12/24/where-is-this-ai-stuff-going/" target="_blank" rel="noopener">where AI is going</a> then you might have picked up on my concerns about the rising cost of memory and compute. For the last few years any time I've recommened hosting to a client I've recommended some variant of AWS, Azure, or GCP. I've done this because I remember how painful it used to be to get a server allocated from an overstressed IT department. When I was on internship years ago for a large petro-chemical company we planned had to plan for deployment 6 or even 12 months in advance. IT had to figure out capacity and plan, plan plan.</p><p>The cloud make this so much easier. We could spin up a server in minutes and with platform as a service we didn't have to worry about underlying compute or updates to operating systems, or patching. It was a dream. I'd tell clients &quot;You're an insurance company leave being an IT company to Microsoft/Amazon/Google&quot;.</p><p>Recently though I've been looking at the cost of hosting an application in Azure. Even for our small applications it is surprisngly expensive. We had a tendency to vastly overbuild the hosting when for applications that have only hundreds or low thousands of users. What really gets us is the cost of the database.</p><p><img src="/images/2026-01-31-hosting-costs/2026-03-22-10-11-01.png" alt=""></p><p>This isn't a huge or highly performant database, but it is still costing us almost $400/month. For a small application with only a few hundred users this is a huge cost. Add on dev and test environments and the cost is even higher. Painful.</p><p>I wanted to understand if I was still recommending the right thing to people so I started looking at alternatives. My first stop was looking at a VPS provider. I wanted something in Canada becasue this is where I live and I wanted to support local businesses. I found a provider called <a href="https://www.ovhcloud.com/en-ca/" target="_blank" rel="noopener">OVH</a> that had a VPS with 8 vCores, 24 Gig of memory and a 200 Gig SSD for $32 a month off contract.</p><p><img src="/images/2026-01-31-hosting-costs/2026-03-22-10-18-01.png" alt=""></p><p>Now obviously vCores aren't a standard measurement unit so it's hard to compares that with something from another offering like Azure but on the surface this feels similar to a D12 from Azure which is $380/month and that's in USD so closer to $500/month CAD. The OVH offering is about 1/15th the cost of Azure.</p><p><img src="/images/2026-01-31-hosting-costs/2026-03-22-10-21-19.png" alt=""></p><p>Yikes!</p><p>So I figured I'd sign up. Maybe the added cost here would be becasue it was really arduous to get signed up. I'll admit going through the process wasn't as polished as a cloud provier. Little things like this</p><p><img src="/images/2026-01-31-hosting-costs/2026-03-22-10-28-27.png" alt=""></p><p>Is that a last name field or a field for people who only have a single name? Is Madonna buying hosting a frequently? The 2FA setup, though, was probably the best I've ever done. It had the really nice feature of letting you record the name of the app you were using for 2FA. I have multiple apps for 2FA and this is a really nice feature.</p><p>I did spot this warning, though</p><p><img src="/images/2026-01-31-hosting-costs/2026-03-22-10-30-35.png" alt=""></p><p>Okay so there might be some latency in getting this server provisioned. That sucks, but perhaps I've gotten too used to the instant gratificaiton of demanding compute and getting it 5 minutes later. The email I got said that the server would be provisioned within the next 7 days. That's a long time to wait but whilte we do let's talk about why this VPS might not be a great idea. What are we losing by not using cloud hosting?</p><p>Well first off it's clear that scaling up and down is going to be so much harder. I can't scale the machine up instantly when there is a spike in traffic. Equally, if that spike subsites I can't scale down quickly. So I'm going to have to be comfortable knowing that I've got to pay for excess capacity. A lot of the line of business applications I work with have almost no traffic on the weekend and evenings but then predictable stable load during the day. I'm going to have to over provision to handle the maximum load. Maybe I can mine bitcoin or something on the weekends. Is that SETI@home project still a thing?</p><p>Next I'm losing the platform as a service benefits. I'm going to have to patch the operating system and database myself. I get to do backups, restores all that myself. So I'm going to trade my time for reduced hosting costs. Let's imagine that I'm setting up a database server and a web server which is kind of typical for a line of business application. This is going to run to $700/month in Azure(I'm going to use Candian dollars from here out). So that gives me $650 a month to play with for manual patching and updating and all that jazz. If I pay myself $200/hour (woo, massive pay raise) then I can spend 3 hours a month on that work and still come out ahead. I don't know if that scales well to dozens of servers but for a small application it might be worth it.</p><p>I'm likely going to have an increased cost for the initial set up of the server. I've got to figure out which web server to use, probably some default settings for PostgreSQL and tie it into a build pipeline. I suppose we'll be deploying via SFTP or something like that.</p><p>Finally, I'm losing the reliability and security of the cloud. I don't have the same level of redundancy and failover that I would get with a cloud provider. If my server goes down, it's on me to get it back up. If there is a security breach, it's on me to fix it. This is a big risk for any business, but especially for small businesses that may not have the resources to deal with these issues.</p><p>The other piece of the puzzle here is that the knowledge and expertise required to manage a VPS is higher than that required to use a cloud provider. I've been doing this computer stuff for year and I'm confident in my ability to teasily manage this but then I'm also still a bit sad about how Open Solaris failed to take off. I think that a lot of this gap can be closed using AI agents these days.</p><p>It's a calculated risk.</p><p>So, dear readers, I pulled the trigger and signed up for the VPS. Once it is provisioned then we'll return here and talk through setting up and deploying to it.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;If you happened to read my previous post about &lt;a href=&quot;https://blog.simontimms.com/2025/12/24/where-is-this-ai-stuff-going/&quot; target=&quot;_bl
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Jujutsu Cheat Sheet</title>
    <link href="https://westerndevs.com/_/jujutsu-cheat-sheet/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/jujutsu-cheat-sheet/</id>
    <published>2025-11-05T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.823Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>I've started playing around a bit with the source control tool <a href="https://github.com/jj-vcs/jj" target="_blank" rel="noopener">Jujutsu</a> which is commonly referred to as <code>jj</code>. Git has been my go to tool for what seems like decades now but in the before times I worked as a release engineer and made use of a huge stable of source control tools as our code base was spread over many versions and had been created from purchasing lots of other companies. For a while there I was working on a daily basis with</p><ul><li>ClearCase</li><li>Perforce</li><li>Subversion</li><li>CVS</li><li>Visual Source Safe</li><li>Mercurial</li><li>Git</li><li>CCC/Harvest</li></ul><p>I'm using Jujutsu at my day job now because it just layers transparently on top of git so I don't need to go seeking permission. I'm only a few days into using it and I'm not thoroughly convinced yet that it is better than git but I'm willing to keep trying.</p><p>Here are some of the commands I'm using so far:</p><p>Get the latest version of the code from a central repository locally</p><figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">jj git fetch</span></span><br></pre></td></tr></table></figure><p>Start new work from the latest mainline</p><figure class="highlight aspectj"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jj <span class="keyword">new</span> main<span class="meta">@origin</span> -m <span class="string">"Whatever I'm going to work on"</span></span><br></pre></td></tr></table></figure><p>Bookmark the work with a name I'm going to use as a branch in git</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">jj </span><span class="keyword">bookmark </span>my-feature-<span class="keyword">branch</span></span><br></pre></td></tr></table></figure><p>Push my work up to Github</p><figure class="highlight gauss"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jj git <span class="keyword">push</span> --allow-<span class="keyword">new</span></span><br></pre></td></tr></table></figure><p>Create a new commit before my current one that I can squash into</p><figure class="highlight haxe"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jj <span class="keyword">new</span> <span class="type"></span>-B @ -m <span class="string">"Some description of the work"</span></span><br></pre></td></tr></table></figure><p>Push individual files into the parent change</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">jj squash <span class="type">path</span>/<span class="keyword">to</span>/file1 <span class="type">path</span>/<span class="keyword">to</span>/file2</span><br></pre></td></tr></table></figure><p>I'll keep expanding this document with new commands as I uncover them.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;I&#39;ve started playing around a bit with the source control tool &lt;a href=&quot;https://github.com/jj-vcs/jj&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Juju
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Open API Generator for C#</title>
    <link href="https://westerndevs.com/_/open-api-generator/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/open-api-generator/</id>
    <published>2025-03-30T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.825Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>Every once in a while I run into the need to generate a C# client for some API which has been nice enough to provide me with OpenAPI specifications. But it's one of those things that I do so infrequently that I always forget how to do it. So I thought I would document it here.</p><p>The first thing to do is to install the OpenAPI generator. It's written in Java which is obviously a decision I'm solidly against. As I run a Mac most of the time I prefer to use brew since it's easier that trying to figure out how to build stuff with Maven.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install openapi-generator</span><br></pre></td></tr></table></figure><p>Now comes the fun part: figuring out the options to use. There are bunch of different generators for different languages and on top of that each generator has options. The C# generator is called <code>csharp</code> and the options are described in some detail here https://openapi-generator.tech/docs/generators/csharp. In my case I was looking to generator a client for a US Government API that they seem to be really snippy about people getting their hands on documentation about it without going through a heap of hoops so we'll just call it <code>USGovAPI</code> because this administration is not one I want to be on the wrong side of.</p><p>In addition I wanted to generate one for .NET 8 because the project is still running on the long term support version of .NET. So the command ended up being</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openapi-generator generate -i swagger.json -g csharp -o out/usgoveapi --additional-properties=packageName=USGovAPI,targetFramework=net8.0</span><br></pre></td></tr></table></figure><p>And with that we get a nice little C# client that we can use to call the USGovAPI. It even includes some unit tests to go along with it.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Every once in a while I run into the need to generate a C# client for some API which has been nice enough to provide me with OpenAPI spec
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Limit Dependabot to .NET 8</title>
    <link href="https://westerndevs.com/_/dependabot-ignore-dotnet-9/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/dependabot-ignore-dotnet-9/</id>
    <published>2024-11-20T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.821Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>Just last week .NET 9 was realeased to much fanfare. There are a ton of cool and exciting things in it but for my current project I want to stick to a long term support version of .NET which is .NET 8. We might update later but for now 8 is great. Unfortunately dependabot isn't able to read my mind so it was continually proposing updating to .NET 9 packages.</p><p>Fixing this is easy enough. I needed to add a couple of lines to my dependabot file to limit the sorts of updates it did to just minor and patch updates. Notice the <code>update-types</code> section.</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span><span class="variable">s:</span></span><br><span class="line">  - package-ecosystem: <span class="string">"nuget"</span></span><br><span class="line">    directory: <span class="string">"/."</span></span><br><span class="line">    group<span class="variable">s:</span></span><br><span class="line">      all_package<span class="variable">s:</span></span><br><span class="line">        <span class="keyword">update</span>-<span class="built_in">type</span><span class="variable">s:</span></span><br><span class="line">          - <span class="string">"minor"</span></span><br><span class="line">          - <span class="string">"patch"</span></span><br><span class="line">        pattern<span class="variable">s:</span></span><br><span class="line">          - <span class="string">"*"</span></span><br><span class="line">    <span class="keyword">open</span>-pull-requests-limi<span class="variable">t:</span> <span class="number">50</span></span><br><span class="line">    schedule:</span><br><span class="line">      interva<span class="variable">l:</span> <span class="string">"weekly"</span></span><br></pre></td></tr></table></figure><p>With this in place dependabot is only proposing minor and patch updates to my .NET packages. It does mean that if we see other major version updates to non-Microsoft packages we'll have to manually update them.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Just last week .NET 9 was realeased to much fanfare. There are a ton of cool and exciting things in it but for my current project I want 
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">RavenDB on Kubernetes</title>
    <link href="https://westerndevs.com/_/ravendb-on-k8s/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/ravendb-on-k8s/</id>
    <published>2024-11-14T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.826Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>I needed to get Particular Service Control up and running on our k8s cluster this week. Part of that is to get an instance of RavenDB running in the cluster and this actually caused me a bit of trouble. I kept running into problems where RavenDB would start up but then report that it couuld not access the data directory. What was up?</p><p>I tried overriding the entry point for the container and attaching to it to see what was going on but I couldn't see anything wrong. I was able to write to the directory without issue. Eventually I stumbled on a note in the <a href="https://ravendb.net/docs/article-page/6.0/csharp/migration/server/docker" target="_blank" rel="noopener">RavenDB documentation</a> which mentioned a change in the 6.x version of RavenDB which meant that Raven no longer ran as root inside the container.</p><p>K8S has the ability to change the ownership of the volume to the user that the container is running as. This is done by setting the <code>fsGroup</code> property in the pod spec. In this case Raven runs as UID 999. So I updated my tanka spec to include the <code>fsGroup</code> property and the problem was solved.</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">...</span></span><br><span class="line"><span class="attr">deployment:</span> <span class="string">deployment.new($._config.containers.ravendb.name)</span> <span class="string">&#123;</span></span><br><span class="line">      <span class="string">metadata+:</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="attr">namespace:</span> <span class="string">'wigglepiggle-'</span> <span class="string">+</span> <span class="string">$._config.environment,</span></span><br><span class="line">        <span class="attr">labels:</span> <span class="string">&#123;</span></span><br><span class="line">          <span class="attr">app:</span> <span class="string">$._config.containers.ravendb.name,</span></span><br><span class="line">        <span class="string">&#125;,</span></span><br><span class="line">      <span class="string">&#125;,</span></span><br><span class="line">      <span class="string">spec+:</span> <span class="string">&#123;</span></span><br><span class="line">        <span class="attr">replicas:</span> <span class="number">1</span><span class="string">,</span></span><br><span class="line">        <span class="attr">selector:</span> <span class="string">&#123;</span></span><br><span class="line">          <span class="attr">matchLabels:</span> <span class="string">$._config.labels,</span></span><br><span class="line">        <span class="string">&#125;,</span></span><br><span class="line">        <span class="attr">template:</span> <span class="string">&#123;</span></span><br><span class="line">          <span class="string">metadata+:</span> <span class="string">&#123;</span></span><br><span class="line">            <span class="attr">labels:</span> <span class="string">$._config.labels,</span></span><br><span class="line">          <span class="string">&#125;,</span></span><br><span class="line">          <span class="string">spec+:</span> <span class="string">&#123;</span></span><br><span class="line">            <span class="attr">securityContext:</span> <span class="string">&#123;</span></span><br><span class="line">              <span class="attr">fsGroup:</span> <span class="number">999</span><span class="string">,</span></span><br><span class="line">              <span class="attr">fsGroupChangePolicy:</span> <span class="string">'OnRootMismatch'</span><span class="string">,</span></span><br><span class="line">            <span class="string">&#125;,</span></span><br><span class="line">            <span class="attr">containers:</span> <span class="string">[</span></span><br><span class="line">              <span class="string">&#123;</span></span><br><span class="line">                <span class="attr">name:</span> <span class="string">$._config.containers.ravendb.name,</span></span><br><span class="line">                <span class="attr">image:</span> <span class="string">$._config.containers.ravendb.image,</span></span><br><span class="line">                <span class="attr">ports:</span> <span class="string">[&#123;</span> <span class="attr">containerPort:</span> <span class="number">8080</span> <span class="string">&#125;,</span> <span class="string">&#123;</span> <span class="attr">containerPort:</span> <span class="number">38888</span> <span class="string">&#125;],</span></span><br><span class="line">                <span class="attr">volumeMounts:</span> <span class="string">[</span></span><br><span class="line">                  <span class="string">&#123;</span></span><br><span class="line">                    <span class="attr">name:</span> <span class="string">'data'</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">mountPath:</span> <span class="string">'/var/lib/ravendb/data'</span><span class="string">,</span></span><br><span class="line">                  <span class="string">&#125;,</span></span><br><span class="line">                <span class="string">],</span></span><br><span class="line">                <span class="attr">env:</span> <span class="string">[</span></span><br><span class="line">                  <span class="string">&#123;</span></span><br><span class="line">                    <span class="attr">name:</span> <span class="string">'RAVEN_Setup_Mode'</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">value:</span> <span class="string">'None'</span><span class="string">,</span></span><br><span class="line">                  <span class="string">&#125;,</span></span><br><span class="line">                  <span class="string">&#123;</span></span><br><span class="line">                    <span class="attr">name:</span> <span class="string">'RAVEN_License_Eula_Accepted'</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">value:</span> <span class="string">'true'</span><span class="string">,</span></span><br><span class="line">                  <span class="string">&#125;,</span></span><br><span class="line">                  <span class="string">&#123;</span></span><br><span class="line">                    <span class="attr">name:</span> <span class="string">'RAVEN_ARGS'</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">value:</span> <span class="string">'--log-to-console'</span><span class="string">,</span></span><br><span class="line">                  <span class="string">&#125;,</span></span><br><span class="line">                  <span class="string">&#123;</span></span><br><span class="line">                    <span class="attr">name:</span> <span class="string">'RAVEN_Security_UnsecuredAccessAllowed'</span><span class="string">,</span></span><br><span class="line">                    <span class="attr">value:</span> <span class="string">'PrivateNetwork'</span><span class="string">,</span></span><br><span class="line">                  <span class="string">&#125;,</span></span><br><span class="line">                <span class="string">],</span></span><br><span class="line">              <span class="string">&#125;,</span></span><br><span class="line">            <span class="string">],</span></span><br><span class="line">            <span class="attr">volumes:</span> <span class="string">[</span></span><br><span class="line">              <span class="string">&#123;</span></span><br><span class="line">                <span class="attr">name:</span> <span class="string">'data'</span><span class="string">,</span></span><br><span class="line">                <span class="attr">persistentVolumeClaim:</span> <span class="string">&#123;</span></span><br><span class="line">                  <span class="attr">claimName:</span> <span class="string">$._config.containers.ravendb.name,</span></span><br><span class="line">                <span class="string">&#125;,</span></span><br><span class="line">              <span class="string">&#125;,</span></span><br><span class="line">            <span class="string">],</span></span><br><span class="line">          <span class="string">&#125;,</span></span><br><span class="line">        <span class="string">&#125;,</span></span><br><span class="line">      <span class="string">&#125;,</span></span><br><span class="line">    <span class="string">&#125;,</span></span><br><span class="line"><span class="string">...</span></span><br></pre></td></tr></table></figure><p>This generated yml like</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">service-control-ravendb</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">service-control-ravendb</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">wigglepiggle-dev</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">app:</span> <span class="string">service-control</span></span><br><span class="line">      <span class="attr">environment:</span> <span class="string">dev</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">app:</span> <span class="string">service-control</span></span><br><span class="line">        <span class="attr">environment:</span> <span class="string">dev</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">env:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">RAVEN_Setup_Mode</span></span><br><span class="line">          <span class="attr">value:</span> <span class="string">None</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">RAVEN_License_Eula_Accepted</span></span><br><span class="line">          <span class="attr">value:</span> <span class="string">"true"</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">RAVEN_ARGS</span></span><br><span class="line">          <span class="attr">value:</span> <span class="string">--log-to-console</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">RAVEN_Security_UnsecuredAccessAllowed</span></span><br><span class="line">          <span class="attr">value:</span> <span class="string">PrivateNetwork</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">ravendb/ravendb:6.0-latest</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">service-control-ravendb</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">containerPort:</span> <span class="number">8080</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">containerPort:</span> <span class="number">38888</span></span><br><span class="line">        <span class="attr">volumeMounts:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">mountPath:</span> <span class="string">/var/lib/ravendb/data</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">data</span></span><br><span class="line">      <span class="attr">securityContext:</span></span><br><span class="line">        <span class="attr">fsGroup:</span> <span class="number">999</span></span><br><span class="line">        <span class="attr">fsGroupChangePolicy:</span> <span class="string">OnRootMismatch</span></span><br><span class="line">      <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">data</span></span><br><span class="line">        <span class="attr">persistentVolumeClaim:</span></span><br><span class="line">          <span class="attr">claimName:</span> <span class="string">service-control-ravendb</span></span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;I needed to get Particular Service Control up and running on our k8s cluster this week. Part of that is to get an instance of RavenDB run
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Where is the disk space?</title>
    <link href="https://westerndevs.com/_/where-is-the-space/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/where-is-the-space/</id>
    <published>2024-11-10T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.830Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>I had a report today from somebody unable to log into our Grafana instance. Super weird because this this has been running fine for months and we haven't touched it. So I jumped onto the machine to see what was up. First up was just looking at the logs from Grafana.</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker logs -f --tail <span class="number">10</span> <span class="number">5</span>efd3ee0074a</span><br></pre></td></tr></table></figure><p>There in the logs was the culprit <code>No space left on device</code>. Uh oh, what's going on here? Sure enough the disk was full.</p><figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">df -h</span></span><br></pre></td></tr></table></figure><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Filesystem                         Size  Used Avail Use% Mounted on</span><br><span class="line">tmpfs                              <span class="number">1.6</span>G  <span class="number">1.9</span>M  <span class="number">1.6</span>G   <span class="number">1</span>% /run</span><br><span class="line">/dev/mapper/ubuntu--vg-ubuntu--lv   <span class="number">96</span>G   <span class="number">93</span>G     <span class="number">0</span> <span class="number">100</span>% /</span><br><span class="line">tmpfs                              <span class="number">7.8</span>G     <span class="number">0</span>  <span class="number">7.8</span>G   <span class="number">0</span>% /dev/shm</span><br><span class="line">tmpfs                              <span class="number">5.0</span>M     <span class="number">0</span>  <span class="number">5.0</span>M   <span class="number">0</span>% /run/lock</span><br><span class="line">/dev/sda2                          <span class="number">2.0</span>G  <span class="number">182</span>M  <span class="number">1.7</span>G  <span class="number">10</span>% /boot</span><br><span class="line">tmpfs                              <span class="number">1.6</span>G   <span class="number">12</span>K  <span class="number">1.6</span>G   <span class="number">1</span>% /run/user/<span class="number">1001</span></span><br></pre></td></tr></table></figure><p>This stuff is always annoying because you get to a point where you can't run any commands because there is no space left. I started with cleaning up some small parts of docker</p><p><code>docker system prune -a</code></p><p>Then cleaned up docker logs</p><p><code>sudo truncate -s 0 /var/lib/docker/containers/**/*-json.log</code></p><p>This then gave me enough space to run <code>docker system df</code> and see where the space was being used. Containers were the culprit. So next was to run</p><p><code>docker ps --size</code></p><p>Which showed me the web scraper container had gone off the rails and was using over a 100GiB of space.</p><figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">7</span>cf14084c56a   webscraper:latest       <span class="string">"wsce start -v -y"</span>       <span class="number">7</span> weeks ago   Up <span class="number">20</span> minutes               <span class="number">0.0</span><span class="number">.0</span><span class="number">.0</span>:<span class="number">7002</span>-&gt;<span class="number">9924</span>/tcp, [::]:<span class="number">7002</span>-&gt;<span class="number">9924</span>/tcp                                webscraper       <span class="number">123</span>GB (virtual <span class="number">125</span>GB)</span><br></pre></td></tr></table></figure><p>This thing is supposed to be stateless so I just killed an removed it.</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">docker</span> <span class="string">kill 7cf14084c56a</span></span><br><span class="line"><span class="attr">docker</span> <span class="string">rm 7cf14084c56a</span></span><br><span class="line"><span class="attr">docker</span> <span class="string">compose up -d webscraper</span></span><br></pre></td></tr></table></figure><p>After a few minutes these completed and all was good again. So we'll keep an eye on that service and perhaps reboot it every few months to keep it in check.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;I had a report today from somebody unable to log into our Grafana instance. Super weird because this this has been running fine for month
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Consuming Github Packages in Yarn</title>
    <link href="https://westerndevs.com/_/consuming-github-packges-in-yarn/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/consuming-github-packges-in-yarn/</id>
    <published>2024-11-05T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.820Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>My life is never without adventure but unfortunately it isn't the living on a beach sort of adventure. No it's the installing yarn packages. I wanted to have a package installed in my project which was one I'd published from another repository. In this case the package was called <code>@stimms/uicomponents</code>. There were a few tricks to getting Github actions to be able to pull the package: first I needed to create a .yarnrc.yml file. This gives yarn instructions about where it should look for packages.</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">nodeLinker:</span> <span class="string">node-modules</span></span><br><span class="line"></span><br><span class="line"><span class="attr">npmScopes:</span></span><br><span class="line">  <span class="attr">stimms:</span></span><br><span class="line">    <span class="attr">npmRegistryServer:</span> <span class="string">"https://npm.pkg.github.com"</span></span><br><span class="line">    </span><br><span class="line"><span class="attr">npmRegistries:</span></span><br><span class="line">  <span class="attr">"https://npm.pkg.github.com":</span></span><br><span class="line">    <span class="attr">npmAlwaysAuth:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>Now in the build I needed to add a step in to populate the GITHUB_TOKEN which can be used for authentication. I found quite a bit of documentation which suggested that the .yarnrc.yml file would be able to read the environment variable but I had no luck with that approach. Instead I added a step to the build to populate the GITHUB_TOKEN in the .npmrc file.</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Configure</span> <span class="string">GitHub</span> <span class="string">Packages</span> <span class="string">Auth</span></span><br><span class="line">    <span class="attr">run:</span> <span class="string">echo</span> <span class="string">"//npm.pkg.github.com/:_authToken=$&#123;GITHUB_TOKEN&#125;"</span> <span class="string">&gt;</span> <span class="string">~/.npmrc</span></span><br><span class="line">    <span class="attr">env:</span></span><br><span class="line">    <span class="attr">GITHUB_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span></span><br><span class="line">    <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line">    <span class="string">yarn</span> <span class="string">install</span></span><br><span class="line">    <span class="string">yarn</span> <span class="string">lint</span></span><br><span class="line">    <span class="string">yarn</span> <span class="string">build</span></span><br></pre></td></tr></table></figure><p>The final thing to remember is that by default the GITHUB_TOKEN here doesn't have read permission over your packages. You'll need to go into the package settings and add the repository to the list of repositories which can use the package. You just need read access.  If you don't do this step you're going to see an error like <code>error Error: https://npm.pkg.github.com/@stimms%2fuicomponents: authentication token not provided</code></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;My life is never without adventure but unfortunately it isn&#39;t the living on a beach sort of adventure. No it&#39;s the installing yarn packag
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Fast Endpoints Listen Port</title>
    <link href="https://westerndevs.com/_/fast-endpoints-port/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/fast-endpoints-port/</id>
    <published>2024-10-12T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.822Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>In order to set the listening port for Fast Endpoints you can use the same mechanism as a regular ASP.NET application. This invovles setting the Urls setting in the appsettings.json file. My file looks like this:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"Logging"</span>: &#123;</span><br><span class="line">    <span class="attr">"LogLevel"</span>: &#123;</span><br><span class="line">      <span class="attr">"Default"</span>: <span class="string">"Information"</span>,</span><br><span class="line">      <span class="attr">"Microsoft.AspNetCore"</span>: <span class="string">"Warning"</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">"Urls"</span>: <span class="string">"http://0.0.0.0:8080"</span>,</span><br><span class="line">  <span class="attr">"AllowedHosts"</span>: <span class="string">"*"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;In order to set the listening port for Fast Endpoints you can use the same mechanism as a regular ASP.NET application. This invovles sett
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Update outdated Nuget packages</title>
    <link href="https://westerndevs.com/_/update-outdated-packages/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/update-outdated-packages/</id>
    <published>2024-09-15T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.829Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>If you're using Visual Studio Code to develop C# applications, you might need to update outdated Nuget packages. You can do that without having to do each one individually on the command line using <a href="https://github.com/dotnet-outdated/dotnet-outdated" target="_blank" rel="noopener">dotnet outdated</a></p><p>Install it with</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet tool install --global dotnet-outdated</span><br></pre></td></tr></table></figure><p>Then you can run it in the root of your project to list the packages which will be updated with</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet outdated</span><br></pre></td></tr></table></figure><p>Then, if you're happy, run it again with</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet outdated -u</span><br></pre></td></tr></table></figure><p>to actually get everything updated.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;If you&#39;re using Visual Studio Code to develop C# applications, you might need to update outdated Nuget packages. You can do that without 
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">NServiceBus Kata 6 - When things go wrong</title>
    <link href="https://westerndevs.com/_/nservicebus-kata-6/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/nservicebus-kata-6/</id>
    <published>2024-09-14T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.824Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>So far in this series things have been going pretty well. We've looked at sending messages, publishing messages, switching transports, long running processes, and timeouts. But what happens when things go wrong? In this kata we're going to look at how to handle errors in NServiceBus.</p><a id="more"></a><ul><li>Kata 1 - <a href="https://www.westerndevs.com/_/nservicebus-kata-1" target="_blank" rel="noopener">Sending a message</a></li><li>Kata 2 - <a href="https://www.westerndevs.com/_/nservicebus-kata-2" target="_blank" rel="noopener">Publishing a message</a></li><li>Kata 3 - <a href="https://www.westerndevs.com/_/nservicebus-kata-3" target="_blank" rel="noopener">Switching transports</a></li><li>Kata 4 - <a href="https://www.westerndevs.com/_/nservicebus-kata-4" target="_blank" rel="noopener">Long running processes</a></li><li>Kata 5 - <a href="https://www.westerndevs.com/_/nservicebus-kata-5" target="_blank" rel="noopener">Timeouts</a></li><li>Kata 6 - When things go wrong</li></ul><p>NServiceBus is built to be reliable. It is designed to be able to handle errors and to be able to recover from them. The most common reason for an error to show up in NServiceBus is that a message fails to process. This could be because the message is malformed, a resource which is needed to process the message is unavailable or because the handler throws an exception. Throwing exceptions in handlers is actually the preferred way to handle errors in NServiceBus.</p><p>When a message fails to process NServiceBus will attempt to reprocess the message. NServiceBus defines two different retry mechanisms: immediate and delayed retry. Immediate retries will reprocess at once and by default happen 5 times. Delayed retries fire at 10, 20 and 30 second after the failed retries and they too will fire immediate retries. So by the time a message has fully failed it's been attempted 20 times. If all these attempts fail then the message is shuffled off to an error queue. This error queue is not typically watched by NService Bus and recovering a message form it is a manual process.</p><p>In NServiceBus installations I've worked on we monitor the error queues fairly closely and work to make the application resilient to errors such that the queues are only populated by messages which have failed due to resources being unavailable. Don't just ignore these messages as they are likely important user interactions or business processes which will cost you money if you don't address them.</p><h1>The Kata</h1><p>Let's make one of our messages fail to process and watch what happens. In this scenario let's fail to properly sanitize our inputs to the messages and have them take on a value which makes no sense. We have a message <code>EatCake</code> which has a property <code>NumberOfCakes</code>. Let's make it so that the <code>NumberOfCakes</code> is set to -1 and that we have a validation in place in our message handler to throw when we see this problem. Let's also log a message so we can see the retries happening.</p><h1>My Solution</h1><ol><li>Modify the <code>EatCake</code> message hander to have a validation to make sure cakes are not eaten in the negative.</li></ol><figure class="highlight arduino"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> async <span class="built_in">Task</span> <span class="title">Handle</span><span class="params">(EatCake message, IMessageHandlerContext context)</span></span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (message.NumberOfCakes &lt; <span class="number">0</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="built_in">Console</span>.WriteLine(<span class="string">"Negative cake? Really?"</span>);</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> Exception(<span class="string">"Negative cake? Really?"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (message.NumberOfCakes == <span class="number">0</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="built_in">Console</span>.WriteLine(<span class="string">"No cake to eat"</span>);</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> Exception(<span class="string">"No cake to eat"</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="built_in">Console</span>.WriteLine($<span class="string">"Cake eaten, NumberOfCakes = &#123;message.NumberOfCakes&#125;; Flavour = &#123;message.Flavour&#125;"</span>);</span><br><span class="line">        await context.Publish(<span class="keyword">new</span> CakeEaten</span><br><span class="line">        &#123;</span><br><span class="line">            EatingFinishedAt = DateTime.Now</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>Modify the sender to send a message with a negative number of cakes</li></ol><figure class="highlight arduino"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">switch</span> (<span class="built_in">line</span>)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">"send"</span>:</span><br><span class="line">        await endpointInstance.Send(<span class="string">"NServiceBusKataReceiver"</span>, <span class="keyword">new</span> EatCake</span><br><span class="line">        &#123;</span><br><span class="line">            Flavour = <span class="string">"Coconut"</span>,</span><br><span class="line">            NumberOfCakes = <span class="number">-1</span></span><br><span class="line">        &#125;);</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">        ...</span><br></pre></td></tr></table></figure><ol start="3"><li>You may need to configure the error queue in the receiver endpoint.</li></ol><figure class="highlight abnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">endpointConfiguration.SendFailedMessagesTo(<span class="string">"NServiceBusKataReceiverError"</span>)<span class="comment">;</span></span><br></pre></td></tr></table></figure><p>And build the error queue using the handy dandy rabbitmq-transport tool</p><figure class="highlight elm"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title">rabbitmq</span>-trans<span class="keyword">port</span> endpoint create NServiceBusKataAnotherReceiver -c "host=localhost" <span class="comment">--errorQueue NServiceBusKataAnotherReceiverError</span></span><br></pre></td></tr></table></figure><p>Things to try now</p><ol><li>Run the sender and produce some messages with negative numbers of cakes</li><li>Observe the logs for the receiver and see the retries happening</li><li>Wait a bit for the retries to be exhausted and see the message end up in the error queue</li><li>Open up the rabbitmq management console and look at the error queue. Pay special attention to the headers of the message. You'll see the number of retries and the time the message was first sent as well as the exception message.</li></ol><h1>Things to think about</h1><p>Looking at messages in the RabbitMQ management console is great for a single message but in the aggregate there is often a need for better tooling. Particular provide some tooling in the form of ServiceInsight which can be used to monitor the flow of messages through the system. This can be used to see where messages are failing and to help diagnose the problem.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;So far in this series things have been going pretty well. We&#39;ve looked at sending messages, publishing messages, switching transports, long running processes, and timeouts. But what happens when things go wrong? In this kata we&#39;re going to look at how to handle errors in NServiceBus.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">NServiceBus Kata 5 - Timeouts</title>
    <link href="https://westerndevs.com/_/nservicebus-kata-5/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/nservicebus-kata-5/</id>
    <published>2024-09-09T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.824Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>In the previous kata we looked at sagas which are a way to coordinate long running processes. In this kata we're going to look at another tool in the NServiceBus toolbox: timeouts. Timeouts are a way to schedule a message to be sent at some point in the future. This is a powerful tool for building out complex processes.</p><a id="more"></a><ul><li>Kata 1 - <a href="https://www.westerndevs.com/_/nservicebus-kata-1" target="_blank" rel="noopener">Sending a message</a></li><li>Kata 2 - <a href="https://www.westerndevs.com/_/nservicebus-kata-2" target="_blank" rel="noopener">Publishing a message</a></li><li>Kata 3 - <a href="https://www.westerndevs.com/_/nservicebus-kata-3" target="_blank" rel="noopener">Switching transports</a></li><li>Kata 4 - <a href="https://www.westerndevs.com/_/nservicebus-kata-4" target="_blank" rel="noopener">Long running processes</a></li><li>Kata 5 - Timeouts</li><li>Kata 6 - <a href="https://www.westerndevs.com/_/nservicebus-kata-6" target="_blank" rel="noopener">When things go wrong</a></li></ul><p>I think we're pretty used to the idea that a timeout is something we want to avoid - it usually means that a server isn't available or that we've run a query that is taking too long. But there are lots of places in business processes where timeout are just part of the process we're modeling and aren't an error at all. As an example consider a shopping cart: if a user adds an item to the cart and then doesn't check out within a certain period of time we might want to send them a reminder and even offer them a discount on their purchases. If we've modeled the checkout process as a saga then this reminder can be set up as a timeout.</p><h1>The Kata</h1><p>We just implemented a cake order saga in the last kata. Let's extend that saga to include a timeout. If the cake isn't shipped within 2 minutes of the order being placed then we should send a message to the bakery to ask them to check on the order. Obviously 2 minutes is a pretty quick turn around on a cake but I don't imagine you want to hang around for hours writing this kata.</p><h1>My Solution</h1><ol><li>Add a new message to the messages project</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> <span class="title">messages</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeOrderStalled</span> : <span class="title">IMessage</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> Guid OrderId &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>Modify the saga to start a timeout when an order is placed. The CakeOrderPlaced handler becomes</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeOrderPlaced message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">$"Order <span class="subst">&#123;message.OrderId&#125;</span> placed"</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> RequestTimeout&lt;CakeOrderStalled&gt;(context, TimeSpan.FromMinutes(<span class="number">2</span>), <span class="keyword">new</span> CakeOrderStalled&#123; OrderId = message.OrderId &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>Add a handler for the timeout message</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Task <span class="title">Timeout</span>(<span class="params">CakeOrderStalled state, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">$"Order <span class="subst">&#123;Data.OrderId&#125;</span> stalled!"</span>);</span><br><span class="line">    <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="4"><li>Have the saga implement</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">IHandleTimeouts&lt;CakeOrderStalled&gt;</span><br></pre></td></tr></table></figure><ol start="4"><li>We missed it previously but we need to have canceling the order or completing the order mark the saga as complete. To do that add <code>MarkAsComplete</code> to the handlers for those messages</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeOrderCanceled message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    Console.WriteLine(<span class="string">$"Order <span class="subst">&#123;message.OrderId&#125;</span> canceled"</span>);</span><br><span class="line">    MarkAsComplete();</span><br><span class="line">    <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Things to try now</p><ol><li>Run the applications and in the sender app try starting the saga with <code>o</code> to start an order. Wait a couple of minutes and see that the timeout fires.</li><li>Try starting the saga and then canceling it with <code>o</code> followed by <code>c</code> and see what happens when the timeout fires.</li></ol><h1>Things to think about</h1><p>Once a timeout is registered there is no way to cancel it. How come? What would you do if you needed to cancel a timeout? If messages are delayed is it possible that timeouts could fire when an expected action has actually happened? How would you mitigate that risk?</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;In the previous kata we looked at sagas which are a way to coordinate long running processes. In this kata we&#39;re going to look at another tool in the NServiceBus toolbox: timeouts. Timeouts are a way to schedule a message to be sent at some point in the future. This is a powerful tool for building out complex processes.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">NServiceBus Kata 4 - Long Running Processes</title>
    <link href="https://westerndevs.com/_/nservicebus-kata-4/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/nservicebus-kata-4/</id>
    <published>2024-09-02T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.824Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>We now have a pretty solid way to send messages, publish messages and we've got those messages flowing over a reliable transport mechanism. Sending and publishing individual messages only gets us so far. We often need a way to coordinate complex processes which involve multiple services. For this NServiceBus has the concept of sagas which some might call process managers.</p><a id="more"></a><ul><li>Kata 1 - <a href="https://www.westerndevs.com/_/nservicebus-kata-1" target="_blank" rel="noopener">Sending a message</a></li><li>Kata 2 - <a href="https://www.westerndevs.com/_/nservicebus-kata-2" target="_blank" rel="noopener">Publishing a message</a></li><li>Kata 3 - <a href="https://www.westerndevs.com/_/nservicebus-kata-3" target="_blank" rel="noopener">Switching transports</a></li><li>Kata 4 - Long running processes</li><li>Kata 5 - <a href="https://www.westerndevs.com/_/nservicebus-kata-5" target="_blank" rel="noopener">Timeouts</a></li><li>Kata 6 - <a href="https://www.westerndevs.com/_/nservicebus-kata-6" target="_blank" rel="noopener">When things go wrong</a></li></ul><p>Consider the bakery which is creating the cake for me to eat: when it starts making a new cake because I ate the last one it has a bunch of things it needs to coordinate. They need to preheat ovens, gather ingredients, mix ingredients, grease pans, fill pans, put pans in the oven, remember to take the cake out, cool it, ice it... the list goes on and on - no wonder cake is so expensive. Coordinating all these activities is complex in a distributed system, really in any system. There are a lot of corner cases that we usually fail to consider in a non-distributed system which become much more apparent when building out a process manager. What if we preheat the oven but then discover that we're all out of flour? In that monolithic system we might throw an exception and hope that somebody is monitoring for it in a log file somewhere. Realistically that's never going to happen. In the meantime nobody has shut the oven off and the bakery burns down.</p><p>A saga allows us to store the state of a process, to react to messages as they come in and send new messages. We use this to coordinate the activities of the bakery. In our example above we can probably call the process &quot;BakeCakeSaga&quot;. When messages come in relating to the order then we need to be able to find a way to look up the state and make modifications to it. NServiceBus implements this through a method called <code>ConfigureHowToFindSaga</code>. This function will provide a mapping from every message that interacts with the saga to find the saga data. For our example we'd probably use something like an order id.</p><p>Let's build out a very simple saga which responds to just a few messages in our system so we can see how it works. Saga can get pretty complex but they are quite testable so that's nice.</p><h1>The Kata</h1><p>Create a saga which handles the messages <code>CakeOrderPlaced</code>, <code>CakeOrderCanceled</code>, <code>CakeOrderShipped</code>. Each of these messages will contain an <code>OrderId</code>, a GUID, which will be used to identify the saga as well as whatever information might be associated with those messages. For now just write out to the console when each of these messages is received - unless you want to bake me a cake which I will accept.</p><h1>The Solution</h1><ol><li>Add some mechanism to handle the persistence of saga data. For now we'll just use the in learning persistence. In the various program.cs files add</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> persistence = endpointConfiguration.UsePersistence&lt;LearningPersistence&gt;();</span><br></pre></td></tr></table></figure><ol start="2"><li>Create messages classes in the messages project</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> <span class="title">messages</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeOrderPlaced</span> : <span class="title">IEvent</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> Guid OrderId &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> Guid CustomerId &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> Date OrderDate &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeOrderCanceled</span> : <span class="title">IEvent</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> Guid OrderId &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> Reason &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeOrderShipped</span> : <span class="title">IEvent</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> Guid OrderId &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> ShippingReferenceNumber &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>Create a saga class in the receiver project</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line">public class CakeOrderSaga : Saga&lt;CakeOrderSagaData&gt;,</span><br><span class="line">    IAmStartedByMessages&lt;CakeOrderCanceled&gt;,</span><br><span class="line">    IAmStartedByMessages&lt;CakeOrderPlaced&gt;,</span><br><span class="line">    IAmStartedByMessages&lt;CakeOrderShipped&gt;</span><br><span class="line">&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> <span class="title">ConfigureHowToFindSaga</span>(<span class="params">SagaPropertyMapper&lt;CakeOrderSagaData&gt; mapper</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        mapper.MapSaga(sagaData =&gt; sagaData.OrderId)</span><br><span class="line">            .ToMessage&lt;CakeOrderCanceled&gt;(x =&gt; x.OrderId)</span><br><span class="line">            .ToMessage&lt;CakeOrderPlaced&gt;(x =&gt; x.OrderId)</span><br><span class="line">            .ToMessage&lt;CakeOrderShipped&gt;(x =&gt; x.OrderId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeOrderPlaced message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Order <span class="subst">&#123;message.OrderId&#125;</span> placed"</span>);</span><br><span class="line">        <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeOrderCanceled message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Order <span class="subst">&#123;message.OrderId&#125;</span> canceled"</span>);</span><br><span class="line">        Data.OrderCanceled = <span class="literal">true</span>;</span><br><span class="line">        <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeOrderShipped message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Order <span class="subst">&#123;message.OrderId&#125;</span> shipped"</span>);</span><br><span class="line">        Data.OrderShipped = <span class="literal">true</span>;</span><br><span class="line">        <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="4"><li>Add a saga data class to the receiver project</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeOrderSagaData</span> : <span class="title">ContainSagaData</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> Guid OrderId &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">bool</span> OrderCanceled &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">bool</span> OrderShipped &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="5"><li>Modify the sender project's program.cs to send the messages adding a loop which sends all the different messages involved in the saga.</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">bool</span> continueMessages = <span class="literal">true</span>;</span><br><span class="line">Guid orderId = Guid.NewGuid();</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (continueMessages)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">var</span> line = Console.ReadLine();</span><br><span class="line">    <span class="keyword">switch</span> (line)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"p"</span>:</span><br><span class="line">            <span class="keyword">await</span> endpointInstance.Publish(<span class="keyword">new</span> CakeOrderPlaced &#123; OrderId = orderId &#125;);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"c"</span>:</span><br><span class="line">            <span class="keyword">await</span> endpointInstance.Publish(<span class="keyword">new</span> CakeOrderCanceled &#123; OrderId = orderId &#125;);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"s"</span>:</span><br><span class="line">            <span class="keyword">await</span> endpointInstance.Publish(<span class="keyword">new</span> CakeOrderShipped &#123; OrderId = orderId &#125;);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">"q"</span>:</span><br><span class="line">            continueMessages = <span class="literal">false</span>;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Things to try now</p><ol><li>Run the applications and in the sender app try pressing some keys like <code>p</code> or <code>c</code> or <code>s</code> to see the messages being handled by the saga.</li><li>Try starting the applications in different orders and see how the saga handles the messages.</li></ol><h1>Things to Notice</h1><p>Notice that the Saga can be started by 3 different events. Why would a saga be started by a cancel message? You can't cancel an order which hasn't even been placed yet - right? Well it turns out you can. Without some serious hoop jumping through the order of message delivery it not guaranteed. So in fact orders can be canceled or shipped before we get the message telling us the order has been placed. It is sort of mind-blowing.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;We now have a pretty solid way to send messages, publish messages and we&#39;ve got those messages flowing over a reliable transport mechanism. Sending and publishing individual messages only gets us so far. We often need a way to coordinate complex processes which involve multiple services. For this NServiceBus has the concept of sagas which some might call process managers.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">NServiceBus Kata 3 - Switching transports</title>
    <link href="https://westerndevs.com/_/nservicebus-kata-3/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/nservicebus-kata-3/</id>
    <published>2024-09-01T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.824Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>In the previous article we looked at publishing messages and the one before that sending messages. But in both cases we cheated a little bit: we used the LearningTransport. This is effectively just a directory on disk. It cannot be used as real world transport. Let's change out this transport for something more production ready.</p><a id="more"></a><ul><li>Kata 1 - <a href="https://www.westerndevs.com/_/nservicebus-kata-1" target="_blank" rel="noopener">Sending a message</a></li><li>Kata 2 - <a href="https://www.westerndevs.com/_/nservicebus-kata-2/" target="_blank" rel="noopener">Publishing a message</a></li><li>Kata 3 - Switching transports</li><li>Kata 4 - <a href="https://www.westerndevs.com/_/nservicebus-kata-4/" target="_blank" rel="noopener">Long running processes</a></li><li>Kata 5 - <a href="https://www.westerndevs.com/_/nservicebus-kata-5" target="_blank" rel="noopener">Timeouts</a></li><li>Kata 6 - <a href="https://www.westerndevs.com/_/nservicebus-kata-6" target="_blank" rel="noopener">When things go wrong</a></li></ul><p>One of the really nice things about NServiceBus is that it abstracts away a lot of the mess of dealing with transports. You can spend less time fiddling with plumbing and more time getting to the nitty gritty of building business value. We have a lot of options for transport and the ability to write new ones if needed. The official supported transports are</p><ul><li>Azure Service Bus</li><li>Azure Storage Queues</li><li>Amazon SQS</li><li>RabbitMQ</li><li>SQL Server</li><li>MSMQ</li></ul><p>Each of these has advantages and disadvantages which you can read about in some detail in the <a href="https://docs.particular.net/transports/" target="_blank" rel="noopener">documentation</a>. In a production environment I'd push heavily towards Azure Service Bus but it has a major disadvantage: no local emulator. There is a github issue open to <a href="https://github.com/Azure/azure-service-bus/issues/223" target="_blank" rel="noopener">add an emulator</a> which has been open since 2018. Normally I'd grumble a bit about how this is never going to be solved but actually the team has dedicated resources to building one out and think they'll have one ready by the end of 2024. Only you, dear readers from the future, know if this came to fruition.</p><p>For now we're going to take advantage of the interchangeability of transports and switch out the LearningTransport for the RabbitMQ transport. This can be run with a container on docker.</p><h1>The Kata</h1><p>Take the solution developed already and switch out the learning transport for the RabbitMQ transport running inside of a container. Everything should continue to work as it did before but now using RabbitMQ.</p><h1>My Solution</h1><ol><li>Download and run the RabbitMQ container</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d --hostname nsbkata --name nsbkata-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management</span><br></pre></td></tr></table></figure><ol start="2"><li>Install the RabbitMQ transport package in all the projects</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dotnet add sender package NServiceBus.RabbitMQ</span><br><span class="line">dotnet add receiver package NServiceBus.RabbitMQ</span><br><span class="line">dotnet add anotherReceiver package NServiceBus.RabbitMQ</span><br></pre></td></tr></table></figure><ol start="3"><li>Modify the endpoint configurations across all the projects swapping</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> transport = endpointConfiguration.UseTransport&lt;LearningTransport&gt;();</span><br></pre></td></tr></table></figure><p>with</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> transport = endpointConfiguration.UseTransport&lt;RabbitMQTransport&gt;();</span><br><span class="line">transport.ConnectionString(<span class="string">"host=localhost"</span>);</span><br><span class="line">transport.UseConventionalRoutingTopology(QueueType.Quorum);</span><br></pre></td></tr></table></figure><ol start="4"><li>It is generally best that you take control of creating queues for NServiceBus but tooling is provided. Install that tooling</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet tool install -g NServiceBus.Transport.RabbitMQ.CommandLine</span><br></pre></td></tr></table></figure><p>Next create the queues for the endpoints (and the delays queues)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rabbitmq-transport delays create  -c <span class="string">"host=localhost"</span></span><br><span class="line">rabbitmq-transport endpoint create NServiceBusKataAnotherReceiver -c <span class="string">"host=localhost"</span></span><br><span class="line">rabbitmq-transport endpoint create NServiceBusKataReceiver -c <span class="string">"host=localhost"</span></span><br><span class="line">rabbitmq-transport endpoint create NServiceBusKataSender -c <span class="string">"host=localhost"</span></span><br></pre></td></tr></table></figure><p>Things to try now</p><ol><li><p>Run the sender, receiver and anotherReceiver projects. You should see the messages being sent and received as before.</p></li><li><p>Log into the RabbitMQ management console(running at http://localhost:15672/ with credentials <code>guest</code>/<code>guest</code>) you should see the queues created and in fact a message processed</p></li></ol><p><img src="../images/nservicebus-kata-3/2024-09-01-12-09-20.png" alt="A RabbitMQ queue"></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;In the previous article we looked at publishing messages and the one before that sending messages. But in both cases we cheated a little bit: we used the LearningTransport. This is effectively just a directory on disk. It cannot be used as real world transport. Let&#39;s change out this transport for something more production ready.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">NServiceBus Kata 2</title>
    <link href="https://westerndevs.com/_/nservicebus-kata-2/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/nservicebus-kata-2/</id>
    <published>2024-08-31T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.824Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>In the previous kata we sent a message from one application to another. This is a common pattern in messaging systems. In this kata we're going to look at a different pattern: publishing a message.</p><a id="more"></a><ul><li>Kata 1 - <a href="https://blog.simontimms.com/2024/08/30/nservicebus-kata-1" target="_blank" rel="noopener">Sending a message</a></li><li>Kata 2 - Publishing a message</li><li>Kata 3 - <a href="https://www.westerndevs.com/_/nservicebus-kata-3/" target="_blank" rel="noopener">Switching transports</a></li><li>Kata 4 - <a href="https://www.westerndevs.com/_/nservicebus-kata-4/" target="_blank" rel="noopener">Long running processes</a></li><li>Kata 5 - <a href="https://www.westerndevs.com/_/nservicebus-kata-5" target="_blank" rel="noopener">Timeouts</a></li><li>Kata 6 - <a href="https://www.westerndevs.com/_/nservicebus-kata-6" target="_blank" rel="noopener">When things go wrong</a></li></ul><h1>The Problem</h1><p>It is great being able to send a message from one system to another - you can instruct a remote system to take some action which you don't know how to do. For instance sending an email. In a large system lots of processes will likely result in an email but if you can centralize the logic around how to send an email it makes it very easy to do things like change email providers as that functionality is isolated and independent.</p><p>If you looked at my solution for the first Kata then you might have notice that I named my message <code>EatCake</code> this is named in the imperative form. This is a common pattern in messaging systems and is, I think, remarkably helpful to understand the command pattern. When you issue a command you know exactly who or what you're commanding and in the same way the command is sent to a known endpoint.</p><p>Sometimes, however, you don't have a particular target in mind and you just want to let other systems know that you have done something. In this case you can publish a message. Unlike a command you don't know who is going to act on it - it may be nobody or it may be multiple people. In a messaging system we call this an <code>Event</code>.</p><p>Let's extend our cake eating example to imagine some consequences of what might happen if I happily reported that I had eaten cake: <code>CakeEaten</code>. Finding out that the cake had been eaten the bakery might start baking more cake assuming that I'd want more (very likely). My gym might free up a treadmill knowing that my cake-induced guilt might drive me to run until I'd burned off the delicious butter-cream. It might also be of interest to my wife who can text me and see if I enjoyed the cake. I don't actually know who would be interested in the event so it is the responsibility of those who are interested to subscribe to the event.</p><h1>The Kata</h1><p>Using the solution to the first kata as a starting point extend the system to publish a <code>CakeEaten</code> message. Subscribe to the message in the sender and write out a message to the console. Also create another new project similar to the others and have it subscribe to the message.</p><h1>My Solution</h1><ol><li>Create a new message in the messages assembly</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> <span class="title">messages</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeEaten</span> : <span class="title">IEvent</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> DateTime EatingFinishedAt &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>Modify the Hander in the receiver to publish the new event. Notice that we have access to a <code>IMessageHandlerContext</code> which we can use to publish messages.</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">PlaceOrderHandler</span> :</span><br><span class="line">    IHandleMessages&lt;EatCake&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">async</span> Task <span class="title">Handle</span>(<span class="params">EatCake message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Cake eaten, NumberOfCakes = <span class="subst">&#123;message.NumberOfCakes&#125;</span>; Flavour = <span class="subst">&#123;message.Flavour&#125;</span>"</span>);</span><br><span class="line">        <span class="keyword">await</span> context.Publish(<span class="keyword">new</span> CakeEaten</span><br><span class="line">        &#123;</span><br><span class="line">            EatingFinishedAt = DateTime.Now</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>Modify the sender to subscribe to the event by adding a Handler to the project.</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeEatenHandler</span> :</span><br><span class="line">    IHandleMessages&lt;CakeEaten&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeEaten message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Uh oh, somebody ate a cake at <span class="subst">&#123;message.EatingFinishedAt&#125;</span>"</span>);</span><br><span class="line">        <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="4"><li>Optionally modify the sender to remain running so that it can receive the message. This is done by adding a <code>Console.ReadLine()</code> at the end of the <code>Main</code> method.</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line">Console.Title = <span class="string">"NServiceBusKata - Sender"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointConfiguration = <span class="keyword">new</span> EndpointConfiguration(<span class="string">"NServiceBusKataReceiver"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Choose JSON to serialize and deserialize messages</span></span><br><span class="line">endpointConfiguration.UseSerialization&lt;SystemJsonSerializer&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> transport = endpointConfiguration.UseTransport&lt;LearningTransport&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointInstance = <span class="keyword">await</span> Endpoint.Start(endpointConfiguration);</span><br><span class="line"></span><br><span class="line">Console.WriteLine(<span class="string">"Press Enter to exit..."</span>);</span><br><span class="line">Console.ReadLine();</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> endpointInstance.Stop();</span><br></pre></td></tr></table></figure><p>Pause here and ensure that when running the sender and receiver that you see both a message sent and published.</p><ol start="5"><li>Create a new project to act as a second subscriber or receiver</li></ol><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dotnet new<span class="built_in"> console </span>-o anotherReceiver</span><br><span class="line">dotnet <span class="builtin-name">add</span> anotherReceiver reference <span class="built_in">..</span>/messages</span><br><span class="line">dotnet <span class="builtin-name">add</span> anotherReceiver package NServiceBus</span><br></pre></td></tr></table></figure><ol start="6"><li>Add a subscriber class to the project</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">CakeEatenHandler</span> :</span><br><span class="line">    IHandleMessages&lt;CakeEaten&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">CakeEaten message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Awesome, somebody ate a cake at <span class="subst">&#123;message.EatingFinishedAt&#125;</span>. Time to bake another"</span>);</span><br><span class="line">        <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="7"><li>Update the Program.cs to initiate NServiceBus</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line">Console.Title = <span class="string">"NServiceBusKata - AnotherPublisher"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointConfiguration = <span class="keyword">new</span> EndpointConfiguration(<span class="string">"NServiceBusKataAnotherReceiver"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Choose JSON to serialize and deserialize messages</span></span><br><span class="line">endpointConfiguration.UseSerialization&lt;SystemJsonSerializer&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> transport = endpointConfiguration.UseTransport&lt;LearningTransport&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointInstance = <span class="keyword">await</span> Endpoint.Start(endpointConfiguration);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Console.ReadLine();</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> endpointInstance.Stop();</span><br></pre></td></tr></table></figure><p>Things to try now:</p><ol><li>Start the receiver and the another receiver then the sender - ensure you see messages flowing correctly</li><li>Start services in different orders and see which messages might be lost</li></ol><p>Sending and receiving messages and publishing messages should now be working great. But to get this example going we've made some simplifications which we'll have to address in the next kata.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;In the previous kata we sent a message from one application to another. This is a common pattern in messaging systems. In this kata we&#39;re going to look at a different pattern: publishing a message.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">NServiceBus Kata 1</title>
    <link href="https://westerndevs.com/_/nservicebus-kata-1/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/nservicebus-kata-1/</id>
    <published>2024-08-30T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.824Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>Exciting times for me, I get to help out on an NServiceBus project! It's been way too long since I did anything with NServiceBus but I'm back, baby! Most of the team has never used NServiceBus before so I thought it would be a good idea to do a little kata to get them up to speed. I'll probably do 2 or 3 of these and if they help my team they might as well help you, too.</p><a id="more"></a><ul><li>Kata 1 - Sending a message</li><li>Kata 2 - <a href="https://www.westerndevs.com/_/nservicebus-kata-2/" target="_blank" rel="noopener">Publishing a message</a></li><li>Kata 3 - <a href="https://www.westerndevs.com/_/nservicebus-kata-3/" target="_blank" rel="noopener">Switching transports</a></li><li>Kata 4 - <a href="https://www.westerndevs.com/_/nservicebus-kata-4/" target="_blank" rel="noopener">Long running processes</a></li><li>Kata 5 - <a href="https://www.westerndevs.com/_/nservicebus-kata-5" target="_blank" rel="noopener">Timeouts</a></li><li>Kata 6 - <a href="https://www.westerndevs.com/_/nservicebus-kata-6" target="_blank" rel="noopener">When things go wrong</a></li></ul><h2>The Problem</h2><p>Our goal is to very simply demonstrate reliable messaging. If you're communicating between two processes on different machines a usual approach is to send a message using HTTP. Problem is that sometimes the other end isn't reachable. Could be that the service is down, could be that the network is down or it could be that the remote location was hit by a meteor. HTTP won't help us in this case - what we want is a reliable protocol which will save the message somewhere safe and deliver it when the endpoint does show up.</p><p>For this we use a message queue. There are approximately 9 billion different messaging technologies out there but we're going to use NServiceBus. NServiceBus is a .NET library which wraps up a lot of the complexity of messaging. It is built to be able to use a variety of transport such as  RabbitMQ and Azure Service Bus.</p><p>We want to make use of NServiceBus and a few C# applications to demonstrate reliable messaging.</p><h1>The Kata</h1><p>I like cake but I feel bad about eating it because it's not good for me. So in this kata you need to command me to eat cake. I can't refuse a command to eat cake so I can't possibly feel bad about it.</p><p>Create a sender application which sends a message to a receiver application. The receiver application should be able to receive the message and write it to the console. The sender application should be able to send the message and then exit. The receiver application should be able to start up and receive the message even if the sender application isn't running.</p><p>Now go do it!</p><p>Useful resources:</p><ul><li><a href="https://docs.particular.net/tutorials/nservicebus-step-by-step/1-getting-started/" target="_blank" rel="noopener">NServiceBus getting started</a></li></ul><h1>My Solution</h1><ol><li>Create a new directory for the project</li></ol><figure class="highlight dos"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> kata1</span><br><span class="line"><span class="built_in">cd</span> kata1</span><br></pre></td></tr></table></figure><ol start="2"><li>Create a new console project for the sender</li></ol><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet new<span class="built_in"> console </span>-o sender</span><br></pre></td></tr></table></figure><ol start="3"><li>Create a new console project for the receiver</li></ol><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet new<span class="built_in"> console </span>-o receiver</span><br></pre></td></tr></table></figure><ol start="4"><li>Create a new class library for the messages</li></ol><figure class="highlight haxe"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dotnet <span class="keyword">new</span> <span class="type">classlib</span> -o messages</span><br></pre></td></tr></table></figure><ol start="5"><li>Add a reference to the messages project in the sender and receiver projects</li></ol><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dotnet <span class="builtin-name">add</span> sender reference <span class="built_in">..</span>/messages</span><br><span class="line">dotnet <span class="builtin-name">add</span> receiver reference <span class="built_in">..</span>/messages</span><br></pre></td></tr></table></figure><ol start="6"><li>Add a reference to NServiceBus in all the projects</li></ol><figure class="highlight ada"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dotnet add sender <span class="keyword">package</span> <span class="title">NServiceBus</span></span><br><span class="line">dotnet add receiver <span class="keyword">package</span> <span class="title">NServiceBus</span></span><br><span class="line">dotnet add messages <span class="keyword">package</span> <span class="title">NServiceBus</span></span><br></pre></td></tr></table></figure><ol start="7"><li>Create a new class in the messages project (and remove Class1.cs)</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> <span class="title">messages</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">EatCake</span>: <span class="title">ICommand</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">int</span> NumberOfCakes &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">string</span> Flavour &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">"Chocolate"</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="8"><li>Update Program.cs in the sender project to send a message</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line">Console.Title = <span class="string">"NServiceBusKata - Sender"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointConfiguration = <span class="keyword">new</span> EndpointConfiguration(<span class="string">"NServiceBusKataSender"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Choose JSON to serialize and deserialize messages</span></span><br><span class="line">endpointConfiguration.UseSerialization&lt;SystemJsonSerializer&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> transport = endpointConfiguration.UseTransport&lt;LearningTransport&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointInstance = <span class="keyword">await</span> Endpoint.Start(endpointConfiguration);</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> endpointInstance.Send(<span class="string">"NServiceBusKataReceiver"</span>, <span class="keyword">new</span> EatCake&#123;</span><br><span class="line">    Flavour = <span class="string">"Coconut"</span>,</span><br><span class="line">    NumberOfCakes = <span class="number">2</span> <span class="comment">//don't be greedy</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> endpointInstance.Stop();</span><br></pre></td></tr></table></figure><ol start="9"><li>Update Program.cs in the receiver project to be an endpoint</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> NServiceBus;</span><br><span class="line"></span><br><span class="line">Console.Title = <span class="string">"NServiceBusKata - Reciever"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointConfiguration = <span class="keyword">new</span> EndpointConfiguration(<span class="string">"NServiceBusKataReceiver"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Choose JSON to serialize and deserialize messages</span></span><br><span class="line">endpointConfiguration.UseSerialization&lt;SystemJsonSerializer&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> transport = endpointConfiguration.UseTransport&lt;LearningTransport&gt;();</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> endpointInstance = <span class="keyword">await</span> Endpoint.Start(endpointConfiguration);</span><br><span class="line"></span><br><span class="line">Console.WriteLine(<span class="string">"Press Enter to exit..."</span>);</span><br><span class="line">Console.ReadLine();</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> endpointInstance.Stop();</span><br></pre></td></tr></table></figure><ol start="10"><li>Add a message handler to the receiver project</li></ol><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> messages;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">EatCakeHandler</span> :</span><br><span class="line">    IHandleMessages&lt;EatCake&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Task <span class="title">Handle</span>(<span class="params">EatCake message, IMessageHandlerContext context</span>)</span></span><br><span class="line"><span class="function"></span>    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">$"Cake eaten, NumberOfCakes = <span class="subst">&#123;message.NumberOfCakes&#125;</span>; Flavour = <span class="subst">&#123;message.Flavour&#125;</span>"</span>);</span><br><span class="line">        <span class="keyword">return</span> Task.CompletedTask;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Things to try now:</p><ol><li>Run the sender project - it will send a message but the receiver won't be running so nothing will happen</li><li>Run the receiver project - it will start listening for messages and find the message which was left for it</li><li>Run the sender project again - it will send a message and the receiver will pick it up and write to the console</li></ol><p>This demonstrates reliable messaging with NServiceBus</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Exciting times for me, I get to help out on an NServiceBus project! It&#39;s been way too long since I did anything with NServiceBus but I&#39;m back, baby! Most of the team has never used NServiceBus before so I thought it would be a good idea to do a little kata to get them up to speed. I&#39;ll probably do 2 or 3 of these and if they help my team they might as well help you, too.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Plinko Diagram</title>
    <link href="https://westerndevs.com/_/plinko-diagram/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/plinko-diagram/</id>
    <published>2024-07-31T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.825Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>One of my team members mentioned that they envision the process flow of our code as a Plinko board. If you've never watched The Price is Right, a Plinko board is a vertical board with pegs that a contestant drops a disc down. The disc bounces off the pegs and lands in a slot at the bottom. The slots have different values and the contestant wins the value of the slot the disc lands in.</p><p><img src="/images/2024-07-31-plinko-diagram.md/2024-07-31-09-53-16.png" alt="">)</p><p>I just loved this mental model.  Each peg in the board is an fork in the code and a different path can be taken from that point on. It's basically an execution tree but with a fun visual that's easy to explain to people.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">flowchart TB</span><br><span class="line">    A --&gt; B &amp; C</span><br><span class="line">    B --&gt; D &amp; E</span><br><span class="line">    C --&gt; F &amp; G</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;One of my team members mentioned that they envision the process flow of our code as a Plinko board. If you&#39;ve never watched The Price is 
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">An exploration of Azure Functions for a side project</title>
    <link href="https://westerndevs.com/_/azure-functions-for-side-projects/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/azure-functions-for-side-projects/</id>
    <published>2024-06-23T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.819Z</updated>
	<author>
	
	  
	  <name>Kyle Baley</name>
	  <email>kyle@baley.org</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>This is a short PSA for people (or, more likely, just future me) to describe two issues I ran into while migrating a bash file to Azure Functions. Namely:</p><ul><li>How do I see the exceptions that happened?</li><li>How do I configure an Azure Function app to save a PDF to a Google Drive folder</li></ul><p>This is from the perspective of someone who doesn't need their app to be highly available/scalable/reliable/tenable/affable/inscrutable/explicable, which the documentation for all cloud products seems to assume (and, it must be said, rightfully so).</p><a id="more"></a><p>I like crossword puzzles. And I like doing them on paper. I subscribe to a few services, including the New York Times, <a href="https://avxwords.com/" target="_blank" rel="noopener">AVCX</a>, <a href="https://xwordcontest.com/" target="_blank" rel="noopener">Matt Gaffney</a>, the <a href="https://pmxwords.com/" target="_blank" rel="noopener">Muller Monthly Music Meta</a>, and a couple of others, both free and paid. For years, I've maintained a <a href="https://github.com/kbaley/xword-downloader" target="_blank" rel="noopener">bash file</a> that downloads PDFs from various services that allow it (and some that likely don't) and merges them into a single document for printing. It works fine but I wanted to try my hand at moving it to an Azure Function app that ran on a timer rather than on demand like I do with the bash file.</p><p>So the app in question is pretty simple and the code itself isn't super interesting. Here are the logistics:</p><ul><li>Runs nightly</li><li>Downloads PDFs from various crossword providers (three so far)</li><li>Saves the PDF to Google Drive</li></ul><p>The last one is only because that's where I keep them after they're printed. I have an archive of the puzzles I've downloaded going back a number of years. I've no idea why except that I have the storage available. Maybe I'll run through them again in retirement but the more likely scenario is that my children will mass delete them in a housecleaning exercise after I die.</p><p>With that in mind, let's get to the two areas where I struggled.</p><h2>App Insights</h2><p>By most accounts, App Insights on Azure is a powerful tool and telemetry in general is a big business. But I've always shied away from the Diagnose/Investigate/Metrics tabs because they're just plain overwhelming for an aging hillbilly who's used to scrolling through IIS log files. So when I wake up in the morning and the latest Times puzzle isn't there, I need to figure out where to go.</p><p>My solution (which may not align with <em>the</em> solution) is pretty simple in the end:</p><ul><li>Navigate to the Function app in Azure</li><li>Go to Monitoring | Logs</li><li>Query the <code>exceptions</code> table with no other qualifers and with an appropriate time range</li></ul><p>This shows the exceptions and the stack traces within the time period. Given this runs nightly, there likely won't be more than a couple in the last 24 hours.</p><h2>Connect to a Google Drive folder</h2><p>Ugh...</p><p>I've said this on more than one occasion: I hate all things security-related including, but not limited to: certificates, OAuth, face ID, fingerprint scanners, digital signatures, encryption, authentication providers, 2FA, MFA, SSL, VPN, CVE, IAM, JWT, SSO, AES, TSA, passkeys, passwords, passports, key fobs, car alarms, and bike locks.</p><p>Setting up an Azure Function App to connect to Google Drive touches on several of these pet peeves and adds a few more. Here's what I eventually did to get this to work:</p><ul><li>Create a project in my Google Developer Console</li><li>Enable the Google Drive API (it wasn't enabled by default)</li><li>Create a <a href="https://cloud.google.com/iam/docs/service-account-overview" target="_blank" rel="noopener">service account</a> credential</li><li>Under the new service account, create a <em>key</em>. This downloads a file to your computer.</li><li>Upload the file to the Azure Storage account for the Azure Functions app in a File Share folder called <code>secrets</code></li><li>Create two environment variables for the Azure Function:<ul><li>GoogleApiSecretsFileName: the name of the file (include the .json extension) I downloaded for the key</li><li>GoogleDriveFolderId: The ID of the folder where the puzzles should be saved. (Navigate to the folder in a browser. The ID is everything after <code>folder/</code> in the URL.)</li></ul></li><li>Share the Google drive folder with the Google API service account (with Editor access). The email address is on the Credentials page in the Google Developer Console.</li></ul><p>Oh, and also write the actual code.</p><p>The <a href="https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-google" target="_blank" rel="noopener">Azure documentation</a> and ChatGPT suggest strongly that OAuth2 credentials (instead of a service account) should work if you set up Google as an authenticator on the Azure Functions app. I ran into problems with this and I <em>think</em> it's because I was running locally. I have the app set up to do everything through a console app instead of through the Azure Functions host when running in DEBUG mode and I suspect the problems are because my local app hasn't gone through the necessary authentication process. Either way, a service account explicitly says it's for unattended server-to-server scenarios, which this is, so I've justified it in my head, even if the Azure documentation suggests something else.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;This is a short PSA for people (or, more likely, just future me) to describe two issues I ran into while migrating a bash file to Azure Functions. Namely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How do I see the exceptions that happened?&lt;/li&gt;
&lt;li&gt;How do I configure an Azure Function app to save a PDF to a Google Drive folder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is from the perspective of someone who doesn&#39;t need their app to be highly available/scalable/reliable/tenable/affable/inscrutable/explicable, which the documentation for all cloud products seems to assume (and, it must be said, rightfully so).&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Truly syncing multiple calendars</title>
    <link href="https://westerndevs.com/_/syncing-multiple-calendars/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/syncing-multiple-calendars/</id>
    <published>2024-06-17T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.828Z</updated>
	<author>
	
	  
	  <name>Kyle Baley</name>
	  <email>kyle@baley.org</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>For reasons that I really need to investigate, I'm apparently a busy fellow. Such that I now maintain a rather ungainly number of calendars. Three to be exact. That's one personal calendar and one for each of two clients.</p><a id="more"></a><p>It's a fairly simple process to <em>view</em> all of these calendars in a single place. All the major calendar providers generously provide iCal links at the very least to let you combine everything in a single view and you can even turn individual calendars on and off.</p><p>I have two problems with this.</p><p>First, I often don't want to have all of these calendars visible in a single view on my screen. Screen-sharing is common and on more than one occasion, I've said, &quot;let's schedule a meeting&quot; and switched to the shared calendar view where all my personal <a href="https://en.wikipedia.org/wiki/Dudeism" target="_blank" rel="noopener">Dudeism meetings</a> are there for all to see.</p><p>Second (and more important than risking an argument with a nihilist), people booking meetings with me don't see the unavailable slots on the other calendars, leading to double-bookings. I didn't know how good I had it in high school when this was...let's just say, &quot;not a problem&quot;.</p><p>So I went on the hunt for a solution that met these criteria:</p><ol><li>The ability to book an event on one calendar and have the time blocked on the other two.</li><li>Have the time on other calendar show as a generic &quot;busy&quot; or &quot;unavailable&quot;. I.e. don't broadcast the details of the events from other calendars.</li><li>Have a centralized view <em>somewhere</em> of all the events <em>without</em> duplicates. I.e. without a bunch of &quot;busy&quot; or &quot;unavailable&quot; events.</li><li>Ideally, works with both Google and Microsoft calendars</li></ol><p><img src="/images/flintstones-camera.jpg" alt="The early days of online meetings">)</p><p>The easiest way to achieve this, of course, is to do it yourself. When you book an event on one calendar, you go through the motions of creating a generic event on the other two. This meets all the criteria but depending on how heavily your client leans into the &quot;all meetings/all the time&quot; project management style, this gets old real fast. But to be fair, it's served me reasonably well for many years.</p><p>This led naturally to a search for a technical solution, which yielded the following options:</p><ul><li><a href="https://www.onecal.io/" target="_blank" rel="noopener">OneCal</a></li><li><a href="https://calendarbridge.com/" target="_blank" rel="noopener">CalendarBridge</a></li><li><a href="https://flexibits.com/fantastical" target="_blank" rel="noopener">Fantastical</a></li><li><a href="https://calendly.com" target="_blank" rel="noopener">Calendly</a></li><li><a href="https://reclaim.ai/" target="_blank" rel="noopener">Reclaim.ai</a></li><li><a href="https://www.spikenow.com/" target="_blank" rel="noopener">Spike</a></li><li><a href="https://www.calendar.com/blog/how-to-sync-your-calendar-across-all-devices/" target="_blank" rel="noopener">Syncing calendars</a></li></ul><p>I have OneCal at the top because it's (currently) my favorite option. But I'll skim through the rest and provide reasons why I didn't go with them. I'll use my tried-and-true practice of claiming that my choice is the One True Way™ and everyone else is wrong as a means of encouraging people to comment with their own options, if only to prove me wrong. Which I'm quite happy to be. But still, that's not the case here. I'm right and you're wrong. Comment below if you disagree.</p><p>Back to the evaluations. I'll start with some of the quicker ones.</p><h3>Syncing calendars</h3><p>I'll be honest, I just kind of skimmed this article. I have kind of a vague idea of what they're going for but this seems geared more toward people that are managing someone else's calendar. Plus I don't <em>think</em> it meets the criteria of actually booking the time off on the other calendar. Either way, it looks kind of cumbersome to set up and to manage.</p><h3>Spike</h3><p>I didn't give this much more than a glance. It does <em>way</em> more than I want it to and calendar sync doesn't appear to be among their more popular features. In any case, the banner says &quot;email and chat app&quot; and the byline talks about teams and partners. I don't think I'm their target audience.</p><h3>Reclaim.ai</h3><p>Also has many features I don't need nor want though at least it's focused on calendars. Google Calendar specifically. Outlook is coming soon. So while calendar sync is one of the highlighted features, the lack of Outlook, as well as the focus on collaboration and AI and &quot;smart&quot; also suggests it's overkill.</p><h3>Calendly</h3><p>I've used Calendly before to send people links to schedule calls with me. It's a lovely app and the free version is quite powerful. I'd recommend it even over Google Calendar's built-in &quot;scheduling slots&quot; or &quot;appointment schedules&quot; or whatever they're calling the feature now.</p><p>That said, based on what I read, I don't think they offer what I need. It looks like the paid plans will let you connect to multiple calendars so that you can give someone a link with a more comprehensive view of your availability. But I need something for internal people who have direct access to my calendar.</p><h3>Fantastical</h3><p>This is just plain a gorgeous app, if you'll forgive an awkwardly-worded almost-oxymoron. I keep threatening to use it every couple of years but can't really justify it. The major selling points are syncing across multiple devices, the natural language parsing, the UX, and integration with a lot of other productivity tools I don't use. Seriously, you should check this app out...</p><p>...except if all you need is to sync events across multiple calendars. I'm still in the midst of testing the app but the process to ensure an event appears as a block of unavailable time on multiple calendars seems to be to duplicate it. Which, to be fair, they make <em>really</em> easy. And if you keep the details the same in all the clones, the unified calendar view removes the duplicates. But that doesn't meet criteria #2 for me above. And if you change the name, it shows as a separate event which is a bit clumsy. For this feature specifically, it's not much better than what I can achieve in Google Calendar natively. But still, give it a whirl.</p><h3>Final verdict: OneCal (CalendarBridge runner up)</h3><p>That leaves two options: OneCal and CalendarBridge. Both do the same thing though their respective marketing teams might squabble over technicalities. Their primary purpose is to sync multiple calendars and avoid double-bookings. Perfect.</p><p>Both services sync one calendar to another by cloning events between them and monitoring the source calendar for new events. Both give you control over what information you want to copy over and have similar options like excluding events of a given colour or excluding events where you're marked free (useful if you track birthdays in your calendar). Once syncing was set up, my calendars looked pretty much exactly how they did when I was doing it manually.</p><p>Neither service has a mobile app from what I can tell which is a shame, especially in the case of OneCal because unlike CalendarBridge, they offer a unified calendar view of all your calendars with the option to hide the cloned events. This is a nice touch and while the view is mobile friendly, it's missing some features, like a three-day view or the ability to put a widget on your phone.</p><p>Pricing is virtually identical on both services in that the one I would need runs about $100/year to sync three calendars. OneCal has the ability to do two-way syncs between calendars or to broadcast a one-way sync from one calendar to multiple other calendars, something that I don't <em>think</em> CalendarBridge does. In CalendarBridge, a two-way sync is done with two one-way syncs and their pricing reflects that. The basic plan for both essentially covers two calendars so I'd be looking at premium for both.</p><p>Setting up the syncs seemed nicer in OneCal to me though I can't 100% say why. Both essentially walk you through a three-ish-step wizard to do it. Maybe OneCal worded things better or laid things out more intuitively for me. I found myself wondering if I was doing it right a couple of times in CalendarBridge. OneCal also felt zippier in general.</p><p>Both apps give you booking links (a la Calendly) which is nice if you need that. OneCal apparently integrates with Zoom to automatically add a meeting link to appointments booked online but I didn't test that out since I don't need it. Neither requires a credit card to sign up which is an underrated feature these days.</p><p>A couple of little UX things. First, OneCal has another underrated feature: a big ol' Delete My Account button at the bottom of the settings page. This is great for someone who wants to test things out, decide it's not for them, and move on. I'm not planning to use CalendarBridge and I've cancelled my free trial but the account remains. There's no obvious way to delete it so I guess I'm a CalendarBridge user indefinitely now. I suppose that's good for them to pump up their user numbers in case someone wants to buy them out but doesn't do me any good.</p><p>Second thing is CalendarBridge's login mechanism. To log in, I enter my email address, they email me a code, I enter the code. It's basically the standard 2FA mechanism from fifteen years ago. There's no password, no Google/Apple/Facebook/etc option. It's kinda weird. Like the developers have a personal agenda against traditional authentication and this is their manifesto.</p><p>So both apps do what I want and cost roughly the same but I give the edge to OneCal for their usability and their unified Calendar view. The price point is high enough that I'll give it a few days before pulling the trigger so I can decide if I want this more than I want to watch seasons 2 of Silo and Severance.</p><p>The alternative is to set up an auto-decline automation for all meetings which I haven't fully taken off the table yet.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;For reasons that I really need to investigate, I&#39;m apparently a busy fellow. Such that I now maintain a rather ungainly number of calendars. Three to be exact. That&#39;s one personal calendar and one for each of two clients.&lt;/p&gt;
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Setting Container Cors Rules in Azure</title>
    <link href="https://westerndevs.com/_/set-blob-container-cors/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/set-blob-container-cors/</id>
    <published>2024-05-24T04:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.827Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>This week I'm busy upgrading some legacy code to the latest version of the Azure SDKs. This code is so old it was using packages like <code>WindowsAzure.Storage</code>. Over the years this library has evolved significantly and is now part of the <code>Azure.Storage.Blobs</code> package. What this code I was updating was doing was setting the CORS rules on a blob container. These days I think I would solve this problem using Terraform and set the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account" target="_blank" rel="noopener">blob properties</a> directly on the container. But since I was already in the code I figured I would just update it there.</p><p>So what we want is to allow anybody to link into these and download them with a GET</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Azure.Storage.Blobs;</span><br><span class="line"><span class="keyword">using</span> Azure.Storage.Blobs.Models;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">var</span> bsp = <span class="keyword">new</span> BlobServiceProperties &#123; HourMetrics = <span class="literal">null</span>, MinuteMetrics = <span class="literal">null</span>, Logging = <span class="literal">null</span> &#125;;</span><br><span class="line">bsp.Cors.Add(<span class="keyword">new</span> BlobCorsRule</span><br><span class="line">&#123;</span><br><span class="line">    AllowedHeaders =  <span class="string">"*"</span>,</span><br><span class="line">    AllowedMethods = <span class="string">"GET"</span>,</span><br><span class="line">    AllowedOrigins = <span class="string">"*"</span>,</span><br><span class="line">    ExposedHeaders = <span class="string">"*"</span>,</span><br><span class="line">    MaxAgeInSeconds = <span class="number">60</span> * <span class="number">30</span> <span class="comment">// 30 minutes</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// from a nifty little T4 template</span></span><br><span class="line"><span class="keyword">var</span> connectionString = <span class="keyword">new</span> ConnectionStrings().StorageConnectionString;</span><br><span class="line">BlobServiceClient blobServiceClient = <span class="keyword">new</span> BlobServiceClient(connectionString);</span><br><span class="line">blobServiceClient.SetProperties(bsp);</span><br></pre></td></tr></table></figure><p>Again, in hindsight I feel like these rules are overly permissive and I would probably want to lock them down a bit more.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;This week I&#39;m busy upgrading some legacy code to the latest version of the Azure SDKs. This code is so old it was using packages like &lt;co
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title type="html">Docker COPY not Finding Files</title>
    <link href="https://westerndevs.com/_/cannot-copy/" rel="alternate" type="text/html"/>
    <id>https://westerndevs.com/_/cannot-copy/</id>
    <published>2023-11-23T05:00:00.000Z</published>
    <updated>2026-03-22T17:42:31.820Z</updated>
	<author>
	
	  
	  <name>Simon Timms</name>
	  <email>stimms@gmail.com</email>
	
	  <uri>https://westerndevs.com</uri>
	</author>
    
    <content type="html"><![CDATA[<p>My dad once told me that there are no such things a problems just solutions waiting to be applied. I don't know what book he'd just read or course he'd just been on to spout such nonsense but I've never forgotten it.</p><p>Today my not problem was running a docker build wasn't copying the files I was expecting it to. In particular I had a <code>themes</code> directory which was not ending up in the image and in fact the build was failing with something like</p><figure class="highlight subunit"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ERROR: </span>failed to solve: failed to compute cache key: failed to calculate checksum of ref b1f3faa4-fdeb<span class="string">-41</span>ed-b016-fac3862d370a::pjh3jwhj2huqmcgigjh9udlh2: "/themes": not found</span><br></pre></td></tr></table></figure><p>I was really confused because <code>themes</code> absolutly did exist on disk. It was as if it wasn't being added to the build context. In fact it wasn't being added and, as it turns out, this was because my .dockerignore file contained</p><figure class="highlight gams"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">**</span></span><br></pre></td></tr></table></figure><p>Which ignores everything from the local directory. That seemed a bit extreme so I changed it to</p><figure class="highlight asciidoc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">** </span></span><br><span class="line">!themes</span><br></pre></td></tr></table></figure><p>With this in place the build worked as expected.</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;My dad once told me that there are no such things a problems just solutions waiting to be applied. I don&#39;t know what book he&#39;d just read 
    
    </summary>
    
    
  </entry>
  
</feed>
