diff --git a/.gitignore b/.gitignore
index b8d6205c..7c711d17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,5 +30,8 @@ bundle.min.css
/**/coverage*.xml
/CoverageReport/
+# Upgrade Assistant backups
+backups/
+
# MacOS
.DS_Store
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1c75ce9a..4a8e5b20 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -58,5 +58,6 @@
+
diff --git a/LinkDotNet.Blog.sln b/LinkDotNet.Blog.sln
index a5112d62..5e87a41f 100644
--- a/LinkDotNet.Blog.sln
+++ b/LinkDotNet.Blog.sln
@@ -43,40 +43,128 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{A931171C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.Blog.CriticalCSS", "tools\LinkDotNet.Blog.CriticalCSS\LinkDotNet.Blog.CriticalCSS.csproj", "{8CB83177-C078-4953-BC27-8968D2A6E0FE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.Blog.UpgradeAssistant", "tools\LinkDotNet.Blog.UpgradeAssistant\LinkDotNet.Blog.UpgradeAssistant.csproj", "{B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.Blog.UpgradeAssistant.Tests", "tests\LinkDotNet.Blog.UpgradeAssistant.Tests\LinkDotNet.Blog.UpgradeAssistant.Tests.csproj", "{8CD027B6-B188-4091-A1A0-FD678DF98C52}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x64.Build.0 = Debug|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x86.Build.0 = Debug|Any CPU
{6D6519BF-9265-488D-AA3B-C879F427930F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D6519BF-9265-488D-AA3B-C879F427930F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x64.ActiveCfg = Release|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x64.Build.0 = Release|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x86.ActiveCfg = Release|Any CPU
+ {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x86.Build.0 = Release|Any CPU
{18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x64.Build.0 = Debug|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x86.Build.0 = Debug|Any CPU
{18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x64.ActiveCfg = Release|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x64.Build.0 = Release|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x86.ActiveCfg = Release|Any CPU
+ {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x86.Build.0 = Release|Any CPU
{E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x64.Build.0 = Debug|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x86.Build.0 = Debug|Any CPU
{E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x64.ActiveCfg = Release|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x64.Build.0 = Release|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x86.ActiveCfg = Release|Any CPU
+ {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x86.Build.0 = Release|Any CPU
{5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x64.Build.0 = Debug|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x86.Build.0 = Debug|Any CPU
{5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x64.ActiveCfg = Release|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x64.Build.0 = Release|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x86.ActiveCfg = Release|Any CPU
+ {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x86.Build.0 = Release|Any CPU
{DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x64.Build.0 = Debug|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x86.Build.0 = Debug|Any CPU
{DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x64.ActiveCfg = Release|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x64.Build.0 = Release|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x86.ActiveCfg = Release|Any CPU
+ {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x86.Build.0 = Release|Any CPU
{310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x64.Build.0 = Debug|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x86.Build.0 = Debug|Any CPU
{310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x64.ActiveCfg = Release|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x64.Build.0 = Release|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x86.ActiveCfg = Release|Any CPU
+ {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x86.Build.0 = Release|Any CPU
{8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x64.Build.0 = Debug|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x86.Build.0 = Debug|Any CPU
{8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x64.ActiveCfg = Release|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x64.Build.0 = Release|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x86.ActiveCfg = Release|Any CPU
+ {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x86.Build.0 = Release|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x64.Build.0 = Debug|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x86.Build.0 = Debug|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x64.ActiveCfg = Release|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x64.Build.0 = Release|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x86.ActiveCfg = Release|Any CPU
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x86.Build.0 = Release|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x64.Build.0 = Debug|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x86.Build.0 = Debug|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x64.ActiveCfg = Release|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x64.Build.0 = Release|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x86.ActiveCfg = Release|Any CPU
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -89,6 +177,8 @@ Global
{DEFDA17A-9586-4E50-83FB-8F75AC29D39A} = {CAD2F4A3-1282-49B5-B0AB-655CDBED0A35}
{310ABEE1-C131-43E6-A759-F2DB75A483DD} = {CAD2F4A3-1282-49B5-B0AB-655CDBED0A35}
{8CB83177-C078-4953-BC27-8968D2A6E0FE} = {A931171C-22A6-4DB5-802B-67286B536BD2}
+ {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4} = {A931171C-22A6-4DB5-802B-67286B536BD2}
+ {8CD027B6-B188-4091-A1A0-FD678DF98C52} = {CAD2F4A3-1282-49B5-B0AB-655CDBED0A35}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FB9B0642-F1F0-4BD8-9EDD-15C95F082180}
diff --git a/MIGRATION.md b/MIGRATION.md
index a2dccdfe..afeb510f 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -1,6 +1,32 @@
# Migration Guide
This document describes the changes that need to be made to migrate from one version of the blog to another.
+## Automated Upgrade Assistant
+
+Starting with version 12.0, we provide an **Upgrade Assistant** tool that automates most configuration migrations. This tool:
+- Automatically detects your current configuration version
+- Applies necessary transformations to `appsettings.json` files
+- Creates backups before making changes
+- Provides colorful console output with clear warnings and instructions
+
+**Usage:**
+```bash
+# From your blog directory
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant
+
+# Preview changes without applying
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run
+
+# See all options
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --help
+```
+
+For detailed documentation, see [docs/Migrations/UpgradeAssistant.md](docs/Migrations/UpgradeAssistant.md).
+
+**Note:** While the Upgrade Assistant handles most configuration changes automatically, some migrations still require manual steps (especially database schema changes). These are noted below.
+
+---
+
## 11.0 to 12.0
`ShowBuildInformation` setting was added on the root level of the `appsettings.json` file. This setting controls whether build information (like build date) is shown in the `Footer` component.
diff --git a/Readme.md b/Readme.md
index 6d97ca09..a36c69e8 100644
--- a/Readme.md
+++ b/Readme.md
@@ -27,6 +27,8 @@ This also includes source code snippets. Highlighting is done via [highlight.js]
- **About Me Page** - Customizable profile page that showcases skills and experience
- **RSS Feed** - Allow readers to subscribe to content updates
- **Visit Counter** - Get visitor counters for each blog post in the internal dashboard
+- **Automated Configuration Migration** - Upgrade Assistant tool automatically migrates appsettings.json files between versions
+- **Automated Database Migrations** - Seamless database schema updates using Entity Framework Migrations
## In Action
diff --git a/docs/Migrations/Readme.md b/docs/Migrations/Readme.md
index 5eb0637c..88787a7f 100644
--- a/docs/Migrations/Readme.md
+++ b/docs/Migrations/Readme.md
@@ -6,4 +6,33 @@ This is contrasted by Minor changes. These are things where the user does not ne
Breaking changes are recorded in the [MIGRATION.md](../../MIGRATION.md). Since version 9 of the blog, βEntity Framework Migrationsβ has been introduced for all SQL providers. You can read more in the [documentation](../Storage/Readme.md). In a nutshell, this means that database migration can be carried out easily via the βef migrationβ CLI tool. More on this in the documentation linked above.
-Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md.
\ No newline at end of file
+Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md.
+
+## Automated Configuration Migration (Version 12+)
+
+Starting with **version 12**, the blog includes an **Automated Upgrade Assistant** that handles appsettings.json migrations automatically. This tool eliminates the need for manual configuration changes in most cases.
+
+### What the Upgrade Assistant Does
+
+- **Automatic Detection** - Detects your current configuration version
+- **Sequential Migration** - Applies necessary migrations step-by-step
+- **Safe Backups** - Creates timestamped backups before making any changes
+- **Smart Filtering** - Skips version-controlled `appsettings.json`, only migrates environment-specific files
+- **Colorful Output** - Provides clear, color-coded feedback about changes and warnings
+
+### How to Use
+
+Run the Upgrade Assistant from your blog directory:
+
+```bash
+# Preview changes without applying them
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run
+
+# Migrate all environment-specific appsettings files
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant
+
+# Migrate a specific file
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path appsettings.Production.json
+```
+
+For detailed documentation on the Upgrade Assistant, see [UpgradeAssistant.md](./UpgradeAssistant.md).
\ No newline at end of file
diff --git a/docs/Migrations/UpgradeAssistant.md b/docs/Migrations/UpgradeAssistant.md
new file mode 100644
index 00000000..00ee3abd
--- /dev/null
+++ b/docs/Migrations/UpgradeAssistant.md
@@ -0,0 +1,322 @@
+# Configuration Upgrade Assistant
+
+The Blog Upgrade Assistant is an automated tool that helps you migrate your `appsettings.json` configuration files to the latest version when upgrading the blog to a new major version. This was introduced in version 12 of the blog. The update software tries to upgrade from version 11 to 12 automatically but not from earlier versions.
+
+## Why Use the Upgrade Assistant?
+
+When upgrading the blog to a new major version, the configuration schema may change:
+- New mandatory settings may be added
+- Settings may be moved between sections
+- Default values may change
+
+The Upgrade Assistant automates these changes, reducing errors and making upgrades easier.
+
+## Quick Start
+
+1. **Before upgrading**, backup your configuration files (the tool also does this automatically)
+2. Navigate to your blog installation directory
+3. Run the upgrade assistant:
+ ```bash
+ dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant
+ ```
+4. Review the changes and update any values as needed
+5. Complete any additional manual steps from [MIGRATION.md](../../MIGRATION.md)
+
+## Installation
+
+The tool is included with the blog source code in the `tools/LinkDotNet.Blog.UpgradeAssistant` directory. No separate installation is needed.
+
+To build the tool:
+```bash
+cd tools/LinkDotNet.Blog.UpgradeAssistant
+dotnet build
+```
+
+## Usage Guide
+
+### Basic Migration
+
+The simplest way to use the tool is to run it from your blog directory:
+
+```bash
+# Navigate to your blog installation
+cd /path/to/your/blog
+
+# Run the upgrade assistant
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant
+```
+
+This will:
+1. Find all `appsettings*.json` files in the current directory
+2. Detect the current configuration version
+3. Apply all necessary migrations
+4. Save backups to `./backups` directory
+
+### Preview Changes (Dry Run)
+
+To see what changes will be made without actually modifying files:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run
+```
+
+The tool will display:
+- Which migrations will be applied
+- A preview of the modified configuration
+- Warnings about new settings that require attention
+
+### Migrate Specific Files
+
+To migrate a specific configuration file:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/appsettings.Production.json
+```
+
+To migrate all files in a specific directory:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/config
+```
+
+### Custom Backup Location
+
+By default, backups are saved to `./backups`. To use a custom location:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --backup-dir /path/to/backup/location
+```
+
+## Understanding the Output
+
+The tool uses color-coded output for clarity:
+
+- π’ **Green (Success)**: Operation completed successfully
+- π΅ **Cyan (Info)**: Informational messages
+- π‘ **Yellow (Warning)**: Warnings about settings that need attention
+- π΄ **Red (Error)**: Errors that need to be resolved
+- π£ **Magenta (Headers)**: Section headers
+
+### Example Output
+
+```
+βββ Blog Upgrade Assistant βββ
+
+βΉ Target: /path/to/blog
+βΉ Backup directory: ./backups
+βΉ Found 2 file(s) to process.
+
+βββ Processing: appsettings.json βββ
+
+βΉ Current version: Not set (pre-12.0)
+ β Found 1 migration(s) to apply:
+ β β’ 11.0 β 12.0: Adds ShowBuildInformation setting to control build information display.
+β Backup created: ./backups/appsettings_20251221_120000.json
+ β Applying migration: 11.0 β 12.0
+βΉ Added 'ShowBuildInformation' setting. Controls display of build information in the footer.
+β Migration 11.0 β 12.0 applied successfully.
+...
+β File updated successfully: /path/to/blog/appsettings.json
+
+βββ Migration Complete βββ
+
+β All files processed successfully!
+```
+
+## How It Works
+
+### Version Detection
+
+The tool looks for a `ConfigVersion` field in your `appsettings.json`:
+
+```json
+{
+ "ConfigVersion": "12.0",
+ ...
+}
+```
+
+If this field doesn't exist, the tool assumes you're running version 8.0 or earlier and will apply all necessary migrations.
+
+### Migration Chain
+
+The tool applies migrations sequentially:
+1. Detects current version (e.g., 12.0)
+2. Finds all migrations from current to latest (11.0β12.0)
+3. Applies each migration in order
+4. Updates the `ConfigVersion` field to the latest version
+
+### Backup Process
+
+Before making any changes, the tool:
+1. Creates a `backups` directory (or uses the specified backup location)
+2. Copies each file with a timestamp: `appsettings_20241221_120000.json`
+3. Preserves the original file structure and formatting
+
+### Idempotency
+
+The tool is **idempotent** - running it multiple times on the same file is safe:
+- If no migrations are needed, no changes are made
+- Already migrated files show "No migrations needed"
+- Version tracking ensures migrations aren't applied twice
+
+## Migration Details
+
+
+**Changes:**
+- Adds `UseMultiAuthorMode` setting (default: `false`)
+
+**After:**
+```json
+{
+ "UseMultiAuthorMode": false
+}
+```
+
+**Manual Steps Required:**
+- Set to `true` if you want multi-author support
+- Configure author information per blog post
+
+### Version 11.0 β 12.0
+
+**Changes:**
+- Adds `ShowBuildInformation` setting (default: `true`)
+
+**After:**
+```json
+{
+ "ShowBuildInformation": true
+}
+```
+
+**Manual Steps Required:**
+- None (setting is optional)
+
+## Command-Line Reference
+
+```
+Usage: upgrade-assistant [options]
+
+Options:
+ -p, --path Path to appsettings.json file or directory
+ Defaults to current directory
+ -d, --dry-run Preview changes without applying them
+ -b, --backup-dir Custom backup directory path
+ Defaults to './backups'
+ -h, --help Display help message
+ -v, --version Display tool version
+```
+
+## Troubleshooting
+
+### "No appsettings.json files found"
+
+**Cause:** Tool can't find configuration files in the specified location.
+
+**Solution:**
+```bash
+# Specify the correct path
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /correct/path
+```
+
+### "Invalid JSON in appsettings.json"
+
+**Cause:** Your configuration file has JSON syntax errors.
+
+**Solution:**
+1. Open the file in a JSON validator or editor
+2. Fix syntax errors (missing commas, brackets, etc.)
+3. Run the tool again
+
+### "Configuration is already up to date"
+
+**Cause:** Your configuration is already at the latest version.
+
+**Solution:** No action needed! Your configuration is current.
+
+### Migration Applied but Application Fails
+
+**Possible Causes:**
+- Database migrations not applied
+- Custom configuration values need adjustment
+- Environment-specific settings need updating
+
+**Solution:**
+1. Check [MIGRATION.md](../../MIGRATION.md) for additional manual steps
+2. Review application logs for specific errors
+3. Verify all required database tables exist
+4. Check environment-specific configuration files
+
+### Need to Revert Changes
+
+**Solution:**
+1. Navigate to the backup directory (default: `./backups`)
+2. Find the backup file with the timestamp before migration
+3. Copy it back to your configuration location
+4. Restart the application
+
+Example:
+```bash
+cp backups/appsettings_20251221_120000.json appsettings.json
+```
+
+## Advanced Usage
+
+### Batch Processing
+
+Process multiple configuration directories:
+
+```bash
+for dir in /path/to/blog1 /path/to/blog2 /path/to/blog3; do
+ dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path $dir
+done
+```
+
+### Automated CI/CD Integration
+
+Include in your deployment pipeline:
+
+```yaml
+# Example GitHub Actions workflow
+- name: Upgrade Configuration
+ run: |
+ dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant \
+ --path ./deployment/config \
+ --backup-dir ./backups
+```
+
+### Scripted Migration with Verification
+
+```bash
+#!/bin/bash
+# Upgrade with verification
+
+# Run dry-run first
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run --path ./config
+
+# Ask for confirmation
+read -p "Proceed with migration? (y/n) " -n 1 -r
+echo
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ # Run actual migration
+ dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path ./config
+ echo "Migration complete. Please review the changes."
+fi
+```
+
+## Additional Resources
+
+- [MIGRATION.md](../../MIGRATION.md) - Complete migration guide with manual steps
+- [Configuration Documentation](../Setup/Configuration.md) - All configuration options
+- [Tool README](../../tools/LinkDotNet.Blog.UpgradeAssistant/README.md) - Developer documentation
+
+## Getting Help
+
+If you encounter issues:
+
+1. Check this documentation and [MIGRATION.md](../../MIGRATION.md)
+2. Review the tool's output messages - they often contain helpful information
+3. Open an issue on GitHub with:
+ - Tool output (with sensitive data removed)
+ - Your configuration version
+ - Error messages or unexpected behavior
diff --git a/src/LinkDotNet.Blog.Web/appsettings.json b/src/LinkDotNet.Blog.Web/appsettings.json
index 01c582f5..bbddcc7d 100644
--- a/src/LinkDotNet.Blog.Web/appsettings.json
+++ b/src/LinkDotNet.Blog.Web/appsettings.json
@@ -1,4 +1,5 @@
{
+ "ConfigVersion": "12.0",
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs
new file mode 100644
index 00000000..38ec2ec2
--- /dev/null
+++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs
@@ -0,0 +1,5 @@
+global using System;
+global using System.IO;
+global using System.Threading.Tasks;
+global using Shouldly;
+global using Xunit;
diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj
new file mode 100644
index 00000000..a09ed594
--- /dev/null
+++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj
@@ -0,0 +1,12 @@
+ο»Ώ
+
+
+ Exe
+ false
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs
new file mode 100644
index 00000000..5811e01e
--- /dev/null
+++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs
@@ -0,0 +1,61 @@
+using System.Text.Json;
+using LinkDotNet.Blog.UpgradeAssistant.Migrations;
+
+namespace LinkDotNet.Blog.UpgradeAssistant.Tests;
+
+public class Migration11To12Tests
+{
+ [Fact]
+ public void Should_Add_ShowBuildInformation_Setting()
+ {
+ // Arrange
+ var migration = new Migration11To12();
+ var json = """
+ {
+ "BlogName": "Test Blog"
+ }
+ """;
+ var document = JsonDocument.Parse(json);
+
+ // Act
+ var result = migration.Apply(document, ref json);
+
+ // Assert
+ result.ShouldBeTrue();
+ json.ShouldContain("\"ShowBuildInformation\": true");
+ document.Dispose();
+ }
+
+ [Fact]
+ public void Should_Not_Change_When_Setting_Already_Exists()
+ {
+ // Arrange
+ var migration = new Migration11To12();
+ var json = """
+ {
+ "BlogName": "Test Blog",
+ "ShowBuildInformation": false
+ }
+ """;
+ var document = JsonDocument.Parse(json);
+
+ // Act
+ var result = migration.Apply(document, ref json);
+
+ // Assert
+ result.ShouldBeFalse();
+ document.Dispose();
+ }
+
+ [Fact]
+ public void Should_Have_Correct_Version_Info()
+ {
+ // Arrange
+ var migration = new Migration11To12();
+
+ // Act & Assert
+ migration.FromVersion.ShouldBe("11.0");
+ migration.ToVersion.ShouldBe("12.0");
+ migration.GetDescription().ShouldNotBeNullOrEmpty();
+ }
+}
diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs
new file mode 100644
index 00000000..ea483f18
--- /dev/null
+++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs
@@ -0,0 +1,156 @@
+using TestContext = Xunit.TestContext;
+
+namespace LinkDotNet.Blog.UpgradeAssistant.Tests;
+
+public sealed class MigrationManagerTests : IDisposable
+{
+ private readonly string testDirectory;
+
+ public MigrationManagerTests()
+ {
+ testDirectory = Path.Combine(Path.GetTempPath(), $"blog-test-{Guid.NewGuid()}");
+ Directory.CreateDirectory(testDirectory);
+ }
+
+ [Fact]
+ public async Task Should_Migrate_From_11_To_12()
+ {
+ // Arrange
+ var testFile = Path.Combine(testDirectory, "appsettings.Development.json");
+ var json = """
+ {
+ "BlogName": "Test Blog"
+ }
+ """;
+ await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken);
+ var manager = new MigrationManager();
+ var backupDir = Path.Combine(testDirectory, "backups");
+
+ // Act
+ var result = await manager.MigrateFileAsync(testFile, false, backupDir);
+
+ // Assert
+ result.ShouldBeTrue();
+ var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken);
+ content.ShouldContain("\"ConfigVersion\": \"12.0\"");
+ content.ShouldContain("\"ShowBuildInformation\": true");
+
+ // Verify backup was created
+ var backupFiles = Directory.GetFiles(backupDir);
+ backupFiles.ShouldNotBeEmpty();
+ }
+
+ [Fact]
+ public async Task Should_Not_Modify_Already_Migrated_File()
+ {
+ // Arrange
+ var testFile = Path.Combine(testDirectory, "appsettings.Production.json");
+ var json = """
+ {
+ "ConfigVersion": "12.0",
+ "BlogName": "Test Blog",
+ "ShowBuildInformation": true
+ }
+ """;
+ await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken);
+ var manager = new MigrationManager();
+ var backupDir = Path.Combine(testDirectory, "backups");
+
+ // Act
+ var result = await manager.MigrateFileAsync(testFile, false, backupDir);
+
+ // Assert
+ result.ShouldBeTrue();
+ var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken);
+ content.ShouldBe(json); // Should not change
+ }
+
+ [Fact]
+ public async Task Should_Skip_Version_Controlled_Appsettings_Json()
+ {
+ // Arrange
+ var testFile = Path.Combine(testDirectory, "appsettings.json");
+ var json = """
+ {
+ "BlogName": "Test Blog"
+ }
+ """;
+ await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken);
+ var manager = new MigrationManager();
+ var backupDir = Path.Combine(testDirectory, "backups");
+
+ // Act
+ var result = await manager.MigrateFileAsync(testFile, false, backupDir);
+
+ // Assert
+ result.ShouldBeTrue();
+ var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken);
+ content.ShouldBe(json); // Should not change
+ Directory.Exists(backupDir).ShouldBeFalse(); // No backup created
+ }
+
+ [Fact]
+ public async Task Should_Handle_Invalid_Json()
+ {
+ // Arrange
+ var testFile = Path.Combine(testDirectory, "appsettings.Invalid.json");
+ await File.WriteAllTextAsync(testFile, "{ invalid json }", TestContext.Current.CancellationToken);
+ var manager = new MigrationManager();
+ var backupDir = Path.Combine(testDirectory, "backups");
+
+ // Act
+ var result = await manager.MigrateFileAsync(testFile, false, backupDir);
+
+ // Assert
+ result.ShouldBeFalse();
+ }
+
+ [Fact]
+ public async Task Should_Handle_Missing_File()
+ {
+ // Arrange
+ var testFile = Path.Combine(testDirectory, "nonexistent.json");
+ var manager = new MigrationManager();
+ var backupDir = Path.Combine(testDirectory, "backups");
+
+ // Act
+ var result = await manager.MigrateFileAsync(testFile, false, backupDir);
+
+ // Assert
+ result.ShouldBeFalse();
+ }
+
+ [Fact]
+ public async Task DryRun_Should_Not_Modify_File()
+ {
+ // Arrange
+ var testFile = Path.Combine(testDirectory, "appsettings.Development.json");
+ var json = """
+ {
+ "BlogName": "Test Blog"
+ }
+ """;
+ await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken);
+ var manager = new MigrationManager();
+ var backupDir = Path.Combine(testDirectory, "backups");
+
+ // Act
+ var result = await manager.MigrateFileAsync(testFile, true, backupDir);
+
+ // Assert
+ result.ShouldBeTrue();
+ var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken);
+ content.ShouldBe(json); // Should not change in dry-run mode
+
+ // Verify no backup was created in dry-run mode
+ Directory.Exists(backupDir).ShouldBeFalse();
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(testDirectory))
+ {
+ Directory.Delete(testDirectory, true);
+ }
+ }
+}
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs
new file mode 100644
index 00000000..9903eacd
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs
@@ -0,0 +1,38 @@
+using Spectre.Console;
+
+namespace LinkDotNet.Blog.UpgradeAssistant;
+
+public static class ConsoleOutput
+{
+ public static void WriteSuccess(string message)
+ {
+ AnsiConsole.MarkupLine($"[green]β {message}[/]");
+ }
+
+ public static void WriteError(string message)
+ {
+ AnsiConsole.MarkupLine($"[red]β {message}[/]");
+ }
+
+ public static void WriteWarning(string message)
+ {
+ AnsiConsole.MarkupLine($"[yellow]β {message}[/]");
+ }
+
+ public static void WriteInfo(string message)
+ {
+ AnsiConsole.MarkupLine($"[cyan]βΉ {message}[/]");
+ }
+
+ public static void WriteHeader(string message)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine($"[magenta bold]βββ {message} βββ[/]");
+ AnsiConsole.WriteLine();
+ }
+
+ public static void WriteStep(string message)
+ {
+ AnsiConsole.MarkupLine($"[white] β {message}[/]");
+ }
+}
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs
new file mode 100644
index 00000000..ac9562d5
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs
@@ -0,0 +1,28 @@
+using System.Text.Json;
+
+namespace LinkDotNet.Blog.UpgradeAssistant;
+
+public interface IMigration
+{
+ ///
+ /// The source version that this migration upgrades from.
+ ///
+ string FromVersion { get; }
+
+ ///
+ /// The target version that this migration upgrades to.
+ ///
+ string ToVersion { get; }
+
+ ///
+ /// Apply the migration to the JSON document.
+ ///
+ /// The JSON document to migrate.
+ /// True if changes were made, false otherwise.
+ bool Apply(JsonDocument document, ref string jsonContent);
+
+ ///
+ /// Get a description of changes this migration will make.
+ ///
+ string GetDescription();
+}
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj b/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj
new file mode 100644
index 00000000..dc1642ad
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj
@@ -0,0 +1,12 @@
+ο»Ώ
+
+
+ Exe
+ enable
+
+
+
+
+
+
+
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs
new file mode 100644
index 00000000..ddc075f4
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs
@@ -0,0 +1,193 @@
+using System.Globalization;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using LinkDotNet.Blog.UpgradeAssistant.Migrations;
+
+namespace LinkDotNet.Blog.UpgradeAssistant;
+
+public sealed class MigrationManager
+{
+ private readonly List _migrations;
+ private readonly string _currentVersion;
+
+ public MigrationManager()
+ {
+ _migrations = new List
+ {
+ new Migration11To12()
+ };
+
+ _currentVersion = DetermineCurrentVersionFromMigrations();
+ }
+
+ private string DetermineCurrentVersionFromMigrations()
+ {
+ return _migrations.Count > 0
+ ? _migrations.Max(m => m.ToVersion) ?? "11.0"
+ : "11.0";
+ }
+
+ public async Task MigrateFileAsync(string filePath, bool dryRun, string backupDirectory)
+ {
+ if (!File.Exists(filePath))
+ {
+ ConsoleOutput.WriteError($"File not found: {filePath}");
+ return false;
+ }
+
+ var fileName = Path.GetFileName(filePath);
+ if (IsVersionControlledAppsettingsFile(fileName))
+ {
+ ConsoleOutput.WriteInfo($"Skipping version-controlled file: {fileName}");
+ return true;
+ }
+
+ ConsoleOutput.WriteHeader($"Processing: {fileName}");
+
+ var content = await File.ReadAllTextAsync(filePath);
+ JsonDocument? document;
+
+ try
+ {
+ document = JsonDocument.Parse(content);
+ }
+ catch (JsonException ex)
+ {
+ ConsoleOutput.WriteError($"Invalid JSON in {filePath}: {ex.Message}");
+ return false;
+ }
+
+ var currentVersion = GetVersion(document);
+ ConsoleOutput.WriteInfo($"Current version: {currentVersion ?? $"Not set (pre-{_currentVersion})"}");
+
+ var applicableMigrations = GetApplicableMigrations(currentVersion);
+
+ if (applicableMigrations.Count == 0)
+ {
+ ConsoleOutput.WriteSuccess("No migrations needed. Configuration is up to date.");
+ document.Dispose();
+ return true;
+ }
+
+ ConsoleOutput.WriteStep($"Found {applicableMigrations.Count} migration(s) to apply:");
+ foreach (var migration in applicableMigrations)
+ {
+ ConsoleOutput.WriteStep($" β’ {migration.FromVersion} β {migration.ToVersion}: {migration.GetDescription()}");
+ }
+
+ if (dryRun)
+ {
+ ConsoleOutput.WriteWarning("DRY RUN MODE - No changes will be saved.");
+ }
+ else
+ {
+ var backupPath = CreateBackup(filePath, backupDirectory);
+ ConsoleOutput.WriteSuccess($"Backup created: {backupPath}");
+ }
+
+ var modifiedContent = content;
+ var hasAnyChanges = false;
+
+ foreach (var migration in applicableMigrations)
+ {
+ ConsoleOutput.WriteStep($"Applying migration: {migration.FromVersion} β {migration.ToVersion}");
+
+ using var migrationDoc = JsonDocument.Parse(modifiedContent);
+
+ if (migration.Apply(migrationDoc, ref modifiedContent))
+ {
+ ConsoleOutput.WriteSuccess($"Migration {migration.FromVersion} β {migration.ToVersion} applied successfully.");
+ hasAnyChanges = true;
+ }
+ else
+ {
+ ConsoleOutput.WriteInfo($"Migration {migration.FromVersion} β {migration.ToVersion} - No changes needed.");
+ }
+ }
+
+ if (hasAnyChanges)
+ {
+ modifiedContent = SetVersion(modifiedContent, _currentVersion);
+
+ if (!dryRun)
+ {
+ await File.WriteAllTextAsync(filePath, modifiedContent);
+ ConsoleOutput.WriteSuccess($"File updated successfully: {filePath}");
+ }
+ else
+ {
+ ConsoleOutput.WriteInfo("Preview of changes:");
+ Console.WriteLine(modifiedContent);
+ }
+ }
+
+ document.Dispose();
+ return true;
+ }
+
+ private static bool IsVersionControlledAppsettingsFile(string fileName)
+ {
+ return fileName.Equals("appsettings.json", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string? GetVersion(JsonDocument document)
+ {
+ if (document.RootElement.TryGetProperty("ConfigVersion", out var versionElement))
+ {
+ return versionElement.GetString();
+ }
+
+ return null;
+ }
+
+ private List GetApplicableMigrations(string? currentVersion)
+ {
+ var result = new List();
+ var startVersion = currentVersion ?? "11.0";
+ var currentMigrationVersion = startVersion;
+ var foundMigration = true;
+
+ while (foundMigration)
+ {
+ foundMigration = false;
+ foreach (var migration in _migrations)
+ {
+ if (migration.FromVersion == currentMigrationVersion)
+ {
+ result.Add(migration);
+ currentMigrationVersion = migration.ToVersion;
+ foundMigration = true;
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static string CreateBackup(string filePath, string backupDirectory)
+ {
+ var fileName = Path.GetFileName(filePath);
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture);
+ var backupFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_{timestamp}{Path.GetExtension(fileName)}";
+ var backupPath = Path.Combine(backupDirectory, backupFileName);
+
+ Directory.CreateDirectory(backupDirectory);
+ File.Copy(filePath, backupPath);
+
+ return backupPath;
+ }
+
+ private static string SetVersion(string jsonContent, string version)
+ {
+ var jsonNode = JsonNode.Parse(jsonContent);
+ if (jsonNode is JsonObject rootObject)
+ {
+ rootObject["ConfigVersion"] = version;
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ return jsonNode.ToJsonString(options);
+ }
+
+ return jsonContent;
+ }
+}
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs
new file mode 100644
index 00000000..be03312a
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace LinkDotNet.Blog.UpgradeAssistant.Migrations;
+
+///
+/// Migration from version 11.0 to 12.0.
+/// Adds ShowBuildInformation setting.
+///
+public sealed class Migration11To12 : IMigration
+{
+ public string FromVersion => "11.0";
+ public string ToVersion => "12.0";
+
+ public bool Apply(JsonDocument document, ref string jsonContent)
+ {
+ var jsonNode = JsonNode.Parse(jsonContent);
+ if (jsonNode is not JsonObject rootObject)
+ {
+ return false;
+ }
+
+ var hasChanges = false;
+
+ if (!rootObject.ContainsKey("ShowBuildInformation"))
+ {
+ rootObject["ShowBuildInformation"] = true;
+ hasChanges = true;
+ ConsoleOutput.WriteInfo("Added 'ShowBuildInformation' setting. Controls display of build information in the footer.");
+ }
+
+ if (hasChanges)
+ {
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ jsonContent = jsonNode.ToJsonString(options);
+ }
+
+ return hasChanges;
+ }
+
+ public string GetDescription()
+ {
+ return "Adds ShowBuildInformation setting to control build information display.";
+ }
+}
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs
new file mode 100644
index 00000000..57cb1ecd
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs
@@ -0,0 +1,159 @@
+ο»Ώusing LinkDotNet.Blog.UpgradeAssistant;
+using Spectre.Console;
+
+var targetPath = ".";
+var backupDirectory = "backups";
+var dryRun = false;
+var showHelp = false;
+var showVersion = false;
+
+ParseCommandLineArguments(args, ref targetPath, ref backupDirectory, ref dryRun, ref showHelp, ref showVersion);
+
+if (showHelp)
+{
+ ShowHelp();
+ return 0;
+}
+
+if (showVersion)
+{
+ ShowVersion();
+ return 0;
+}
+
+targetPath = Path.GetFullPath(targetPath);
+backupDirectory = Path.GetFullPath(backupDirectory);
+
+ConsoleOutput.WriteHeader("Blog Upgrade Assistant");
+ConsoleOutput.WriteInfo($"Target: {targetPath}");
+ConsoleOutput.WriteInfo($"Backup directory: {backupDirectory}");
+
+if (dryRun)
+{
+ ConsoleOutput.WriteWarning("Running in DRY RUN mode - no changes will be saved.");
+}
+
+var manager = new MigrationManager();
+var files = GetAppsettingsFiles(targetPath);
+
+if (files.Count == 0)
+{
+ ConsoleOutput.WriteError("No appsettings files found to migrate.");
+ ConsoleOutput.WriteInfo("Please specify a valid path using --path option.");
+ return 1;
+}
+
+ConsoleOutput.WriteInfo($"Found {files.Count} file(s) to process.");
+AnsiConsole.WriteLine();
+
+var allSuccessful = true;
+foreach (var file in files)
+{
+ var success = await manager.MigrateFileAsync(file, dryRun, backupDirectory);
+ allSuccessful = allSuccessful && success;
+ AnsiConsole.WriteLine();
+}
+
+if (allSuccessful)
+{
+ ConsoleOutput.WriteHeader("Migration Complete");
+ ConsoleOutput.WriteSuccess("All files processed successfully!");
+
+ if (!dryRun)
+ {
+ ConsoleOutput.WriteInfo($"Backups saved to: {backupDirectory}");
+ }
+
+ ConsoleOutput.WriteInfo("Please review the changes and update any configuration values as needed.");
+ ConsoleOutput.WriteInfo("See MIGRATION.md for additional manual steps (database migrations, etc.).");
+ return 0;
+}
+
+ConsoleOutput.WriteError("Some files could not be processed. Please review the errors above.");
+return 1;
+
+static List GetAppsettingsFiles(string path)
+{
+ if (File.Exists(path) && path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
+ {
+ return new List { path };
+ }
+
+ if (Directory.Exists(path))
+ {
+ return Directory.GetFiles(path, "appsettings*.json", SearchOption.TopDirectoryOnly)
+ .Where(f => !Path.GetFileName(f).Equals("appsettings.json", StringComparison.OrdinalIgnoreCase))
+ .OrderBy(f => f)
+ .ToList();
+ }
+
+ return new List();
+}
+
+static void ShowHelp()
+{
+ AnsiConsole.Write(new FigletText("Blog Upgrade Assistant").Color(Color.Magenta1));
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine("[bold]Automatically migrates appsettings configuration files to the latest version.[/]");
+ AnsiConsole.WriteLine();
+
+ var table = new Table()
+ .Border(TableBorder.Rounded)
+ .AddColumn(new TableColumn("[bold cyan]Option[/]"))
+ .AddColumn(new TableColumn("[bold cyan]Description[/]"));
+
+ table.AddRow("[yellow]-p, --path [/]", "Path to appsettings file or directory\nDefaults to current directory");
+ table.AddRow("[yellow]-d, --dry-run[/]", "Preview changes without applying them");
+ table.AddRow("[yellow]-b, --backup-dir [/]", "Custom backup directory path\nDefaults to './backups'");
+ table.AddRow("[yellow]-h, --help[/]", "Display this help message");
+ table.AddRow("[yellow]-v, --version[/]", "Display tool version");
+
+ AnsiConsole.Write(table);
+ AnsiConsole.WriteLine();
+
+ AnsiConsole.MarkupLine("[bold]Examples:[/]");
+ AnsiConsole.MarkupLine(" [dim]# Migrate files in current directory[/]");
+ AnsiConsole.MarkupLine(" [green]dotnet run[/]");
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(" [dim]# Preview changes[/]");
+ AnsiConsole.MarkupLine(" [green]dotnet run -- --dry-run[/]");
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(" [dim]# Migrate specific file[/]");
+ AnsiConsole.MarkupLine(" [green]dotnet run -- --path appsettings.Production.json[/]");
+}
+
+static void ShowVersion()
+{
+ AnsiConsole.Write(new FigletText("v1.0.0").Color(Color.Cyan1));
+ AnsiConsole.MarkupLine("[bold]Blog Upgrade Assistant[/]");
+ AnsiConsole.MarkupLine($"[dim]Target Blog Version: 12.0[/]");
+}
+
+static void ParseCommandLineArguments(string[] args, ref string targetPath, ref string backupDirectory, ref bool dryRun, ref bool showHelp, ref bool showVersion)
+{
+ var i = 0;
+ while (i < args.Length)
+ {
+ switch (args[i])
+ {
+ case "-p" or "--path" when i + 1 < args.Length:
+ i++;
+ targetPath = args[i];
+ break;
+ case "-b" or "--backup-dir" when i + 1 < args.Length:
+ i++;
+ backupDirectory = args[i];
+ break;
+ case "-d" or "--dry-run":
+ dryRun = true;
+ break;
+ case "-h" or "--help":
+ showHelp = true;
+ break;
+ case "-v" or "--version":
+ showVersion = true;
+ break;
+ }
+ i++;
+ }
+}
diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/README.md b/tools/LinkDotNet.Blog.UpgradeAssistant/README.md
new file mode 100644
index 00000000..1d3e4b79
--- /dev/null
+++ b/tools/LinkDotNet.Blog.UpgradeAssistant/README.md
@@ -0,0 +1,135 @@
+# Blog Upgrade Assistant
+
+A command-line tool to automatically migrate `appsettings.json` files to the latest configuration version for the LinkDotNet.Blog application.
+
+## Features
+
+- **Automatic migration** - Detects current configuration version and applies necessary migrations
+- **Version tracking** - Adds `ConfigVersion` field to track configuration schema version
+- **Safe backups** - Creates timestamped backups before making any changes
+- **Colorful output** - Uses color-coded console messages for better visibility
+- **Dry-run mode** - Preview changes without applying them
+- **Multi-file support** - Can process multiple appsettings files in a directory
+
+## Installation
+
+Build the tool from source:
+
+```bash
+cd tools/LinkDotNet.Blog.UpgradeAssistant
+dotnet build
+```
+
+## Usage
+
+### Basic Usage
+
+Navigate to your project directory containing `appsettings.json` and run:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant
+```
+
+### Specify Path
+
+Migrate a specific file or directory:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/appsettings.json
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/config/directory
+```
+
+### Dry Run
+
+Preview changes without applying them:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run
+```
+
+### Custom Backup Directory
+
+Specify a custom backup location:
+
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --backup-dir ./my-backups
+```
+
+## Command-Line Options
+
+| Option | Short | Description |
+|--------|-------|-------------|
+| `--path ` | `-p` | Path to appsettings.json file or directory. Defaults to current directory. |
+| `--dry-run` | `-d` | Preview changes without applying them. |
+| `--backup-dir ` | `-b` | Custom backup directory path. Defaults to './backups'. |
+| `--help` | `-h` | Display help message. |
+| `--version` | `-v` | Display tool version. |
+
+## Supported Migrations
+
+The tool currently supports migrations from version 8.0 to 12.0:
+
+### 8.0 β 9.0
+- Moves donation settings (`KofiToken`, `GithubSponsorName`, `PatreonName`) to `SupportMe` section
+- Adds `ShowSimilarPosts` setting
+
+### 9.0 β 11.0
+- Adds `UseMultiAuthorMode` setting
+
+### 11.0 β 12.0
+- Adds `ShowBuildInformation` setting
+
+## Configuration Version
+
+After migration, your `appsettings.json` will include a `ConfigVersion` field:
+
+```json
+{
+ "ConfigVersion": "12.0",
+ ...
+}
+```
+
+This field is used to track the configuration schema version and determine which migrations need to be applied.
+
+## Important Notes
+
+- **Always backup** your configuration files before running migrations (the tool does this automatically)
+- **Review changes** after migration to ensure all settings are correct
+- **Database migrations** may be required separately - see `MIGRATION.md` for details
+- The tool is idempotent - running it multiple times on the same file is safe
+
+## Examples
+
+### Migrate all appsettings files in current directory
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant
+```
+
+### Preview changes before applying
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run
+```
+
+### Migrate specific file with custom backup location
+```bash
+dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- \
+ --path ./appsettings.Production.json \
+ --backup-dir ./config-backups
+```
+
+## Troubleshooting
+
+### "No appsettings.json files found"
+Make sure you're in the correct directory or specify the path using `--path` option.
+
+### "Invalid JSON"
+Ensure your appsettings file is valid JSON before running the migration.
+
+### Configuration already up to date
+If you see "No migrations needed", your configuration is already at the latest version.
+
+## See Also
+
+- [MIGRATION.md](../../MIGRATION.md) - Manual migration guide
+- [Configuration Documentation](../../docs/Setup/Configuration.md) - Configuration options