<?xml version="1.0" encoding="utf-8" standalone="no"?><feed xmlns="http://www.w3.org/2005/Atom"><subtitle>A blog on software development, gadgets, security and some more</subtitle>

  <title>Volkan Paksoy's Blog</title>
  <link href="https://volkanpaksoy.com/atom.xml" rel="self"/>
  <link href="https://volkanpaksoy.com/"/>
  <updated>2025-10-12T16:34:59+00:00</updated>
  <id>https://volkanpaksoy.com/</id>
  <author>
    <name><![CDATA[Volkan Paksoy]]></name>
    
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

  
  
  <entry>
    <title type="html"><![CDATA[How to create an RSS Podcast Feed from local files with C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/12/How-to-create-an-RSS-Podcast-Feed-from-local-files-with-C/"/>
    <updated>2025-10-12T11:30:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/12/How-to-create-an-RSS-Podcast-Feed-from-local-files-with-C#</id>
    <content type="html"><![CDATA[<p>I have a few old podcast series that are not available online anymore. Every now and then, I enjoy listening to an old episode. I keep them in a hard drive connected to a Raspberry Pi, which serves them over the local network. Then I connect to this share on my mobile device and consume the content. It works fine most of the time. The problem is it’s hard to remember the last episode I listened to since everything is treated as files with no history. I thought I could leverage my podcast app on my phone if I served these files via an RSS feed. This tutorial will show how to generate the RSS feed using C# and serve the content over your local network. If this sounds like a problem you would like to solve, let’s get started.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>To follow this tutorial, you need the following software installed:</p>

<ul>
  <li>
    <p><a href="https://docs.docker.com/engine/install/">Docker engine</a></p>
  </li>
  <li>
    <p><a href="https://dotnet.microsoft.com/en-us/download">.NET SDK</a></p>
  </li>
</ul>

<h2 id="set-up-the-web-server">Set up the Web Server</h2>

<p>Since you will host only static files (RSS feed which is an XML file and some audio files), an Nginx instance running in a Docker container is sufficient.</p>

<p>First, designate a local directory on your computer to put the files. In the tutorial, I will use the following path: <code class="language-plaintext highlighter-rouge">~/Temp/webroot</code>. Modify this to match your environment.</p>

<p>Run the following command to start your podcast server:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--name</span> podcast-server <span class="nt">-p</span> 9876:80 <span class="nt">-v</span> ~/Temp/webroot:/usr/share/nginx/html:ro <span class="nt">-d</span> nginx
</code></pre></div></div>

<p>The command above;</p>

<ul>
  <li>
    <p>Maps port 9876 on your machine to the internal port 80 in the container. (-p 9876:80)</p>
  </li>
  <li>
    <p>Runs the container in the background as a daemon (-d)</p>
  </li>
  <li>
    <p>Mounts the <code class="language-plaintext highlighter-rouge">~/Temp/webroot</code> directory on your machine to the <code class="language-plaintext highlighter-rouge">/usr/share/nginx/html</code> directory on the container. This means when Nginx serves content in its HTML directory, it looks into the ~/Temp/webroot directory. This way, you can manage the content without going into the container’s file system.</p>
  </li>
</ul>

<p>If you open a browser tab and go to <em>http://localhost:9876</em>, you should get a <em>403 Forbidden</em> response from the web server. This is expected because you haven’t put any files to serve yet.</p>

<h2 id="set-up-content">Set up Content</h2>

<p>To test the application, let’s start with a small amount of content. Go to file-examples.com and download 2 MP3 files and rename them as “episode1.mp3” and “episode2.mp3”. So the root of your web server should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/local-rss/contents.png" alt="Contents of the root directory showing webroot directory, content directory under it and two files named episode1.mp3 and episode2.mp3" /></p>

<p>Now, if you request one of these files in your browser (e.g. <em>http://localhost:9876/content/episode1.mp3</em>), you should be able to hear the MP3 playing. In the next section, you will implement the application that creates the RSS feed so that you can consume the feed via your podcatcher too.</p>

<h2 id="implement-the-rss-generator">Implement the RSS Generator</h2>

<p>An RSS feed is simply an XML file. It includes the name and description of the show, as well as the titles and URLs of the individual episodes. If this feed were meant to be published publicly, you would add more details such as icons, categories, iTunes-specific tags etc., but for personal consumption, the following format is sufficient:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;rss</span> <span class="na">version=</span><span class="s">"2.0"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;channel&gt;</span>
    <span class="nt">&lt;title&gt;</span>{Show Title}<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;description&gt;</span>{Show Description}<span class="nt">&lt;/description&gt;</span>
    <span class="nt">&lt;category&gt;</span>{Category}<span class="nt">&lt;/category&gt;</span>
    <span class="nt">&lt;item&gt;</span>
      <span class="nt">&lt;title&gt;</span>{Episode Title}<span class="nt">&lt;/title&gt;</span>
      <span class="nt">&lt;description&gt;</span>{Episode Description}<span class="nt">&lt;/description&gt;</span>
      <span class="nt">&lt;enclosure</span> <span class="na">url=</span><span class="s">"{Episode URL}"</span> <span class="na">type=</span><span class="s">"audio/mpeg"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/item&gt;</span>
  <span class="nt">&lt;/channel&gt;</span>
<span class="nt">&lt;/rss&gt;</span>
</code></pre></div></div>

<p>Create a new dotnet console application:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet new console <span class="nt">--name</span> RssFeedGenerator <span class="nt">--output</span> <span class="nb">.</span>
</code></pre></div></div>

<p>Open the project with your IDE.</p>

<p>To serialize to the XML above, you will need a data structure. There are tools that can generate C# classes from a sample XML, so you don’t have to manually create the classes yourself. I generated the following at Xml2Charp. The result looks like this (after a bit of formatting):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* 
 Licensed under the Apache License, Version 2.0
 
 http://www.apache.org/licenses/LICENSE-2.0
 */</span>

<span class="k">using</span> <span class="nn">System.Xml.Serialization</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">RssFeedGenerator</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">XmlRoot</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"enclosure"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">Enclosure</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="nf">XmlAttribute</span><span class="p">(</span><span class="n">AttributeName</span><span class="p">=</span><span class="s">"url"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Url</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlAttribute</span><span class="p">(</span><span class="n">AttributeName</span><span class="p">=</span><span class="s">"type"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Type</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="nf">XmlRoot</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"item"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">Item</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"title"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Title</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"description"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Description</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"enclosure"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">Enclosure</span> <span class="n">Enclosure</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="nf">XmlRoot</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"channel"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">Channel</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"title"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Title</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"description"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Description</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"category"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Category</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"item"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;</span> <span class="n">Item</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="nf">XmlRoot</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"rss"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">Rss</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="nf">XmlElement</span><span class="p">(</span><span class="n">ElementName</span><span class="p">=</span><span class="s">"channel"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">Channel</span> <span class="n">Channel</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
        <span class="p">[</span><span class="nf">XmlAttribute</span><span class="p">(</span><span class="n">AttributeName</span><span class="p">=</span><span class="s">"version"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span> <span class="n">Version</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Create a file called <em>Rss.cs</em> in your project and paste the above code. The biggest change I made to the auto-generated version is to replace the single <strong>Item</strong> property in the Channel class with a **List<Item>**, as you will need multiple entries per podcast.</Item></p>

<p>Now, update <em>Program.cs</em> with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Xml.Serialization</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">RssFeedGenerator</span><span class="p">;</span>

<span class="kt">string</span> <span class="n">serverIPAddress</span> <span class="p">=</span> <span class="s">"192.168.1.20"</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">serverPort</span> <span class="p">=</span> <span class="m">9876</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">contentFullPath</span> <span class="p">=</span> <span class="s">"/Temp/webroot/content"</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">feedFullPath</span> <span class="p">=</span> <span class="s">"/Temp/webroot/feed.rss"</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">audioRootUrl</span> <span class="p">=</span> <span class="s">$"http://</span><span class="p">{</span><span class="n">serverIPAddress</span><span class="p">}</span><span class="s">:</span><span class="p">{</span><span class="n">serverPort</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">rss</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Rss</span>
<span class="p">{</span>
    <span class="n">Version</span> <span class="p">=</span> <span class="s">"2.0"</span><span class="p">,</span>
    <span class="n">Channel</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Channel</span>
    <span class="p">{</span>
        <span class="n">Title</span> <span class="p">=</span> <span class="s">"[Local] Test Podcast"</span><span class="p">,</span>
        <span class="n">Description</span> <span class="p">=</span> <span class="s">"Testing generating RSS feed from local MP3 files"</span><span class="p">,</span>
        <span class="n">Category</span> <span class="p">=</span> <span class="s">"test"</span><span class="p">,</span>
        <span class="n">Item</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Item</span><span class="p">&gt;()</span>
    <span class="p">}</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">allMp3s</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">DirectoryInfo</span><span class="p">(</span><span class="n">contentFullPath</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="s">"*.mp3"</span><span class="p">,</span> <span class="n">SearchOption</span><span class="p">.</span><span class="n">AllDirectories</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">OrderBy</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="n">Name</span><span class="p">);</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">mp3</span> <span class="k">in</span> <span class="n">allMp3s</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">rss</span><span class="p">.</span><span class="n">Channel</span><span class="p">.</span><span class="n">Item</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">Item</span>
    <span class="p">{</span>
        <span class="n">Description</span> <span class="p">=</span> <span class="n">mp3</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
        <span class="n">Title</span> <span class="p">=</span> <span class="n">mp3</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
        <span class="n">Enclosure</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Enclosure</span>
        <span class="p">{</span>
            <span class="n">Url</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">audioRootUrl</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="n">mp3</span><span class="p">.</span><span class="n">Directory</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="n">mp3</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">Type</span> <span class="p">=</span> <span class="s">"audio/mpeg"</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">}</span>

<span class="kt">var</span> <span class="n">serializer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">XmlSerializer</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">Rss</span><span class="p">));</span>
<span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">writer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StreamWriter</span><span class="p">(</span><span class="n">feedFullPath</span><span class="p">))</span>
<span class="p">{</span>
    <span class="n">serializer</span><span class="p">.</span><span class="nf">Serialize</span><span class="p">(</span><span class="n">writer</span><span class="p">,</span> <span class="n">rss</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Make sure to update the settings at the top of the file before you run the application.</p>

<p>Run the application by running the following command in the terminal:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run
</code></pre></div></div>

<p>Under your web server’s root directory, you should see the <em>feed.rss</em> file that looks like this:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;rss</span> <span class="na">xmlns:xsi=</span><span class="s">"http://www.w3.org/2001/XMLSchema-instance"</span> <span class="na">xmlns:xsd=</span><span class="s">"http://www.w3.org/2001/XMLSchema"</span> <span class="na">version=</span><span class="s">"2.0"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;channel&gt;</span>
    <span class="nt">&lt;title&gt;</span>[Local] Test Podcast<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;description&gt;</span>Testing generating RSS feed from local MP3 files<span class="nt">&lt;/description&gt;</span>
    <span class="nt">&lt;category&gt;</span>test<span class="nt">&lt;/category&gt;</span>
    <span class="nt">&lt;item&gt;</span>
      <span class="nt">&lt;title&gt;</span>episode1.mp3<span class="nt">&lt;/title&gt;</span>
      <span class="nt">&lt;description&gt;</span>episode1.mp3<span class="nt">&lt;/description&gt;</span>
      <span class="nt">&lt;enclosure</span> <span class="na">url=</span><span class="s">"http://192.168.1.20:9876/content/episode1.mp3"</span> <span class="na">type=</span><span class="s">"audio/mpeg"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item&gt;</span>
      <span class="nt">&lt;title&gt;</span>episode2.mp3<span class="nt">&lt;/title&gt;</span>
      <span class="nt">&lt;description&gt;</span>episode2.mp3<span class="nt">&lt;/description&gt;</span>
      <span class="nt">&lt;enclosure</span> <span class="na">url=</span><span class="s">"http://192.168.1.20:9876/content/episode2.mp3"</span> <span class="na">type=</span><span class="s">"audio/mpeg"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/item&gt;</span>
  <span class="nt">&lt;/channel&gt;</span>
<span class="nt">&lt;/rss&gt;</span>
</code></pre></div></div>

<p>At this point, you have your RSS feed and all your content under your web server. The final step is to consume this content using your podcast app.</p>

<h2 id="add-your-podcast-to-your-podcast-app">Add Your Podcast to your Podcast App</h2>

<p>My podcast app of choice is Overcast. I’m very happy with it and have been using it for many years. The following might be a limitation of Overcast, but apparently, it cannot access feeds over the local network. So to tackle this issue, I used <a href="https://ngrok.com/">NGrok</a> to tunnel web traffic to my local web server.</p>

<p>If you are having the same issue, install ngrok and run the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ngrok http 9876
</code></pre></div></div>

<p>This should generate a public URL and route traffic to your local server. In my case, it looks like this:</p>

<p><img src="/images/vpblogimg/2025/10/local-rss/ngrok.png" alt="ngrok output showing the traffic is routed to localhost:9876" /></p>

<p>Now you can access your feed via the <strong>{public URL}/feed.rss</strong>.</p>

<p>In Overcast, I add the URL by clicking the <strong>+ button on the</strong> top right and then clicking <strong>Add URL</strong> link.</p>

<p><img src="/images/vpblogimg/2025/10/local-rss/adding-podcast.png" alt="" /></p>

<p>After it fetches and parses the RSS feed, it should appear in your podcast list.</p>

<p>The only thing that’s left is to open the podcast and play the episodes:</p>

<p><img src="/images/vpblogimg/2025/10/local-rss/podcast.png" alt="" /></p>

<p>Even though Overcast cannot fetch the RSS feed over the local network, it can still play the episodes locally. You can stop ngrok and continue to play the episodes. The downside of this approach is if this podcast is an active one and you want to refresh the feed, you will need to delete and re-add the feed because the ngrok address will have changed the next time you try to get the updates.</p>

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

<p>I love hosting my own content in my own network. Even though having some offline podcasts stored locally is not a common use case, I had to implement my solution to solve the issue and decided to make it public and share it in case anyone else would like to use the same approach or build on it and make it better. The final source code can be found in my <a href="https://github.com/Dev-Power/rss-podcast-feed-from-local-files">GitHub repository</a>.</p>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[How to manage Google Sheets with C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/11/How-to-manage-Google-Sheets-with-C/"/>
    <updated>2025-10-11T09:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/11/How-to-manage-Google-Sheets-with-C#</id>
    <content type="html"><![CDATA[<p>Spreadsheets are quite powerful tools. They can also act as a simple database with an intuitive UI. You can use the spreadsheet as a temporary database and GSheets API as a CRUD API while you prototype your own application. This article will teach you how to manage Google Sheets using your C# application.</p>

<h2 id="prepare-your-spreadsheet">Prepare your spreadsheet</h2>

<p>The sample application will be a simple shopping list manager console application. It will insert/update/delete items in the shopping list and keep a log of each event in another sheet.</p>

<p>You will need a Google Account to follow along. You can sign up for free <a href="https://accounts.google.com/signup">here</a> if you don’t have one.</p>

<p>While logged in to your Google account, open your <a href="https://drive.google.com/">Google Drive</a>.</p>

<p>Click the <strong>New button</strong> on the top left, then click <strong>Google Sheets</strong> in the menu.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/new-spreadsheet.png" alt="New menu shows available google services such as Google Docs, Google Sheets, Google Slides, Google Forms and a more link at the end" /></p>

<p>Name your spreadsheet <strong>Shopping List</strong>.</p>

<p>At the bottom of the screen, right-click on the sheet name (Sheet1) and rename it to <strong>Cart.</strong></p>

<p>Set the value of A1 to Item and B1 to Quantity. You can style the cells the way you like.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cart-sheet-headers.png" alt="Spreadsheet showing the title Shopping List, the value of A1 cell as &quot;Item&quot; and the value of B1 cell as &quot;Quantity&quot;" /></p>

<p>Now that the “database” is ready, move on to the next section to set up the permissions.</p>

<h2 id="generate-credentials">Generate Credentials</h2>

<p>Go to <a href="https://console.cloud.google.com/">Google API Console</a>.</p>

<p>Click <strong>APIs &amp; Services</strong> and <strong>Library</strong></p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-01.png" alt="" /></p>

<p>Search <strong>sheets</strong> and click <strong>Google Sheets API</strong> in the search results.</p>

<p>In the API settings, click the <strong>Enable</strong> button.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-02.png" alt="" /></p>

<p>Click the <strong>Create Credentials</strong> button.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-03.png" alt="" /></p>

<p>In the credential type, select <strong>Application Data</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-04.png" alt="" /></p>

<p>It will ask if you’re planning to use it with Kubernetes etc. Select <strong>“No, I’m not using them”</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-05.png" alt="" /></p>

<p>Click <strong>Next</strong>.</p>

<p>In the service account settings, enter <strong>shopping-list-service-account</strong> as the service account name and click <strong>Create and Continue</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-06.png" alt="" /></p>

<p>Click <strong>Continue</strong> to proceed.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-07.png" alt="" /></p>

<p>Click <strong>Done</strong> to finish the account creation.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-08.png" alt="" /></p>

<p>Click <strong>Credentials</strong> and the <strong>new service account</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-09.png" alt="" /></p>

<p>Click <strong>Keys</strong> and <strong>Add Key</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-10.png" alt="" /></p>

<p>Click <strong>Create new key</strong>, keep <strong>JSON</strong> as the selected option and click <strong>Create</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cred-11.png" alt="" /></p>

<p>A download should automatically start with your credentials. You will need this file later on.</p>

<p>Finally, you need to share your spreadsheet with the new service account. Go to your sheet and click the <strong>Share</strong> button.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/share-01.png" alt="" /></p>

<p>Copy th<strong>e email address generated for your service account</strong> and paste it into the <strong>Add people and groups</strong> textbox.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/share-02.png" alt="" /></p>

<p>Click the <strong>Share</strong> button and close the dialog.</p>

<p>Now you can proceed to create the application.</p>

<h2 id="implement-the-sample-application">Implement the sample application</h2>

<p>In a terminal, navigate to the root directory that you want to create the project in and run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet new console <span class="nt">--name</span> ShoppingList <span class="nt">--output</span> <span class="nb">.</span>
</code></pre></div></div>

<p>Add the necessary Google Sheets SDK, via NuGet:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Google.Apis.Sheets.v4
</code></pre></div></div>

<p>Copy the downloaded credentials to the project folder and open the project with your IDE.</p>

<p>To access your spreadsheet fro myour program, you will need the id of the sheet which you can find in the URL:</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/sheet-id.png" alt="" /></p>

<p>First things first: Confirm your access to your spreadsheet. To achieve that, replace the code in <em>Program.cs</em> with the following code:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Google.Apis.Auth.OAuth2</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.Services</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.Sheets.v4</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">spreadsheetId</span> <span class="p">=</span> <span class="s">"{ YOUR SPREADSHEET'S ID }"</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">range</span> <span class="p">=</span> <span class="s">"Cart!A1:B"</span><span class="p">;</span>

<span class="n">GoogleCredential</span> <span class="n">credential</span><span class="p">;</span>
<span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">stream</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FileStream</span><span class="p">(</span><span class="s">"credentials.json"</span><span class="p">,</span> <span class="n">FileMode</span><span class="p">.</span><span class="n">Open</span><span class="p">,</span> <span class="n">FileAccess</span><span class="p">.</span><span class="n">Read</span><span class="p">))</span>
<span class="p">{</span>
    <span class="n">credential</span> <span class="p">=</span> <span class="n">GoogleCredential</span><span class="p">.</span><span class="nf">FromStream</span><span class="p">(</span><span class="n">stream</span><span class="p">).</span><span class="nf">CreateScoped</span><span class="p">(</span><span class="k">new</span> <span class="kt">string</span><span class="p">[]</span> <span class="p">{</span> <span class="n">SheetsService</span><span class="p">.</span><span class="n">Scope</span><span class="p">.</span><span class="n">Spreadsheets</span> <span class="p">});</span>
<span class="p">}</span>

<span class="kt">var</span> <span class="n">sheetsService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SheetsService</span><span class="p">(</span><span class="k">new</span> <span class="n">BaseClientService</span><span class="p">.</span><span class="nf">Initializer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">HttpClientInitializer</span> <span class="p">=</span> <span class="n">credential</span><span class="p">,</span>
    <span class="n">ApplicationName</span> <span class="p">=</span> <span class="s">"ShoppingList"</span>
<span class="p">});</span>

<span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">GetRequest</span> <span class="n">getRequest</span> <span class="p">=</span> <span class="n">sheetsService</span><span class="p">.</span><span class="n">Spreadsheets</span><span class="p">.</span><span class="n">Values</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">spreadsheetId</span><span class="p">,</span> <span class="n">range</span><span class="p">);</span>
       
<span class="kt">var</span> <span class="n">getResponse</span> <span class="p">=</span> <span class="k">await</span> <span class="n">getRequest</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
<span class="n">IList</span><span class="p">&lt;</span><span class="n">IList</span><span class="p">&lt;</span><span class="n">Object</span><span class="p">&gt;&gt;</span> <span class="n">values</span> <span class="p">=</span> <span class="n">getResponse</span><span class="p">.</span><span class="n">Values</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">values</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">&amp;&amp;</span> <span class="n">values</span><span class="p">.</span><span class="n">Count</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">row</span> <span class="k">in</span> <span class="n">values</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="m">0</span><span class="p">]);</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="m">1</span><span class="p">]);</span>
    <span class="p">}</span>
<span class="p">}</span>

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

<p>Replace <code class="language-plaintext highlighter-rouge">{ YOUR SPREADSHEET'S ID }</code> with the actual value and run the application. The column titles (Item and Quantity) should be displayed in your terminal.</p>

<p>You can add some items to your shopping list and test again.</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cart-with-items-01.png" alt="" /></p>

<p>Before going further, refactor the code. You will encapsulate all GSheets-related functions in a called <em>GSheetsHelper.cs</em>. Create the file and update the code as below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Sheets.v4;

namespace ShoppingList;

public class GSheetsHelper
{
    private SheetsService _sheetsService;
    private string _spreadsheetId = "{ YOUR SPREADSHEET'S ID }";
    private string _range = "Cart!A2:B";
    
    public GSheetsHelper()
    {
        GoogleCredential credential;
        using (var stream = new FileStream("credentials.json", FileMode.Open, FileAccess.Read))
        {
            credential = GoogleCredential.FromStream(stream).CreateScoped(SheetsService.Scope.Spreadsheets);
        }

        _sheetsService = new SheetsService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credential,
            ApplicationName = "ShoppingList"
        });        
    }

    public async Task PrintCartItems()
    {
        SpreadsheetsResource.ValuesResource.GetRequest getRequest = _sheetsService.Spreadsheets.Values.Get(_spreadsheetId, _range);
       
        var getResponse = await getRequest.ExecuteAsync();
        IList&lt;IList&lt;Object&gt;&gt; values = getResponse.Values;
        if (values != null &amp;&amp; values.Count &gt; 0)
        {
            Console.WriteLine("Item\t\t\tQuantity");
            
            foreach (var row in values)
            {
                Console.WriteLine($"{row[0]}\t\t\t{row[1]}");
            }
        }
    }
}
</code></pre></div></div>

<p>Run the application now and you should see your items in your cart printed in your terminal:</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/output-01.png" alt="" /></p>

<h3 id="convert-the-application-to-a-cli">Convert the application to a CLI</h3>

<p>You now have the functionality to get your cart, but it will do the same thing every time. To add more commands, convert your application into a CLI. First, add the <strong>System.CommandLine</strong> package to your project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package System.CommandLine <span class="nt">--version</span> 2.0.0-beta4.22272.1
</code></pre></div></div>

<p>If you are interested in developing your own CLIs with C#, make sure to check out these articles as well: <a href="https://https://volkanpaksoy.com/archive/2025/10/02/develop-your-own-cli-with-csharp">Develop your own CLI with C#</a> and <a href="https://https://volkanpaksoy.com/archive/2025/10/07/how-to-develop-an-interactive-cli-with-csharp">How to Develop an Interactive CLI with C# and dotnet 6.0</a></p>

<p>Create your first command, the same functionality as above, to print the cart items. Replace <em>Program.cs</em> with the following code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>using System.CommandLine;
using ShoppingList;

var rootCommand = new RootCommand("Manage your shopping cart");

var printCartCommand = new Command("print", "Print the items in the cart");
printCartCommand.SetHandler(async () =&gt;
{
    var gsheetsHelper = new GSheetsHelper();
    try
    {
        await gsheetsHelper.PrintCartItems();
    }
    catch (Exception e)
    {
        Console.Error.WriteLine(e.Message);
    }
});
rootCommand.AddCommand(printCartCommand);

return rootCommand.InvokeAsync(args).Result;
</code></pre></div></div>

<p>Run the application with <code class="language-plaintext highlighter-rouge">dotnet run</code> command and you should now see an info message explaining the supported commands:</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/output-02.png" alt="" /></p>

<p>You now have to specify the command name to print the items. Run it as <code class="language-plaintext highlighter-rouge">dotnet run print</code> to pass the command name and you should see the contents of your cart again.</p>

<h3 id="add-items-yo-your-cart">Add Items yo your Cart</h3>

<p>The program can now be enhanced simply by adding more commands.</p>

<p>Add the following function to <em>GSheetsHelper.cs</em>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">AddItem</span><span class="p">(</span><span class="kt">string</span> <span class="n">itemName</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">quantity</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">valuesToInsert</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="n">itemName</span><span class="p">,</span>
        <span class="n">quantity</span>
    <span class="p">};</span>

    <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">AppendRequest</span><span class="p">.</span><span class="n">ValueInputOptionEnum</span> <span class="n">valueInputOption</span> <span class="p">=</span> <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">AppendRequest</span><span class="p">.</span><span class="n">ValueInputOptionEnum</span><span class="p">.</span><span class="n">RAW</span><span class="p">;</span>
    <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">AppendRequest</span><span class="p">.</span><span class="n">InsertDataOptionEnum</span> <span class="n">insertDataOption</span> <span class="p">=</span> <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">AppendRequest</span><span class="p">.</span><span class="n">InsertDataOptionEnum</span><span class="p">.</span><span class="n">INSERTROWS</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">requestBody</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ValueRange</span><span class="p">();</span>
    <span class="n">requestBody</span><span class="p">.</span><span class="n">Values</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">IList</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;&gt;();</span>
    <span class="n">requestBody</span><span class="p">.</span><span class="n">Values</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">valuesToInsert</span><span class="p">);</span>

    <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">AppendRequest</span> <span class="n">appendRequest</span> <span class="p">=</span> <span class="n">_sheetsService</span><span class="p">.</span><span class="n">Spreadsheets</span><span class="p">.</span><span class="n">Values</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="n">requestBody</span><span class="p">,</span> <span class="n">_spreadsheetId</span><span class="p">,</span> <span class="n">_range</span><span class="p">);</span>
    <span class="n">appendRequest</span><span class="p">.</span><span class="n">ValueInputOption</span> <span class="p">=</span> <span class="n">valueInputOption</span><span class="p">;</span>
    <span class="n">appendRequest</span><span class="p">.</span><span class="n">InsertDataOption</span> <span class="p">=</span> <span class="n">insertDataOption</span><span class="p">;</span>
    
    <span class="k">await</span> <span class="n">appendRequest</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To invoke this method, add the command to <em>Program.cs</em>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var itemNameOption = new Option&lt;string&gt;(
    new[] {"--item-name", "-n"},
    description: "The name of the item"
);
itemNameOption.IsRequired = true;

var quantityOption = new Option&lt;decimal&gt;(
    new[] {"--quantity", "-q"},
    description: "The quantity of the item"
);
quantityOption.IsRequired = true;

var addItemCommand = new Command("add", "Add an item to the cart")
{
    itemNameOption,
    quantityOption
};
addItemCommand.SetHandler(async (itemName, quantity) =&gt;
{
    var gsheetsHelper = new GSheetsHelper();
    try
    {
        await gsheetsHelper.AddItem(itemName, quantity);
    }
    catch (Exception e)
    {
        Console.Error.WriteLine(e.Message);
    }
}, itemNameOption, quantityOption);
rootCommand.AddCommand(addItemCommand);
</code></pre></div></div>

<p>Run the command with some item like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run add <span class="nt">--item-name</span> Steaks <span class="nt">--quantity</span> 2
</code></pre></div></div>

<p>You should see the new item in your cart:</p>

<p><img src="/images/vpblogimg/2025/10/manage-gsheets-with-csharp/cart-after-insert.png" alt="" /></p>

<h3 id="remove-items-from-your-cart">Remove Items From Your Cart</h3>

<p>To have remove functionality, add the following method to GSheetsHelper:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">RemoveItem</span><span class="p">(</span><span class="kt">string</span> <span class="n">itemName</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">GetRequest</span> <span class="n">getRequest</span> <span class="p">=</span> <span class="n">_sheetsService</span><span class="p">.</span><span class="n">Spreadsheets</span><span class="p">.</span><span class="n">Values</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="n">_spreadsheetId</span><span class="p">,</span> <span class="n">_range</span><span class="p">);</span>
    
    <span class="kt">var</span> <span class="n">getResponse</span> <span class="p">=</span> <span class="k">await</span> <span class="n">getRequest</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
    <span class="n">IList</span><span class="p">&lt;</span><span class="n">IList</span><span class="p">&lt;</span><span class="n">Object</span><span class="p">&gt;&gt;</span> <span class="n">values</span> <span class="p">=</span> <span class="n">getResponse</span><span class="p">.</span><span class="n">Values</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">values</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">&amp;&amp;</span> <span class="n">values</span><span class="p">.</span><span class="n">Count</span> <span class="p">&gt;</span> <span class="m">0</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">values</span><span class="p">.</span><span class="n">Count</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
        <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">values</span><span class="p">[</span><span class="n">i</span><span class="p">][</span><span class="m">0</span><span class="p">].</span><span class="nf">ToString</span><span class="p">()</span> <span class="p">==</span> <span class="n">itemName</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Request</span>
                <span class="p">{</span>
                    <span class="n">DeleteDimension</span> <span class="p">=</span> <span class="k">new</span> <span class="n">DeleteDimensionRequest</span>
                    <span class="p">{</span>
                        <span class="n">Range</span> <span class="p">=</span> <span class="k">new</span> <span class="n">DimensionRange</span>
                        <span class="p">{</span>
                            <span class="n">SheetId</span> <span class="p">=</span> <span class="m">0</span><span class="p">,</span>
                            <span class="n">Dimension</span> <span class="p">=</span> <span class="s">"ROWS"</span><span class="p">,</span>
                            <span class="n">StartIndex</span> <span class="p">=</span> <span class="n">i</span> <span class="p">+</span> <span class="m">1</span><span class="p">,</span>
                            <span class="n">EndIndex</span> <span class="p">=</span> <span class="n">i</span> <span class="p">+</span> <span class="m">2</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">};</span>
                
                <span class="kt">var</span> <span class="n">deleteRequest</span> <span class="p">=</span> <span class="k">new</span> <span class="n">BatchUpdateSpreadsheetRequest</span> <span class="p">{</span><span class="n">Requests</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Request</span><span class="p">&gt;</span> <span class="p">{</span><span class="n">request</span><span class="p">}};</span>
                <span class="kt">var</span> <span class="n">batchUpdateRequest</span> <span class="p">=</span> <span class="k">new</span> <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="nf">BatchUpdateRequest</span><span class="p">(</span><span class="n">_sheetsService</span><span class="p">,</span> <span class="n">deleteRequest</span><span class="p">,</span> <span class="n">_spreadsheetId</span><span class="p">);</span>
                <span class="k">await</span> <span class="n">batchUpdateRequest</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Similar to add command, define it in Program.cs by adding the following code block:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">removeItemCommand</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Command</span><span class="p">(</span><span class="s">"remove"</span><span class="p">,</span> <span class="s">"Remove an item from the cart"</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">itemNameOption</span>
<span class="p">};</span>
<span class="n">removeItemCommand</span><span class="p">.</span><span class="nf">SetHandler</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="n">itemName</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">gsheetsHelper</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">GSheetsHelper</span><span class="p">();</span>
    <span class="k">try</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">gsheetsHelper</span><span class="p">.</span><span class="nf">RemoveItem</span><span class="p">(</span><span class="n">itemName</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">e</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="n">Error</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">Message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">},</span> <span class="n">itemNameOption</span><span class="p">);</span>
<span class="n">rootCommand</span><span class="p">.</span><span class="nf">AddCommand</span><span class="p">(</span><span class="n">removeItemCommand</span><span class="p">);</span>
</code></pre></div></div>

<p>Run the application as <code class="language-plaintext highlighter-rouge">dotnet run remove -n Milk</code>, and you should see the item removed from your cart.</p>

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

<p>This article covered the basics of setting up a new Google Sheets spreadsheet, creating API credentials and granting access to the sheet. It also showed how to develop a basic CLI to list, add and remove items from the spreadsheet. You can get the final version of the application from my <a href="https://github.com/Dev-Power/manage-gsheets-with-csharp">GitHub repo</a>.</p>

<p>Even though it’s a simple project, I hope it helped you learn the basics of managing a Google Sheets spreadsheet.</p>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Scheduled MagPi Magazine Tracker with C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/10/Scheduled-MagPi-Magazine-Tracker-with-C/"/>
    <updated>2025-10-10T10:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/10/Scheduled-MagPi-Magazine-Tracker-with-C#</id>
    <content type="html"><![CDATA[<p>There is a slightly different version of this article published recently on Twilio Blog: <a href="https://www.twilio.com/blog/get-notified-of-new-magazine-issues-using-web-scraping-and-sms-with-csharp-dotnet">Get notified of new magazine issues using web scraping and SMS with C# .NET</a>. That version uses a worker service rather than a scheduled console application and uses SMS as the notification channel. The article you’re about to read has the extra step of importing the PDFs into Calibre. If you’re interested in the topic, I recommend checking out both versions.</p>

<p>As a Raspberry PI fan, I like to read <a href="https://magpi.raspberrypi.com/">The MagPi Magazine</a>, which is freely available as PDFs. The problem is I tend to forget to download it manually every month, so I decided to automate the process. I use Calibre as my ebook management software (I blogged about my setup <a href="https://myhomelab.rocks/host-your-ebook-library-with-calibre-on-raspberry-pi/">here</a>).</p>

<h2 id="the-magpi-magazine-tracker-architecture-and-workflow">The MagPi Magazine Tracker Architecture and Workflow</h2>

<p>I wanted this project to periodically check the latest issue, download it when it’s available and import it into Calibre and send me a notification email so that I can connect to my ebook library and check out the issue. So here’s the architecture to achieve this goal:</p>

<p><img src="/images/vpblogimg/2025/10/magpi-tracker/01-magpi-tracker-architecture.png" alt="The MagPi Magazine Tracker Architecture and Workflow diagram" /></p>

<p>Here’s the workflow:</p>

<ol>
  <li>The application is triggered based on a schedule. Since MagPi is a monthly magazine, it should be fine to run it every week.</li>
  <li>The application fetches the MagPi page and parses the HTML to find out the latest issue.</li>
  <li>It then checks its database (a flat file or a JSON would suffice for this project).</li>
  <li>If it’s a new issue, it then downloads the PDF to the local file system.</li>
  <li>It imports the PDF into Calibre using Calibre’s CLI.</li>
  <li>It sends a notification telling the user that a new issue is available.</li>
  <li>The user (which is me!) connects to <a href="https://hub.docker.com/r/linuxserver/calibre-web">calibre-web</a> (the web frontend I used to view my Calibre libraries) and reads the magazine.</li>
</ol>

<h2 id="prerequisites">Prerequisites</h2>

<p>The full source code is freely available on my <a href="https://github.com/Dev-Power/scheduled-magpi-magazine-tracker">GitHub repo</a> if you’re just interested in getting a copy of the application and playing around with it. If you’re new to GitHub, you might want to have a look at this article: <a href="https://volkanpaksoy.com/archive/2025/10/06/how-to-clone-a-github-repository/">How to clone a GitHub repository</a>.</p>

<p>To follow along and implement the project, you will need the following:</p>

<ul>
  <li><a href="https://calibre-ebook.com/download">Calibre</a></li>
  <li>A Twilio SendGrid account (with an API key and sender address set up. The beginning of <a href="https://www.twilio.com/blog/send-emails-with-csharp-handlebars-templating-and-dynamic-email-templates">this article</a> may be useful to set up your account)</li>
</ul>

<h2 id="implementation">Implementation</h2>

<p>For a scheduled task, you generally have two options:</p>

<ul>
  <li>External scheduler: This is generally part of the operating system (such as Task Scheduler for Windows and Crontab for macOS/Linux)</li>
  <li>Internal scheduler: Run the main application in an infinite loop with sleeping the amount of time you want to wait for the next run.</li>
</ul>

<p>I think an internal scheduler works better if your application needs to run quite often, like every hour or so. Using an external scheduler makes more sense to me for longer cycles, like running an application once a week, such as this project. Therefore, I’m going to implement it using a Console Application and schedule it using Crontab. If you are on Windows, you can take a look at <a href="https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10">this</a> article or search for scheduling tasks on Windows.</p>

<p>To create a Console Application, run the following commands at the root level of your project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>MagPiTracker
<span class="nb">cd </span>MagPiTracker
dotnet new console
</code></pre></div></div>

<h3 id="persistence">Persistence</h3>

<p>Let’s start with the persistence layer. All you need to read/write is the latest issue you saved in your Calibre library, so create a folder called Persistence and add an interface named <em>IMagPiRepository.cs</em> that looks like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.Persistence</span><span class="p">;</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">IMagPiRepository</span>
<span class="p">{</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">GetLastSavedIssueNumber</span><span class="p">();</span>
    <span class="n">Task</span> <span class="nf">SaveLastSavedIssueNumber</span><span class="p">(</span><span class="kt">int</span> <span class="n">newIssueNumber</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The actual implementation is going to be a simple JSON reader/writer for this project. You can choose to implement a SQLite database or a simple txt file. For JSON, add the <em>Newtonsoft.Json</em> package to your project by running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package NewtonSoft.Json
</code></pre></div></div>

<p>Then, create a file called db.json and set the initial value to 0:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"lastSavedIssueNumber"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Also, make sure that it is going to be copied to the output directory. Right-click on properties and set <strong>Copy to output directory</strong> to <strong>Copy always</strong>. I prefer Copy always to Copy if newer because it’s more straightforward and predictable.</p>

<p>Create a new file named <em>JsonMagPiRepository.cs</em> under the <em>Persistence</em> folder. Update the code as below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Newtonsoft.Json.Linq</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">MagPiTracker.Persistence</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">JsonMagPiRepository</span> <span class="p">:</span> <span class="n">IMagPiRepository</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">DB_PATH</span> <span class="p">=</span> <span class="s">"./persistence/db.json"</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">GetLastSavedIssueNumber</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">rawContents</span> <span class="p">=</span> <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">ReadAllTextAsync</span><span class="p">(</span><span class="n">DB_PATH</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">JObject</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">rawContents</span><span class="p">).</span><span class="nf">GetValue</span><span class="p">(</span><span class="s">"lastSavedIssueNumber"</span><span class="p">).</span><span class="n">Value</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">SaveLastSavedIssueNumber</span><span class="p">(</span><span class="kt">int</span> <span class="n">newIssueNumber</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">newValue</span> <span class="p">=</span> <span class="k">new</span> <span class="p">{</span> <span class="n">lastSavedIssueNumber</span> <span class="p">=</span> <span class="n">newIssueNumber</span> <span class="p">};</span>
        <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">DB_PATH</span><span class="p">,</span> <span class="n">Newtonsoft</span><span class="p">.</span><span class="n">Json</span><span class="p">.</span><span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">newValue</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To test the data layer, update <em>Program.cs</em> with the following code:</p>

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

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Running The MagPi Tracker..."</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">repository</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonMagPiRepository</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">lastSavedIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetLastSavedIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Last Saved Issue Number: </span><span class="p">{</span><span class="n">lastSavedIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="c1">// Test write and read back</span>
<span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">SaveLastSavedIssueNumber</span><span class="p">(</span><span class="m">120</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"[Test] Last Saved Issue Number: </span><span class="p">{</span><span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetLastSavedIssueNumber</span><span class="p">()}</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>The output should look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Running The MagPi Tracker...
Last Saved Issue Number: 0
Test Last Saved Issue Number: 120

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

<h3 id="issue-checker">Issue Checker</h3>

<p>The next task is to implement the service to check the latest available issue on The MagPi Magazine page. What we need to get out of this service is:</p>

<ul>
  <li>The latest issue number</li>
  <li>The link to the PDF</li>
  <li>The link to the cover image (to make the notification email prettier)</li>
</ul>

<p>So create an interface called <em>IMagPiService.cs</em> that looks like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.MagPi</span><span class="p">;</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">IMagPiService</span>
<span class="p">{</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueNumber</span><span class="p">();</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetIssuePdfUrl</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">);</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueCoverUrl</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Create a class called MagPiService that implements the interface, and that looks like this initially:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.MagPi</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">MagPiService</span> <span class="p">:</span> <span class="n">IMagPiService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueNumber</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">NotImplementedException</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetIssuePdfUrl</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">NotImplementedException</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueCoverUrl</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">NotImplementedException</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, let’s focus on getting the latest issue number. The easiest way to find the latest issue number is by going to the <a href="https://magpi.raspberrypi.com/issues/">issues page</a>, which looks like this at the time of this writing:</p>

<p><img src="/images/vpblogimg/2025/10/magpi-tracker/02-magpi-issues-page.png" alt="The MagPI Magazine issues page showing the latest issue" /></p>

<p>We’re going to utilize some web scraping to get the job done. In this project, I used a library called <a href="https://anglesharp.github.io/">AngleSharp</a> to achieve this. Run the following command to add it to your project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package AngleSharp
</code></pre></div></div>

<p>Then, update your <em>GetLatestIssueNumber</em> implementation as shown below:</p>

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

<span class="k">namespace</span> <span class="nn">MagPiTracker.MagPi</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">MagPiService</span> <span class="p">:</span> <span class="n">IMagPiService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">MAGPI_ROOT_URL</span> <span class="p">=</span> <span class="s">"https://magpi.raspberrypi.com"</span><span class="p">;</span>
    
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueNumber</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">config</span> <span class="p">=</span> <span class="n">AngleSharp</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">WithDefaultLoader</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">context</span> <span class="p">=</span> <span class="n">BrowsingContext</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="n">config</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">document</span> <span class="p">=</span> <span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="nf">OpenAsync</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="n">MAGPI_ROOT_URL</span><span class="p">}</span><span class="s">/issues/"</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">latestCoverLinkSelector</span> <span class="p">=</span> <span class="s">".c-latest-issue &gt; .c-latest-issue__cover &gt; a"</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">latestCoverLink</span> <span class="p">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">QuerySelector</span><span class="p">(</span><span class="n">latestCoverLinkSelector</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">rawLink</span> <span class="p">=</span> <span class="n">latestCoverLink</span><span class="p">.</span><span class="n">Attributes</span><span class="p">.</span><span class="nf">GetNamedItem</span><span class="p">(</span><span class="s">"href"</span><span class="p">).</span><span class="n">Value</span><span class="p">;</span>
        <span class="k">return</span> <span class="kt">int</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">rawLink</span><span class="p">.</span><span class="nf">Substring</span><span class="p">(</span><span class="n">rawLink</span><span class="p">.</span><span class="nf">LastIndexOf</span><span class="p">(</span><span class="sc">'/'</span><span class="p">)</span> <span class="p">+</span> <span class="m">1</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetIssuePdfUrl</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">NotImplementedException</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueCoverUrl</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">NotImplementedException</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To put this to the test, update your <em>Program.cs</em> as below and run the application:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">MagPiTracker.MagPi</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.Persistence</span><span class="p">;</span>

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Running The MagPi Tracker..."</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">repository</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonMagPiRepository</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">magpiService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MagPiService</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">lastSavedIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetLastSavedIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Last Saved Issue Number: </span><span class="p">{</span><span class="n">lastSavedIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">latestIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">magpiService</span><span class="p">.</span><span class="nf">GetLatestIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Latest Issue Number: </span><span class="p">{</span><span class="n">latestIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>You should see an output similar to this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Running The MagPi Tracker...
Last Saved Issue Number: 120
Latest Issue Number: 121
</code></pre></div></div>

<p>Your latest issue will probably be different depending on when you are running it.</p>

<h3 id="download-the-pdf">Download the PDF</h3>

<p>The next challenge is to find the direct link to the PDF. If you click the Download Free PDF link, the page does not start the download automatically. Instead, you land on a donation page that looks like this:</p>

<p><img src="/images/vpblogimg/2025/10/magpi-tracker/03-magpi-donation-page.png" alt="The MagPi Magazine donation page" /></p>

<p><strong>I’d strongly recommend everybody to consider donating. This is a great magazine with professional quality, and it’s full of valuable knowledge about everything Raspberry Pi.</strong></p>

<p>Since they are allowing free downloads and Raspberry Pi is mostly a favourite among maker-community, I’m hoping they wouldn’t mind this little project.</p>

<p>If you click on the “<em>No thanks, take me to the free PDF</em>” link, the PDF download starts automatically. This is actually done by a redirect that contains an iframe with the src property set to the URL of the PDF. So to download the PDF, you need to parse the URL.</p>

<p>Create a new project directory called <em>Downloader</em> and add a new interface named IDownloadService.cs that looks like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.Downloader</span><span class="p">;</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">IDownloadService</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="nf">DownloadFile</span><span class="p">(</span><span class="kt">string</span> <span class="n">url</span><span class="p">,</span> <span class="kt">string</span> <span class="n">localPath</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can tell from the method name and its arguments, this service is going to download the file at the given URL and save it to the local file system.</p>

<p>For the actual implementation, create a class called DownloadService implementing the interface and update the code with this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.Downloader</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">DownloadService</span> <span class="p">:</span> <span class="n">IDownloadService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">DownloadFile</span><span class="p">(</span><span class="kt">string</span> <span class="n">url</span><span class="p">,</span> <span class="kt">string</span> <span class="n">localPath</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">using</span> <span class="nn">HttpClient</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span> <span class="c1">// use HttpClient factory in production</span>
        <span class="k">using</span> <span class="nn">HttpResponseMessage</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>
        <span class="k">using</span> <span class="nn">Stream</span> <span class="n">downloadedFileStream</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStreamAsync</span><span class="p">();</span>
        
        <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">localFileStream</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FileStream</span><span class="p">(</span><span class="n">localPath</span><span class="p">,</span> <span class="n">FileMode</span><span class="p">.</span><span class="n">Create</span><span class="p">,</span> <span class="n">FileAccess</span><span class="p">.</span><span class="n">Write</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">await</span> <span class="n">downloadedFileStream</span><span class="p">.</span><span class="nf">CopyToAsync</span><span class="p">(</span><span class="n">localFileStream</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To test these changes, update the <em>Program.cs</em> file with the following code:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">MagPiTracker.Downloader</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.MagPi</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.Persistence</span><span class="p">;</span>

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Running The MagPi Tracker..."</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">repository</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonMagPiRepository</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">magpiService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MagPiService</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">downloadService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">DownloadService</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">lastSavedIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetLastSavedIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Last Saved Issue Number: </span><span class="p">{</span><span class="n">lastSavedIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">latestIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">magpiService</span><span class="p">.</span><span class="nf">GetLatestIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Latest Issue Number: </span><span class="p">{</span><span class="n">latestIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="n">latestIssueNumber</span> <span class="p">&gt;</span> <span class="n">lastSavedIssueNumber</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">pdfUrl</span> <span class="p">=</span> <span class="k">await</span> <span class="n">magpiService</span><span class="p">.</span><span class="nf">GetIssuePdfUrl</span><span class="p">(</span><span class="n">latestIssueNumber</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">localPath</span> <span class="p">=</span> <span class="s">$"TheMagPiMagazine_</span><span class="p">{</span><span class="n">latestIssueNumber</span><span class="p">.</span><span class="nf">ToString</span><span class="p">().</span><span class="nf">PadLeft</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="sc">'0'</span><span class="p">)}</span><span class="s">.pdf"</span><span class="p">;</span>
    <span class="k">await</span> <span class="n">downloadService</span><span class="p">.</span><span class="nf">DownloadFile</span><span class="p">(</span><span class="n">pdfUrl</span><span class="p">,</span> <span class="n">localPath</span><span class="p">);</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Latest Issue PDF has been saved to </span><span class="p">{</span><span class="n">localPath</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"No new issue found. Exiting."</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The final version of the application checks its database and compares it to the latest issue. If the latest one is newer, then it downloads the PDF. Run the application, and you should see the new PDF downloaded to your local machine. Your output should look like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Running</span> <span class="n">The</span> <span class="n">MagPi</span> <span class="n">Tracker</span><span class="p">...</span>
<span class="n">Last</span> <span class="n">Saved</span> <span class="n">Issue</span> <span class="n">Number</span><span class="p">:</span> <span class="m">120</span>
<span class="n">Latest</span> <span class="n">Issue</span> <span class="n">Number</span><span class="p">:</span> <span class="m">121</span>
<span class="n">Latest</span> <span class="n">Issue</span> <span class="n">PDF</span> <span class="n">has</span> <span class="n">been</span> <span class="n">saved</span> <span class="n">to</span> <span class="n">TheMagPiMagazine_121</span><span class="p">.</span><span class="n">pdf</span>
</code></pre></div></div>

<h3 id="importing-to-calibre">Importing to Calibre</h3>

<p>The next step is to import this file into Calibre. An easy way to wrap external CLIs is the <a href="https://github.com/Tyrrrz/CliWrap">CliWrap</a> library. Add it to your project via NuGet by running the command below:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package CliWrap
</code></pre></div></div>

<p>Create a new folder called <em>Calibre</em> and a new interface called <em>ICalibreService.cs</em> under it.</p>

<p>Update the interface with this code:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.Calibre</span><span class="p">;</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">ICalibreService</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="nf">ImportMagPiMagazine</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">,</span> <span class="kt">string</span> <span class="n">pdfPath</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This method is going to be tailored for The MagPi Magazine. The MagPi-related information can be stripped out of the method and put somewhere else, like a config file, but since I’m not aiming to make this a generic downloader, for the time being, it should do the job.</p>

<p>Create the implementation class named CalibreService and implement the interface like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Text</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliWrap</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">MagPiTracker.Calibre</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">CalibreService</span> <span class="p">:</span> <span class="n">ICalibreService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">LIBRARY_PATH</span> <span class="p">=</span> <span class="s">"{PATH TO YOUR CALIBRE LIBRARY}"</span><span class="p">;</span>
    
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">ImportMagPiMagazine</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">,</span> <span class="kt">string</span> <span class="n">pdfPath</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">issueTitle</span> <span class="p">=</span> <span class="s">$"The MagPi Issue </span><span class="p">{</span><span class="n">issueNumber</span><span class="p">.</span><span class="nf">ToString</span><span class="p">().</span><span class="nf">PadLeft</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="sc">'0'</span><span class="p">)}</span><span class="s">"</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">authors</span> <span class="p">=</span> <span class="s">"Raspberry Pi Press"</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">series</span> <span class="p">=</span> <span class="s">"The MagPi Magazine"</span><span class="p">;</span>
        
        <span class="kt">var</span> <span class="n">stdOutBuffer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringBuilder</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">stdErrBuffer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringBuilder</span><span class="p">();</span>
        
        <span class="k">await</span> <span class="n">Cli</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="s">"/Applications/calibre.app/Contents/MacOS/calibredb"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">WithArguments</span><span class="p">(</span><span class="s">$"add --title \"</span><span class="p">{</span><span class="n">issueTitle</span><span class="p">}</span><span class="s">\" --with-library \"</span><span class="p">{</span><span class="n">LIBRARY_PATH</span><span class="p">}</span><span class="s">\" --authors \"</span><span class="p">{</span><span class="n">authors</span><span class="p">}</span><span class="s">\" --series \"</span><span class="p">{</span><span class="n">series</span><span class="p">}</span><span class="s">\" \"</span><span class="p">{</span><span class="n">pdfPath</span><span class="p">}</span><span class="s">\""</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">WithStandardOutputPipe</span><span class="p">(</span><span class="n">PipeTarget</span><span class="p">.</span><span class="nf">ToStringBuilder</span><span class="p">(</span><span class="n">stdOutBuffer</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">WithStandardErrorPipe</span><span class="p">(</span><span class="n">PipeTarget</span><span class="p">.</span><span class="nf">ToStringBuilder</span><span class="p">(</span><span class="n">stdErrBuffer</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
        
        <span class="kt">var</span> <span class="n">stdOut</span> <span class="p">=</span> <span class="n">stdOutBuffer</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">stdErr</span> <span class="p">=</span> <span class="n">stdErrBuffer</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
        
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">stdOut</span><span class="p">);</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">stdErr</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Make sure to update LIBRARY_PATH. Also, update the application path depending on your operating system.</p>

<p>Then, update the Program.cs like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>using MagPiTracker.Calibre;
using MagPiTracker.Downloader;
using MagPiTracker.MagPi;
using MagPiTracker.Notifications;
using MagPiTracker.Persistence;

Console.WriteLine("Running The MagPi Tracker...");

var repository = new JsonMagPiRepository();
var magpiService = new MagPiService();

var lastSavedIssueNumber = await repository.GetLastSavedIssueNumber();
Console.WriteLine($"Last Saved Issue Number: {lastSavedIssueNumber}");

var latestIssueNumber = await magpiService.GetLatestIssueNumber();
Console.WriteLine($"Latest Issue Number: {latestIssueNumber}");

if (latestIssueNumber &gt; lastSavedIssueNumber)
{
    var pdfUrl = await magpiService.GetIssuePdfUrl(latestIssueNumber);
    var localPath = $"TheMagPiMagazine_{latestIssueNumber.ToString().PadLeft(3, '0')}.pdf";
 
    var downloadService = new DownloadService();
    await downloadService.DownloadFile(pdfUrl, localPath);
    Console.WriteLine($"Latest Issue PDF has been saved to {localPath}");
    
    var calibreService = new CalibreService();
    await calibreService.ImportMagPiMagazine(latestIssueNumber, new FileInfo(localPath).FullName);
    Console.WriteLine($"Latest Issue has been imported into Calibre");
}
else
{
    Console.WriteLine($"No new issue found. Exiting.");
}
</code></pre></div></div>

<p>Run the application, and you should see an output like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Running The MagPi Tracker...
Last Saved Issue Number: 120
Latest Issue Number: 121
Latest Issue PDF has been saved to TheMagPiMagazine_121.pdf
    /Users/.../scheduled-magpi-magazine-tracker/src/TheMagPiMagazine_121.pdf

The following books were not added as they already exist in the database (see --duplicates option or --automerge option):
  The MagPi Issue 121

Latest Issue has been imported into Calibre

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

<p>You can go ahead and open your Calibre application, and you should see the newly imported issue in your library:</p>

<p><img src="/images/vpblogimg/2025/10/magpi-tracker/04-magpi-in-calibre.png" alt="The latest MagPi issue shown in Calibre" /></p>

<h3 id="cover-image-url">Cover Image URL</h3>

<p>In the previous section, we left out implementing the third method. This is not strictly necessary, but having the cover image would make your notification email look nicer. Also, from a practical point of view, if you’re not interested in the topics covered in that issue, you may delay looking at that issue.</p>

<p>To get the cover URL, revisit MagPiService class and update the <em>GetLatestIssueCoverUrl</em> method’s implementation as below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetLatestIssueCoverUrl</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">config</span> <span class="p">=</span> <span class="n">AngleSharp</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="n">Default</span><span class="p">.</span><span class="nf">WithDefaultLoader</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">context</span> <span class="p">=</span> <span class="n">BrowsingContext</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="n">config</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">document</span> <span class="p">=</span> <span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="nf">OpenAsync</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="n">MAGPI_ROOT_URL</span><span class="p">}</span><span class="s">/issues/"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">latestCoverImageSelector</span> <span class="p">=</span> <span class="s">".c-latest-issue &gt; .c-latest-issue__cover &gt; a &gt; img"</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">latestCoverImage</span> <span class="p">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">QuerySelector</span><span class="p">(</span><span class="n">latestCoverImageSelector</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">latestCoverImageUrl</span> <span class="p">=</span> <span class="n">latestCoverImage</span><span class="p">.</span><span class="n">Attributes</span><span class="p">.</span><span class="nf">GetNamedItem</span><span class="p">(</span><span class="s">"src"</span><span class="p">).</span><span class="n">Value</span><span class="p">;</span>
    
    <span class="k">return</span> <span class="n">latestCoverImageUrl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This will come in handy in the next section.</p>

<h3 id="notifications">Notifications</h3>

<p>It would be nice to know when a new issue is imported into your library, so the next step is to add a notification mechanism to the application. In this example, I will use email notifications as that’s the cheapest and simplest method. I will use SendGrid as my SMTP provider.</p>

<p>To store the API key, initialize <strong>dotnet user-secrets</strong> and create a new secret by running the following commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet user-secrets init
dotnet user-secrets <span class="nb">set </span>SendGrid:ApiKey <span class="o">{</span>YOUR API KEY<span class="o">}</span>
</code></pre></div></div>

<p>In the project, create a new project directory called Notifications and a new interface called <em>INewIssueNotificationService.cs</em> with the following code:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">MagPiTracker.Notifications</span><span class="p">;</span>

<span class="k">public</span> <span class="k">interface</span> <span class="nc">INewIssueNotificationService</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="nf">SendNewIssueNotification</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">,</span> <span class="kt">string</span> <span class="n">coverUrl</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Before implementing the class, add SendGrid SDK by running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package SendGrid
</code></pre></div></div>

<p>Now add a new class called <em>EmailNotificationService.cs</em> and update its code with this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Reflection</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Extensions.Configuration</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">SendGrid</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">SendGrid.Helpers.Mail</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">MagPiTracker.Notifications</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">EmailNotificationService</span> <span class="p">:</span> <span class="n">INewIssueNotificationService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">SendNewIssueNotification</span><span class="p">(</span><span class="kt">int</span> <span class="n">issueNumber</span><span class="p">,</span> <span class="kt">string</span> <span class="n">coverUrl</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">IConfiguration</span> <span class="n">config</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ConfigurationBuilder</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">AddUserSecrets</span><span class="p">(</span><span class="n">Assembly</span><span class="p">.</span><span class="nf">GetExecutingAssembly</span><span class="p">(),</span> <span class="n">optional</span><span class="p">:</span> <span class="k">true</span><span class="p">,</span> <span class="n">reloadOnChange</span><span class="p">:</span> <span class="k">false</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
        
        <span class="kt">var</span> <span class="n">sendGridClient</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SendGridClient</span><span class="p">(</span><span class="n">apiKey</span><span class="p">:</span> <span class="n">config</span><span class="p">[</span><span class="s">"SendGrid:ApiKey"</span><span class="p">]);</span>

        <span class="kt">var</span> <span class="k">from</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">EmailAddress</span><span class="p">(</span><span class="s">"{YOUR VERIFIED SENDER EMAIL ADDRESS}"</span><span class="p">,</span> <span class="s">"The MagPi Magazine Issue Checker"</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">to</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">EmailAddress</span><span class="p">(</span><span class="s">"{YOUR RECIPIENT EMAIL ADDRESS}"</span><span class="p">,</span> <span class="s">"{YOUR DISPLAY NAME}"</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">htmlContent</span> <span class="p">=</span> <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">ReadAllTextAsync</span><span class="p">(</span><span class="s">"./notifications/email-template.html"</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">htmlWithData</span> <span class="p">=</span> <span class="n">htmlContent</span><span class="p">.</span><span class="nf">Replace</span><span class="p">(</span><span class="s">"%{COVER_URL}"</span><span class="p">,</span> <span class="n">coverUrl</span><span class="p">);</span>
        
        <span class="kt">var</span> <span class="n">msg</span> <span class="p">=</span> <span class="n">MailHelper</span><span class="p">.</span><span class="nf">CreateSingleEmail</span><span class="p">(</span><span class="k">from</span><span class="p">,</span> <span class="n">to</span><span class="p">,</span> <span class="s">"The MagPi Magazine New Issue"</span><span class="p">,</span> <span class="n">htmlWithData</span><span class="p">,</span> <span class="n">htmlWithData</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">sendGridClient</span><span class="p">.</span><span class="nf">SendEmailAsync</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This code reads the API key from user secrets so that it’s never accidentally pushed to source control. Also, it reads the email template from an HTML file. Create a new file named email-template.html, and set it to be copied to the output always (as you did with db.json) and update its contents as shown below:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="nt">&lt;html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">&gt;</span>
<span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/head&gt;</span>
<span class="nt">&lt;body&gt;</span>
<span class="nt">&lt;h1&gt;</span>New MagPi Magazine is out!<span class="nt">&lt;/h1&gt;</span>
<span class="nt">&lt;p&gt;</span>
    <span class="nt">&lt;img</span> <span class="na">src=</span><span class="s">"%{COVER_URL}"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>There are better ways for variable replacement (using a templating engine such as Handlebars, Razor etc.), but to keep things simple, I just put a placeholder and replaced the string. Update the Program.cs to reflect the latest changes and test:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">MagPiTracker.Calibre</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.Downloader</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.MagPi</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.Notifications</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">MagPiTracker.Persistence</span><span class="p">;</span>

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Running The MagPi Tracker..."</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">repository</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">JsonMagPiRepository</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">magpiService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MagPiService</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">lastSavedIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetLastSavedIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Last Saved Issue Number: </span><span class="p">{</span><span class="n">lastSavedIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">latestIssueNumber</span> <span class="p">=</span> <span class="k">await</span> <span class="n">magpiService</span><span class="p">.</span><span class="nf">GetLatestIssueNumber</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Latest Issue Number: </span><span class="p">{</span><span class="n">latestIssueNumber</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="n">latestIssueNumber</span> <span class="p">&gt;</span> <span class="n">lastSavedIssueNumber</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">pdfUrl</span> <span class="p">=</span> <span class="k">await</span> <span class="n">magpiService</span><span class="p">.</span><span class="nf">GetIssuePdfUrl</span><span class="p">(</span><span class="n">latestIssueNumber</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">localPath</span> <span class="p">=</span> <span class="s">$"TheMagPiMagazine_</span><span class="p">{</span><span class="n">latestIssueNumber</span><span class="p">.</span><span class="nf">ToString</span><span class="p">().</span><span class="nf">PadLeft</span><span class="p">(</span><span class="m">3</span><span class="p">,</span> <span class="sc">'0'</span><span class="p">)}</span><span class="s">.pdf"</span><span class="p">;</span>
 
    <span class="kt">var</span> <span class="n">downloadService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">DownloadService</span><span class="p">();</span>
    <span class="k">await</span> <span class="n">downloadService</span><span class="p">.</span><span class="nf">DownloadFile</span><span class="p">(</span><span class="n">pdfUrl</span><span class="p">,</span> <span class="n">localPath</span><span class="p">);</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Latest Issue PDF has been saved to </span><span class="p">{</span><span class="n">localPath</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    
    <span class="kt">var</span> <span class="n">calibreService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">CalibreService</span><span class="p">();</span>
    <span class="k">await</span> <span class="n">calibreService</span><span class="p">.</span><span class="nf">ImportMagPiMagazine</span><span class="p">(</span><span class="n">latestIssueNumber</span><span class="p">,</span> <span class="k">new</span> <span class="nf">FileInfo</span><span class="p">(</span><span class="n">localPath</span><span class="p">).</span><span class="n">FullName</span><span class="p">);</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Latest Issue has been imported into Calibre"</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">latestIssueCoverUrl</span> <span class="p">=</span> <span class="k">await</span> <span class="n">magpiService</span><span class="p">.</span><span class="nf">GetLatestIssueCoverUrl</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">notificationService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">EmailNotificationService</span><span class="p">();</span>
    <span class="k">await</span> <span class="n">notificationService</span><span class="p">.</span><span class="nf">SendNewIssueNotification</span><span class="p">(</span><span class="n">latestIssueNumber</span><span class="p">,</span> <span class="n">latestIssueCoverUrl</span><span class="p">);</span>

    <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">SaveLastSavedIssueNumber</span><span class="p">(</span><span class="n">latestIssueNumber</span><span class="p">);</span>
    <span class="n">File</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="n">localPath</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"No new issue found. Exiting."</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In addition to the previous tasks, you are now sending the notification email. Once everything is done successfully, the code updates the local JSON file with the latest issue number so that it doesn’t download the same issue over and over again. Also, it deletes the local PDF to keep things nice and tidy.</p>

<p>Run the application, and you should receive a notification that looks like this:</p>

<p><img src="/images/vpblogimg/2025/10/magpi-tracker/05-notification-email.png" alt="Screenshot of the final notification email showing MagPi cover" /></p>

<p>You can add more stuff like the link to the PDF, issue number etc., but just to ping myself this much information is enough for me.</p>

<h3 id="scheduling">Scheduling</h3>

<p>Let’s bring this home by scheduling the application so that it does its thing in an automated fashion.</p>

<p>As mentioned before, on Windows, I’d recommend using the built-in Task Scheduler. On macOS/Linux systems, crontab does the job.</p>

<p>Firstly, build your application and place the deployment package wherever you want to run the application. To edit cron jobs, run</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>crontab -e
</code></pre></div></div>

<p>I will run the application every Friday at 5 AM and will use this cron expression: 0 5 * * 5</p>

<p>To find where the dotnet executable is located, you can use the which command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>which dotnet
</code></pre></div></div>

<p>Also, the cron job will be run in a different working directory. To avoid path issues, it’s best to change to our application directory before running it. So the cron job looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0 5 * * 5 cd /Users/.../Deployment/MagPiTracker &amp;&amp; /usr/local/share/dotnet/dotnet MagPiTracker.dll
</code></pre></div></div>

<p>Crontab uses the following syntax, and you can customize your schedule based on this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* * * * * command
* - minute (0-59)
* - hour (0-23)
* - day of the month (1-31)
* - month (1-12)
* - day of the week (0-6, 0 is Sunday)

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

<h3 id="why-no-docker">Why No Docker?</h3>

<p>Normally I try to run everything in Docker containers. In this project, I chose to run the application on bare metal. The reason for this is to be able to import the PDFs into my Calibre library, which is also running on bare metal. If I were to run this application in Docker, I wouldn’t be able to run Calibre CLI on the host computer. If I didn’t have this constraint, I would have definitely Dockerized the application.</p>

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

<p>I hope you enjoyed this little project. As a reminder, please consider donating to the Raspberry Pi Press and use their own mechanism, but if you cannot afford it and since the PDFs are already available out there, you can go ahead and use this project and hopefully learn some new technologies along the way.</p>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Migrate YouTube Subscriptions into Another Account with C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/09/Migrate-YouTube-Subscriptions-into-Another-Account-with-C/"/>
    <updated>2025-10-09T10:30:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/09/Migrate-YouTube-Subscriptions-into-Another-Account-with-C#</id>
    <content type="html"><![CDATA[<p>I’m not a big fan of Youtube’s web application. I’d like to categorise my subscriptions, but YouTube doesn’t allow this. I have multiple Google accounts, and I use them to group certain videos. When I’m in the mode of watching software development videos, I switch to one account. If I’m in the LEGO mood, I switch to another. The problem is I’m not watching YouTube on Kodi on Raspberry Pi (about which I wrote <a href="https://volkanpaksoy.com/archive/2025/09/06/How-to-watch-YouTube-ad-free-on-Kodi/">a tutorial</a>), and managing multiple accounts is harder, so I decided to combine all my subscriptions in one account. This article shows I managed to do it.</p>

<h2 id="authentication">Authentication</h2>

<p>First, Google needs to know you have permission to fetch the subscriptions from your YouTube account. To achieve this, go to <a href="https://console.cloud.google.com/">Google Cloud Console</a>.</p>

<p>Create a new project:</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/01-new-project.png" alt="New project screen" /></p>

<p>Then, click <strong>Library</strong> on the left menu and search for YouTube. Select <strong>YouTube Data API v3</strong> and click <strong>Enable</strong>.</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/02-enable-youtube.png" alt="enable youtube API screen" /></p>

<p>Click the <strong>OAuth consent screen</strong> link. Select <strong>External</strong> user type and click the <strong>Create</strong> button.</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/03-oauth-consent-user-type.png" alt="OAuth consent screen select user type screen" /></p>

<p>Give your app a name and select your account’s email as “User support email”. The app name appears on your confirmation screen so I’d recommend giving it a meaningful name such as <em>YouTube-Migration-Source</em> (there will be a destination too).</p>

<p>Enter your email again as “Developer contact information”.</p>

<p>Click <strong>Save and Continue</strong>.</p>

<p>In the <strong>Scopes</strong> screen, click <strong>Add or Remove Scopes</strong> and select <strong>youtube</strong>:</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/05-youtube-scope.png" alt="Add or remove scope screen showing youtube selected" /></p>

<p>Click <strong>Save and Continue</strong>.</p>

<p>Adding Test users is not mandatory but is helpful in the next steps so I’d recommend adding your email address as a test user. This way you can still use the application without having to publish it.</p>

<p>Click <strong>Save and Continue</strong> after you’ve added your email address as a test user.</p>

<p>Then, click the <strong>Credentials</strong> link on the menu.</p>

<p>Click <strong>Create Credentials</strong> and select <strong>OAuth client ID</strong>.</p>

<p>Select <strong>Desktop app</strong> as your application type, give it a name and click the <strong>Create</strong> button.</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/06-oauth-app-type.png" alt="create OAuth client application type selection screen" /></p>

<p>Click <strong>Download JSON</strong> in the confirmation dialog box:</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/07-credential-download-screen.png" alt="OAuth client created confirmation dialog with Download JSON button" /></p>

<p>Now, the good news is that you have the credentials to access your source YouTube account. The bad news is you have to repeat the same steps for the destination account. In the end, rename your credential files to <em>client_secrets_source.json</em> and <em>client_secrets_destination.json</em> and move on to the next section to implement the application.</p>

<h2 id="implement-the-application">Implement the Application</h2>

<p>Now that the boring part is over let’s write some code and have fun.</p>

<p>Create a new dotnet console project by running</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>YouTubeMigrationClient
<span class="nb">cd </span>YouTubeMigrationClient
dotnet new console
</code></pre></div></div>

<p>Then add Google YouTube SDK to the project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Google.Apis.YouTube.v3
</code></pre></div></div>

<p>Copy <em>client_secrets_source.json</em> and <em>client_secrets_destination.json</em> files under this project’s folder.</p>

<p>Add a new C# file named <em>YouTubeServiceFactory.cs</em> and replace its contents with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Reflection</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.Auth.OAuth2</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.Util.Store</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.YouTube.v3</span><span class="p">;</span>

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

<span class="k">public</span> <span class="k">class</span> <span class="nc">YouTubeServiceFactory</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">UserCredential</span><span class="p">&gt;</span> <span class="nf">CreateCredential</span><span class="p">(</span><span class="kt">string</span> <span class="n">credentialFileName</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">UserCredential</span> <span class="n">credential</span><span class="p">;</span>
        <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">stream</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FileStream</span><span class="p">(</span><span class="n">credentialFileName</span><span class="p">,</span> <span class="n">FileMode</span><span class="p">.</span><span class="n">Open</span><span class="p">,</span> <span class="n">FileAccess</span><span class="p">.</span><span class="n">Read</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="n">credential</span> <span class="p">=</span> <span class="k">await</span> <span class="n">GoogleWebAuthorizationBroker</span><span class="p">.</span><span class="nf">AuthorizeAsync</span><span class="p">(</span>
                <span class="p">(</span><span class="k">await</span> <span class="n">GoogleClientSecrets</span><span class="p">.</span><span class="nf">FromStreamAsync</span><span class="p">(</span><span class="n">stream</span><span class="p">)).</span><span class="n">Secrets</span><span class="p">,</span>
                <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">YouTubeService</span><span class="p">.</span><span class="n">Scope</span><span class="p">.</span><span class="n">Youtube</span> <span class="p">},</span>
                <span class="s">"user"</span><span class="p">,</span>
                <span class="n">CancellationToken</span><span class="p">.</span><span class="n">None</span><span class="p">,</span>
                <span class="k">new</span> <span class="nf">FileDataStore</span><span class="p">(</span><span class="n">Assembly</span><span class="p">.</span><span class="nf">GetExecutingAssembly</span><span class="p">().</span><span class="nf">GetType</span><span class="p">().</span><span class="nf">ToString</span><span class="p">())</span>
            <span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">credential</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To test the credentials, edit the <em>Program.cs</em> and replace the code with this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Reflection</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.Services</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.YouTube.v3</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">YouTubeMigrationClient</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">sourceYouTubeCredential</span> <span class="p">=</span> <span class="k">await</span> <span class="n">YouTubeServiceFactory</span><span class="p">.</span><span class="nf">CreateCredential</span><span class="p">(</span><span class="s">"client_secrets_source.json"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">sourceYouTubeService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">YouTubeService</span><span class="p">(</span><span class="k">new</span> <span class="n">BaseClientService</span><span class="p">.</span><span class="nf">Initializer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">HttpClientInitializer</span> <span class="p">=</span> <span class="n">sourceYouTubeCredential</span><span class="p">,</span>
    <span class="n">ApplicationName</span> <span class="p">=</span> <span class="n">Assembly</span><span class="p">.</span><span class="nf">GetExecutingAssembly</span><span class="p">().</span><span class="nf">GetType</span><span class="p">().</span><span class="nf">ToString</span><span class="p">()</span>
<span class="p">});</span>

<span class="kt">var</span> <span class="n">sourceSubscriptionListRequest</span> <span class="p">=</span> <span class="n">sourceYouTubeService</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">List</span><span class="p">(</span><span class="s">"id,snippet"</span><span class="p">);</span>
<span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="n">Mine</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">sourceSubscriptions</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">subscription</span> <span class="k">in</span> <span class="n">sourceSubscriptions</span><span class="p">.</span><span class="n">Items</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"ChannelId: </span><span class="p">{</span><span class="n">subscription</span><span class="p">.</span><span class="n">Snippet</span><span class="p">.</span><span class="n">ResourceId</span><span class="p">.</span><span class="n">ChannelId</span><span class="p">}</span><span class="s">\t\tTitle: </span><span class="p">{</span><span class="n">subscription</span><span class="p">.</span><span class="n">Snippet</span><span class="p">.</span><span class="n">Title</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">await</span> <span class="n">sourceYouTubeCredential</span><span class="p">.</span><span class="nf">RevokeTokenAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">CancellationToken</span><span class="p">());</span>
</code></pre></div></div>

<p>The reason we’re revoking the token is to be able to authenticate to both source and destination accounts. If we don’t revoke it now, it still tries to use the source account’s access token when we try to access the destination account. It will be more obvious when you’ve finished implementing the application.</p>

<p>Now run the application in your terminal:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run
</code></pre></div></div>

<p>It should launch your default browser and show a Google account selection screen:</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/09-google-account-selection.png" alt="Google account selection screen" /></p>

<p>As this is an application in testing on Google’s side, it shows a warning:</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/10-not-verified-warning.png" alt="Application not verified warning" /></p>

<p>Click <strong>Continue</strong>.</p>

<p>Then it asks for your permission to allow the app to access your Google Account.</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/11-request-for-access.png" alt="Google asking for permission to access your account by your application " /></p>

<p>Click <strong>Allow</strong>.</p>

<p>After the authorization is complete, you will see a message that you can close the tab. Do so and go back to your terminal window. You should now see up to 5 (default page size) results like this:</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/08-test-channel-list.png" alt="List subscription results" /></p>

<p>I’d recommend running the application again with <em>client_secrets_destination.json</em> to confirm they both work. This is where revoking the token helps because otherwise, you wouldn’t be asked to select an account.</p>

<h3 id="refactor-getting-source-subscriptions">Refactor Getting Source Subscriptions</h3>

<p>The default page size is 5. You can adjust this by setting MaxResults as shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sourceSubscriptionListRequest.MaxResults = 10;
</code></pre></div></div>

<p>Unfortunately, the maximum value allowed is 50. If you have more than 50 subscriptions, your implementation won’t be able to migrate all of them, which is something you need to fix.</p>

<p>Google uses paging in their results, and you can access the previous and next pages by setting the PageToken property to one of those tokens. The refactored version below shows how it works:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>string nextPageToken = null;
var sourceSubscriptionList = new List&lt;Subscription&gt;();

do
{
    var sourceSubscriptionListRequest = sourceYouTubeService.Subscriptions.List("id,snippet");
    sourceSubscriptionListRequest.Mine = true;
    sourceSubscriptionListRequest.MaxResults = 10;
    sourceSubscriptionListRequest.Order = SubscriptionsResource.ListRequest.OrderEnum.Alphabetical;
    sourceSubscriptionListRequest.PageToken = nextPageToken;

    var sourceSubscriptions = await sourceSubscriptionListRequest.ExecuteAsync();
    nextPageToken = sourceSubscriptions.NextPageToken;

    sourceSubscriptionList.AddRange(sourceSubscriptions.Items);
          
    Console.WriteLine(sourceSubscriptions.Items.Count);
} while (nextPageToken != null);

Console.WriteLine(sourceSubscriptionList.Count);
</code></pre></div></div>

<p>The example gets results 10 at a time and keeps doing it as long as NextPageToken is not null.</p>

<p><img src="/images/vpblogimg/2025/10/youtube-migration/12-paged-results.png" alt="Terminal window showing paged result example" /></p>

<p>So now you have access to your entire subscription list, let’s talk about migrating them into the destination account.</p>

<h3 id="import-subscriptions-into-the-destination-account">Import Subscriptions into the Destination Account</h3>

<p>The next step is to iterate over the subscription list and add them to the destination account. Add the following code block to <em>Program.cs</em>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>var targetYouTubeCredential = await YouTubeServiceFactory.CreateCredential("client_secrets_destination.json");
var targetYouTubeService = new YouTubeService(new BaseClientService.Initializer()
{
    HttpClientInitializer = targetYouTubeCredential,
    ApplicationName = Assembly.GetExecutingAssembly().GetType().ToString()
});

foreach (var subscription in sourceSubscriptionList)
{
    Console.WriteLine($"ChannelId: {subscription.Snippet.ResourceId.ChannelId}\t\tTitle: {subscription.Snippet.Title}");
            
    var targetSubscription = new Subscription
    {
        Snippet = new SubscriptionSnippet
        {
            ResourceId = new ResourceId
            {
                Kind = "youtube#subscription",
                ChannelId = subscription.Snippet.ResourceId.ChannelId
            }
        }
    };
    
    try
    {
        await targetYouTubeService.Subscriptions.Insert(targetSubscription, "id,snippet").ExecuteAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

await targetYouTubeCredential.RevokeTokenAsync(new CancellationToken());
</code></pre></div></div>

<p>The exception handling is to ensure the program keeps running if you already have the same subscription in the destination account.</p>

<p>Run the application again, and you should see it adding the subscriptions one by one. After you’re done, refresh your destination account and confirm the results.</p>

<p>Here’s the final version of the <em>Program.cs</em> in case you didn’t follow along:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Reflection</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.Services</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.YouTube.v3</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Apis.YouTube.v3.Data</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">YouTubeMigrationClient</span><span class="p">;</span>

<span class="c1">// Get the subscriptions from the Source Account</span>
<span class="kt">var</span> <span class="n">sourceYouTubeCredential</span> <span class="p">=</span> <span class="k">await</span> <span class="n">YouTubeServiceFactory</span><span class="p">.</span><span class="nf">CreateCredential</span><span class="p">(</span><span class="s">"client_secrets_source.json"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">sourceYouTubeService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">YouTubeService</span><span class="p">(</span><span class="k">new</span> <span class="n">BaseClientService</span><span class="p">.</span><span class="nf">Initializer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">HttpClientInitializer</span> <span class="p">=</span> <span class="n">sourceYouTubeCredential</span><span class="p">,</span>
    <span class="n">ApplicationName</span> <span class="p">=</span> <span class="n">Assembly</span><span class="p">.</span><span class="nf">GetExecutingAssembly</span><span class="p">().</span><span class="nf">GetType</span><span class="p">().</span><span class="nf">ToString</span><span class="p">()</span>
<span class="p">});</span>

<span class="kt">string</span> <span class="n">nextPageToken</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">sourceSubscriptionList</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Subscription</span><span class="p">&gt;();</span>

<span class="k">do</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">sourceSubscriptionListRequest</span> <span class="p">=</span> <span class="n">sourceYouTubeService</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">List</span><span class="p">(</span><span class="s">"id,snippet"</span><span class="p">);</span>
    <span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="n">Mine</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    <span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="n">MaxResults</span> <span class="p">=</span> <span class="m">50</span><span class="p">;</span>
    <span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="n">Order</span> <span class="p">=</span> <span class="n">SubscriptionsResource</span><span class="p">.</span><span class="n">ListRequest</span><span class="p">.</span><span class="n">OrderEnum</span><span class="p">.</span><span class="n">Alphabetical</span><span class="p">;</span>
    <span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="n">PageToken</span> <span class="p">=</span> <span class="n">nextPageToken</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">sourceSubscriptions</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sourceSubscriptionListRequest</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
    <span class="n">nextPageToken</span> <span class="p">=</span> <span class="n">sourceSubscriptions</span><span class="p">.</span><span class="n">NextPageToken</span><span class="p">;</span>

    <span class="n">sourceSubscriptionList</span><span class="p">.</span><span class="nf">AddRange</span><span class="p">(</span><span class="n">sourceSubscriptions</span><span class="p">.</span><span class="n">Items</span><span class="p">);</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">nextPageToken</span> <span class="p">!=</span> <span class="k">null</span><span class="p">);</span>

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Retrieved </span><span class="p">{</span><span class="n">sourceSubscriptionList</span><span class="p">.</span><span class="n">Count</span><span class="p">}</span><span class="s"> subscriptions from the source account"</span><span class="p">);</span>

<span class="k">await</span> <span class="n">sourceYouTubeCredential</span><span class="p">.</span><span class="nf">RevokeTokenAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">CancellationToken</span><span class="p">());</span>

<span class="c1">// Import subscriptions into the Destination Account</span>
<span class="kt">var</span> <span class="n">targetYouTubeCredential</span> <span class="p">=</span> <span class="k">await</span> <span class="n">YouTubeServiceFactory</span><span class="p">.</span><span class="nf">CreateCredential</span><span class="p">(</span><span class="s">"client_secrets_destination.json"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">targetYouTubeService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">YouTubeService</span><span class="p">(</span><span class="k">new</span> <span class="n">BaseClientService</span><span class="p">.</span><span class="nf">Initializer</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">HttpClientInitializer</span> <span class="p">=</span> <span class="n">targetYouTubeCredential</span><span class="p">,</span>
    <span class="n">ApplicationName</span> <span class="p">=</span> <span class="n">Assembly</span><span class="p">.</span><span class="nf">GetExecutingAssembly</span><span class="p">().</span><span class="nf">GetType</span><span class="p">().</span><span class="nf">ToString</span><span class="p">()</span>
<span class="p">});</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">subscription</span> <span class="k">in</span> <span class="n">sourceSubscriptionList</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"ChannelId: </span><span class="p">{</span><span class="n">subscription</span><span class="p">.</span><span class="n">Snippet</span><span class="p">.</span><span class="n">ResourceId</span><span class="p">.</span><span class="n">ChannelId</span><span class="p">}</span><span class="s">\t\tTitle: </span><span class="p">{</span><span class="n">subscription</span><span class="p">.</span><span class="n">Snippet</span><span class="p">.</span><span class="n">Title</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            
    <span class="kt">var</span> <span class="n">targetSubscription</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Subscription</span>
    <span class="p">{</span>
        <span class="n">Snippet</span> <span class="p">=</span> <span class="k">new</span> <span class="n">SubscriptionSnippet</span>
        <span class="p">{</span>
            <span class="n">ResourceId</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ResourceId</span>
            <span class="p">{</span>
                <span class="n">Kind</span> <span class="p">=</span> <span class="s">"youtube#subscription"</span><span class="p">,</span>
                <span class="n">ChannelId</span> <span class="p">=</span> <span class="n">subscription</span><span class="p">.</span><span class="n">Snippet</span><span class="p">.</span><span class="n">ResourceId</span><span class="p">.</span><span class="n">ChannelId</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">};</span>
    
    <span class="k">try</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">targetYouTubeService</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Insert</span><span class="p">(</span><span class="n">targetSubscription</span><span class="p">,</span> <span class="s">"id,snippet"</span><span class="p">).</span><span class="nf">ExecuteAsync</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">e</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">Message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">await</span> <span class="n">targetYouTubeCredential</span><span class="p">.</span><span class="nf">RevokeTokenAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">CancellationToken</span><span class="p">());</span>
</code></pre></div></div>

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

<p>This is a simple tutorial about managing your Youtube account by using your own code. Other tools do the same job, but nothing beats the experience and satisfaction of achieving something by software that you built yourself. I hope you enjoyed it too.</p>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[A Number Guessing Game with C# and Twilio SMS]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/08/A-Number-Guessing-Game-with-C-and-Twilio-SMS/"/>
    <updated>2025-10-08T10:30:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/08/A-Number-Guessing-Game-with-C#-and-Twilio-SMS</id>
    <content type="html"><![CDATA[<p>I like using Twilio Voice and SMS APIs to develop voice and text-based applications. I thought an SMS-based game would be fun to implement.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>If you want to follow along, you will need the following:</p>

<ul>
  <li>A <a href="https://console.twilio.com/">Twilio</a> account</li>
  <li>A Twilio number with SMS capabilities</li>
  <li>A free <a href="https://dashboard.ngrok.com/signup">ngrok</a> account and <a href="https://ngrok.com/download">ngrok CLI</a></li>
</ul>

<h2 id="setting-up-the-project">Setting Up the Project</h2>

<p>First, start by creating an ASP.NET Core Web API project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>HighLowNumbergame
<span class="nb">cd </span>HighLowNumbergame
dotnet new webapi
</code></pre></div></div>

<p>You’ll use Twilio SDK, so add that NuGet package along with ASP.NET helpers to the project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Twilio
dotnet add package Twilio.AspNet.Core
</code></pre></div></div>

<p>Then, add a controller named <em>IncomingSmsController</em> and update its contents as below:</p>

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

<span class="k">namespace</span> <span class="nn">HighLowNumbergame.Controllers</span><span class="p">;</span>
 
<span class="p">[</span><span class="n">ApiController</span><span class="p">]</span>
<span class="p">[</span><span class="nf">Route</span><span class="p">(</span><span class="s">"[controller]"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">IncomingSmsController</span> <span class="p">:</span> <span class="n">TwilioController</span>
<span class="p">{</span>    
    <span class="p">[</span><span class="n">HttpPost</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">TwiMLResult</span><span class="p">&gt;</span> <span class="nf">Index</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">messagingResponse</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MessagingResponse</span><span class="p">();</span>
        <span class="n">messagingResponse</span><span class="p">.</span><span class="nf">Message</span><span class="p">(</span><span class="s">"Testing..."</span><span class="p">);</span>
        <span class="k">return</span> <span class="nf">TwiML</span><span class="p">(</span><span class="n">messagingResponse</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then, run your application:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run
</code></pre></div></div>

<p>You should see the HTTP port that your application is listening on (e.g http://localhost:5004)</p>

<p>Next, run ngrok and redirect to your local URL such as :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ngrok http http://localhost:5004
</code></pre></div></div>

<p>This will generate a public URL for you and tunnel all your requests to your local host.</p>

<p>The final step to receiving SMS is to inform Twilio that we accept incoming SMS at the public URL that ngrok gave.</p>

<p>Go to Twilio Console, select your number and update the <strong>A Message Comes In</strong> section to webhook. Set the webhook URL to your ngrok URL followed by /IncomingSMS, for example, <strong>https://{some random number}.eu.ngrok.io/IncomingSms</strong>.</p>

<p>Now when you send an SMS to that number, your API will receive the message. Once you’ve tested and received an SMS response saying “Testing…” you can move on to the game implementation.</p>

<h2 id="implementing-the-game">Implementing the Game</h2>

<p>The game logic is simple. The user is expected either the commands play (starts a new game), exit (ends the existing game) or a number between 1 and 100.</p>

<p>The key to the implementation is state management. As you can imagine, normally, your application receives a new SMS every time. You can, of course, choose to store the state in a database, but for small amounts of data, you can use <strong>cookies</strong>.</p>

<p>You can use cookies with Twilio SMS just like you would normally use in a web application. They chose not to reinvent the wheel and stuck with the web standards (You can read more on Twilio cookies <a href="https://support.twilio.com/hc/en-us/articles/223136287-How-do-Twilio-cookies-work-">here</a>).</p>

<p>In this implementation, it’s used like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">userMessage</span> <span class="p">==</span> <span class="s">"play"</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">currentGame</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Game</span><span class="p">();</span>
    <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">$"Welcome to number guessing game. Send your guesses between </span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">MIN_NUMBER</span><span class="p">}</span><span class="s"> and </span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">MAX_NUMBER</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
    <span class="n">Response</span><span class="p">.</span><span class="n">Cookies</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="s">"GAME_DATA"</span><span class="p">,</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">currentGame</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Game class just stores the target and guess count:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">HighLowNumbergame</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">Game</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Target</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Random</span><span class="p">((</span><span class="kt">int</span><span class="p">)</span><span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">.</span><span class="n">Ticks</span><span class="p">).</span><span class="nf">Next</span><span class="p">(</span><span class="n">Constants</span><span class="p">.</span><span class="n">MIN_NUMBER</span><span class="p">,</span> <span class="n">Constants</span><span class="p">.</span><span class="n">MAX_NUMBER</span> <span class="p">+</span> <span class="m">1</span><span class="p">);</span> <span class="c1">// Ceiling is exclusive so add 1</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">GuessCount</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>and the minimum and maximum numbers are defined in a file named <em>Constants.cs</em>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Constants</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">MIN_NUMBER</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">int</span> <span class="n">MAX_NUMBER</span> <span class="p">=</span> <span class="m">100</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Final step: Replace the controller code with the code below:</p>

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

<span class="k">namespace</span> <span class="nn">HighLowNumbergame.Controllers</span><span class="p">;</span>
 
<span class="p">[</span><span class="n">ApiController</span><span class="p">]</span>
<span class="p">[</span><span class="nf">Route</span><span class="p">(</span><span class="s">"[controller]"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">IncomingSmsController</span> <span class="p">:</span> <span class="n">TwilioController</span>
<span class="p">{</span>
    <span class="n">Game</span> <span class="n">currentGame</span><span class="p">;</span>
    <span class="kt">bool</span> <span class="n">CHEAT_MODE</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    
    <span class="p">[</span><span class="n">HttpPost</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">TwiMLResult</span><span class="p">&gt;</span> <span class="nf">Index</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">form</span> <span class="p">=</span> <span class="k">await</span> <span class="n">Request</span><span class="p">.</span><span class="nf">ReadFormAsync</span><span class="p">();</span>

        <span class="n">currentGame</span> <span class="p">=</span> <span class="nf">ResumeOrCreateGame</span><span class="p">();</span>
        
        <span class="kt">var</span> <span class="n">userMessage</span> <span class="p">=</span> <span class="n">form</span><span class="p">[</span><span class="s">"Body"</span><span class="p">].</span><span class="nf">ToString</span><span class="p">().</span><span class="nf">Trim</span><span class="p">().</span><span class="nf">ToLowerInvariant</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">responseMessage</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
        
        <span class="k">if</span> <span class="p">(</span><span class="n">userMessage</span> <span class="p">==</span> <span class="s">"play"</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">currentGame</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Game</span><span class="p">();</span>
            <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">$"Welcome to number guessing game. Send your guesses between </span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">MIN_NUMBER</span><span class="p">}</span><span class="s"> and </span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">MAX_NUMBER</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
            <span class="n">Response</span><span class="p">.</span><span class="n">Cookies</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="s">"GAME_DATA"</span><span class="p">,</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">currentGame</span><span class="p">));</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">userMessage</span> <span class="p">==</span> <span class="s">"exit"</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">currentGame</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">"No game in progress"</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">else</span>
            <span class="p">{</span>
                <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">$"Quiting game. The target was </span><span class="p">{</span><span class="n">currentGame</span><span class="p">.</span><span class="n">Target</span><span class="p">}</span><span class="s">. You guesses </span><span class="p">{</span><span class="n">currentGame</span><span class="p">.</span><span class="n">GuessCount</span><span class="p">}</span><span class="s"> times. Better luck next time!"</span><span class="p">;</span>
                <span class="n">Response</span><span class="p">.</span><span class="n">Cookies</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="s">"GAME_DATA"</span><span class="p">);</span>                
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="kt">int</span><span class="p">.</span><span class="nf">TryParse</span><span class="p">(</span><span class="n">userMessage</span><span class="p">,</span> <span class="k">out</span> <span class="kt">int</span> <span class="n">guessedNumber</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">currentGame</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">"No game in progress"</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">else</span>
            <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">guessedNumber</span> <span class="p">&lt;</span> <span class="n">Constants</span><span class="p">.</span><span class="n">MIN_NUMBER</span> <span class="p">||</span> <span class="n">guessedNumber</span> <span class="p">&gt;</span> <span class="n">Constants</span><span class="p">.</span><span class="n">MAX_NUMBER</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">$"Please guess between </span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">MIN_NUMBER</span><span class="p">}</span><span class="s"> and </span><span class="p">{</span><span class="n">Constants</span><span class="p">.</span><span class="n">MAX_NUMBER</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
                <span class="p">}</span>
                <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">guessedNumber</span> <span class="p">==</span> <span class="n">currentGame</span><span class="p">.</span><span class="n">Target</span><span class="p">)</span>
                <span class="p">{</span>
                    <span class="n">currentGame</span><span class="p">.</span><span class="n">GuessCount</span><span class="p">++;</span>
                    <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">$"Congratulations!. You've guessed correctly in </span><span class="p">{</span><span class="n">currentGame</span><span class="p">.</span><span class="n">GuessCount</span><span class="p">}</span><span class="s"> guesses."</span><span class="p">;</span>
                    <span class="n">Response</span><span class="p">.</span><span class="n">Cookies</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="s">"GAME_DATA"</span><span class="p">);</span>
                <span class="p">}</span>
                <span class="k">else</span>
                <span class="p">{</span>
                    <span class="n">currentGame</span><span class="p">.</span><span class="n">GuessCount</span><span class="p">++;</span>
                    
                    <span class="k">if</span> <span class="p">(</span><span class="n">guessedNumber</span> <span class="p">&gt;</span> <span class="n">currentGame</span><span class="p">.</span><span class="n">Target</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">"Too high!"</span><span class="p">;</span>
                    <span class="p">}</span>
                    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">guessedNumber</span> <span class="p">&lt;</span> <span class="n">currentGame</span><span class="p">.</span><span class="n">Target</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">"Too low!"</span><span class="p">;</span>
                    <span class="p">}</span>

                    <span class="n">Response</span><span class="p">.</span><span class="n">Cookies</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="s">"GAME_DATA"</span><span class="p">,</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">currentGame</span><span class="p">));</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">else</span>
        <span class="p">{</span>
            <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">"Unknown command"</span><span class="p">;</span>
        <span class="p">}</span>
        
        <span class="kt">var</span> <span class="n">messagingResponse</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MessagingResponse</span><span class="p">();</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">CHEAT_MODE</span> <span class="p">&amp;&amp;</span> <span class="n">currentGame</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">responseMessage</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">responseMessage</span><span class="p">}</span><span class="s">\n</span><span class="p">{</span><span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">currentGame</span><span class="p">)}</span><span class="s">"</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">messagingResponse</span><span class="p">.</span><span class="nf">Message</span><span class="p">(</span><span class="n">responseMessage</span><span class="p">);</span>
        <span class="k">return</span> <span class="nf">TwiML</span><span class="p">(</span><span class="n">messagingResponse</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="n">Game</span> <span class="nf">ResumeOrCreateGame</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">cookies</span> <span class="p">=</span> <span class="n">Request</span><span class="p">.</span><span class="n">Cookies</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">cookies</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="s">"GAME_DATA"</span><span class="p">,</span> <span class="k">out</span> <span class="kt">string</span> <span class="n">rawGameJson</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="n">DeserializeObject</span><span class="p">&lt;</span><span class="n">Game</span><span class="p">&gt;(</span><span class="n">rawGameJson</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="k">new</span> <span class="nf">Game</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, run the application again, and you should be able to play the game. By default, I added a cheat mode flag that appends the game object to the output messages. This helps with debugging and testing.</p>

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

<p>In this post, I shared a small project I developed with Twilio SMS API. It was a fun little project, and I hope you enjoyed it as well. If you would like to get the full source code, you can clone my <a href="https://github.com/Dev-Power/sms-based-high-low-number-guessing-game">repository</a>.</p>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[How-to-Develop-an-Interactive-CLI-with-C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/07/How-to-Develop-an-Interactive-CLI-with-CSharp/"/>
    <updated>2025-10-07T10:30:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/07/How-to-Develop-an-Interactive-CLI-with-CSharp</id>
    <content type="html"><![CDATA[<p>In <a href="https://https://volkanpaksoy.com/archive/2025/10/02/develop-your-own-cli-with-csharp">a previous</a> article, we looked into how to develop your own CLI tools with dotnet CLI and CliFx library. This article builds on that knowledge and adds more advanced features of CliFx. It also adds a new library called Sharprompt to add interactive features to your CLI. Without further ado, let’s develop an interactive CLI.</p>

<h2 id="set-up-the-project">Set up the project</h2>

<p>First, clone the starter project to get the project up and running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/volkanpaksoy/sendgrid-dynamic-template-email-manager-cli.git <span class="nt">--branch</span> 00-starter-project
</code></pre></div></div>

<p>Then, open the solution in your IDE and look at <em>Program.cs</em>:</p>

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

<span class="k">await</span> <span class="k">new</span> <span class="nf">CliApplicationBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddCommandsFromThisAssembly</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">SetExecutableName</span><span class="p">(</span><span class="s">"dtm"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">SetDescription</span><span class="p">(</span><span class="s">"CLI to manage SendGrid Dynamic Email Templates"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">SetTitle</span><span class="p">(</span><span class="s">"SendGrid Dynamic Email Template Manager"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">RunAsync</span><span class="p">();</span>
</code></pre></div></div>

<p>This setup is all it takes to add <em>CliFx</em> to the Console Application and convert it into a CLI. Next, run the application, and you should see a result as below:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/01-terminal-output-for-cli-with-no-commands.png" alt="Terminal window showing output of dotnet run command showing the application running as a CLI and showing the output of the help command" /></p>

<p>A nice feature of a dotnet CLI is that it can be installed globally on your computer so that you don’t have to navigate to the project folder every time to run it.</p>

<p>To convert a Console Application into an installable dotnet tool, you add the following three lines to the <em>.csproj</em> file (the highlighted lines 8-10):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">&lt;</span><span class="n">Project</span> <span class="n">Sdk</span><span class="p">=</span><span class="s">"Microsoft.NET.Sdk"</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="n">PropertyGroup</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">OutputType</span><span class="p">&gt;</span><span class="n">Exe</span><span class="p">&lt;/</span><span class="n">OutputType</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">TargetFramework</span><span class="p">&gt;</span><span class="n">net6</span><span class="p">.</span><span class="m">0</span><span class="p">&lt;/</span><span class="n">TargetFramework</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">ImplicitUsings</span><span class="p">&gt;</span><span class="n">enable</span><span class="p">&lt;/</span><span class="n">ImplicitUsings</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">Nullable</span><span class="p">&gt;</span><span class="n">enable</span><span class="p">&lt;/</span><span class="n">Nullable</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">DockerDefaultTargetOS</span><span class="p">&gt;</span><span class="n">Linux</span><span class="p">&lt;/</span><span class="n">DockerDefaultTargetOS</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">PackAsTool</span><span class="p">&gt;</span><span class="k">true</span><span class="p">&lt;/</span><span class="n">PackAsTool</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">ToolCommandName</span><span class="p">&gt;</span><span class="n">dtm</span><span class="p">&lt;/</span><span class="n">ToolCommandName</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="n">PackageOutputPath</span><span class="p">&gt;./</span><span class="n">nupkg</span><span class="p">&lt;/</span><span class="n">PackageOutputPath</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="n">PropertyGroup</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="n">ItemGroup</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="n">PackageReference</span> <span class="n">Include</span><span class="p">=</span><span class="s">"CliFx"</span> <span class="n">Version</span><span class="p">=</span><span class="s">"2.2.6"</span> <span class="p">/&gt;</span>
    <span class="p">&lt;/</span><span class="n">ItemGroup</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="n">Project</span><span class="p">&gt;</span>

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

<p>Now, run the following command to publish the project as a NuGet package under the <em>./nupkg</em> folder:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet pack
</code></pre></div></div>

<p>Finally, install the CLI by running the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet tool <span class="nb">install</span> <span class="nt">--global</span> <span class="nt">--add-source</span> ./nupkg DynamicTemplateManager.Cli
</code></pre></div></div>

<p>You should see that your CLI is now installed as a dotnet tool:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/02-terminal-output-confirming-successful-installation.png" alt="Terminal window showing the successful output of dotnet tool install command" /></p>

<p>Now you can open a terminal and run <strong>dtm</strong> as a command, and your application will run:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/03-terminal-output-for-cli-execution.png" alt="Terminal window showing the output of dtm command. It shows the help text of the CLI" /></p>

<p>At this point, you have a working CLI developed with the <strong>dotnet tool</strong> and <strong>CliFx,</strong> but you don’t have any functionality to manage SendGrid Dynamic Email Templates.</p>

<h2 id="set-up-sendgrid">Set up SendGrid</h2>

<p>To send Emails with SendGrid, you will need to have a SendGrid account, an API key and a valid sender email address or domain. Setting all these deserves a separate blog post. I will not add all those details in this article to keep the focus on developing the CLI. Instead, I recommend reading this article to complete the basic setup. The rest of the article is going to assume you completed these steps.</p>

<p>All commands that call the SendGrid API need access to an API key value. It’s best to leverage environment variables to avoid providing the API key every time you run a command.</p>

<p>Fortunately, <a href="https://github.com/Tyrrrz/CliFx#environment-variables">CliFx supports Environment Variables</a>. Therefore, we can create an option and give it an alternative environment variable name. If we don’t provide the parameter from the command line, it uses the value stored in the environment variable. To see this in action, create a new folder inside your solution called <em>Commands</em>. Then, under this folder, create a new file named <em>SendGridCommandBase.cs</em> and replace its contents with the code below:</p>

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

<span class="k">namespace</span> <span class="nn">DynamicTemplateManager.Cli.Commands</span><span class="p">;</span>

<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">SendGridCommandBase</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">CommandOption</span><span class="p">(</span><span class="s">"sendgridApiKey"</span><span class="p">,</span> <span class="n">IsRequired</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span> <span class="n">EnvironmentVariable</span> <span class="p">=</span> <span class="s">"SENDGRID_API_KEY"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">SendGridApiKey</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This class is going to be the base class of all SendGrid commands.</p>

<p>To test environment variables, create a temporary command in a file named <em>EnvVarTestCommand.cs</em>.</p>

<p>Replace the contents with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">CliFx</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Attributes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Infrastructure</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">DynamicTemplateManager.Cli.Commands</span><span class="p">;</span>

<span class="p">[</span><span class="n">Command</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">EnvVarTestCommand</span> <span class="p">:</span> <span class="n">SendGridCommandBase</span><span class="p">,</span> <span class="n">ICommand</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">ValueTask</span> <span class="nf">ExecuteAsync</span><span class="p">(</span><span class="n">IConsole</span> <span class="n">console</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">SendGridApiKey</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">default</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Add an environment variable named SENDGRID_API_KEY to your system:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SENDGRID_API_KEY</span><span class="o">=</span>from_env_var
</code></pre></div></div>

<p>Now, run the application and provide the <em>sendGridApiKey</em> parameter as shown below:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run <span class="nt">--sendgridApiKey</span> from_cmd
</code></pre></div></div>

<p>The output should show <em>from_cmd</em>.</p>

<p>Rerun the application, but this time, don’t provide any parameters as shown below:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run
</code></pre></div></div>

<p>This time you should see <em>from_env_var</em> on your screen:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/04-environment-variable-test-output.png" alt="Terminal window showing the output of dotnet run. It shows when sendGridApiKey parameter is supplied it uses that. Otherwise it uses environment variable." /></p>

<h2 id="implement-the-first-cli-command">Implement the first CLI command</h2>

<p>It’s time to get to the meat of the project. You are now going to implement the individual commands for the CLI.</p>

<p>First, delete the <em>EnvVarTestCommand.cs</em> file from the project. If you haven’t followed along so far and would like to start now, you can do that by checking out the branch called <em>01-sendgrid-configuration</em>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout 01-sendgrid-configuration
git pull
</code></pre></div></div>

<p>In the Commands folder, create a subfolder named TemplateCommands, and inside it, create a file named ListTemplatesCommand.cs</p>

<p>Replace the contents of the new file with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">CliFx</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Attributes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Infrastructure</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">DynamicTemplateManager.Cli.Commands.TemplateCommands</span><span class="p">;</span>

<span class="p">[</span><span class="nf">Command</span><span class="p">(</span><span class="s">"list-templates"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ListTemplatesCommand</span> <span class="p">:</span> <span class="n">ICommand</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">ValueTask</span> <span class="nf">ExecuteAsync</span><span class="p">(</span><span class="n">IConsole</span> <span class="n">console</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"list-templates command"</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">default</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now run the application to see if the command is available to use:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run
</code></pre></div></div>

<p>The output should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/05-list-templates-command-in-the-help-output.png" alt="Terminal window showing output of dotnet run. It shows list-templates command in the help text." /></p>

<p>You can see the list-templates command is discovered by CliFx because we initially set it up as</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nf">AddCommandsFromThisAssembly</span><span class="p">()</span>
</code></pre></div></div>

<p>Another thing you might have noticed is that when you ran the application, it displayed the help output instead of executing the command. When you were testing the SendGrid API key, <em>EnvVarTestCommand</em> was executed automatically. The difference is that EnvVarTestCommand was decorated with</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Command</span><span class="p">]</span>
</code></pre></div></div>

<p><em>EnvVarTestCommand</em> didn’t have a name assigned to it, which made it the default command, whereas <em>ListTemplatesCommand</em> is decorated as below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Command</span><span class="p">(</span><span class="s">"list-templates"</span><span class="p">)]</span>
</code></pre></div></div>

<p>To execute the <em>list-templates</em> command, you need to specify the command explicitly, as shown below:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run list-templates
</code></pre></div></div>

<p>Putting all SendGrid-related code in a separate service is good practice to make the code more testable. So first, create a solution folder named <em>Services</em>. Then, create two more folders under this folder: <em>Interfaces</em> and <em>Impl</em>. Under Interfaces, create a file named <em>IDynamicTemplateService.cs</em>; under the Impl folder, create a file named <em>DynamicTemplateService.cs</em>. Your solution structure at this point should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/06-folder-structure-of-application-01.png" alt="Rider IDE project structure showing all the classes and folders in the solution" /></p>

<p>Before you start calling SendGrid API, you need to add SendGrid dotnet SDK. Also, to display the results nicely in the console, add the ConsoleTables NuGet package. Finally, for this stage, you are going to refactor the <em>Program.cs</em> file and use dependency injection to instantiate commands and services. To achieve this, you will need Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting packages.</p>

<p>Run the following code to add these packages to the project:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package SendGrid
dotnet add package ConsoleTables
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
</code></pre></div></div>

<p>Update the <em>Program.cs</em> file with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">CliFx</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Commands.TemplateCommands</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Services.Impl</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Services.Interfaces</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Extensions.DependencyInjection</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Extensions.Hosting</span><span class="p">;</span>

<span class="k">using</span> <span class="nn">IHost</span> <span class="n">host</span> <span class="p">=</span> <span class="n">Host</span><span class="p">.</span><span class="nf">CreateDefaultBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ConfigureServices</span><span class="p">((</span><span class="n">hostBuilderContext</span><span class="p">,</span> <span class="n">services</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">services</span>
        <span class="c1">// Register services</span>
        <span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">IDynamicTemplateService</span><span class="p">,</span> <span class="n">DynamicTemplateService</span><span class="p">&gt;()</span>
        
        <span class="c1">// Register commands</span>
        <span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">ListTemplatesCommand</span><span class="p">&gt;()</span>
    <span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="k">await</span> <span class="k">new</span> <span class="nf">CliApplicationBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddCommandsFromThisAssembly</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">SetExecutableName</span><span class="p">(</span><span class="s">"dtm"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">SetDescription</span><span class="p">(</span><span class="s">"CLI to manage SendGrid Dynamic Email Templates"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">SetTitle</span><span class="p">(</span><span class="s">"SendGrid Dynamic Email Template Manager"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">UseTypeActivator</span><span class="p">(</span><span class="n">host</span><span class="p">.</span><span class="n">Services</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">RunAsync</span><span class="p">();</span>
        

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

<p>Update DynamicTemplateService.cs as below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Services.Interfaces</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Newtonsoft.Json.Linq</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">SendGrid</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">DynamicTemplateManager.Cli.Services.Impl</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">DynamicTemplateService</span> <span class="p">:</span> <span class="n">IDynamicTemplateService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">)&gt;&gt;</span> <span class="nf">ListTemplates</span><span class="p">(</span><span class="kt">string</span> <span class="n">apiKey</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">sendGridClient</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SendGridClient</span><span class="p">(</span><span class="n">apiKey</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">queryParams</span> <span class="p">=</span> <span class="s">@"{
            'generations': 'dynamic',
            'page_size': 100
        }"</span><span class="p">;</span>
        
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
            <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">GET</span><span class="p">,</span>
            <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates"</span><span class="p">,</span>
            <span class="n">queryParams</span><span class="p">:</span> <span class="n">queryParams</span>
        <span class="p">);</span>
        
        <span class="k">if</span> <span class="p">(!</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">HandleFailedResponse</span><span class="p">(</span><span class="n">response</span><span class="p">);</span>
        <span class="p">}</span>
        
        <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">Body</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">().</span><span class="n">Result</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">resultJson</span> <span class="p">=</span> <span class="n">JObject</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">result</span><span class="p">);</span>
        
        <span class="kt">var</span> <span class="n">templateIdNameTuples</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">)&gt;();</span>
        <span class="kt">var</span> <span class="n">templates</span> <span class="p">=</span> <span class="n">JArray</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">resultJson</span><span class="p">[</span><span class="s">"result"</span><span class="p">].</span><span class="nf">ToString</span><span class="p">());</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">template</span> <span class="k">in</span> <span class="n">templates</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">templateIdNameTuples</span><span class="p">.</span><span class="nf">Add</span><span class="p">((</span><span class="n">template</span><span class="p">[</span><span class="s">"name"</span><span class="p">].</span><span class="nf">ToString</span><span class="p">(),</span> <span class="n">template</span><span class="p">[</span><span class="s">"id"</span><span class="p">].</span><span class="nf">ToString</span><span class="p">()));</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">templateIdNameTuples</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">private</span> <span class="k">void</span> <span class="nf">HandleFailedResponse</span><span class="p">(</span><span class="n">Response</span> <span class="n">response</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="n">Body</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">().</span><span class="n">Result</span><span class="p">;</span>
        
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">StatusCode</span><span class="p">);</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">result</span><span class="p">);</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>

        <span class="k">throw</span> <span class="k">new</span> <span class="nf">Exception</span><span class="p">(</span><span class="s">$"API call failed with code </span><span class="p">{</span><span class="n">response</span><span class="p">.</span><span class="n">StatusCode</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="p">}</span> 
<span class="p">}</span>
</code></pre></div></div>

<p>and update ListTemplatesCommand.cs file with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">CliFx</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Attributes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Infrastructure</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ConsoleTables</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Services.Impl</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Services.Interfaces</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">DynamicTemplateManager.Cli.Commands.TemplateCommands</span><span class="p">;</span>

<span class="p">[</span><span class="nf">Command</span><span class="p">(</span><span class="s">"list-templates"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ListTemplatesCommand</span> <span class="p">:</span> <span class="n">SendGridCommandBase</span><span class="p">,</span> <span class="n">ICommand</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IDynamicTemplateService</span> <span class="n">_dynamicTemplateService</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">ListTemplatesCommand</span><span class="p">(</span><span class="n">IDynamicTemplateService</span> <span class="n">dynamicTemplateService</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_dynamicTemplateService</span> <span class="p">=</span> <span class="n">dynamicTemplateService</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">public</span> <span class="n">ValueTask</span> <span class="nf">ExecuteAsync</span><span class="p">(</span><span class="n">IConsole</span> <span class="n">console</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">templates</span> <span class="p">=</span> <span class="n">_dynamicTemplateService</span><span class="p">.</span><span class="nf">ListTemplates</span><span class="p">(</span><span class="n">SendGridApiKey</span><span class="p">).</span><span class="n">Result</span><span class="p">;</span>

        <span class="kt">var</span> <span class="n">table</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ConsoleTable</span><span class="p">(</span><span class="s">"Template Name"</span><span class="p">,</span> <span class="s">"Template Id"</span><span class="p">);</span>
        
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">templateIdNameTuple</span> <span class="k">in</span> <span class="n">templates</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">table</span><span class="p">.</span><span class="nf">AddRow</span><span class="p">(</span><span class="n">templateIdNameTuple</span><span class="p">.</span><span class="n">Item1</span><span class="p">,</span> <span class="n">templateIdNameTuple</span><span class="p">.</span><span class="n">Item2</span><span class="p">);</span>
        <span class="p">}</span>
        
        <span class="n">console</span><span class="p">.</span><span class="n">Output</span><span class="p">.</span><span class="nf">Write</span><span class="p">(</span><span class="n">table</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>

        <span class="k">return</span> <span class="k">default</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Before you test the code, update the SENDGRID_API_KEY environment variable with the actual value:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SENDGRID_API_KEY</span><span class="o">={</span> YOUR SENDGRID API KEY <span class="o">}</span>
</code></pre></div></div>

<p>To test the code, run the list-templates command as shown below:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run list-templates
</code></pre></div></div>

<p>If all went well, you should see a successful result (an empty table is also a successful result) like this:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/07-list-templates-command-output.png" alt="Terminal window showing successful output of list-templates command" /></p>

<p>If you are getting errors, you can compare your code to the finished version at this stage by checking out the branch:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout 02-sendgrid-commands
git pull
</code></pre></div></div>

<p>To recap, you implemented your first CLI command by creating a class that implements the <em>ICommand</em> interface that comes with the CliFx library and decorating it with <em>[Command(“command name”)]</em> decorator.</p>

<p>Also, you implemented DynamicTemplateService, which will act as our SendGrid client to manage dynamic email templates.</p>

<p>Finally, you amended the original program setup to use dependency injection to instantiate commands and services.</p>

<p>Now you can move on to the details of Dynamic Template Service to get into SendGrid Dynamic Template API details.</p>

<h2 id="refactor-dynamictemplateservice">Refactor DynamicTemplateService</h2>

<p>Before diving into the API details, refactor the application a bit so it’s simplified.</p>

<p>Now that you’re using dependency injection, you can also read the environment variables at the start-up:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">IHost</span> <span class="n">host</span> <span class="p">=</span> <span class="n">Host</span><span class="p">.</span><span class="nf">CreateDefaultBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ConfigureServices</span><span class="p">((</span><span class="n">hostBuilderContext</span><span class="p">,</span> <span class="n">services</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">services</span>
        <span class="c1">// Register services</span>
        <span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">IDynamicTemplateService</span><span class="p">,</span> <span class="n">DynamicTemplateService</span><span class="p">&gt;()</span>

        <span class="c1">// Register commands</span>
        <span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">ListTemplatesCommand</span><span class="p">&gt;()</span>
    
        <span class="c1">// Configure settings</span>
        <span class="p">.</span><span class="n">Configure</span><span class="p">&lt;</span><span class="n">SendGridSettings</span><span class="p">&gt;(</span><span class="n">hostBuilderContext</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="nf">GetSection</span><span class="p">(</span><span class="s">"SendGridSettings"</span><span class="p">))</span>
    <span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>So that we can have SendGridSettings injected into the DynamicTemplateService:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">readonly</span> <span class="n">SendGridSettings</span> <span class="n">_sendGridSettings</span><span class="p">;</span>

<span class="k">public</span> <span class="nf">DynamicTemplateService</span><span class="p">(</span><span class="n">IOptions</span><span class="p">&lt;</span><span class="n">SendGridSettings</span><span class="p">&gt;</span> <span class="n">sendGridSettings</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">_sendGridSettings</span> <span class="p">=</span> <span class="n">sendGridSettings</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">)&gt;&gt;</span> <span class="nf">ListTemplates</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">sendGridClient</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SendGridClient</span><span class="p">(</span><span class="n">_sendGridSettings</span><span class="p">.</span><span class="n">ApiKey</span><span class="p">);</span>
<span class="c1">// ...</span>
</code></pre></div></div>

<p>This way, you don’t need SendGridCommandBase.cs. Delete it from the project and remove its reference from the ListTemplatesCommand class:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ListTemplatesCommand</span> <span class="p">:</span> <span class="n">ICommand</span>
</code></pre></div></div>

<p>Since you are no longer passing the API key to the ListTemplates method, you can also simplify the interface as follows:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Task</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">)&gt;&gt;</span> <span class="nf">ListTemplates</span><span class="p">();</span>
</code></pre></div></div>

<p>After refactoring, rerun the project and ensure the list-templates command still works.</p>

<p>Before moving on to implementing the CLI commands, there is one more refactoring you should do: Inject SendGridClient using dependency injection and <strong>SendGrid.Extensions.DependencyInjection</strong> library.</p>

<p>First, run the following command to install the library:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package SendGrid.Extensions.DependencyInjection
</code></pre></div></div>

<p>Update constructing the builder section in <em>Program.cs</em> as below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">IHost</span> <span class="n">host</span> <span class="p">=</span> <span class="n">Host</span><span class="p">.</span><span class="nf">CreateDefaultBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ConfigureServices</span><span class="p">((</span><span class="n">hostBuilderContext</span><span class="p">,</span> <span class="n">services</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">services</span>
        <span class="c1">// Register services</span>
        <span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">IDynamicTemplateService</span><span class="p">,</span> <span class="n">DynamicTemplateService</span><span class="p">&gt;()</span>

        <span class="c1">// Register commands</span>
        <span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">ListTemplatesCommand</span><span class="p">&gt;()</span>
        
        <span class="c1">// Register SendGridClient</span>
        <span class="p">.</span><span class="nf">AddSendGrid</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span> <span class="n">options</span><span class="p">.</span><span class="n">ApiKey</span> <span class="p">=</span> <span class="n">hostBuilderContext</span><span class="p">.</span><span class="n">Configuration</span><span class="p">[</span><span class="s">"SendGridSettings:ApiKey"</span><span class="p">])</span>
    <span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>Notice how we don’t need to read the ApiKey and pass it on to the DynamicTemplateService anymore. It’s handled when the SendGridClient is instantiated. So go ahead and remove the Configuration folder and SendGridSettings class inside it.</p>

<p>Update DynamicTemplateService.cs constructor and variable initialization section with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">readonly</span> <span class="n">ISendGridClient</span> <span class="n">_sendGridClient</span><span class="p">;</span>

<span class="k">public</span> <span class="nf">DynamicTemplateService</span><span class="p">(</span><span class="n">ISendGridClient</span> <span class="n">sendGridClient</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">_sendGridClient</span> <span class="p">=</span> <span class="n">sendGridClient</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now you can remove all the manual SendGridClient instantiations and replace them with the private variable, such as this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">)&gt;&gt;</span> <span class="nf">ListTemplates</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">queryParams</span> <span class="p">=</span> <span class="s">@"{
        'generations': 'dynamic',
        'page_size': 100
    }"</span><span class="p">;</span>
    
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
        <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">GET</span><span class="p">,</span>
        <span class="n">urlPath</span><span class="p">:</span> <span class="s">"templates"</span><span class="p">,</span>
        <span class="n">queryParams</span><span class="p">:</span> <span class="n">queryParams</span>
    <span class="p">);</span>
<span class="c1">// ...</span>
</code></pre></div></div>

<p>Notice in line 8 <strong>_sendGridClient</strong> is used now. Repeat this replacement in all the methods.</p>

<p>This way, the code is more concise. Finally, rerun the application and confirm the <em>list-templates</em> command still works.</p>

<p>You can get the latest version of the code up until this point by checking out 03-dynamic-template-service by running the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout 03-dynamic-template-service
git pull
</code></pre></div></div>

<h2 id="implement-all-api-calls-for-dynamic-template-manager-cli">Implement all API Calls for Dynamic Template Manager CLI</h2>

<p>Now it’s time to get into the details of the SendGrid API calls. To send requests to the SendGrid API, you can use the <strong>RequestAsync</strong> method on the <strong>SendGridClient</strong> class. Depending on the operation, you need to provide the HTTP method, endpoint URL, request body and query params.</p>

<h3 id="list-templates">List Templates</h3>

<p>You already implemented the List Templates command in the previous section, so first, let’s look at the API call behind that command:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">queryParams</span> <span class="p">=</span> <span class="s">@"{
    'generations': 'dynamic',
    'page_size': 100
}"</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">GET</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">"templates"</span><span class="p">,</span>
    <span class="n">queryParams</span><span class="p">:</span> <span class="n">queryParams</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="create-template">Create Template</h3>

<p>To create a new template, you need to send a <strong>POST</strong> request to the <strong>templates</strong> endpoint. </p>

<p>The following snippet shows the data and API call to create a new template:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">new</span>
<span class="p">{</span>
    <span class="n">name</span> <span class="p">=</span> <span class="n">templateName</span><span class="p">,</span> 
    <span class="n">generation</span> <span class="p">=</span> <span class="s">"dynamic"</span>
<span class="p">};</span>
  
<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">POST</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">"templates"</span><span class="p">,</span>
    <span class="n">requestBody</span><span class="p">:</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="p">);</span>

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

<p>The default value of the <em>generation</em> parameter is <em>legacy</em>, so make sure you set this value to <em>dynamic</em> to ensure the template supports dynamic replacement.</p>

<h3 id="update-template">Update Template</h3>

<p>You can only update the template’s name once it’s been created. You can do this by sending a PATCH request to the <strong>templates/{templateId}</strong> endpoint:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">new</span>
<span class="p">{</span>
    <span class="n">name</span> <span class="p">=</span> <span class="n">templateName</span><span class="p">,</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">PATCH</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
    <span class="n">requestBody</span><span class="p">:</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="delete-template">Delete Template</h3>

<p>Deleting the template is very similar to getting the template details. You send a request to the <strong>templates/{templateId}</strong> endpoint. The difference is you use the DELETE HTTP method, as shown in the snippet below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">DELETE</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">"</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="duplicate-template">Duplicate Template</h3>

<p>You send a POST request to the <strong>templates/{templateId}</strong> endpoint to duplicate an existing template. You also provide the new name of the duplicated template as shown below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">new</span>
<span class="p">{</span>
    <span class="n">name</span> <span class="p">=</span> <span class="n">templateName</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">POST</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
    <span class="n">requestBody</span><span class="p">:</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="list-versions">List Versions</h3>

<p>To get the version of a specific template, you send a <strong>GET</strong> request to the <strong>templates/{templateId}</strong> endpoint, where you provide the template id as shown below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">GET</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">"</span>
<span class="p">);</span>
</code></pre></div></div>

<p>The versions are listed as an array, so you parse with <em>JArray.Parse()</em>. Only names and ids are needed, so only those fields are returned in this example. There are more fields returned by the API, such as whether or not the version is active and the last time it was updated.</p>

<h3 id="create-version">Create Version</h3>

<p>You can create a new version by sending a <strong>POST</strong> request to the <strong>“templates/{templateId}/versions”</strong> endpoint:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">new</span>
<span class="p">{</span>
    <span class="n">template_id</span> <span class="p">=</span> <span class="n">templateId</span><span class="p">,</span>
    <span class="n">active</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span>
    <span class="n">name</span> <span class="p">=</span> <span class="n">versionName</span><span class="p">,</span>
    <span class="n">html_content</span> <span class="p">=</span> <span class="n">htmltemplateData</span><span class="p">,</span>
    <span class="n">generate_plain_content</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span>
    <span class="n">subject</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
    <span class="n">editor</span> <span class="p">=</span> <span class="s">"code"</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">POST</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">/versions"</span><span class="p">,</span>
    <span class="n">requestBody</span><span class="p">:</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p>The request generation is similar to creating a template that you saw earlier. The main difference is the endpoint. This request is sent to the <em>templates/{templateId}/versions</em> endpoint, where the template id is the id of your template.</p>

<p>Another difference is that you read the complete HTML data from the file and send it to the SendGrid API as our template. Also, note that we set the <em>code</em> value to the <em>editor</em> property. You also make the version active by setting the <em>active</em> property to 1.</p>

<h3 id="update-version">Update Version</h3>

<p>When you make changes to your template HTML, you don’t need to keep creating new versions. You can update an existing version with your updated HTML. The code below sends the request to update an existing template:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">new</span>
<span class="p">{</span>
    <span class="n">template_id</span> <span class="p">=</span> <span class="n">templateId</span><span class="p">,</span>
    <span class="n">active</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span>
    <span class="n">name</span> <span class="p">=</span> <span class="n">versionName</span><span class="p">,</span>
    <span class="n">html_content</span> <span class="p">=</span> <span class="n">htmltemplateData</span><span class="p">,</span>
    <span class="n">generate_plain_content</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span>
    <span class="n">subject</span> <span class="p">=</span> <span class="s">""</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">PATCH</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">/versions/</span><span class="p">{</span><span class="n">versionId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
    <span class="n">requestBody</span><span class="p">:</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p>You specify the template and version ids in the endpoint URL: <strong>“templates/{templateId}/versions/{versionId}”</strong></p>

<p>Other than the endpoint, it looks like creating a new version. However, there is one key difference: the <em>editor</em> property. </p>

<p>Notice in the code above that you don’t set editor. If you set it to code just like creating a new version, you get the following error:</p>

<blockquote>
  <p>You cannot switch editors once a dynamic template version has been created.</p>
</blockquote>

<p>Even if you specify the same value, it still gives this error, so it looks like it doesn’t check what the original value is. In the end, changing the editor when updating the version is not supported. So as soon as the API sees this value in the request, it rejects it.</p>

<h3 id="delete-version">Delete Version</h3>

<p>You can also delete a version by sending a GET request to the <strong>templates/{templateId}/versions/{versionId}</strong> endpoint as shown below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_sendGridClient</span><span class="p">.</span><span class="nf">RequestAsync</span><span class="p">(</span>
    <span class="n">method</span><span class="p">:</span> <span class="n">SendGridClient</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">DELETE</span><span class="p">,</span>
    <span class="n">urlPath</span><span class="p">:</span> <span class="s">$"templates/</span><span class="p">{</span><span class="n">templateId</span><span class="p">}</span><span class="s">/versions/</span><span class="p">{</span><span class="n">versionId</span><span class="p">}</span><span class="s">"</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="more-on-transactional-templates-api">More On Transactional Templates API</h3>

<p>The sample project and this article show how to use some API endpoints to manage your dynamic email templates. Please check out the official API documentation to learn more about all the operations supported by the SendGrid API.</p>

<h2 id="implement-all-cli-commands">Implement all CLI Commands</h2>

<p>By this point, you have a good understanding of how a CLI command is implemented. For example, the <em>list-templates</em> command does not require parameters. However, some commands do need parameters. For example, if you want to list the versions of a template, you need to pass the template id to the SendGrid API. You can achieve this with the current setup, but you would have to copy/paste the template id from the list-templates command output every time. Alternatively, you can go to the SendGrid dashboard and find your template id there, but there is always an extra step involved.</p>

<p>This is where <a href="https://github.com/shibayan/Sharprompt">Sharprompt</a> comes in. It is a library to make your CLI interactive. This way, we can display all the templates in your account and pass the selected template’s id to the SendGrid API. This approach eliminates the need to copy/paste the id from another source, making your CLI more user-friendly.</p>

<p>First, install the library:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Sharprompt
</code></pre></div></div>

<p>Under the TemplateCommands folder, add a new file called ListVersionsCommand.cs and replace the contents with the code below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">CliFx</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Attributes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">CliFx.Infrastructure</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ConsoleTables</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">DynamicTemplateManager.Cli.Services.Interfaces</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Sharprompt</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">DynamicTemplateManager.Cli.Commands.TemplateCommands</span><span class="p">;</span>

<span class="p">[</span><span class="nf">Command</span><span class="p">(</span><span class="s">"list-versions"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ListVersionsCommand</span> <span class="p">:</span> <span class="n">ICommand</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IDynamicTemplateService</span> <span class="n">_dynamicTemplateService</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">ListVersionsCommand</span><span class="p">(</span><span class="n">IDynamicTemplateService</span> <span class="n">dynamicTemplateService</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_dynamicTemplateService</span> <span class="p">=</span> <span class="n">dynamicTemplateService</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="k">public</span> <span class="n">ValueTask</span> <span class="nf">ExecuteAsync</span><span class="p">(</span><span class="n">IConsole</span> <span class="n">console</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">templates</span> <span class="p">=</span> <span class="n">_dynamicTemplateService</span><span class="p">.</span><span class="nf">ListTemplates</span><span class="p">().</span><span class="n">Result</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">templates</span><span class="p">.</span><span class="n">Count</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentException</span><span class="p">(</span><span class="s">"No available templates. Please create a template first."</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="kt">var</span> <span class="n">templateName</span> <span class="p">=</span> <span class="n">Prompt</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="s">"Please select a template"</span><span class="p">,</span> <span class="n">templates</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">t</span> <span class="p">=&gt;</span> <span class="n">t</span><span class="p">.</span><span class="n">Item1</span><span class="p">).</span><span class="nf">OrderBy</span><span class="p">(</span><span class="n">t</span> <span class="p">=&gt;</span> <span class="n">t</span><span class="p">).</span><span class="nf">ToList</span><span class="p">());</span>
        <span class="kt">var</span> <span class="n">templateId</span> <span class="p">=</span> <span class="n">templates</span><span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">t</span> <span class="p">=&gt;</span> <span class="n">t</span><span class="p">.</span><span class="n">Item1</span> <span class="p">==</span> <span class="n">templateName</span><span class="p">).</span><span class="n">Item2</span><span class="p">;</span>
        
        <span class="kt">var</span> <span class="n">versions</span> <span class="p">=</span> <span class="n">_dynamicTemplateService</span><span class="p">.</span><span class="nf">ListVersions</span><span class="p">(</span><span class="n">templateId</span><span class="p">).</span><span class="n">Result</span><span class="p">;</span>

        <span class="kt">var</span> <span class="n">table</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ConsoleTable</span><span class="p">(</span><span class="s">"Template Name"</span><span class="p">,</span> <span class="s">"Version Name"</span><span class="p">,</span> <span class="s">"Version Id"</span><span class="p">);</span>
        
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">versionIdNameTuple</span> <span class="k">in</span> <span class="n">templates</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">table</span><span class="p">.</span><span class="nf">AddRow</span><span class="p">(</span><span class="n">templateName</span><span class="p">,</span> <span class="n">versionIdNameTuple</span><span class="p">.</span><span class="n">Item1</span><span class="p">,</span> <span class="n">versionIdNameTuple</span><span class="p">.</span><span class="n">Item2</span><span class="p">);</span>
        <span class="p">}</span>
        
        <span class="n">console</span><span class="p">.</span><span class="n">Output</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">table</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
        <span class="k">return</span> <span class="k">default</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Notice on line 28 you are prompting the user to select one of the existing templates. When you run the application, the output should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/08-template-selection-in-list-versions-command.png" alt="Terminal window showing output of list-versions command. User is prompted to select an existing template from a list." /></p>

<p>After you’ve chosen a template, it should fetch the versions of that template and display them in a table:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/09-list-of-versions-of-a-template.png" alt="Terminal window showing the successful output of versions of a template listed on the screen in table format." /></p>

<p>The rest of the commands follow a similar pattern. You can check out the branch 04-cli-commands to get the code so far:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout 04-cli-commands
git pull
</code></pre></div></div>

<h2 id="update-the-final-cli-with-the-dotnet-tool">Update the final CLI with the dotnet tool</h2>

<p>Now, bring it home and update your installed CLI so you can run it anywhere in a terminal window. Run the following commands to get the final version of the project and update the installed tool:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout main
git pull
dotnet pack
dotnet tool update <span class="nt">--global</span> <span class="nt">--add-source</span> ./nupkg DynamicTemplateManager.Cli
</code></pre></div></div>

<p>Next, open a new terminal and run the following command to see the available commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dtm
</code></pre></div></div>

<p>Your output should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/dtm-cli/10-dtm-help-screen.png" alt="Terminal window showing the default help text and listing all the available commands" /></p>

<p>You can now use your CLI to manage your templates 🎉.</p>

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

<p>Congratulations! You’ve made it this far, which means you implemented a complete interactive CLI to manage your SendGrid Dynamic Email Templates. In addition, you learned how to use CliFx and Sharprompt libraries and the dotnet tool to install and update your CLI.</p>

<p>I hope you enjoyed this tutorial as much as I enjoyed writing it.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://https://volkanpaksoy.com/archive/2025/10/02/develop-your-own-cli-with-csharp">Develop your own CLI with C#</a></li>
  <li><a href="https://github.com/volkanpaksoy/sendgrid-dynamic-template-email-manager-cli">Dynamic Template Manager source code</a></li>
  <li><a href="https://github.com/Tyrrrz/CliFx">CliFx GitHub repo</a></li>
  <li><a href="https://github.com/shibayan/Sharprompt">Sharprompt GitHub repo</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[How to clone a GitHub repository]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/06/How-to-clone-a-GitHub-repository/"/>
    <updated>2025-10-06T12:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/06/How to clone a GitHub repository</id>
    <content type="html"><![CDATA[<p>These days it’s essential to know how to clone a GitHub repository as GitHub is the de-facto standard for source code repositories. According to <a href="https://expandedramblings.com/index.php/github-statistics/">this</a> source, GitHub has 100 million users. If you follow tech blogs, such as this one, you will be asked to clone a GitHub repository to follow along. In this post, you will look into all the ways to download code from GitHub to quickly get the source code you need without setting it up on the spot.</p>

<p>You can pick any public repository on GitHub to follow along. In this example, I will choose one from <a href="https://github.com/trending">trending repositories</a>: <a href="https://github.com/decalage2/awesome-security-hardening">Awesome Security Hardening</a>.</p>

<h2 id="method-1-download-zip-file">Method 1: Download Zip File</h2>

<p>If you don’t have anything on your computer, the easiest way to get the latest code from a GitHub repository is the <strong>Download Zip</strong> option.</p>

<p>When you land on the repository page, you will see a green button that says <strong>Code</strong> in the middle:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/01-code-button-on-repository-page.png" alt="Repository page showing Code button" /></p>

<p>Click the <strong>Code</strong> button and click the <strong>Download ZIP</strong> button in the dialog:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/02-download-zip-link.png" alt="A dialog box is displayed after clicking the Code button, showing the Download ZIP button." /></p>

<p>Your download should start automatically.</p>

<p>This method works even if you’re not logged in to GitHub.</p>

<p>Locate the downloaded file and extract:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/03-downloaded-code-repo.png" alt="The downloaded zip file is extracted to a folder and contents of the folder are shown." /></p>

<p>This example contains a single README.md file, as shown in the screenshot above.</p>

<h2 id="method-2-https">Method 2: HTTPS</h2>

<p>You need to have Git CLI installed on your computer for this method.</p>

<p>Click the <strong>Code</strong> button and copy the link shown in the <strong>HTTPS</strong> tab:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/04-copy-https-link.png" alt="A dialog box is displayed after clicking the Code button, showing the HTTPS tab and the link copied to the clipboard by clicking the Copy button next to the link" /></p>

<p>In a terminal, navigate to the parent folder where you want to save the source code and run the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/decalage2/awesome-security-hardening.git
</code></pre></div></div>

<p>You should see the results that look like this:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/05-git-clone-results-on-terminal.png" alt="A terminal window showing successful results of a git clone command" /></p>

<h2 id="method-3-ssh">Method 3: SSH</h2>

<p>You need to have <a href="https://git-scm.com/downloads">Git CLI</a> installed on your computer for this method. Also, you have to be logged in to your GitHub account and have created and set up an SSH key. See <a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh">the GitHub support page</a> to set up an SSH key.</p>

<p>As you may have noticed in the screenshots of Method 1 and Method 2, the SSH method is not visible when you are not logged in to your GitHub account.</p>

<p>First, log in to your account for the SSH method to work.</p>

<p>Then, visit the URL of the repository and click the Code button and copy the SSH link:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/06-copy-ssh-link.png" alt="Code button clicked and dialog showing SSH tab" /></p>

<p>In the terminal, run the following code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone git@github.com:decalage2/awesome-security-hardening.git
</code></pre></div></div>

<p>The output should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/07-git-clone-via-ssh-results-on-terminal.png" alt="Terminal window showing successful output of git clone command using SSH method" /></p>

<h2 id="method-4-github-cli">Method 4: GitHub CLI</h2>

<p>If you are using GitHub frequently, I’d recommend installing GitHub CLI.</p>

<p>Visit <a href="http://cli.github.com">cli.github.com</a> and click the Download button on the screen:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/09-github-cli-main-page.png" alt="Landing page of GitHub CLI" /></p>

<p>If you use Windows or Linux, GitHub will automatically show you the relevant download link.</p>

<p>If you are using macOS, I’d recommend using brew to install. <em>Brew</em> is a package manager for macOS and Linux, and you can install it by going to <a href="http://brew.sh">brew.sh</a> and running the installation script.</p>

<p>Then you can install GitHub CLI by simply running the following code in your terminal:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew install gh
</code></pre></div></div>

<p>After you’ve installed the CLI, run the following command to confirm it’s installed successfully:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh version
</code></pre></div></div>

<p>You should see something like this:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/10-gh-version-output.png" alt="Terminal window showing output of gh version command" /></p>

<p>Once you have the CLI installed, you have to log in to your GitHub by running</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh auth login
</code></pre></div></div>

<p>Select the options that match your environment and circumstances using the interactive CLI. For example, in the example below, I selected GitHub.com, HTTPS and Yes to log in with GitHub credentials and selected Login with a web browser:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/08-gh-auth-login-step-01.png" alt="Terminal window showing output of gh auth login command. Selected GitHub.com, HTTPS, GitHub credentials and login with a web browser options" /></p>

<p>Then, open GitHub.com in your default browser and enter the one-time code displayed on your terminal.</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/11-enter-authentication-code.png" alt="GitHub webpage asking for a Device Activation code" /></p>

<p>Then accept the authorization request for the CLI to access your GitHub account:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/12-accept-auth-request.png" alt="Authorize GitHub CLI page asking for permissions for CLI to access GitHub" /></p>

<p>After the CLI has been installed and authorized, cloning a GitHub repository is very easy. Run the following command to clone the example repository:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gh repo clone decalage2/awesome-security-hardening
</code></pre></div></div>

<p>and you should see the successful results:</p>

<p><img src="/images/vpblogimg/2025/10/cloning-git-repo/13-git-clone-via-gh-cli-results-on-terminal.png" alt="Terminal window showing successful output of gh repo clone command" /></p>

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

<p>You walked through all four methods to clone a GitHub repository in this article. Having the ability to clone a repository on GitHub is very important for a developer. So I hope you enjoyed this article and spent some time setting up your environment to be a more productive developer.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://git-scm.com/downloads">Git CLI</a></li>
  <li><a href="https://cli.github.com/">GitHub CLI</a></li>
</ul>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Programmatically Manage Excel Spreadsheets with .NET]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/05/Programmatically-Manage-Excel-Spreadsheets-with-dotnet/"/>
    <updated>2025-10-05T10:30:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/05/Programmatically-Manage-Excel-Spreadsheets-with-dotnet</id>
    <content type="html"><![CDATA[<p>Spreadsheets are great tools. They are user interface and database combined in one application and have been infinitely helpful for ages. In this post, we are going to look into a handy NuGet package called <strong>ClosedXML</strong> to create and manipulate Excel spreadsheets by using a CLI demo application.</p>

<h2 id="demo-use-case">Demo Use Case</h2>

<p>As the energy crisis is getting worse, electricity prices are skyrocketing everywhere. So I decided to create a spreadsheet to log daily electricity costs programmatically. This generally is possible if you have a Smart Meter and can get daily costs from your supplier. Either way, the main objective is to demonstrate using .NET and ClosedXML NuGet package.</p>

<p>The final spreadsheet will look like this:</p>

<p><img src="/images/vpblogimg/2025/10/programmatically-manage-excel-spreadsheets-with-dotnet/finished-spreadsheet.png" alt="Spreadsheet showing daily electricity costs and monthly total cost" /></p>

<h2 id="usage">Usage</h2>

<p>The project is a CLI project created using the dotnet tool and CliFx. You can also find the detailed <a href="https://volkanpaksoy.com/archive/develop-your-own-cli-with-c#">blog post</a> about creating your own CLIs published on this blog.</p>

<p>After you’ve cloned the repository, publish the application as a dotnet tool.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet pack
dotnet tool <span class="nb">install</span> <span class="nt">--global</span> <span class="nt">--add-source</span> ./ElectricityCost.CLI/nupkg/ ElectricityCost.CLI
</code></pre></div></div>

<p>Then you can use it anywhere on your machine.</p>

<p>For example, to create a new spreadsheet from the built-in template, you can run this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ec closedxml template new <span class="nt">--path</span> ElectricityCosts.xlsx
</code></pre></div></div>

<p>And you can add new costs by providing the day of the month and the cost value:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ec closedxml add --path ElectricityCosts.xlsx --day 2 --cost 3.44
</code></pre></div></div>

<h2 id="implementation-using-closexml---an-easier-alternative-to-openxml-sdk">Implementation: Using CloseXML - An easier alternative to OpenXML SDK</h2>

<p><a href="https://www.nuget.org/packages/ClosedXML/">ClosedXML</a> is a wrapper around OpenXML that makes Excel spreadsheet manipulation a breeze.</p>

<p>When working with a spreadsheet, you often want to address the cells by row and columns as you would typically do in a table and read/write data into it. ClosedXML allows us to do precisely that.</p>

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

<span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">workbook</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">XLWorkbook</span><span class="p">())</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">worksheet</span> <span class="p">=</span> <span class="n">workbook</span><span class="p">.</span><span class="n">Worksheets</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"ClosedXMLDemo"</span><span class="p">);</span>
    <span class="n">worksheet</span><span class="p">.</span><span class="nf">Cell</span><span class="p">(</span><span class="s">"A1"</span><span class="p">).</span><span class="n">Value</span> <span class="p">=</span> <span class="s">"Hello World!"</span><span class="p">;</span>
    <span class="n">workbook</span><span class="p">.</span><span class="nf">SaveAs</span><span class="p">(</span><span class="s">"Sample1.xlsx"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The code snippet above creates a new Excel file and saves it in the Sample.xlsx file. SaveAs might sound like it only handles existing files, but it is called to create new files.</p>

<p>Working on existing spreadsheets is also relatively straightforward. Pass the file’s path to the XLWorkbook constructor, and you can find the spreadsheet by a simple LINQ query.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">workbook</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">XLWorkbook</span><span class="p">(</span><span class="s">"Sample1.xlsx"</span><span class="p">)</span> <span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">worksheet</span> <span class="p">=</span> <span class="n">workbook</span><span class="p">.</span><span class="n">Worksheets</span><span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="n">ws</span> <span class="p">=&gt;</span> <span class="n">ws</span><span class="p">.</span><span class="n">Name</span> <span class="p">==</span> <span class="s">"ClosedXMLDemo"</span><span class="p">);</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">worksheet</span><span class="p">.</span><span class="nf">Cell</span><span class="p">(</span><span class="s">"A1"</span><span class="p">).</span><span class="n">Value</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And setting formulas is also as simple as setting the value. The following snippet shows a formula to calculate the sum of daily costs:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">worksheet</span><span class="p">.</span><span class="nf">Cell</span><span class="p">(</span><span class="s">"E1"</span><span class="p">).</span><span class="n">FormulaA1</span> <span class="p">=</span> <span class="s">$"=SUM(B2:B</span><span class="p">{</span><span class="n">numberOfDaysInCurrentMonth</span> <span class="p">+</span> <span class="m">1</span><span class="p">}</span><span class="s">)"</span><span class="p">;</span>
</code></pre></div></div>

<p>Managing the styles is also quite intuitive:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">rngSubTotals</span> <span class="p">=</span> <span class="n">rngTable</span><span class="p">.</span><span class="nf">Range</span><span class="p">(</span><span class="s">"D2:E3"</span><span class="p">);</span>
<span class="n">rngSubTotals</span><span class="p">.</span><span class="n">Style</span><span class="p">.</span><span class="n">Alignment</span><span class="p">.</span><span class="n">Horizontal</span> <span class="p">=</span> <span class="n">XLAlignmentHorizontalValues</span><span class="p">.</span><span class="n">Center</span><span class="p">;</span>
<span class="n">rngSubTotals</span><span class="p">.</span><span class="n">Style</span><span class="p">.</span><span class="n">Font</span><span class="p">.</span><span class="n">Bold</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
<span class="n">rngSubTotals</span><span class="p">.</span><span class="n">Style</span><span class="p">.</span><span class="n">Font</span><span class="p">.</span><span class="n">FontSize</span> <span class="p">=</span> <span class="m">20</span><span class="p">;</span>
<span class="n">rngSubTotals</span><span class="p">.</span><span class="n">Style</span><span class="p">.</span><span class="n">Font</span><span class="p">.</span><span class="n">FontColor</span> <span class="p">=</span> <span class="n">XLColor</span><span class="p">.</span><span class="n">Red</span><span class="p">;</span>
<span class="n">rngSubTotals</span><span class="p">.</span><span class="n">Style</span><span class="p">.</span><span class="n">NumberFormat</span><span class="p">.</span><span class="n">Format</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">CURRENCY_SYMBOL</span><span class="p">}</span><span class="s"> #,##0.00"</span><span class="p">;</span>
</code></pre></div></div>

<p>For the sake of brevity, I’m not going to put all the code in this post. So, please visit the <a href="https://github.com/Dev-Power/programmatically-manage-excel-spreadsheets-with-dotnet/tree/main">GitHub repo</a> and play around with the code.</p>

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

<p>In this post, we looked into using a very intuitive and powerful NuGet package: ClosedXML. However, dealing with OpenXML directly can be overwhelming so having the ability to manipulate Excel spreadsheets with a straightforward tool is handy.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://github.com/ClosedXML/ClosedXML">ClosedXML GitHub repository</a></li>
  <li><a href="https://github.com/Dev-Power/programmatically-manage-excel-spreadsheets-with-dotnet/tree/main">Source code of the project</a></li>
</ul>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Semantic Versioning]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/04/Semantic-Versioning/"/>
    <updated>2025-10-04T12:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/04/Semantic-Versioning</id>
    <content type="html"><![CDATA[<p>Versioning is an important and one of the rather tricky aspects of software development. Simply put, versioning assigns a unique number that identifies a specific package or release. This post will look into a popular versioning method called Semantic Versioning.</p>

<h2 id="what-is-semantic-versioning">What is Semantic Versioning?</h2>

<p>Semantic Versioning is a popular versioning scheme that mainly uses a three-part version number. The version number is in the following format (there are pre-release versions as well, which we will look into in detail later):</p>

<blockquote>
  <p>MAJOR.MINOR.PATCH</p>
</blockquote>

<p>For example, if the version number is 2.3.54, the individual parts would mean:</p>

<ul>
  <li>2: Major version</li>
  <li>3: Minor version</li>
  <li>54: Patch version</li>
</ul>

<p>The rule of thumb when it comes to incrementing these numbers is:</p>

<h3 id="major-version">Major version</h3>

<p>The major version number is incremented when you make incompatible changes.</p>

<p>Examples:</p>

<ul>
  <li>
    <p>You removed an entire public class from a NuGet package</p>
  </li>
  <li>
    <p>You changed the parameters that the API endpoint accepts</p>
  </li>
</ul>

<h3 id="minor-version">Minor version</h3>

<p>The minor version is incremented when you add new functionality in a backwards-compatible manner.</p>

<p>Examples:</p>

<ul>
  <li>
    <p>You add a new endpoint to a public API</p>
  </li>
  <li>
    <p>You add a new parameter to a method with a default value so that old consumer can still call the method without breaking it.</p>
  </li>
</ul>

<h3 id="patch-version">Patch version</h3>

<p>The Patch version is incremented when making backwards-compatible fixes.</p>

<p>Examples:</p>

<ul>
  <li>You fixed a bug without making backwards-incompatible changes</li>
</ul>

<h2 id="pre-releases">Pre-releases</h2>

<p>In addition to the major, minor and patch versions, we may want to use pre-release versions. This would indicate that the product is not finalized. Examples of pre-release versions:</p>

<blockquote>
  <p>2.0.0-alpha</p>

  <p>2.0.0-alpha.1</p>

  <p>2.0.0-beta</p>

  <p>2.0.0-beta.1</p>

  <p>2.0.0-beta.2</p>

  <p>2.0.0-rc.1</p>
</blockquote>

<p>All the examples above are pre-release versions and are ordered from the lowest precedence to the highest one.</p>

<p>Please note semantic versioning has no knowledge of the words “alpha”, “beta”, or “rc”. The comparison is purely made by alphabetical order for non-numeric versions.</p>

<h2 id="build-metadata">Build metadata</h2>

<p>In addition to all the release and pre-release versions, we can also use extra metadata by using a plus sign (+) as a separator. This part is not used in precedence calculations and has informational purposes only. The metadata that follows the plus sign can be a series of dot-separated identifier lists.</p>

<p>For example, a version number with build metadata could look like this:</p>

<blockquote>
  <p>2.1.5+20220531 // Append the date of the release</p>

  <p>1.8-beta+sha.a4b5d6 // Append a hash value of the package</p>
</blockquote>

<h2 id="refactoring">Refactoring</h2>

<p>How about refactoring? It’s not a significant breaking change, and you don’t add new functionality. Also, it doesn’t count as bug fixes. It might help prevent bugs from being introduced in the future, but it doesn’t strictly count as fixing anything.</p>

<p>We can find the answer in the Semantic Versioning specs:</p>

<blockquote>
  <table>
    <tbody>
      <tr>
        <td>Patch version Z (x.y.Z</td>
        <td>x &gt; 0) MUST be incremented if only backwards compatible bug fixes are introduced. A bug fix is defined as an internal change that fixes incorrect behavior.</td>
      </tr>
    </tbody>
  </table>
</blockquote>

<p>So if you change the code for whatever reason, you must at least increment the patch version to maintain the uniqueness of the package/release.</p>

<h2 id="front-end-versioning">Front-end Versioning</h2>

<p>A common question and debated issue is how to version front-ends. The Semantic Versioning specification is all about “API changes”. API in this context can refer to a HTTP API or a package (NuGet, npm, Maven etc.).</p>

<p>Front-ends are consumed by end-users, and they are not consumed by other software.</p>

<p>By saying not consumed by other software, I’m not counting the web scrapers. When you develop a front-end page, you don’t make a contract with an external tool whose goal is to scrape data from your page. Most likely, it happens without your consent. Therefore, if you make a change in your markup that breaks web scrapers, it doesn’t constitute breaking change in an API.</p>

<p>Defining a “breaking change” in a front-end is not easy. For example, if you move functionality to another page, it might be seen as a breaking change as some users might fail to find the new location of the functionality. Does this mean that you should increment the major version? If you did, what would that mean to the end-users? They still need to use the application/website like before. When interacting with actual human users, version numbers don’t mean much.</p>

<h3 id="users-dont-care-about-your-versions">Users don’t care about your versions</h3>

<p>The only exception I can think of is a complete project overhaul. If everything changes so drastically, you may choose to refer to it as version 2.0 of your application.</p>

<p>Other than that, I’d argue the best way to handle changes in a user-facing application is by leveraging <strong>changelogs</strong>.</p>

<p>When a user logs in, you can display them a nice little pop-up and briefly explain the key things that changed. Some more complicated changes might need some interactive walkthroughs etc. The main point is, that a user doesn’t want to or need to know that you deployed 2.10.24 version of your application. So show them how it affects their experience and leave the technical details out.</p>

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

<p>Semantic Versioning is a very popular versioning scheme as it’s simple and flexible. Some aspects are debatable whether or not it should be used, such as front-end versioning, but this doesn’t mean that it does a good job most of the time. We also discussed how to handle versioning in user-facing applications.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://semver.org">Semantic Versioning</a></li>
</ul>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Exploring Chart.js with Star Wars data]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/03/Exploring-ChartJS-with-Star-Wars-data/"/>
    <updated>2025-10-03T12:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/03/Exploring-ChartJS-with-Star-Wars-data</id>
    <content type="html"><![CDATA[<p>Chart.js is a popular JavaScript library for creating beautiful charts with JavaScript. In this post, we will have some fun with it by visualising Star Wars data.</p>

<h2 id="installation">Installation</h2>

<p>There are various methods to install the library.</p>

<p>When it’s used in a project, I’d recommend using NPM. It’s simple, and it doesn’t involve any external CDN dependencies. You can install it by running the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i chart.js
</code></pre></div></div>

<p>Another way to use Chart.js is using a CDN. For example, you can use Cloudflare CDN:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;</span><span class="nx">script</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.8.0/chart.min.js</span><span class="dl">"</span> <span class="nx">integrity</span><span class="o">=</span><span class="dl">"</span><span class="s2">sha512-sW/w8s4RWTdFFSduOTGtk4isV1+190E/GghVffMA9XczdJ2MDzSzLEubKAs5h0wzgSJOQTRYyaz73L3d6RtJSg==</span><span class="dl">"</span> <span class="nx">crossorigin</span><span class="o">=</span><span class="dl">"</span><span class="s2">anonymous</span><span class="dl">"</span> <span class="nx">referrerpolicy</span><span class="o">=</span><span class="dl">"</span><span class="s2">no-referrer</span><span class="dl">"</span><span class="o">&gt;&lt;</span><span class="sr">/script</span><span class="err">&gt;
</span></code></pre></div></div>

<p>In the sample project, I set the responsive option to false so that the data is more visible. Otherwise, it uses the entire window, making it hard to see the whole chart.</p>

<h2 id="data-source">Data Source</h2>

<p>In this little project, our data will be coming from <a href="https://swapi.dev/">Star Wars API</a></p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/star-wars-api-1024x413.png" alt="Webpage showing landing page of Star Wars API" /></p>

<p>It’s a free-to-use API that returns Star Wars-related data which can be fun to use in little projects such as this one.</p>

<h2 id="source-code">Source Code</h2>

<p>I will not include all the source code in this post as it becomes too lengthy. You can access the complete code here: Source code of the project.</p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/finished-demo-animated.gif" alt="Animated GIF showing various chart types implemented in the sample application" /></p>

<h2 id="getting-started">Getting Started</h2>

<h3 id="bar-chart">Bar Chart</h3>

<p>Let’s get started with a bar chart that shows the number of characters and species shown in each film:</p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/bar-chart.png" alt="Bar chart showing the number of characters and species in Star Wars movies" /></p>

<ul>
  <li>In this example, we use dynamic data instead of a fixed array</li>
</ul>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="nx">data</span><span class="p">:</span> <span class="p">{</span>
<span class="nl">labels</span><span class="p">:</span> <span class="nx">data</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">d</span> <span class="o">=&gt;</span> <span class="nx">d</span><span class="p">.</span><span class="nx">title</span><span class="p">),</span>
<span class="nx">datasets</span><span class="p">:</span> <span class="p">[</span>
<span class="c1">// ...</span>
</code></pre></div></div>

<ul>
  <li>Chart.js doesn’t have built-in support to generate random colours automatically. So I used a function to return random colour values and called it for each member in the data array.</li>
  <li>The data we display on the Y-axis is specified in the data.datasets property. I only added two datasets in this example, but you can add as many as you like.</li>
</ul>

<h3 id="line-chart">Line chart</h3>

<p>The structure is very similar. The main change is to set the <strong>type</strong> property to the <strong>line</strong>.</p>

<ul>
  <li>In this example, I showed how to pass in object arrays rather than primitives as data.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// ...
data: data.map(d =&gt; ({ x: d.name, y: d.height })),
// ...
</code></pre></div></div>

<p>This way, I was able to specify the x and y-axis values in one statement.</p>

<ul>
  <li>One note about responsiveness and how it handles data. The people endpoint returns 82 people, and in the image below, you can see all their heights:</li>
</ul>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/line-chart-all-names-1024x334.png" alt="Line chart showing the height of the characters in Star Wars movies" /></p>

<p>If you disable responsiveness and draw the chart on a smaller canvas, it hides some labels and shows what it can that is still readable. I think this is a smart approach. There is no way to try to put 82 labels in a small space when none of them can be read.</p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/line-chart-some-names-hidden.png" alt="Line chart showing it's responsive and it will remove some labels when there is not enough space" /></p>

<p>Also, if you hover over the data points on the chart, it shows the label and the value (by which we can learn Yarael Poof is the tallest person with 264 cm. and Yoda is the shortest with 66 cm.)</p>

<h3 id="pie-and-doughnut-charts">Pie and Doughnut Charts</h3>

<p>These charts are good at visualising a percentage compared to the whole. Unfortunately, Star Wars API didn’t have box office data to show, so I gathered box office values from another site and displayed them in pie and doughnut charts:</p>

<p>Pie chart:</p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/box-office-values-in-pie-chart.png" alt="Pie chart showing Star Wars box Office values in USD" /></p>

<p>Doughnut chart:</p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/box-office-values-in-doughnut-chart.png" alt="Doughnut chart showing Star Wars box Office values in USD" /></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">doughnut</span><span class="dl">'</span>
</code></pre></div></div>

<p>Both chart types are very similar. The only difference is the middle is empty in the doughnut chart. So the only difference code-wise was to change the type.</p>

<h3 id="mixed-charts">Mixed Charts</h3>

<p>You can display different types in the same chart. The chart type for this type of chart needs to be a <strong>bar</strong>. It’s not very intuitive, but it’s how it works.</p>

<p>In the following example, I show the number of planets as a line chart, and the number of vehicles and starships as bar charts, all in the same visual:</p>

<p><img src="/images/vpblogimg/2025/10/exploring-chartjs-with-star-wars-data/mixed-chart.png" alt="Mixed chart showing the number of planets as a line chart, number of vehicles and starships as a bar chart" /></p>

<h3 id="other-charts">Other Charts</h3>

<p>In addition to the basic charts, Chart.js supports many other charts such as bubble charts, radar charts, scatter charts etc. I’d recommend checking the resources section for documentation and samples to see them in action.</p>

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

<p>This post covered how to install and use the Chart.Js library, which can be very useful in creating beautiful charts. We used Star Wars API to visualise Star Wars trivia to make the project even more fun!</p>

<p>I hope you found this post valuable and fun. Please let me know what you think in the comments below.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://www.chartjs.org/">Official Chart.js website</a></li>
  <li><a href="https://www.chartjs.org/docs/latest/samples/information.html">Samples</a></li>
  <li><a href="https://www.chartjs.org/docs/latest/">Documentation</a></li>
  <li><a href="https://swapi.dev/">Star Wars API</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Hidden gems in JavaScript Console API]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/01/Hidden-gems-in-JavaScript-Console-API/"/>
    <updated>2025-10-01T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/01/Hidden-gems-in-JavaScript-Console-API</id>
    <content type="html"><![CDATA[<p>Any developer who wrote any JavaScript code must have used the console.log() method at some point to log informational messages or for <a href="https://en.wikipedia.org/wiki/Debugging#Print_debugging">print debugging</a>. However, while that method is quite useful, it is not the only one in our arsenal. So let’s look at some useful methods that are not very commonly known to most developers.</p>

<h2 id="time--timelog--timeend">time() / timeLog() / timeEnd()</h2>

<p>If you have some long-running tasks and want to get some insights into how long a task takes, these methods are handy. You can start a timer by calling:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">console</span><span class="p">.</span><span class="nx">time</span><span class="p">();</span>
</code></pre></div></div>

<p>and end the timer by calling</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">console</span><span class="p">.</span><span class="nx">timeEnd</span><span class="p">();</span>
</code></pre></div></div>

<p>You can also pass a label to these methods to make the logs more readable. Otherwise, the times are logged under the <em>“default”</em> label.</p>

<p><em>timeLog()</em> method can be used anywhere between time and timeEnd to log the timer’s current value.</p>

<p>The example below uses all the features discussed above:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">console</span><span class="p">.</span><span class="nx">time</span><span class="p">();</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">10000000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">===</span> <span class="mi">5000000</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">timeLog</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">timeEnd</span><span class="p">();</span>
</code></pre></div></div>

<p>It loops 10 million times and logs the time halfway through (console.timeLog()) and at the end (console.timeEnd())</p>

<p>and the output looks like this:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-time-output.png" alt="Output of timeLog function showing the time it took for a loop" /></p>

<h2 id="table">table()</h2>

<p>This neat little feature can be useful when displaying tabular data. In this example, we are going to get the first ten results from people endpoint on <a href="https://swapi.py4e.com/">Star Wars API</a> and display them in a table:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">fetch</span> <span class="p">(</span><span class="dl">"</span><span class="s2">https://swapi.py4e.com/api/people/</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="nx">data</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
  <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="p">{</span> 
    <span class="nx">console</span><span class="p">.</span><span class="nx">table</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">response</span><span class="p">).</span><span class="nx">results</span><span class="p">);</span>
  <span class="p">});</span>
</code></pre></div></div>

<p>Let’s check out the results:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-table-output.png" alt="Output of table function call showing Star Wars displayed in a table format" /></p>

<p>And all it takes is five lines of code to produce this!</p>

<h2 id="warn--error">warn() / error()</h2>

<p>console.log() is fine for logging informational statements, but to make the warning and errors more readable, you can use warn() and error() methods. When checking the console, they stand out among all the other log lines.</p>

<p>For example, the following code:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;=</span> <span class="mi">10</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">===</span> <span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">warning: half-way through</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">error: all gone now</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>produces this output:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-warn-error-output.png" alt="Output of warn and error functions among information lines in the console" /></p>

<h2 id="debug">debug()</h2>

<p>I chose to cover debug in a separate section even though it works very similarly to log(), warn() and error() methods. By default, the log level in browsers is not set to display debug messages.</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/browser-log-level-settings.png" alt="Default debug levels dropdown opened showing all levels selected except Verbose" /></p>

<p>So if we run the previous example with <em>log()</em> call replaced by <em>debug()</em> call, the output looks very simplified:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;=</span> <span class="mi">10</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">debug</span><span class="p">(</span><span class="nx">i</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">===</span> <span class="mi">5</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="dl">"</span><span class="s2">warning: half-way through</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">error: all gone now</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>Output with debugging:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-debug-output-hidden.png" alt="Output showing only warn and error outputs when Verbose is de-selected" /></p>

<p>If we want to see all the statements, we need to explicitly set the logging level to verbose and get the same results as before.</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/log-level-set-to-verbose.png" alt="Debug levels dropdown expanded and showing all levels selected" /></p>

<h2 id="group--groupend--groupcollapsed">group() / groupEnd() / groupCollapsed()</h2>

<p><em>group()</em> and <em>groupEnd()</em> let us create indented log entries within a group label.</p>

<p>For example, the following code</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">console</span><span class="p">.</span><span class="nx">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">Group 1</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 1</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 2</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 3</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">groupEnd</span><span class="p">(</span><span class="dl">"</span><span class="s2">Group 1</span><span class="dl">"</span><span class="p">);</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">Group 2</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 1</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 2</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 3</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">groupEnd</span><span class="p">(</span><span class="dl">"</span><span class="s2">Group 2</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>produces this result:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-group-output.png" alt="group and groupEnd function output" /></p>

<p>We can also create log entries in a collapsed state so that they can only be viewed when we explicitly expand the log group:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">console</span><span class="p">.</span><span class="nx">group</span><span class="p">(</span><span class="dl">"</span><span class="s2">Group 1</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 1</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 2</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">entry 3</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">groupCollapsed</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hidden stuff until you expand</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hidden entry 1</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hidden entry 2</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">groupEnd</span><span class="p">(</span><span class="dl">"</span><span class="s2">Group 1</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>By default, the output looks like this:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-group-collapsed.png" alt="group, groupCollapsed and groupEnd function call output. Some lines are shown as collapsed. " /></p>

<p>To see the contents of the collapsed group, we need to expand it explicitly:</p>

<p><img src="/images/vpblogimg/2025/10/javascript-console/console-group-expanded.png" alt="Collapsed lines are expanded to show the originally hidden lines" /></p>

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

<p>This post looked into useful Console API methods that are not commonly known. To ensure compatibility, we didn’t look into non-standard methods such as timeStamp() and profile(). Instead, I’d recommend visiting the resources below and looking into all the methods. Some of them are not covered here but might be valuable to you.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://www.w3schools.com/jsref/obj_console.asp">W3Schools: Window Console Object</a></li>
  <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Console_API">MDN Web Docs: Console API</a></li>
  <li><a href="https://swapi.py4e.com/">Star Wars API</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Develop your own CLI with C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/10/01/Develop-your-own-CLI-with-CSharp/"/>
    <updated>2025-10-01T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/10/01/Develop-your-own-CLI-with-CSharp</id>
    <content type="html"><![CDATA[<p>Command-Line Interfaces (CLI) are invaluable tools for a developer. We use them daily to interact with AWS, Docker, GitHub, dotnet etc. We can develop scripts based on CLI commands to carry out complex tasks. In this post, we are going to develop a CLI for ourselves. Let’s get started!</p>

<h2 id="getting-started">Getting Started</h2>

<p>We are going to use two things:</p>

<ul>
  <li>dotnet tool command</li>
  <li>a very handy NuGet package called <a href="https://github.com/Tyrrrz/CliFx">CliFx</a></li>
</ul>

<h2 id="dotnet-tool">dotnet tool</h2>

<p>The simplest way to describe a dotnet tool is a <strong>console application</strong> distributed as a NuGet package.</p>

<p>Usually, when you go to a NuGet source site such as <a href="https://nuget.org">Nuget.org</a>, you deal with <strong>class libraries</strong>. You download the class library and consume it in your application.</p>

<p>Similarly, you can publish your console application as a dotnet tool in NuGet package format. This allows installing applications by simply using dotnet CLI, such as:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet tool <span class="nb">install</span> <span class="nt">--global</span> <span class="nt">--add-source</span> <span class="o">{</span>PACKAGE PATH<span class="o">}</span> <span class="o">{</span>PACKAGE NAME<span class="o">}</span>
</code></pre></div></div>

<p>To achieve that, all we have to do is create a new console application and modify the <em>csproj</em> file by adding the following lines:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;PackAsTool&gt;</span>true<span class="nt">&lt;/PackAsTool&gt;</span>
<span class="nt">&lt;ToolCommandName&gt;</span>{ COMMAND NAME }<span class="nt">&lt;/ToolCommandName&gt;</span>
<span class="nt">&lt;PackageOutputPath&gt;</span>./nupkg<span class="nt">&lt;/PackageOutputPath&gt;</span>
</code></pre></div></div>

<p>Now let’s have a walkthrough and see it in action:</p>

<ol>
  <li>Create a console application using dotnet CLI:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet new console
</code></pre></div></div>

<ol>
  <li>Edit the <em>csproj</em> file. In this example, I’m going to use JetBrains Rider IDE to edit, but you can use any IDE/text editor you want:</li>
</ol>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/edit-csproj.png" alt="Rider IDE showing Edit menu expanded and Edit .csproj file selected" /></p>

<ol>
  <li>Add the following lines inside the <em>PropertyGroup</em> element so that it looks something like this:</li>
</ol>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/add-xml-elements.png" alt="IDE showing .csproj file edited and new XML lines added" /></p>

<ol>
  <li>Run the following command to create the NuGet package:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet pack
</code></pre></div></div>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/dotnet-pack-output.png" alt="Finder window showing NuGet package created as output of dotnet pack command" /></p>

<ol>
  <li>Install it globally on your computer by running the following command:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet tool install --global --add-source ./nupkg develop-a-cli-with-csharp
</code></pre></div></div>

<p>Please note the last argument is the name of the root namespace, not the name of the CLI we are creating.</p>

<ol>
  <li>Now you can test the tool simply by running the name of the tool in the terminal:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mycli
</code></pre></div></div>

<p>and the output should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/tool-output.png" alt="Terminal window showing output of mycli command" /></p>

<p>Great! We have our tool installed nicely on the computer. We can run it anywhere in the terminal (regardless of the path we are in). But there is more to a CLI than simply executing a console application. The most important of a CLI is to have commands and subcommands. For example, when we use the dotnet CLI, we enter the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet tool <span class="nb">install</span> <span class="nt">--global</span> <span class="nt">--add-source</span> ./nupkg develop-a-cli-with-csharp
</code></pre></div></div>

<p>In this example,</p>

<ul>
  <li><em>dotnet</em> is the name of the CLI</li>
  <li><em>tool</em> is the command</li>
  <li><em>install</em> is the subcommand</li>
  <li>The rest are arguments passed to the subcommand</li>
</ul>

<p>We don’t have any mechanism to understand commands, subcommands and arguments. This is where CliFx comes in.</p>

<h2 id="clifx">CliFx</h2>

<p>CliFx is a simple to use NuGet package that adds the full capabilities of a CLI to our console application.</p>

<ol>
  <li>Let’s start with installing the package:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package CliFx
</code></pre></div></div>

<p>You should be able to see the package after running the command above:</p>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/package-install-output.png" alt="Rider IDE showing CliFx package added to the project" /></p>

<ol>
  <li>Replace the Main method with the following code:</li>
</ol>

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

<span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">Program</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">Main</span><span class="p">()</span> <span class="p">=&gt;</span>
        <span class="k">await</span> <span class="k">new</span> <span class="nf">CliApplicationBuilder</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">AddCommandsFromThisAssembly</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">SetExecutableName</span><span class="p">(</span><span class="s">"mycli"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">SetTitle</span><span class="p">(</span><span class="s">"My CLI"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">SetDescription</span><span class="p">(</span><span class="s">"A useful CLI tool to demo"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Build</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">RunAsync</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<ol>
  <li>Now, let’s create our commands by creating two new classes: HelloCommand and WorldCommand. They should look like the below:</li>
</ol>

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

<span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">Program</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">Main</span><span class="p">()</span> <span class="p">=&gt;</span>
        <span class="k">await</span> <span class="k">new</span> <span class="nf">CliApplicationBuilder</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">AddCommandsFromThisAssembly</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">SetExecutableName</span><span class="p">(</span><span class="s">"mycli"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">SetTitle</span><span class="p">(</span><span class="s">"My CLI"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">SetDescription</span><span class="p">(</span><span class="s">"A useful CLI tool to demo"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Build</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">RunAsync</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">Command</span><span class="p">(</span><span class="s">"hello world"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">WorldCommand</span> <span class="p">:</span> <span class="n">ICommand</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">ValueTask</span> <span class="nf">ExecuteAsync</span><span class="p">(</span><span class="n">IConsole</span> <span class="n">console</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">console</span><span class="p">.</span><span class="n">Output</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">default</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<ol>
  <li>Now run the application in the terminal without any parameters. You should get a nice help output:</li>
</ol>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/clifx-help-output.png" alt="Terminal window showing the output of mycli command. Only hello command is shown." /></p>

<ol>
  <li>Test the command and subcommand by running the following commands:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run <span class="nt">--</span> hello
dotnet run <span class="nt">--</span> hello world
</code></pre></div></div>

<p>The output should look like this:</p>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/clifx-command-output.png" alt="Terminal window showing application output showing hello and hello world commands executed" /></p>

<p>Notice that by running the “hello” command, we are executing the <em>ExecuteAsync</em> method in HelloCommand class. WorldCommand is a subcommand of the hello command, so we can execute a different method by running “hello world”.</p>

<p>At this point, our installed tool is not affected by these changes. So we have to pack and update our tool now by running the following commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet pack
dotnet tool update <span class="nt">--global</span> <span class="nt">--add-source</span> ./nupkg develop-a-cli-with-csharp
</code></pre></div></div>

<p>You can confirm the tool is updated by looking for output like this:</p>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/dotnet-tool-update-output.png" alt="Terminal window showing the output of dotnet tool update command" /></p>

<ol>
  <li>Finally, open another terminal window and type the CLI name</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mycli
</code></pre></div></div>

<p>and you should see the new help output listing the available commands in the CLI:</p>

<p><img src="/images/vpblogimg/2025/10/dotnet-cli/clifx-final-output.png" alt="Terminal window showing the output of mycli command running as a CLI" /></p>

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

<p>CLIs are handy tools for developers. In this post, we looked into creating a CLI capable of creating commands and subcommands. It can also be installed as a dotnet tool and distributed as a NuGet package.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install">dotnet tool documentation</a></li>
  <li><a href="https://github.com/Tyrrrz/CliFx">CliFx GitHub repo</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[How to download audio/video from an RSS feed with C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/30/How-to-download-audio-video-from-an-RSS-feed-with-C/"/>
    <updated>2025-09-30T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/30/How-to-download-audio-video-from-an-RSS-feed-with-C#</id>
    <content type="html"><![CDATA[<p>When you listen to a podcast, your podcast player downloads the <a href="https://en.wikipedia.org/wiki/RSS">RSS</a> feed of the publisher. Then, it checks the locally downloaded files and downloads the new ones as they come along. In this small project, I will develop a small C# application that downloads the entire media from an RSS feed.</p>

<p>All the information in the RSS feed is public, and all RSS clients download these media files.</p>

<p>Here’s how the program works in a nutshell:</p>

<ul>
  <li>It accepts two arguments: the URL of the RSS feed and the target directory to save the files into</li>
  <li>It then downloads the RSS feed into a temporary XML file</li>
  <li>It parses the XML and gets the following values: Title, publication date and the URL to download</li>
  <li>It loops through all the entries in the feed and saves the files to the local file system (It skips existing files so you can run it multiple times, and it won’t re-download unnecessarily)</li>
  <li>Finally, it deletes the temp XML file</li>
</ul>

<p>That’s all! Not a fancy podcatcher; it’s a fun little project you can use to archive your favourite podcasts.</p>

<p>The entire source code for the C# Console Application is below. Enjoy!</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Xml</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Net</span><span class="p">;</span>

<span class="kt">string</span> <span class="n">feedUrl</span> <span class="p">=</span> <span class="n">args</span><span class="p">[</span><span class="m">0</span><span class="p">];</span>
<span class="kt">string</span> <span class="n">feedLocalFileNnme</span> <span class="p">=</span> <span class="s">"temp-feed.xml"</span><span class="p">;</span>

<span class="kt">string</span> <span class="n">targetLocalDirectory</span> <span class="p">=</span> <span class="n">args</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="nf">TrimEnd</span><span class="p">(</span><span class="sc">'/'</span><span class="p">);</span>

<span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">WebClient</span><span class="p">())</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Downloading </span><span class="p">{</span><span class="n">feedUrl</span><span class="p">}</span><span class="s"> to </span><span class="p">{</span><span class="n">feedLocalFileNnme</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="n">client</span><span class="p">.</span><span class="nf">DownloadFile</span><span class="p">(</span><span class="n">feedUrl</span><span class="p">,</span> <span class="n">feedLocalFileNnme</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">string</span> <span class="n">rawXml</span> <span class="p">=</span> <span class="n">File</span><span class="p">.</span><span class="nf">ReadAllText</span><span class="p">(</span><span class="n">feedLocalFileNnme</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">xmlDocument</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">XmlDocument</span><span class="p">();</span>
<span class="n">xmlDocument</span><span class="p">.</span><span class="nf">LoadXml</span><span class="p">(</span><span class="n">rawXml</span><span class="p">);</span>

<span class="n">XmlNodeList</span> <span class="n">itemNodeList</span> <span class="p">=</span> <span class="n">xmlDocument</span><span class="p">.</span><span class="nf">SelectNodes</span><span class="p">(</span><span class="s">"/rss/channel/item"</span><span class="p">);</span>

<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">itemNodeList</span><span class="p">.</span><span class="n">Count</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="p">{</span>
    <span class="n">XmlNode</span> <span class="n">titleNode</span> <span class="p">=</span> <span class="n">itemNodeList</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">SelectNodes</span><span class="p">(</span><span class="s">"title"</span><span class="p">)[</span><span class="m">0</span><span class="p">];</span>
    <span class="n">XmlNode</span> <span class="n">enclosureNode</span> <span class="p">=</span> <span class="n">itemNodeList</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">SelectNodes</span><span class="p">(</span><span class="s">"enclosure"</span><span class="p">)[</span><span class="m">0</span><span class="p">];</span>
    <span class="n">XmlNode</span> <span class="n">pubDateNode</span> <span class="p">=</span> <span class="n">itemNodeList</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="nf">SelectNodes</span><span class="p">(</span><span class="s">"pubDate"</span><span class="p">)[</span><span class="m">0</span><span class="p">];</span>
    <span class="kt">string</span> <span class="n">urlToDownload</span> <span class="p">=</span> <span class="n">enclosureNode</span><span class="p">.</span><span class="n">Attributes</span><span class="p">[</span><span class="s">"url"</span><span class="p">].</span><span class="n">Value</span><span class="p">;</span>
    <span class="n">DateTime</span> <span class="n">pubDate</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">pubDateNode</span><span class="p">.</span><span class="n">InnerText</span><span class="p">);</span>
    <span class="kt">string</span> <span class="n">localFileName</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">targetLocalDirectory</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="nf">GetDate</span><span class="p">(</span><span class="n">pubDate</span><span class="p">)}</span><span class="s">-</span><span class="p">{</span><span class="n">titleNode</span><span class="p">.</span><span class="n">InnerText</span><span class="p">}</span><span class="s">.mp3"</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">File</span><span class="p">.</span><span class="nf">Exists</span><span class="p">(</span><span class="n">localFileName</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">WebClient</span><span class="p">())</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Downloading </span><span class="p">{</span><span class="n">urlToDownload</span><span class="p">}</span><span class="s"> to </span><span class="p">{</span><span class="n">localFileName</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            <span class="n">client</span><span class="p">.</span><span class="nf">DownloadFile</span><span class="p">(</span><span class="n">urlToDownload</span><span class="p">,</span> <span class="n">localFileName</span><span class="p">);</span>
            <span class="n">Thread</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="m">2000</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Skipping. File at </span><span class="p">{</span><span class="n">localFileName</span><span class="p">}</span><span class="s"> already exists."</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="kt">string</span> <span class="nf">GetDate</span><span class="p">(</span><span class="n">DateTime</span> <span class="n">pubDate</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="s">$"</span><span class="p">{</span><span class="n">pubDate</span><span class="p">.</span><span class="n">Year</span><span class="p">}</span><span class="s">-</span><span class="p">{</span><span class="n">pubDate</span><span class="p">.</span><span class="n">Month</span><span class="p">.</span><span class="nf">ToString</span><span class="p">().</span><span class="nf">PadLeft</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="sc">'0'</span><span class="p">)}</span><span class="s">-</span><span class="p">{</span><span class="n">pubDate</span><span class="p">.</span><span class="n">Day</span><span class="p">.</span><span class="nf">ToString</span><span class="p">().</span><span class="nf">PadLeft</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="sc">'0'</span><span class="p">)}</span><span class="s">"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="n">File</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="n">feedLocalFileNnme</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="usage">Usage</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet run <span class="nt">--</span> <span class="o">{</span>RSS URL<span class="o">}</span> <span class="o">{</span>Target Local Directory<span class="o">}</span>
</code></pre></div></div>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://www.w3schools.com/xml/xml_rss.asp">XML RSS</a></li>
</ul>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Anonymous Types in C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/29/Anonymous-Types-in-C/"/>
    <updated>2025-09-29T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/29/Anonymous-Types-in-C#</id>
    <content type="html"><![CDATA[<p>In the previous post, we discussed using the var keyword. One of the primary use cases of the var keyword is <strong>anonymous types</strong>. In this post, we are going to look closer into anonymous types.</p>

<h2 id="anonymous-types">Anonymous types</h2>

<p>An anonymous type is a nameless class that inherits from an object.</p>

<p>The type is inferred, by the compiler, at initialization.</p>

<p>For example, a typical anonymous declaration would look like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">person</span> <span class="p">=</span> <span class="k">new</span> 
<span class="p">{</span>
  <span class="n">FirstName</span> <span class="p">=</span> <span class="s">"John"</span><span class="p">,</span>
  <span class="n">LastName</span> <span class="p">=</span> <span class="s">"Power"</span><span class="p">,</span>
  <span class="n">Age</span> <span class="p">=</span> <span class="m">33</span>
<span class="p">};</span>
</code></pre></div></div>

<p>We can see, just like any other object, it supports IntelliSense. We can see all the properties we defined and the methods coming from the <em>Object class, such as ToString() and Equals(). So it is a strongly-typed class. We</em> don’t know the type.</p>

<p><img src="/images/vpblogimg/2025/09/csharp-anonymous-types/simple-anonymous-type-with-intellisense.png" alt="Auto-complete showing the properties of person object" /></p>

<p>The properties are read-only. If we try to assign another value, we get the following error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compilation error (line 11, col 3): Property or indexer 'AnonymousType#1.Age' cannot be assigned to -- it is read only
</code></pre></div></div>

<p><img src="/images/vpblogimg/2025/09/csharp-anonymous-types/read-only-assignment-error.png" alt="IDE showing error when assigning value to property" /></p>

<p>Since we don’t have a handle on the class, how can we create another object of this type? The answer is simple: We make another anonymous type with the same properties.</p>

<p>For example, the following example would compile successfully:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Program</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="nf">Main</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">firstPerson</span> <span class="p">=</span> <span class="k">new</span> 
        <span class="p">{</span>
          <span class="n">FirstName</span> <span class="p">=</span> <span class="s">"John"</span><span class="p">,</span>
          <span class="n">LastName</span> <span class="p">=</span> <span class="s">"Power"</span><span class="p">,</span>
          <span class="n">Age</span> <span class="p">=</span> <span class="m">33</span>
        <span class="p">};</span>

        <span class="kt">var</span> <span class="n">secondPerson</span> <span class="p">=</span> <span class="k">new</span> 
        <span class="p">{</span>
          <span class="n">FirstName</span> <span class="p">=</span> <span class="s">"Jane"</span><span class="p">,</span>
          <span class="n">LastName</span> <span class="p">=</span> <span class="s">"Power"</span><span class="p">,</span>
          <span class="n">Age</span> <span class="p">=</span> <span class="m">44</span>
        <span class="p">};</span>

        <span class="n">firstPerson</span> <span class="p">=</span> <span class="n">secondPerson</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>secondPerson can be assigned to <em>firstPerson</em> as they have the same type. An assignment is only possible if all the properties match. If we remove the Age property from the <em>secondPerson</em>, we get the error shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compilation error (line 18, col 17): Cannot implicitly convert type 'AnonymousType#1' to 'AnonymousType#2'
</code></pre></div></div>

<h3 id="shorthand-declarations">Shorthand Declarations</h3>

<p>We don’t need to specify the property names if we assign the object from another. So, for example, if we wanted to create a second object of the same type with the same values, we could use this syntax:</p>

<p><img src="/images/vpblogimg/2025/09/csharp-anonymous-types/shorthand-declaration.png" alt="Auto-complete showing the properties on secondPerson object" /></p>

<p>As shown in the screenshot above, we can still see the same property names as the firstPerson object.</p>

<p>The same feature exists in ES6.</p>

<h3 id="internals-of-anonymous-types">Internals of Anonymous Types</h3>

<p>So what happens when we compile our application with anonymous types? The type names are generated automatically by the compiler.</p>

<p>The example below shows what our class looks like with an IL viewer:</p>

<p><img src="/images/vpblogimg/2025/09/csharp-anonymous-types/anonymous-type-in-IL-viewer.png" alt="The output of IL viewer showing the compiler output of anonymous types" /></p>

<p>We declared the firstPerson and secondPerson objects defined (of the same type) as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>instance void class '&lt;&gt;f__AnonymousType0`3'&lt;string, string, int32&gt;::.ctor(!0/*string*/, !1/*string*/, !2/*int32*/)
</code></pre></div></div>

<p>These auto-generated type names are hidden from the developer because we don’t need to know what they are. So it’s generally a bad practice to find out these types via reflection.</p>

<p>The final example shows the IL output when I’ve removed the <em>Age</em> property from the secondPerson object. Now that the properties don’t match with <em>firstPerson</em>, the compiler generates a new type for <em>secondPerson</em> named <em>&lt;&gt; f__AnonymousType1’2</em>:</p>

<p><img src="/images/vpblogimg/2025/09/csharp-anonymous-types/multiple-anonymous-types-in-IL-viewer.png" alt="The output of IL viewer showing the compiler output of anonymous types when Age property is removed from secondPerson object" /></p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/anonymous-types">Anonymous Types</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Using var keyword in C#]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/28/Using-var-keyword-in-C/"/>
    <updated>2025-09-28T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/28/Using-var-keyword-in-C#</id>
    <content type="html"><![CDATA[<p>The <strong>var</strong> keyword in C# gives the programmer freedom to declare variables with implicit types. However, when to use it is a highly debated subject in the C# community. This post will look at the origins of the var keyword and our conclusion on when to use it.</p>

<h2 id="origins">Origins</h2>

<p>Microsoft introduced the var keyword in C# 3.0, which allows declaring “implicit” variable declarations. However, the variables declared with the var keyword are still strongly typed. The difference is that you don’t need to declare it yourself; the compiler determines the type.</p>

<p>For example, the following code would not compile:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">i</span> <span class="p">=</span> <span class="m">10</span><span class="p">;</span>
<span class="n">i</span> <span class="p">=</span> <span class="s">"x"</span><span class="p">;</span>
</code></pre></div></div>

<p>When i is declared, the type is determined to be an integer, so the string assignment fails with the following error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compilation error (line 7, col 7): Cannot implicitly convert type 'string' to 'int'
</code></pre></div></div>

<p>For the same reason, as you can imagine, you cannot just declare a variable with var type such as:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">i</span><span class="p">;</span>
</code></pre></div></div>

<p>which would fail with the following error:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Implicitly-typed local variables must be initialized
</code></pre></div></div>

<h2 id="usage">Usage</h2>

<p>Var keyword mainly has two usages:</p>

<ol>
  <li>Declare anonymous types</li>
  <li>Not repeat type name in a variable declaration and object instantiation</li>
</ol>

<p>The first usage is non-debatable, meaning that var is your only option if you declare anonymous types. Below is an example of using the var keyword to declare an anonymous type:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">person</span> <span class="p">=</span> <span class="k">new</span> <span class="p">{</span><span class="n">Id</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Jack"</span><span class="p">,</span> <span class="n">Age</span> <span class="p">=</span> <span class="m">25</span> <span class="p">}</span>
</code></pre></div></div>

<p>Another use case is a query expression where you select a new type.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">somePeople</span> <span class="p">=</span> <span class="k">from</span> <span class="n">person</span> <span class="k">in</span> <span class="n">people</span>
                 <span class="k">where</span> <span class="n">person</span><span class="p">.</span><span class="n">Age</span> <span class="p">&gt;</span> <span class="m">20</span>
                 <span class="k">select</span> <span class="k">new</span> <span class="p">{</span> <span class="n">person</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">Name</span> <span class="p">}</span>
</code></pre></div></div>

<p>In the second example, we create a new type on the fly rather than returning an existing one, so we must use the var keyword.</p>

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

<p>The debate around when to use var revolves around the second usage, as the first one is mandatory if you need anonymous types. So, should we use it as a shortcut and skip type declarations? Does that make the code easier or harder to read?</p>

<p>For example, consider a complex variable declaration as shown below:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;</span> <span class="n">items</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;();</span>
</code></pre></div></div>

<p>We can declare the same variable with the var keyword:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">items</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;();</span>
</code></pre></div></div>

<p>In my opinion, the second option is a lot clearer and makes the code easier to read.</p>

<p>Try to avoid duplication whenever possible.</p>

<p>Now let’s take a look at another example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">userService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">UserService</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="n">userService</span><span class="p">.</span><span class="nf">GetUser</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
</code></pre></div></div>

<p>What is the type of “user”? Without the help of the IDE we are using, there is no way to know for sure just by looking at the code above. It can be a User class, an IUser interface, or a subclass of User. At this point, we don’t know.</p>

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

<p>Here’s my rule of thumb on when to use var:</p>

<p>If the type of the variable is apparent in the initialization of the variable, it’s ok to use the var keyword.</p>

<p>For example, to review the above example, I’d go with explicit usage:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">userService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">UserService</span><span class="p">();</span>
<span class="n">User</span> <span class="n">user</span> <span class="p">=</span> <span class="n">userService</span><span class="p">.</span><span class="nf">GetUser</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
</code></pre></div></div>

<p>But if I were declaring the variable with a new keyword, I’d go with var:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">User</span><span class="p">();</span>
</code></pre></div></div>

<p>The benefit of this approach is especially obvious with complex types like the dictionary shown above:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">items</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;&gt;&gt;();</span>
</code></pre></div></div>

<p>In this case, duplicating the type name doesn’t add more clarity.</p>

<p>I hope this article helps you decide when to use the var keyword in C#.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/var">var (C# reference)</a></li>
  <li><a href="https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/anonymous-types">Anonymous Types</a></li>
</ul>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Working with AWS S3 and file hashes]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/27/Working-with-AWS-S3-and-file-hashes/"/>
    <updated>2025-09-27T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/27/Working-with-AWS-S3-and-file-hashes</id>
    <content type="html"><![CDATA[<p>Hashing is the operation of creating a unique, fixed-length string from any piece of data. The output is called a “hash” or “message digest”. It is a one-way operation meaning that you can obtain the original message by reverse-engineering the digest even if you knew the hashing algorithm used to create it. I love using hashes as they can provide great value in maintaining the <strong>security</strong> and <strong>integrity</strong> of our data.</p>

<h2 id="calculating-file-hashes-using-powershell">Calculating file hashes using PowerShell</h2>

<p>The cmdlet to use in PowerShell is <strong>Get-FileHash.</strong></p>

<p>Usage is very straightforward. You provide it with the path and the hashing algorithm you want to use:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-FileHash</span><span class="w">
   </span><span class="p">[</span><span class="nt">-Path</span><span class="p">]</span><span class="w"> </span><span class="err">&lt;</span><span class="nx">String</span><span class="p">[]</span><span class="err">&gt;</span><span class="w">
   </span><span class="p">[[</span><span class="nt">-Algorithm</span><span class="p">]</span><span class="w"> </span><span class="err">&lt;</span><span class="n">String</span><span class="err">&gt;</span><span class="p">]</span><span class="w">
   </span><span class="p">[</span><span class="err">&lt;</span><span class="n">CommonParameters</span><span class="err">&gt;</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>As you can see above, the Path parameter is a string array, so you can use it to calculate multiple hashes.</p>

<h2 id="how-to-use-file-hashes-with-aws-s3">How to use file hashes with AWS S3</h2>

<p>To verify the file’s integrity during upload, we can use the <strong>Content-MD5</strong> HTTP header. This header is not specific to AWS, but it fits perfectly when uploading files, especially if they are big media files.</p>

<p>You must convert the Content-MD5 value to Base64 before sending it in the request.</p>

<h2 id="preparing-the-lab-environment">Preparing the lab environment</h2>

<h3 id="downloading-a-sample-file">Downloading a sample file</h3>

<p>The file I worked with is a sample that’s publicly available <a href="https://file-examples.com/index.php/sample-audio-files/sample-mp3-download/">here</a>:</p>

<p>So I first fetched the file to my local lab:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://file-examples.com/storage/fee788409562ada83b58ed5/2017/11/file_example_MP3_5MG.mp3
</code></pre></div></div>

<p>The URL of the sample files keep changing, so don’t try the script above directly. Instead, get the link first, then run the command with your link.</p>

<p><img src="/images/vpblogimg/2025/09/file-hashes/wget-sample-file.png" alt="Output of wget command showing the download of a file" /></p>

<h3 id="generate-md5-hash">Generate MD5 hash</h3>

<p>To get the MD5 hash, I ran the following command:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-FileHash</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">/file_example_MP3_5MG.mp3</span><span class="w"> </span><span class="nt">-Algorithm</span><span class="w"> </span><span class="nx">MD5</span><span class="w">
</span></code></pre></div></div>

<p>and the output is:</p>

<p><img src="/images/vpblogimg/2025/09/file-hashes/get-filehash-output.png" alt="Terminal window showing the successful output of Get-FileHash cmdlet " /></p>

<h3 id="create-target-bucket">Create target bucket</h3>

<p>Creating a new S3 bucket is simple as follows:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">New-S3Bucket</span><span class="w"> </span><span class="nt">-BucketName</span><span class="w"> </span><span class="s2">"filehash-workout"</span><span class="w"> 
</span></code></pre></div></div>

<h3 id="send-the-file-with-hash">Send the file with hash</h3>

<p>Fortunately for us, AWS provides an easy way to use MD5 hashes when uploading the file with <strong>Write-S3Object</strong>. It automatically calculates the hash value for us:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Write-S3Object</span><span class="w"> </span><span class="nt">-BucketName</span><span class="w"> </span><span class="s2">"filehash-workout"</span><span class="w"> </span><span class="nt">-File</span><span class="w"> </span><span class="o">.</span><span class="nx">/file_example_MP3_5MG.mp3</span><span class="w">
</span></code></pre></div></div>

<p>The MD5 value is stored as an Etag value. You can see it on AWS Management Console:</p>

<p><img src="/images/vpblogimg/2025/09/file-hashes/md5-on-aws-management-console.png" alt="AWS S3 dashboard showing the Etag value of the uploaded file" /></p>

<h3 id="check-the-file-hash">Check the file hash</h3>

<p>As the final step, we need to pass the MD5 hash of the file on our end and see if it matches the value on AWS:</p>

<p>Please note from the above, the hash value is stored in all lowercase on AWS.</p>

<p>If we send the file hash as we get from Get-FileHash, we get the following error:</p>

<p><img src="/images/vpblogimg/2025/09/file-hashes/etag-mismatch-error.png" alt="Terminal window showing PreconditonFailed error after running Get-S3ObjectMetada cmdlet" /></p>

<p>When we convert the hash value to lowercase, we can get a successful result:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$filehash</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-FileHash</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">/file_example_MP3_5MG.mp3</span><span class="w"> </span><span class="nt">-Algorithm</span><span class="w"> </span><span class="nx">MD5</span><span class="p">)</span><span class="o">.</span><span class="nf">Hash</span><span class="w">
</span><span class="n">Get-S3ObjectMetadata</span><span class="w"> </span><span class="nt">-BucketName</span><span class="w"> </span><span class="s2">"filehash-workout"</span><span class="w"> </span><span class="nt">-Key</span><span class="w"> </span><span class="s2">"file_example_MP3_5MG.mp3"</span><span class="w"> </span><span class="nt">-EtagToMatch</span><span class="w"> </span><span class="s2">"</span><span class="nv">$filehash</span><span class="s2">"</span><span class="o">.</span><span class="nf">ToLower</span><span class="p">()</span><span class="w">
</span></code></pre></div></div>

<p><img src="/images/vpblogimg/2025/09/file-hashes/etag-match-success.png" alt="Terminal output showing successful output of Get-S3ObjectMetada cmdlet executed with correct hash value" /></p>

<p>This technique works for files up to 16MB. For larger files, Write-S3Object uses multipart upload, and the ETag value becomes the MD5 hash of the part.</p>

<h3 id="clean-up">Clean Up</h3>

<p>It’s always a good practice to clean up after a lab session:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Remove-S3Bucket</span><span class="w"> </span><span class="s2">"filehash-workout"</span><span class="w"> </span><span class="nt">-DeleteBucketContent</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="n">Remove-Item</span><span class="w"> </span><span class="o">.</span><span class="nx">/file_example_MP3_5MG.mp3</span><span class="w">
</span></code></pre></div></div>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash">Get-FileHash Documentation</a></li>
  <li><a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html">AWS S3 PutObject API reference</a></li>
</ul>
]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Customizing AWS IAM Sign-in URL]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/26/Customizing-AWS-IAM-Sign-in-URL/"/>
    <updated>2025-09-26T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/26/Customizing-AWS-IAM-Sign-in-URL</id>
    <content type="html"><![CDATA[<p>Usually, the login URL for IAM users is in this format</p>

<blockquote>
  <p>https://{Account Id}.signin.aws.amazon.com/console</p>
</blockquote>

<p>But it is possible to make this URL more memorable and user-friendly.</p>

<p>To achieve this, follow the steps below:</p>

<p><strong>Step 01</strong>: Go to <a href="https://console.aws.amazon.com/iamv2/home#/home">IAM Dashboard</a></p>

<p><strong>Step 02</strong>: Click on the <strong>Create</strong> link that is located right next to the account id:</p>

<p><img src="/images/vpblogimg/2025/09/customizing-iam-url/iam-dashboard.png" alt="AWS IAM dashboard showing Account Alias section with a Create button" /></p>

<p><strong>Step 03</strong>: This brings up the <em>Create Account Alias</em> dialog box.</p>

<p><img src="/images/vpblogimg/2025/09/customizing-iam-url/create-alias-01.png" alt="Create alias for account dialog is shown" /></p>

<p><strong>Step 03</strong>: Enter your desired alias and save.</p>

<p><img src="/images/vpblogimg/2025/09/customizing-iam-url/create-alias-02.png" alt="Create alias for account dialog with preferred alias value provided. It shows an information box saying IAM users can still access the account by account id" /></p>

<p>Since the name we provide goes in the URL, it must be unique globally.</p>

<p>If you don’t pick a unique name, you will get an error like this:</p>

<p><img src="/images/vpblogimg/2025/09/customizing-iam-url/create-alias-error.png" alt="Error message box saying alias not created because it already exists" /></p>

<p>and when you choose your unique name, you should see it in effect:</p>

<p><img src="/images/vpblogimg/2025/09/customizing-iam-url/create-alias-success.png" alt="IAM dashboard showing account alias created" /></p>

<p>You can keep on using the account id. Setting an alias gives an additional URL that you can use.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/console_account-alias.html">AWS Documentation: Your AWS account ID and its alias</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Counting Objects in AWS S3]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/25/Counting-Objects-in-AWS-S3/"/>
    <updated>2025-09-25T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/25/Counting Objects in AWS S3</id>
    <content type="html"><![CDATA[<p>Yesterday I had to find the count of objects in a folder in an S3 bucket. Unfortunately, I only had access to AWS via the command line and was working on a Windows Server.</p>

<h2 id="using-aws-cli">Using AWS CLI</h2>

<p>After digging around, I found the solution using PowerShells’ <em>Measure-Object</em> cmdlet.</p>

<p>The solution to getting the object count was:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 <span class="nb">ls </span>s3://<span class="o">{</span>bucket<span class="o">}</span>/path/to/files | Measure-Object
</code></pre></div></div>

<p>You can use it in local folders as well. It also can be used to get the minimum/maximum/average/total size of the folder too so quite handy to get some quick stats about a folder/bucket</p>

<h2 id="using-aws-tools-for-powershell">Using AWS Tools for PowerShell</h2>

<p>If you have AWS Tools for PowerShell installed, you can achieve the same goal by running the <em>Get-S3Object</em> cmdlet like this:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-S3Object</span><span class="w"> </span><span class="nt">-BucketName</span><span class="w"> </span><span class="p">{</span><span class="n">bucket</span><span class="p">}</span><span class="w"> </span><span class="nt">-Prefix</span><span class="w"> </span><span class="n">path/to/files</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Measure-Object</span><span class="w">
</span></code></pre></div></div>

<p>Alternatively, if you want to get the object count, you can run this as well:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">Get-S3Object</span><span class="w"> </span><span class="nt">-BucketName</span><span class="w"> </span><span class="p">{</span><span class="n">bucket</span><span class="p">}</span><span class="w"> </span><span class="nt">-Prefix</span><span class="w"> </span><span class="n">path/to/files</span><span class="p">)</span><span class="o">.</span><span class="nf">Count</span><span class="w">
</span></code></pre></div></div>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/measure-object">Measure-Object documentation</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[Easy Way to Convert Between CSV/JSON/XML with PowerShell]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/24/Easy-Way-to-Convert-Between-CSV-JSON-XML-with-PowerShell/"/>
    <updated>2025-09-24T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/24/Easy-Way-to Convert-Between-CSV-JSON-XML-with-PowerShell</id>
    <content type="html"><![CDATA[<p>Sometimes I’m amazed by how some tasks are easy to carry out using PowerShell. One such task is converting common data types such as CSV, JSON and XML. PowerShell has built-in support for all these types.</p>

<h2 id="convert-csv-to-json">Convert CSV to JSON</h2>

<p>For example, let’s say we have the following customer data in a CSV file named customers.csv</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Id,FirstName,LastName,Country,Status
1,James,Monroe,USA,Active
2,Diane,Wheatley,UK,Active
3,Sara,Bailey,UK,Suspended
</code></pre></div></div>

<p>If we want to convert this data into JSON, we can run the following command in PowerShell:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Csv</span><span class="w"> </span><span class="o">.</span><span class="nx">/customers.csv</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="o">.</span><span class="nx">/customers.json</span><span class="w">
</span></code></pre></div></div>

<p>we get the following JSON output:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"FirstName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"James"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"LastName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Monroe"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Country"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USA"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Active"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"FirstName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Diane"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"LastName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Wheatley"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Country"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UK"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Active"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"FirstName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sara"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"LastName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bailey"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Country"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UK"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Suspended"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>The output JSON is indented by default, so very easy to read as well.</p>

<h2 id="convert-json-to-csv">Convert JSON to CSV</h2>

<p>If we want to reverse the process and create a CSV file from a JSON input, we can run the following:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">/customers.json</span><span class="w"> </span><span class="nt">-Raw</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Export-CSV</span><span class="w"> </span><span class="o">.</span><span class="nx">/customers.csv</span><span class="w">
</span></code></pre></div></div>

<p>and get a CSV that looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"Id","FirstName","LastName","Country","Status"
"1","James","Monroe","USA","Active"
"2","Diane","Wheatley","UK","Active"
"3","Sara","Bailey","UK","Suspended"
</code></pre></div></div>

<p>I used the <strong>Export-CSV</strong> cmdlet in this example because it saves the output to a file. We can also use <strong>ConvertTo-Csv</strong> cmdlet</p>

<p>For example, the following snippet print the results to the console:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">/customers.json</span><span class="w"> </span><span class="nt">-Raw</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Csv</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Write-Host</span><span class="w">
</span></code></pre></div></div>

<h2 id="convert-json-to-xml">Convert JSON to XML</h2>

<p>Similar to JSON, we can use the <strong>ConvertTo-XML</strong> cmdlet to create an XML file out of our sample JSON:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Export-Clixml</span><span class="w"> </span><span class="nt">-Depth</span><span class="w"> </span><span class="nx">3</span><span class="w"> </span><span class="nt">-InputObject</span><span class="w"> </span><span class="p">((</span><span class="n">Get-Content</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="nx">/customers.json</span><span class="w"> </span><span class="nt">-Raw</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="p">)</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">.</span><span class="n">/customers.xml</span><span class="w">
</span></code></pre></div></div>

<p>and the output looks like this:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Objs</span> <span class="na">Version=</span><span class="s">"1.1.0.1"</span> <span class="na">xmlns=</span><span class="s">"http://schemas.microsoft.com/powershell/2004/04"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;Obj</span> <span class="na">RefId=</span><span class="s">"0"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;TN</span> <span class="na">RefId=</span><span class="s">"0"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;T&gt;</span>System.Object[]<span class="nt">&lt;/T&gt;</span>
      <span class="nt">&lt;T&gt;</span>System.Array<span class="nt">&lt;/T&gt;</span>
      <span class="nt">&lt;T&gt;</span>System.Object<span class="nt">&lt;/T&gt;</span>
    <span class="nt">&lt;/TN&gt;</span>
    <span class="nt">&lt;LST&gt;</span>
      <span class="nt">&lt;Obj</span> <span class="na">RefId=</span><span class="s">"1"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;TN</span> <span class="na">RefId=</span><span class="s">"1"</span><span class="nt">&gt;</span>
          <span class="nt">&lt;T&gt;</span>System.Management.Automation.PSCustomObject<span class="nt">&lt;/T&gt;</span>
          <span class="nt">&lt;T&gt;</span>System.Object<span class="nt">&lt;/T&gt;</span>
        <span class="nt">&lt;/TN&gt;</span>
        <span class="nt">&lt;MS&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Id"</span><span class="nt">&gt;</span>1<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"FirstName"</span><span class="nt">&gt;</span>James<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"LastName"</span><span class="nt">&gt;</span>Monroe<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Country"</span><span class="nt">&gt;</span>USA<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Status"</span><span class="nt">&gt;</span>Active<span class="nt">&lt;/S&gt;</span>
        <span class="nt">&lt;/MS&gt;</span>
      <span class="nt">&lt;/Obj&gt;</span>
      <span class="nt">&lt;Obj</span> <span class="na">RefId=</span><span class="s">"2"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;TNRef</span> <span class="na">RefId=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;MS&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Id"</span><span class="nt">&gt;</span>2<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"FirstName"</span><span class="nt">&gt;</span>Diane<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"LastName"</span><span class="nt">&gt;</span>Wheatley<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Country"</span><span class="nt">&gt;</span>UK<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Status"</span><span class="nt">&gt;</span>Active<span class="nt">&lt;/S&gt;</span>
        <span class="nt">&lt;/MS&gt;</span>
      <span class="nt">&lt;/Obj&gt;</span>
      <span class="nt">&lt;Obj</span> <span class="na">RefId=</span><span class="s">"3"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;TNRef</span> <span class="na">RefId=</span><span class="s">"1"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;MS&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Id"</span><span class="nt">&gt;</span>3<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"FirstName"</span><span class="nt">&gt;</span>Sara<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"LastName"</span><span class="nt">&gt;</span>Bailey<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Country"</span><span class="nt">&gt;</span>UK<span class="nt">&lt;/S&gt;</span>
          <span class="nt">&lt;S</span> <span class="na">N=</span><span class="s">"Status"</span><span class="nt">&gt;</span>Suspended<span class="nt">&lt;/S&gt;</span>
        <span class="nt">&lt;/MS&gt;</span>
      <span class="nt">&lt;/Obj&gt;</span>
    <span class="nt">&lt;/LST&gt;</span>
  <span class="nt">&lt;/Obj&gt;</span>
<span class="nt">&lt;/Objs&gt;</span>
</code></pre></div></div>

<p>A bit noisy, but the data is there.</p>

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

<p>Using PowerShell’s built-in cmdlets, we can easily convert between common data types such as CSV, JSON and XML. For a complete list of supported cmdlets, please check the link in the resources section.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/?view=powershell-7.2">cmdlets in Microsoft.PowerShell.Utility</a></li>
</ul>

]]></content>
  </entry>
  
  
  
  <entry>
    <title type="html"><![CDATA[LINQPad Dump Extension Method]]></title>
    <link href="https://volkanpaksoy.com/archive/2025/09/23/LINQPad-Dump-Extension-Method/"/>
    <updated>2025-09-23T11:00:00+00:00</updated>
    <id>https://volkanpaksoy.com/archive/2025/09/23/LINQPad-Dump-Extension-Method</id>
    <content type="html"><![CDATA[<p>I like LINQPad for prototyping C# applications and trying out short snippets. In many scenarios, I have to see the output of what I’m trying out. I used to treat my snippets as small console applications, and I used to use <em>Console.WriteLine()</em> statements to display the output.</p>

<p>Not anymore! In a tech video on YouTube, I saw this option and loved it:</p>

<p>In LINQPad, there’s a generic extension method called <strong>Dump()</strong>. It writes the output to the console. Exactly as Console.WriteLine but in a much more concise way.</p>

<p>For example:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">nums</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span><span class="p">,</span> <span class="m">6</span> <span class="p">};</span>
<span class="kt">var</span> <span class="n">sum</span> <span class="p">=</span> <span class="n">nums</span><span class="p">.</span><span class="nf">Aggregate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">a</span> <span class="p">+</span> <span class="n">b</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">sum</span><span class="p">);</span>
</code></pre></div></div>

<p>The code block above displays 21, and it can be shortened with the <em>Dump</em> method</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">nums</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span><span class="p">,</span> <span class="m">6</span> <span class="p">};</span>
<span class="n">nums</span><span class="p">.</span><span class="nf">Aggregate</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">a</span> <span class="p">+</span> <span class="n">b</span><span class="p">).</span><span class="nf">Dump</span><span class="p">();</span>
</code></pre></div></div>

<p>It shows the same result but in a shorter way.</p>

<h2 id="resources">Resources</h2>

<ul>
  <li><a href="https://www.linqpad.net/CustomizingDump.aspx">Customizing Dump Output</a></li>
  <li><a href="https://www.danclarke.com/linqpad-tips-and-tricks">LINQPad Tips and Tricks</a></li>
</ul>

]]></content>
  </entry>
  
  
</feed>