<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ghanavats</title>
    <description></description>
    <link>/</link>
    <atom:link href="/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Wed, 17 Jun 2026 16:22:05 +0000</pubDate>
    <lastBuildDate>Wed, 17 Jun 2026 16:22:05 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Stop Recreating .NET Solutions From Scratch</title>
        <description>&lt;p&gt;I have been there myself, so I am not preaching to you.&lt;/p&gt;

&lt;p&gt;I have spent far too much time building new .NET solutions from scratch. Actually, that is not completely true. Most of the time, I was not starting from scratch. I was copying from the first solution I built, or the second one, or the third one, then modifying it again for the fourth or fifth project.&lt;/p&gt;

&lt;p&gt;It worked, but it was painful.&lt;/p&gt;

&lt;p&gt;The worse part is that when you get used to a painful method, it starts to feel normal. Then normal becomes a habit. Then the habit becomes stubborn. Before you know it, you are defending a bad workflow just because you have repeated it enough times.&lt;/p&gt;

&lt;p&gt;That was my mistake.&lt;/p&gt;

&lt;p&gt;Every time I wanted to start a new API or SaaS-style project, I would spend time thinking about the same things again:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;project structure&lt;/li&gt;
  &lt;li&gt;configuration&lt;/li&gt;
  &lt;li&gt;OpenAPI&lt;/li&gt;
  &lt;li&gt;health checks&lt;/li&gt;
  &lt;li&gt;authentication and authorisation&lt;/li&gt;
  &lt;li&gt;logging&lt;/li&gt;
  &lt;li&gt;testing setup&lt;/li&gt;
  &lt;li&gt;architecture tests&lt;/li&gt;
  &lt;li&gt;maybe .NET Aspire&lt;/li&gt;
  &lt;li&gt;all the other boring but important moving parts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these things are useless. They matter. A good solution needs structure.&lt;/p&gt;

&lt;p&gt;But doing the same setup manually again and again is not engineering. It is waste.&lt;/p&gt;

&lt;h1 id=&quot;copy-and-paste-is-not-a-real-template&quot;&gt;Copy and paste is not a real template&lt;/h1&gt;
&lt;p&gt;There is nothing wrong with learning by manually creating a few solutions. In fact, you probably should do that at the beginning. It helps you understand what is actually inside your application.&lt;/p&gt;

&lt;p&gt;The problem starts when you already know your preferred setup, but you still keep copying old projects and cleaning them up by hand.&lt;/p&gt;

&lt;p&gt;That’s why I wrote this article. Believe me, things will go messy if you don’t change your approach.&lt;/p&gt;

&lt;p&gt;You copy an old solution. Then you rename projects. Then you fix namespaces. Then you remove old features. Then you update package versions. Then you change configuration files. Then you realise you forgot something. Then you discover some random leftover name from the previous project.&lt;/p&gt;

&lt;p&gt;That is not a clean starting point. That is a recycled accident.&lt;/p&gt;

&lt;p&gt;A proper template gives you a repeatable way to generate a clean starting point. The .NET CLI uses the Microsoft Template Engine behind &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotnet new&lt;/code&gt; to create projects and artifacts based on templates and options.&lt;/p&gt;

&lt;h1 id=&quot;microsoft-already-gave-us-the-tool&quot;&gt;Microsoft already gave us the tool&lt;/h1&gt;
&lt;p&gt;Microsoft Template Engine is not some obscure trick. It is the system behind &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotnet new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can use existing templates, install template packages, create your own templates, and generate projects from them. Microsoft’s documentation explains that .NET templates can generate projects, files, and resources, and template packages can be installed from NuGet, a NuGet package file, or a file system directory.&lt;/p&gt;

&lt;p&gt;How cool is that?&lt;/p&gt;

&lt;p&gt;That means your standard API setup does not need to live as a half-forgotten GitHub repository that you copy every few months.&lt;/p&gt;

&lt;p&gt;It can become a template.&lt;/p&gt;

&lt;p&gt;For example, instead of manually rebuilding the same solution structure, you could have something like:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;MySolution.Templates
dotnet new mysolution-api &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; MyNewApi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then your solution is generated with the structure and defaults you already decided are useful.&lt;/p&gt;

&lt;h1 id=&quot;what-should-go-into-a-good-net-template&quot;&gt;What should go into a good .NET template?&lt;/h1&gt;

&lt;p&gt;A useful template should not be a dumping ground for every idea you have ever had.&lt;/p&gt;

&lt;p&gt;That is another mistake.&lt;/p&gt;

&lt;p&gt;The goal is not to create a “perfect enterprise solution” before the project even starts. That usually leads to over-engineering. The goal is to create a sensible baseline that saves time without forcing unnecessary complexity.&lt;/p&gt;

&lt;p&gt;For an API template, I would consider including:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;a clean solution and project structure&lt;/li&gt;
  &lt;li&gt;sensible naming&lt;/li&gt;
  &lt;li&gt;OpenAPI setup&lt;/li&gt;
  &lt;li&gt;health checks&lt;/li&gt;
  &lt;li&gt;logging configuration&lt;/li&gt;
  &lt;li&gt;basic validation setup&lt;/li&gt;
  &lt;li&gt;test projects&lt;/li&gt;
  &lt;li&gt;architecture test project&lt;/li&gt;
  &lt;li&gt;common configuration files&lt;/li&gt;
  &lt;li&gt;optional authentication setup&lt;/li&gt;
  &lt;li&gt;optional .NET Aspire support, if it genuinely helps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microsoft’s template system supports replacing values, including and excluding files, and custom processing during template generation, so you can make templates flexible instead of hard-coding everything.&lt;/p&gt;

&lt;h1 id=&quot;the-real-benefit-is-not-speed&quot;&gt;The real benefit is not speed&lt;/h1&gt;
&lt;p&gt;When you create projects manually, every solution slowly becomes different. One has health checks. One does not. One has better logging. One has outdated package references. One has tests in a different structure. One has old naming conventions.&lt;/p&gt;

&lt;p&gt;That inconsistency becomes expensive later.&lt;/p&gt;

&lt;p&gt;A template forces you to make decisions once, improve them over time, and reuse them properly.&lt;/p&gt;

&lt;p&gt;That is how you turn experience into a tool.&lt;/p&gt;

&lt;h1 id=&quot;honest-tip&quot;&gt;Honest tip&lt;/h1&gt;
&lt;p&gt;Don’t do this too early.&lt;/p&gt;

&lt;p&gt;If you have only built one solution, you probably do not have a template yet. You have an experiment.&lt;/p&gt;

&lt;p&gt;Build manually a few times. Learn what repeats. Learn what is actually useful. Learn what you keep deleting. Then create a template from the parts that survive.&lt;/p&gt;

&lt;h1 id=&quot;how-to-create-a-simple-net-solution-template&quot;&gt;How to create a simple .NET solution template&lt;/h1&gt;
&lt;p&gt;Here is the part that confused me at first.&lt;/p&gt;

&lt;p&gt;Microsoft’s documentation explains custom templates, but the examples are mostly based around item templates, project templates, and packaging templates. That is useful, but it can make the process look more complicated than it needs to be when your actual goal is simple:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I already have a full .NET solution structure I like. I want to reuse it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You do not need to start with a strange folder structure such as working, content, and separate test folders just because a tutorial uses that approach.&lt;/p&gt;

&lt;p&gt;You can start with a real solution.&lt;/p&gt;

&lt;p&gt;Build the solution the way you actually want future projects to look.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;TemplateSolution/
│
├── TemplateSolution.sln
│
├── src/
│   ├── TemplateSolution.Api/
│   ├── TemplateSolution.Application/
│   ├── TemplateSolution.Domain/
│   └── TemplateSolution.Infrastructure/
│
├── tests/
│   ├── TemplateSolution.UnitTests/
│   └── TemplateSolution.ArchitectureTests/
│
└── .template.config/
    └── template.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The important part is this:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.template.config/template.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That folder should sit at the root of your template, next to the .sln file.&lt;/p&gt;

&lt;p&gt;Microsoft’s documentation confirms that the template.json file belongs inside a .template.config folder at the root of the template, and that the template source files can be whatever files and folders you want the template engine to use.&lt;/p&gt;

&lt;h1 id=&quot;step-1-create-your-normal-solution&quot;&gt;Step 1: Create your normal solution&lt;/h1&gt;
&lt;p&gt;Create your solution exactly how you like it.&lt;/p&gt;

&lt;p&gt;But be careful.&lt;/p&gt;

&lt;p&gt;A template should include repeatable foundations, not every random idea you once thought was clever.&lt;/p&gt;

&lt;p&gt;Also remove junk before turning it into a template, or use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitignore&lt;/code&gt;. Do not include secrets. Do not include real connection strings. Do not include environment-specific rubbish. That mistake is not “template engineering”; it is negligence.&lt;/p&gt;

&lt;h1 id=&quot;step-2-use-a-clear-placeholder-name&quot;&gt;Step 2: Use a clear placeholder name&lt;/h1&gt;
&lt;p&gt;Pick a source name that appears everywhere in the solution.&lt;/p&gt;

&lt;p&gt;For example &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TemplateSolution&lt;/code&gt;. Then use it in:&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;TemplateSolution.sln
TemplateSolution.Api
TemplateSolution.Application
TemplateSolution.Domain
TemplateSolution.Infrastructure
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This matters because the template engine can replace the configured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sourceName&lt;/code&gt; with the name provided by the user when they run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotnet new&lt;/code&gt;. Microsoft documents &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sourceName&lt;/code&gt; as the value the template engine searches for in file names and file contents, replacing it with the name passed through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-n&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So later, when someone runs this:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new my-api &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; Orders
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The template engine can replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TemplateSolution&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Orders&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the bit that saves you from painful renaming.&lt;/p&gt;

&lt;h1 id=&quot;step-3-add-templateconfigtemplatejson&quot;&gt;Step 3: Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.template.config/template.json&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;At the root of the solution, create this folder:&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.template.config
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Inside it, create this file:&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;template.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Put the following in as a starting point. This is more complicated than the one in Microsoft documentation. 
I needed a few more salt and pepper such as the ability to restore NuGet packages, language name and template type:&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://json.schemastore.org/template&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Saeed Ghanavat&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;classifications&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Common&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;WebApi&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ClassLibrary&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Clean Architecture&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;YourCompany API Solution&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Whatever description you want.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;identity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;YourCompany.ApiSolution.Templates&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;shortName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-api-template&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;language&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;C#&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;solution&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;sourceName&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;TemplateSolution&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;preferNameDirectory&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;primaryOutputs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;YouSolution.Templates.sln&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postActions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;actionId&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;210D431B-A78B-4D2F-B762-4ED3E3EA9025&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Restore NuGet packages required by this project.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;condition&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;(!skipRestore)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;continueOnError&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;manualInstructions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Run &apos;dotnet restore&apos;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$schema&lt;/code&gt; value is useful because editors that support JSON schemas can provide validation and IntelliSense. This helped me a lot because I was not guessing every property manually. Microsoft also documents this schema field and explains that the full schema is available through JSON Schema Store.&lt;/p&gt;

&lt;p&gt;The most important values are:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;shortName
sourceName
identity
name
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shortName&lt;/code&gt; is what you type in the CLI.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new my-api-template
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sourceName&lt;/code&gt; is the placeholder name that gets replaced:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;TemplateSolution
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;identity&lt;/code&gt; should be unique.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; is the friendly display name.&lt;/p&gt;

&lt;h1 id=&quot;step-4-install-the-template-locally&quot;&gt;Step 4: Install the template locally&lt;/h1&gt;
&lt;p&gt;Open a terminal at the root of your solution, where the .sln file and .template.config folder are.&lt;/p&gt;

&lt;p&gt;Then run:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; ./
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dotnet new install&lt;/code&gt; command installs a template package from a path or NuGet package ID. For local development, installing from the current folder is enough.&lt;/p&gt;

&lt;p&gt;Then check that your template is available:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should see your template listed with the short name you configured.&lt;/p&gt;

&lt;h1 id=&quot;step-5-generate-a-new-solution-from-your-template&quot;&gt;Step 5: Generate a new solution from your template&lt;/h1&gt;
&lt;p&gt;Now test it properly.&lt;/p&gt;

&lt;p&gt;Go to a different folder and run:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new my-api-template &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; OrdersService
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That should generate a new solution based on your template.&lt;/p&gt;

&lt;p&gt;Then build it, please. You don’t want a broken code generated off of your template.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;step-6-improve-the-template-over-time&quot;&gt;Step 6: Improve the template over time&lt;/h1&gt;
&lt;p&gt;Your first version will probably not be perfect.&lt;/p&gt;

&lt;p&gt;That is fine.&lt;/p&gt;

&lt;p&gt;Use it. Find what feels wrong. Remove unnecessary things. Add missing things. Keep improving the baseline. This is exactly what I did.&lt;/p&gt;

&lt;p&gt;When you change the template and need to reinstall it, you can uninstall and install it again:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dotnet new uninstall ./
dotnet new &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; ./
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is where the value starts to show.&lt;/p&gt;

&lt;p&gt;Instead of improving one project and forgetting to copy the improvement elsewhere, you improve the template. Then every new project starts from a better place.&lt;/p&gt;

&lt;p&gt;That is it.&lt;/p&gt;

&lt;p&gt;The hard part is not the tooling. The hard part is deciding what your standard solution should look like.&lt;/p&gt;

&lt;p&gt;And that is exactly why templates are useful. They force you to stop rebuilding the same foundation again and again, and they turn your repeated decisions into something reusable.&lt;/p&gt;
</description>
        <pubDate>Sun, 14 Jun 2026 00:57:00 +0000</pubDate>
        <link>/blog/2026-06-14-pack_you_dotnet_project_as_template/</link>
        <guid isPermaLink="true">/blog/2026-06-14-pack_you_dotnet_project_as_template/</guid>
        
        
        <category>DotNet</category>
        
        <category>Microsoft Template Engine</category>
        
        <category>NuGet</category>
        
      </item>
    
      <item>
        <title>Migrate from AWS WorkMail to Microsoft Exchange and take your emails with you!</title>
        <description>&lt;p&gt;AWS WorkMail is being retired, with end of support scheduled for &lt;strong&gt;31 March 2027&lt;/strong&gt;. I have been using it as my personal mail server since I began my AWS journey, so this was not something I wanted to ignore until the last minute.&lt;/p&gt;

&lt;p&gt;This article is specific to &lt;strong&gt;AWS WorkMail&lt;/strong&gt;, not Amazon SES. SES is a separate service and, at the time of writing, this retirement announcement is about WorkMail only.&lt;/p&gt;

&lt;h1 id=&quot;why-i-migrated&quot;&gt;Why I migrated&lt;/h1&gt;
&lt;p&gt;The main reason is obvious: WorkMail is being retired, so I needed to move away from it.&lt;/p&gt;

&lt;p&gt;I also had another reason. In my opinion, AWS WorkMail had a basic UI with basic capabilities. To be fair, that was mostly all I needed. But for my personal setup it was not particularly cheap. I was paying for the WorkMail organisation and each mailbox had its own monthly cost. In total, I was paying around &lt;strong&gt;$30 per month&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When AWS announced the retirement, I wanted to act early. I did not want to leave it until the last minute and then rush the migration under pressure. It also gave me a good reason to review whether I could run my personal email setup in a cheaper and cleaner way.&lt;/p&gt;

&lt;h1 id=&quot;what-needed-to-be-planned&quot;&gt;What needed to be planned&lt;/h1&gt;
&lt;p&gt;The first thing I needed was an alternative.&lt;/p&gt;

&lt;p&gt;After some investigation and comparing the pros and cons of a few options, I decided to use &lt;strong&gt;Microsoft Exchange Online&lt;/strong&gt;. Some may say it is overkill for a personal email setup. I would say it is sustainable, affordable for my use case, feature rich, and easy enough to set up.&lt;/p&gt;

&lt;p&gt;Below is the list I used to plan the migration:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Domain ownership verification&lt;/li&gt;
  &lt;li&gt;MX record reconfiguration&lt;/li&gt;
  &lt;li&gt;Email migration from AWS WorkMail to Exchange Online&lt;/li&gt;
  &lt;li&gt;How to reduce cost when creating mailboxes&lt;/li&gt;
  &lt;li&gt;How many licensed mailboxes I actually needed&lt;/li&gt;
  &lt;li&gt;User downtime risk and how to minimise it&lt;/li&gt;
  &lt;li&gt;What to test before and after the cutover&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;dns-and-domain-considerations&quot;&gt;DNS and domain considerations&lt;/h1&gt;
&lt;p&gt;I decided to keep my domain in &lt;strong&gt;AWS Route 53&lt;/strong&gt;. I also avoided removing any existing DNS records too early because I did not want to risk losing emails in flight.&lt;/p&gt;

&lt;p&gt;After setting up the new Microsoft Exchange Online subscription, I was able to verify ownership of my domain fairly quickly. This gave me room to manoeuvre. I could create the mailboxes, aliases, and shared mailboxes I needed before touching the MX records.&lt;/p&gt;

&lt;p&gt;At the beginning, the DNS and domain side was straightforward. I only had to revisit it properly once I was ready to complete the cutover and point the MX records to Exchange Online.&lt;/p&gt;

&lt;h1 id=&quot;mailboxuser-considerations&quot;&gt;Mailbox/user considerations&lt;/h1&gt;
&lt;p&gt;I decided to use only two licensed users: my account and my wife’s account. Only the two of us needed separate personal mailboxes.&lt;/p&gt;

&lt;p&gt;For everything else, I used &lt;strong&gt;aliases&lt;/strong&gt; and &lt;strong&gt;shared mailboxes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I created a handful of aliases under my own user mailbox. I like doing this because it gives me a meaningful separation of email addresses without creating unnecessary paid mailboxes.&lt;/p&gt;

&lt;p&gt;For emails that both of us need to manage, I created a few shared mailboxes and gave both of us access to read and manage them. When you create shared mailboxes in Exchange Online, Microsoft 365 creates the necessary underlying account objects. In my case, that was not an issue because the shared mailboxes were tied to the main licensed accounts for access and management.&lt;/p&gt;

&lt;p&gt;To give a better picture of what I ended up with, this is a simplified version:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;My licensed account
    &lt;ul&gt;
      &lt;li&gt;Alias 1&lt;/li&gt;
      &lt;li&gt;Alias 2&lt;/li&gt;
      &lt;li&gt;Alias 3&lt;/li&gt;
      &lt;li&gt;Access to all shared mailboxes&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;My wife’s licensed account
    &lt;ul&gt;
      &lt;li&gt;Access to all shared mailboxes&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Shared mailbox 1&lt;/li&gt;
  &lt;li&gt;Shared mailbox 2&lt;/li&gt;
  &lt;li&gt;Shared mailbox 3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduced the number of paid accounts and made the setup easier to manage.&lt;/p&gt;

&lt;h1 id=&quot;cutover-approach&quot;&gt;Cutover approach&lt;/h1&gt;
&lt;p&gt;The best approach I could think of was to set up all the mailboxes, shared mailboxes, and aliases I needed first, then leave AWS WorkMail in place until the end of the process. I wanted to continue receiving emails for as long as possible before changing the MX records.&lt;/p&gt;

&lt;p&gt;I then investigated how to migrate my existing emails from AWS WorkMail to Exchange Online. After around two hours of reading, testing, and trial and error, I landed on the migration tool inside the &lt;strong&gt;Exchange Admin Center&lt;/strong&gt;. The migration type I needed was &lt;strong&gt;IMAP migration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/exchange_migration/image-1.png&quot; alt=&quot;Exchange migration screen&quot; /&gt;
&lt;img src=&quot;/assets/images/posts/exchange_migration/image.png&quot; alt=&quot;Exchange IMAP migration screen&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The Exchange Admin Center has a migration batch tool that lets you migrate mailboxes from different sources. In my case, I only needed IMAP migration.&lt;/p&gt;

&lt;p&gt;Setting up access to my AWS WorkMail mailboxes from the Exchange IMAP migration tool took longer than I first expected. The tool requires a CSV file with the mailbox details. Yes, this includes passwords in plain text. It is what it is, but treat that file carefully and delete it when you no longer need it.&lt;/p&gt;

&lt;p&gt;To save time and reduce pain, the CSV format must look like this:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;EmailAddress,UserName,Password
myuser1@mydomain.co.uk,myuser1@mydomain.co.uk,mysuperstrongpassword
myuser2@mydomain.co.uk,myuser2@mydomain.co.uk,myevenstrongerpassword
myuser3@mydomain.co.uk,myuser3@mydomain.co.uk,someonessuperstrongpassword
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note that the values under &lt;strong&gt;EmailAddress&lt;/strong&gt; and &lt;strong&gt;UserName&lt;/strong&gt; are the same in this example. AWS WorkMail did not like me using only the username. It expected the full email address as the username.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After uploading the CSV file with all the source mailboxes listed, I was able to migrate and sync the emails from AWS WorkMail into the new Exchange Online mailboxes. The migration report showed the number of synced emails from the source. The migration brought across the inbox, sent items, and my custom folders.&lt;/p&gt;

&lt;p&gt;Shared mailboxes can be added to the Outlook client app and appear under the main account. For Mac users, I had to add the shared mailbox through the legacy Outlook UI first, then switch back to the new UI. Adding the shared mailbox directly from the new Outlook UI did not work for me.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;At this stage I had not reconfigured the MX records with the new Exchange Online details. Everything was still untouched in AWS WorkMail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;testing-before-and-after&quot;&gt;Testing before and after&lt;/h1&gt;
&lt;p&gt;Once the migration was done, I went back to the Exchange Admin Center and continued the email setup. Microsoft gave me the DNS records I needed to add, including the new MX record.&lt;/p&gt;

&lt;p&gt;In AWS Route 53, I had to delete the existing MX record because editing it directly did not work cleanly for me. I did this quickly and carefully. In less than 10 minutes, the new records were in place and AWS WorkMail was no longer receiving new inbound email for the domain.&lt;/p&gt;

&lt;p&gt;I then logged into my own mailbox and did a sanity check against the migrated data. To my surprise, because I had never done this before, things looked accurate. The migrated emails were there and the folders looked right.&lt;/p&gt;

&lt;p&gt;For testing, I would recommend checking at least the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Send an email from an external address to the new mailbox&lt;/li&gt;
  &lt;li&gt;Send an email from Exchange Online to an external address&lt;/li&gt;
  &lt;li&gt;Test replies, not just new messages&lt;/li&gt;
  &lt;li&gt;Check aliases&lt;/li&gt;
  &lt;li&gt;Check shared mailboxes&lt;/li&gt;
  &lt;li&gt;Check Outlook desktop/mobile client access&lt;/li&gt;
  &lt;li&gt;Check that old WorkMail mailboxes are no longer receiving new messages after the MX cutover&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;mistakes-or-risks-to-avoid&quot;&gt;Mistakes or risks to avoid&lt;/h1&gt;
&lt;p&gt;I made a funny and expensive mistake when I first set up my email server in AWS WorkMail. I created a separate user account and a completely separate mailbox for almost every purpose.&lt;/p&gt;

&lt;p&gt;I had my personal one, then another one for my wife, then another for family stuff, then another for something else, and then repeated that a few more times.&lt;/p&gt;

&lt;p&gt;That approach is fine for millionaires who are not bothered about spending an extra $15 or $20 per month for a personal email server. For me, however, it was expensive. I am not financially free yet!&lt;/p&gt;

&lt;p&gt;I did not make the same mistake again in my new Exchange Online subscription. I became best friends with aliases, shared mailboxes, and Outlook rules to move emails into the right folders. It is cheaper, easier to maintain, and has less configuration overhead.&lt;/p&gt;

&lt;p&gt;Another risk is treating the migration like it is just a technical copy job. It is not. Email is boring until it breaks. If you are doing this for a business, even a small one, you need to think about downtime, DNS propagation, user access, shared mailboxes, and rollback options.&lt;/p&gt;

&lt;h1 id=&quot;imap-migration-under-the-hood&quot;&gt;IMAP migration under the hood&lt;/h1&gt;
&lt;p&gt;An IMAP migration is best understood as a controlled mailbox copy.&lt;/p&gt;

&lt;p&gt;Exchange Online does not magically &lt;em&gt;move&lt;/em&gt; the mailbox from the source server. Instead, Microsoft 365 connects to the source mail server using IMAP, authenticates against each mailbox, reads the available mail folders, pulls the messages from those folders, and then writes them into the target Exchange Online mailbox.&lt;/p&gt;

&lt;p&gt;For this to work, the target users and mailboxes must already exist in Microsoft 365 before the migration starts. The migration endpoint stores the source IMAP server connection details, and the migration batch uses the CSV mapping file to know which source mailbox maps to which target mailbox.&lt;/p&gt;

&lt;p&gt;It is a simple and direct server-to-server copy process.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;One important limitation: IMAP migration is mainly about email messages and mail folders. It does not migrate everything that exists in a full groupware mailbox. Contacts, calendar items, and tasks are not migrated through IMAP, so those need to be handled separately if they matter to you. This is easy to overlook because users often think of a mailbox as email, calendar, contacts, and settings all together. IMAP does not cover all of that.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre class=&quot;mermaid&quot;&gt;
flowchart LR
    A[AWS WorkMail&lt;br /&gt;Source IMAP Server] --&amp;gt;|IMAPS connection&lt;br /&gt;Port 993 / SSL| B[Microsoft 365&lt;br /&gt;Migration Endpoint]
    B --&amp;gt; C[Exchange Online&lt;br /&gt;Migration Batch]
    D[CSV Mapping File&lt;br /&gt;source mailbox → target mailbox] --&amp;gt; C
    C --&amp;gt;|Authenticate to each source mailbox| A
    C --&amp;gt;|Copy email folders and messages| E[Exchange Online&lt;br /&gt;Target Mailboxes]
    F[DNS MX Record Cutover] --&amp;gt;|New inbound mail routes to Microsoft 365| E
    G[Post-migration Validation] --&amp;gt; H[Check mail flow&lt;br /&gt;Check mailbox content&lt;br /&gt;Check user access]
    E --&amp;gt; G
&lt;/pre&gt;

&lt;h1 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h1&gt;
&lt;p&gt;I found the process fairly straightforward, apart from the trial and error around the mailbox CSV file. If I had known the correct CSV format earlier, the whole process would have been much simpler.&lt;/p&gt;

&lt;p&gt;For my small personal setup, I did not need a separate archive or backup workflow. I also avoided adding anything unnecessary around SES archiving or journaling because it would have added cost and complexity I did not need.&lt;/p&gt;

&lt;p&gt;The key point is this: &lt;strong&gt;IMAP migration is not just a button-click task&lt;/strong&gt;. It is a staged copy-and-cutover process. The mailbox data copy, DNS changes, user communication, Outlook reconfiguration, and final verification all need to be planned together.&lt;/p&gt;

&lt;p&gt;The technical migration may be IMAP, but the real success factor is controlling disruption for the users.&lt;/p&gt;

&lt;p&gt;If you encounter issues during the sync process, take a look at the &lt;a href=&quot;https://learn.microsoft.com/en-us/troubleshoot/exchange/move-or-migrate-mailboxes/troubleshoot-issues-with-imap-mailbox-migration&quot;&gt;Microsoft troubleshooting guide for IMAP mailbox migration&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;what-i-would-do-differently-next-time&quot;&gt;What I would do differently next time&lt;/h1&gt;
&lt;p&gt;Next time, I would prepare the CSV file earlier and test it with one mailbox first. That would have saved me time and removed most of the guesswork.&lt;/p&gt;

&lt;p&gt;I would also create a simple mailbox inventory before starting. Nothing complicated, just a small table showing each source mailbox, the target mailbox, whether it is licensed or shared, and whether it has aliases. For a personal migration this may feel unnecessary, but even a small checklist helps when DNS, passwords, mailboxes, and clients are all involved.&lt;/p&gt;

&lt;p&gt;I would reduce the DNS TTL in advance where possible, then wait before doing the MX cutover. That gives you a better chance of a quicker and cleaner switch when you are ready.&lt;/p&gt;

&lt;p&gt;For a larger or business-critical migration, I would also keep the migration batch running a little longer after the MX cutover and perform a final sync/validation before fully shutting down the old system. I got away with a simple process because my setup was small. I would not treat a business migration casually.&lt;/p&gt;

&lt;p&gt;The biggest lesson is simple: do the boring preparation properly. The migration tool does the copy, but planning avoids the panic.&lt;/p&gt;
</description>
        <pubDate>Wed, 03 Jun 2026 05:22:42 +0000</pubDate>
        <link>/blog/2026-06-03-migrate-from-aws-workmail-to-microsoft-exchange/</link>
        <guid isPermaLink="true">/blog/2026-06-03-migrate-from-aws-workmail-to-microsoft-exchange/</guid>
        
        
        <category>AWS WorkMail</category>
        
        <category>Microsoft Exchange</category>
        
        <category>Email Server</category>
        
      </item>
    
    
      <item>
        <title>AWS Clean Architecture Starter Kit</title>
        <description>&lt;h1 id=&quot;aws-clean-architecture-starter-kit&quot;&gt;AWS Clean Architecture Starter Kit&lt;/h1&gt;

&lt;p&gt;A practical .NET Clean Architecture starter kit for building and deploying serverless APIs on AWS.&lt;/p&gt;

&lt;h2 id=&quot;what-is-it&quot;&gt;What Is It?&lt;/h2&gt;

&lt;p&gt;The AWS Clean Architecture Starter Kit is a deployable .NET solution template that combines Clean Architecture with AWS serverless services.&lt;/p&gt;

&lt;p&gt;The project provides a starting point for developers who want to build APIs on AWS without spending days configuring infrastructure and boilerplate code.&lt;/p&gt;

&lt;p&gt;The starter kit currently includes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Clean Architecture template&lt;/li&gt;
  &lt;li&gt;ASP.NET Core Minimal API&lt;/li&gt;
  &lt;li&gt;AWS Lambda&lt;/li&gt;
  &lt;li&gt;Amazon API Gateway&lt;/li&gt;
  &lt;li&gt;Amazon DynamoDB&lt;/li&gt;
  &lt;li&gt;Amazon CloudWatch Logging&lt;/li&gt;
  &lt;li&gt;AWS CDK Infrastructure as Code&lt;/li&gt;
  &lt;li&gt;OpenAPI Documentation&lt;/li&gt;
  &lt;li&gt;FluentValidation&lt;/li&gt;
  &lt;li&gt;Health Checks&lt;/li&gt;
  &lt;li&gt;Unit Tests&lt;/li&gt;
  &lt;li&gt;Architecture Tests&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-i-built-it&quot;&gt;Why I Built It&lt;/h2&gt;

&lt;p&gt;As a software engineer and AWS Solutions Architect, I found that creating a new AWS-backed API often required significantly more work on infrastructure than on the API itself.&lt;/p&gt;

&lt;p&gt;Packaging Lambda functions, configuring API Gateway, managing permissions, creating infrastructure with CDK, and integrating AWS services all take time.&lt;/p&gt;

&lt;p&gt;I wanted a practical foundation that developers could use as a starting point rather than repeatedly solving the same problems from scratch.&lt;/p&gt;

&lt;p&gt;This starter kit is my attempt to simplify that process while documenting the lessons learned along the way.&lt;/p&gt;

&lt;h2 id=&quot;who-is-it-for&quot;&gt;Who Is It For?&lt;/h2&gt;

&lt;p&gt;This project is intended for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;.NET developers learning AWS&lt;/li&gt;
  &lt;li&gt;Developers building serverless applications&lt;/li&gt;
  &lt;li&gt;Small teams needing a starting point&lt;/li&gt;
  &lt;li&gt;Engineers looking for practical AWS CDK examples&lt;/li&gt;
  &lt;li&gt;Developers interested in Clean Architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not intended to be a framework or a complete enterprise platform.&lt;/p&gt;

&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/gen/projects/aws_clean_architecture/whats_deployed.png&quot; alt=&quot;Infrastructure Architecture&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;key-design-decisions&quot;&gt;Key Design Decisions&lt;/h2&gt;

&lt;h3 id=&quot;aws-lambda-instead-of-ecs&quot;&gt;AWS Lambda Instead of ECS&lt;/h3&gt;

&lt;p&gt;The starter kit uses AWS Lambda as the primary hosting model.&lt;/p&gt;

&lt;p&gt;Lambda reduces operational overhead, scales automatically, and is often a cost-effective option for APIs with variable traffic patterns.&lt;/p&gt;

&lt;h3 id=&quot;infrastructure-as-code-using-aws-cdk&quot;&gt;Infrastructure as Code Using AWS CDK&lt;/h3&gt;

&lt;p&gt;All infrastructure is provisioned using AWS CDK.&lt;/p&gt;

&lt;p&gt;The goal is to allow developers to create, modify and deploy infrastructure without manual AWS Console configuration.&lt;/p&gt;

&lt;h3 id=&quot;dynamodb-as-the-default-data-store&quot;&gt;DynamoDB as the Default Data Store&lt;/h3&gt;

&lt;p&gt;DynamoDB was selected as the default persistence layer because it integrates naturally with serverless workloads and removes the operational burden of managing database servers.&lt;/p&gt;

&lt;p&gt;V1 intentionally keeps the data model simple by using a single partition key.&lt;/p&gt;

&lt;h3 id=&quot;api-gateway-api-keys&quot;&gt;API Gateway API Keys&lt;/h3&gt;

&lt;p&gt;API Gateway API Keys provide a lightweight mechanism for controlling access and applying usage plans.&lt;/p&gt;

&lt;p&gt;The API itself does not validate API Keys. Validation is performed by API Gateway before requests are forwarded to AWS Lambda.&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;Install the template&lt;/li&gt;
  &lt;li&gt;Create a new solution&lt;/li&gt;
  &lt;li&gt;Configure AWS credentials&lt;/li&gt;
  &lt;li&gt;Bootstrap CDK&lt;/li&gt;
  &lt;li&gt;Deploy the infrastructure&lt;/li&gt;
  &lt;li&gt;Start building&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Detailed setup instructions are available in the GitHub repository.&lt;/p&gt;

&lt;h2 id=&quot;source-code&quot;&gt;Source Code&lt;/h2&gt;

&lt;p&gt;The source code is available on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ghanavat/aws-clean-architecture-starter-kit&quot;&gt;AWS Clean Architecture Repository&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;related-projects&quot;&gt;Related Projects&lt;/h2&gt;

&lt;h3 id=&quot;ghanavatsresultpattern&quot;&gt;Ghanavats.ResultPattern&lt;/h3&gt;

&lt;p&gt;The starter kit includes usage examples based on Ghanavats.ResultPattern.&lt;/p&gt;

&lt;p&gt;ResultPattern provides a lightweight approach to modelling success and failure outcomes without relying on exceptions for expected application behaviour.&lt;/p&gt;

&lt;p&gt;Although many similar libraries exist, I continue to maintain and use this package because it aligns with how I structure application and domain logic.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/ghanavat/ResultPattern&quot;&gt;ResultPattern Repository&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;roadmap&quot;&gt;Roadmap&lt;/h2&gt;

&lt;p&gt;Planned improvements include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Authentication and authorisation&lt;/li&gt;
  &lt;li&gt;Additional AWS integrations&lt;/li&gt;
  &lt;li&gt;CI/CD examples&lt;/li&gt;
  &lt;li&gt;Advanced DynamoDB modelling&lt;/li&gt;
  &lt;li&gt;Event-driven architecture examples&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;feedback&quot;&gt;Feedback&lt;/h2&gt;

&lt;p&gt;The project is actively evolving.&lt;/p&gt;

&lt;p&gt;Feedback, suggestions and contributions are welcome.&lt;/p&gt;
</description>
        <pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
        <link>/projects/aws-clean-architecture-starter-kit/</link>
        <guid isPermaLink="true">/projects/aws-clean-architecture-starter-kit/</guid>
        <!-- 
         -->
      </item>
    
  </channel>
</rss>