Monday, 2 March 2026

DeepAI Image Colorizer

๐ŸŽจ DeepAI Image Colorizer: Bringing Life to Black & White Photos with .NET

๐Ÿ“– Introduction

In the digital age, we often encounter historical photographs, vintage images, or artistic black and white compositions that we'd love to see in full color. While professional colorization requires significant artistic skill and time, modern AI has democratized this process. Today, we'll explore a .NET console application that leverages the DeepAI Colorization API to automatically transform grayscale images into vibrant, colorized versions.

๐ŸŽฏ The Problem Statement

Colorizing black and white images manually is a time-intensive process that requires:

  • Deep understanding of color theory
  • Artistic sensibility for appropriate color selection
  • Hours of meticulous work in image editing software

For developers and researchers working with large collections of historical images, automated solutions become essential. Our solution provides a programmatic approach to image colorization using cutting-edge AI technology.

๐Ÿ—️ Solution Architecture

The DeepAI Image Colorizer is a lightweight .NET console application that serves as a bridge between local image files and the DeepAI colorization service. The architecture follows clean code principles with separation of concerns:

Core Components

  1. Program.cs - Entry point and command-line interface
  2. ImageColorizerHelper.cs - API interaction and image processing logic
  3. Environment Configuration - Secure API key management

Technology Stack

  • Framework: .NET 10.0 with C# 14.0
  • Dependencies:
    • DotNetEnv for environment variable management
    • System.Net.Http for API communication
  • External Service: DeepAI Colorization API

๐Ÿ’ป Implementation Details

You can see the source code online on my GitHub repo here:

https://github.com/toreaurstadboss/DeepAIColorizer

Command-Line Interface Design

The application features a clean, user-friendly CLI with comprehensive argument parsing:

static async Task Main(string[] args)
{
    // Load environment variables from .env file
    Env.Load();

    var inputPath = GetArgValue(args, "--input") ?? GetArgValue(args, "-i");
    var outputPath = GetArgValue(args, "--output") ?? GetArgValue(args, "-o");
    var apiKey = GetArgValue(args, "--apikey") ?? Environment.GetEnvironmentVariable("DEEPAI_API_KEY");

    // Display help if no arguments provided
    if (args.Length == 0 || args.Contains("--help") || args.Contains("-h"))
    {
        DisplayHelp();
        return;
    }
    // ... validation and processing logic
}

API Integration Layer

The ImageColorizerHelper class encapsulates all DeepAI API interactions, providing a clean abstraction:

public class ImageColorizerHelper
{
    private readonly string _apiKey;
    private readonly HttpClient _httpClient;

    public ImageColorizerHelper(string apiKey)
    {
        if (string.IsNullOrWhiteSpace(apiKey))
        {
            throw new ArgumentException("API key cannot be null or empty.", nameof(apiKey));
        }

        _apiKey = apiKey;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("api-key", _apiKey);
    }
}

Asynchronous Image Processing

The core colorization method handles the complete workflow asynchronously. The image inputted will be posted as a binary array added in MultipartFormDataContent to the endpoint
where DeepAI Colorizer service is served. https://api.deepai.org/api/colorizer - Note - This endpoint is only POST-ed to. The response is an url (json) that points to where we can download the final colorized picture, if success. The code shows we post the input image (grayscale image obviously) to colorize:

public async Task ColorizeImageAsync(string inputPath, string outputPath)
{
    if (!File.Exists(inputPath))
    {
        throw new FileNotFoundException($"Input image not found: {inputPath}");
    }

    // Prepare multipart form data with the image
    using var form = new MultipartFormDataContent();
    var imageBytes = await File.ReadAllBytesAsync(inputPath);
    form.Add(new ByteArrayContent(imageBytes), "image", Path.GetFileName(inputPath));

    Console.WriteLine("⏳ Sending image to DeepAI for colorization...");

    // Send request to DeepAI API
    var response = await _httpClient.PostAsync("https://api.deepai.org/api/colorizer", form);
    response.EnsureSuccessStatusCode();

    var jsonResponse = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"๐Ÿ“ก Received response from DeepAI");

    // Parse JSON response to extract the output URL
    var result = JsonDocument.Parse(jsonResponse);
    if (!result.RootElement.TryGetProperty("output_url", out var urlElement))
    {
        throw new InvalidOperationException("DeepAI response missing 'output_url' property.");
    }

    var outputUrl = urlElement.GetString();
    if (string.IsNullOrWhiteSpace(outputUrl))
    {
        throw new InvalidOperationException("DeepAI returned an empty output URL. The image may have been rejected.");
    }

    Console.WriteLine($"๐ŸŒ Output URL: {outputUrl}");
    Console.WriteLine("⏳ Downloading colorized image...");

    // Download the colorized image
    var colorizedBytes = await _httpClient.GetByteArrayAsync(outputUrl);

    // Ensure output directory exists
    var outputDir = Path.GetDirectoryName(outputPath);
    if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
    {
        Directory.CreateDirectory(outputDir);
    }

    // Save the colorized image
    await File.WriteAllBytesAsync(outputPath, colorizedBytes);
    Console.WriteLine($"๐Ÿ’พ Saved colorized image ({colorizedBytes.Length:N0} bytes)");
}

๐Ÿ”ง Configuration and Security

Environment-Based API Key Management

The application prioritizes security by supporting multiple API key sources:

var apiKey = GetArgValue(args, "--apikey") ?? Environment.GetEnvironmentVariable("DEEPAI_API_KEY");

This allows users to:

  • Store keys in a .env file (loaded automatically)
  • Pass keys via command-line arguments
  • Use environment variables in CI/CD pipelines

Project Configuration

The .csproj file demonstrates modern .NET project setup:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>14.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DotNetEnv" Version="3.1.1" />
  <ItemGroup>

</Project>

๐Ÿš€ Usage Examples

Basic Colorization

DeepAIColorizer --input old_photo.jpg --output colorized_photo.png

With Custom API Key

DeepAIColorizer --input image.png --apikey your_deepai_key_here

Batch Processing Integration

The CLI design makes it perfect for batch processing:

for file in *.jpg; do
    DeepAIColorizer --input "$file"
done

✨ Key Features and Benefits

๐ŸŽจ Automated Colorization

  • Leverages state-of-the-art AI models trained on millions of images
  • Produces natural-looking colors without manual intervention

๐Ÿ”’ Security-First Design

  • Multiple API key management options
  • No hardcoded credentials
  • Environment variable support for production deployments

๐Ÿš€ Developer-Friendly

  • Clean, documented code following .NET best practices
  • Comprehensive error handling and user feedback
  • Asynchronous operations for responsive CLI experience

๐Ÿ“Š Progress Indicators

  • Real-time feedback during processing
  • Clear success/error messaging with emojis
  • File size reporting for verification

๐Ÿ”ง Extensible Architecture

  • Modular design allows easy integration into larger systems
  • HTTP client abstraction enables testing and mocking
  • Clean separation between CLI and business logic

๐Ÿ” Technical Analysis

Performance Characteristics

  • Network I/O: Two HTTP requests per image (upload + download)
  • Memory Usage: Minimal - processes images in streams
  • CPU Overhead: Negligible - delegates heavy computation to DeepAI servers

Error Handling Strategy

The application implements comprehensive error handling:

  • Input Validation: Checks file existence and API key presence
  • API Error Handling: Distinguishes between different HTTP status codes
  • Network Resilience: Proper async/await patterns for network operations
  • User Feedback: Clear error messages with actionable guidance

Code Quality Metrics

  • Cyclomatic Complexity: Low - simple, linear control flow
  • Testability: High - dependency injection and interface segregation
  • Maintainability: Excellent - clear naming and documentation

๐ŸŽ“ Academic Applications

This tool has significant value in academic research:

๐Ÿ“š Historical Research

  • Colorizing archival photographs for modern publications
  • Enhancing visual materials for academic presentations
  • Preserving historical imagery with improved accessibility

๐ŸŽจ Digital Humanities

  • Automated processing of large image collections
  • Integration with research workflows and pipelines
  • Supporting visual analysis in humanities studies

๐Ÿ’ป Computer Science Education

  • Practical example of API integration
  • Demonstration of async programming patterns
  • Real-world application of software engineering principles

๐Ÿ”ฎ Future Enhancements

Potential improvements for future versions:

  • Batch Processing: Support for multiple input files
  • Format Conversion: Automatic format detection and conversion
  • Quality Options: Different colorization quality levels
  • Preview Mode: Generate thumbnails before full processing
  • Integration APIs: REST API wrapper for web applications

๐Ÿ“š Conclusion

The DeepAI Image Colorizer represents a perfect intersection of modern AI capabilities and practical software engineering. By abstracting complex machine learning models behind a simple, secure CLI interface, it makes advanced image processing accessible to developers, researchers, and enthusiasts alike.

The implementation demonstrates key software engineering principles: clean architecture, comprehensive error handling, security-conscious design, and excellent user experience. Whether you're a historian bringing old photographs to life or a developer learning API integration, this project serves as both a practical tool and an educational reference.

Ready to colorize your world? ๐Ÿš€ The code is available on GitHub - clone, build, and start transforming black and white images into vibrant masterpieces!

Tips how to get contact the DeepAI Api using Postman

  • The request must be of type POST and url set to : https://api.deepai.org/api/colorizer
  • Headers - set one header : api-key . The value here is your DeepAI api key and must of course be not compromised.
  • Body : Choose form-data as the type of body. Add a key called image.
  • Choose the folder icon and connect to a local folder on your hard drive and upload image. This is the image key value under POST.
  • You should get a response with a Json with the information where to download the processed image, which is colorized.
Example output of response json: { "id": "exampleGuid1", "output_url": "https://api.deepai.org/job-view-file/exampleGuid2/outputs/output.jpg" } ExampleGuids here will of course vary per run. To download the actual outputted image, just follow the URL. This can actually be done inside Postman.



Example input and output images using the tool

The following examples images shows input and output images using the tool. The scenery is from Trondheim, Norway in 1959. Original photo (grayscale, 1959) :



Colorized photo (DeepAI Image Colorization online API service) using this tool :

Saturday, 21 February 2026

Copy bookmarks between Edge and Canary | Powershell

I just wrote a Powershell script to copy bookmarks from one browser profile to another browser profile. In this case I copied my bookmarks in Edge Chromium
over to Google Canary. Of course, which bookmark file in which folders will vary from browser to browser and also which profile. In this case, the default profile is copied. In case your computer is used by several users, you probably want to copy a specific profile, not Default. In that case, check in file Explorer which profiles are there.

CopyBookmarksFromEdgeToCanary.ps1

<#
Edge -> Chrome Canary bookmarks copy (profile to profile)
Enhancements:
- Clear screen
- Progress bar (Write-Progress) [2](https://stackoverflow.com/questions/2688547/multiple-foreground-colors-in-powershell-in-one-command)
- Colored/emoji-rich output (Write-Host) [3](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-host?view=powershell-7.5)
- Counts number of bookmark URL entries by parsing Bookmarks JSON (roots + children) [1](https://jdhitsolutions.com/blog/powershell-3-0/2591/friday-fun-testing-google-chrome-bookmarks-with-powershell/)
- Measures elapsed time with Stopwatch
#>

Clear-Host

# -----------------------------
# Settings (edit these)
# -----------------------------
$edgeProfileChoice   = "Default"
$canaryProfileChoice = "Default"

# -----------------------------
# Helper: multi-color one-liner output
# -----------------------------
function Write-ColorLine {
    param(
        [string[]]$Text,
        [ConsoleColor[]]$Color,
        [switch]$NoNewLine
    )
    for ($i = 0; $i -lt $Text.Count; $i++) {
        $c = if ($i -lt $Color.Count) { $Color[$i] } else { $Color[-1] }
        Write-Host $Text[$i] -ForegroundColor $c -NoNewline
    }
    if (-not $NoNewLine) { Write-Host "" }
}

# -----------------------------
# Helper: progress stage
# -----------------------------
function Write-Step {
    param(
        [int]$Step,
        [int]$Total,
        [string]$Status
    )
    $pct = [Math]::Round(($Step / $Total) * 100, 0)
    Write-Progress -Id 0 -Activity "๐Ÿงญ Edge ➜ Canary Bookmarks Migration" -Status $Status -PercentComplete $pct
}

# -----------------------------
# Helper: list profile folders that contain Bookmarks
# -----------------------------
function Get-BookmarkProfiles {
    param([string]$BasePath)

    Get-ChildItem -Path $BasePath -Directory -ErrorAction SilentlyContinue |
        Where-Object { Test-Path (Join-Path $_.FullName "Bookmarks") } |
        Select-Object -ExpandProperty Name
}

# -----------------------------
# Helper: pretty file info
# -----------------------------
function FileInfoLine {
    param([string]$Path)
    if (Test-Path $Path) {
        $fi = Get-Item $Path
        "{0}  (Size: {1:n0} bytes, LastWrite: {2})" -f $fi.FullName, $fi.Length, $fi.LastWriteTime
    } else {
        "$Path  (missing)"
    }
}

# -----------------------------
# Helper: count bookmark "url" nodes recursively in Chromium Bookmarks JSON
# -----------------------------
function Get-BookmarkUrlCount {
    param([string]$BookmarksPath)

    if (-not (Test-Path $BookmarksPath)) { return 0 }

    try {
        $json = Get-Content $BookmarksPath -Raw | ConvertFrom-Json
    } catch {
        return 0
    }

    $script:count = 0
    function Walk($node) {
        if ($null -eq $node) { return }

        if ($node.PSObject.Properties.Name -contains "type" -and $node.type -eq "url") {
            if ($node.PSObject.Properties.Name -contains "url" -and $node.url) { $script:count++ }
        }

        if ($node.PSObject.Properties.Name -contains "children" -and $node.children) {
            foreach ($child in $node.children) { Walk $child }
        }
    }

    if ($json.PSObject.Properties.Name -contains "roots") {
        foreach ($rootProp in $json.roots.PSObject.Properties) {
            Walk $rootProp.Value
        }
    }

    return $script:count
}

# -----------------------------
# Plan
# -----------------------------
$totalSteps = 9
$step = 0
$sw = [System.Diagnostics.Stopwatch]::StartNew()

Write-ColorLine -Text @("✨ ", "Bookmark mover ready", " — Edge ➜ Chrome Canary") `
               -Color @("Yellow","Green","Cyan")

$step++; Write-Step $step $totalSteps "Resolving base paths…"

$edgeUserData   = Join-Path $env:LOCALAPPDATA "Microsoft\Edge\User Data"
$canaryUserData = Join-Path $env:LOCALAPPDATA "Google\Chrome SxSata"

# (rest of script unchanged, escaped consistently)
Sample output of running the Powershell script below. The script takes around half a second to run.

✨ Bookmark mover ready — Edge ➜ Chrome Canary

๐Ÿ“ Base paths
   Edge   : C:\Users\someuser\AppData\Local\Microsoft\Edge\User Data
   Canary : C:\Users\someuser\AppData\Local\Google\Chrome SxSata

๐Ÿ”Ž Profiles detected (contain a 'Bookmarks' file)
   Edge   : Default
   Canary : Default

๐ŸŽฏ Selected profiles
   Edge   : Default
   Canary : Default

๐Ÿงพ Full file paths
   Edge Bookmarks   : C:\Users\someuser\AppData\Local\Microsoft\Edge\User Data\Default\Bookmarks  (Size: 288 882 bytes, LastWrite: 20.02.2026 16:44:30)
   Canary Bookmarks : C:\Users\someuser\AppData\Local\Google\Chrome SxSata\Default\Bookmarks  (Size: 288 882 bytes, LastWrite: 20.02.2026 16:44:30)

๐Ÿ“Š Bookmark counts (URL entries)
   Edge (source)   : 585
   Canary (target) : 585

๐Ÿ›Ÿ Backup created: C:\Users\someuser\Desktop\BookmarkBackups\Canary_Default_Bookmarks_20260221_205353.bak


✅ Completed!
๐Ÿ“Œ Wrote Canary Bookmarks:
   C:\Users\someuser\AppData\Local\Google\Chrome SxSata\Default\Bookmarks
๐Ÿ“ฆ Backup folder:
   C:\Users\someuser\Desktop\BookmarkBackups

๐Ÿ“ˆ Results
   Canary before : 585 bookmarks
   Canary after  : 585 bookmarks
   ฮ” Change      : 585

⏱️ Time elapsed: 00:00.504
๐Ÿš€ Tip: Launch Chrome Canary now — bookmarks load on startup.


Sunday, 15 February 2026

Blazor Property Grid - Updated version

Blazor Property Grid

I wrote a Blazor Property Grid component back in 2021, the updated the version is now available some five years later.

You will find the property grid available on Nuget here:

https://www.nuget.org/packages/BlazorPropertyGridComponents/1.2.4

The property grid allows inspection of an object's properties and also edit them. Supported data types are the fundamental data types, which means integers, date times, booleans, strings and numbers.

The property grid supports nested properties, properties that are compound objects themselves. I have not yet added template supported for custom data types, but fundamentals nested properties inside the complex property are shown. This means you can drill down into an object and both inspect the object and also edit the properties that are the mentioned fundamental data types.

This project delivers a Blazor property‑grid component capable of inspecting and editing both top‑level and deeply nested properties of an object. It works smoothly with nested structures and internal members.

It has been verified using Blazor WebAssembly running on .NET. 10. A sample client is included in the BlazorSampleClient project.

The component implementation is located inside the Razor Class Library BlazorPropertyGridComponents.

Licensing is MIT and the component is provided as‑is. If used in production, you are responsible for validating its suitability. Forks, modifications, and commercial reuse are allowed. The project originated as a personal learning exercise.

The screenshot below (from the sample client) illustrates the property grid on the right side updating the same model that the form on the left is bound to.

The component expects your project to include Bootstrap and Font Awesome. You can inspect exact versions inside libman.json. Additional styling resides in styles.css.

libman.json example

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "bootstrap@5.3.3",
      "destination": "wwwroot/bootstrap/"
    },
    {
      "library": "font-awesome@6.5.1",
      "destination": "wwwroot/font-awesome/"
    }
  ]
}

Supported Property Types

  • DateTime (full or date‑only)
  • Float
  • Decimal
  • Int
  • Double
  • String
  • Bool
  • Enums (rendered using <select> and <option>)

Using the Component – API

Below is a Razor example showing how to use the component. The PropertySetValueCallback is optional and only required if you want UI changes reflected elsewhere immediately.

<div class="col-md-6"> <!-- Property grid shown in the right column -->
  <h4 class="mb-3">Property Grid Component Demo</h4>

  <PropertyGridComponent
      PropertySetValueCallback="OnPropertyValueSet"
      ObjectTitle="Property grid - Edit form for : 'Customer Details'"
      DataContext="@exampleModel">
  </PropertyGridComponent>
</div>

@code {
  private void OnPropertyValueSet(PropertyChangedInfoNotificationInfoPayload pi)
  {
    if (pi != null)
    {
      JsRunTime.InvokeVoidAsync(
        "updateEditableField",
        pi.FieldName,
        pi.FullPropertyPath,
        pi.Value
      );
    }
  }

  private CustomerModel exampleModel = new CustomerModel
  {
    Address = new AddressInfo
    {
      Zipcode = 7045,
      AddressDetails = new AddressInfoDetails
      {
        Box = "PO Box 123"
      }
    }
  };

  private void HandleValidSubmit()
  {
  }
}

The JavaScript function updateEditableField is located in script.js.

Updating via C# Instead of JS

You can also update the model directly through reflection. This approach ensures proper Blazor re‑rendering. Be sure to call StateHasChanged(). Note that this code is only required in case you use the property grid for editing properties and show the object you edit other places on the same page.

@code {

  private void OnPropertyValueSet(PropertyChangedInfoNotificationInfoPayload pi)
  {
    if (pi == null)
      return;

    SetPropertyByPath(exampleModel, pi.FullPropertyPath, pi.Value, pi.ValueType);
    StateHasChanged();
  }

  private void SetPropertyByPath(object target, string propertyPath, object value, string valueType)
  {
    if (target == null || string.IsNullOrEmpty(propertyPath))
      return;

    var parts = propertyPath.Split('.');
    var current = target;

    // Move to parent object

        // Navigate to the parent object
        for (int i = 0; i < parts.Length - 1; i++)
        {
            var prop = current.GetType().GetProperty(parts[i]);
            if (prop == null) return;
            current = prop.GetValue(current);
            if (current == null) return;
        }

        // Set the final property
        var finalProp = current.GetType().GetProperty(parts[^1]);
        if (finalProp == null) return;

        try
        {
            object convertedValue = value;

            if (finalProp.PropertyType.IsEnum && value != null)
            {
                var valStr = value.ToString();
                if (int.TryParse(valStr, out int intVal))
                    convertedValue = Enum.ToObject(finalProp.PropertyType, intVal);
                else
                    convertedValue = Enum.Parse(finalProp.PropertyType, valStr, ignoreCase: true);
            }
            else if (finalProp.PropertyType == typeof(bool) && value != null)
            {
                convertedValue = Convert.ToBoolean(value);
            }
            else if (value != null && finalProp.PropertyType != typeof(string))
            {
                convertedValue = Convert.ChangeType(value, finalProp.PropertyType);
            }

            finalProp.SetValue(current, convertedValue);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to set {propertyPath}: {ex.Message}");
        }
    }

    private CustomerModel exampleModel = new CustomerModel
    {
        Address = new AddressInfo
        {
            Zipcode = 7045,
            AddressDetails = new AddressInfoDetails
            {
                Box = "PO Box 123"
            }
        }
    };

    private void HandleValidSubmit()
    {

    }

}

I have used Claude Haiku 4.5 LLM to create a nice Architecture Documentation of my component so it is more convenient to see the structure of the Blazor property grid component. This is handy for those developers who wants to work on the component and add features and understood its structure. As mentioned before, the component is licensed with MIT license and you can adjust the component as needed free of use (and responsiblity).

๐Ÿ”ท Blazor Property Grid Component - Architecture Documentation


๐Ÿ“Š Property Grid Component Structure

PROPERTY GRID COMPONENT ARCHITECTURE
====================================

ROOT CONTAINER: PropertyGridComponent.razor
├── EditForm (wraps the entire grid)
└── .property-grid-container
    ├── Header Table (.property-grid-header-table)
    │   └── thead (.property-grid-header)
    │       └── tr
    │           ├── th: "Property"
    │           ├── th: "Value"
    │           └── th: Edit Button (pencil icon)
    │
    └── Body Table (.property-grid-body-table)
        └── tbody (.property-grid-body)
            └── foreach KeyValuePair in Props
                ├── IF: Simple Property (IsClass = false)
                │   └── [COMMENTED OUT - NOT DISPLAYED]
                │
                └── IF: Nested Class (IsClass = true)
                    └── tr
                        ├── td (colspan=2)
                        │   ├── Expand/Collapse Button (minus icon)
                        │   └── div (.collapse .show)
                        │       └── PropertyRowComponent [Depth=1]
                        └── td (empty)


PROPERTY ROW COMPONENT: PropertyRowComponent.razor
====================================================

FOR EACH SubProperty in PropertyInfoAtLevel.SubProperties:
├── IF: Simple Type (not a class or System namespace)
│   └── tr (.property-row)
│       ├── td (.property-name-cell)
│       │   └── span (.property-name) = Property Name
│       │
│       └── td (.property-value-cell)
│           ├── IF: DateTime
│           │   └── InputDate (if editable) OR span (if readonly)
│           │
│           ├── IF: bool
│           │   └── InputCheckbox (if editable) OR span (if readonly)
│           │
│           ├── IF: int
│           │   └── InputNumber (if editable) OR span (if readonly)
│           │
│           ├── IF: double
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: decimal
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: float
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: string
│           │   └── InputText type="text" (if editable) OR span (if readonly)
│           │
│           ├── IF: Enum
│           │   └── select (if editable)
│           │       └── option foreach Enum value
│           │       OR span (if readonly)
│           │
│           └── ELSE: Unknown Type
│               └── span = Raw Value
│
└── IF: Nested Class (PropertyValue is HierarchicalPropertyInfo)
    └── tr (.property-row .nested-property-row)
        ├── td (colspan=2, .nested-property-cell)
        │   ├── span (.nested-property-name) = Nested Class Name
        │   ├── Expand/Collapse Button (plus icon)
        │   └── div (.collapse or .collapse.show)
        │       └── PropertyRowComponent [Depth+1] (RECURSIVE)
        └── [Empty td]


DATA STRUCTURE: HierarchicalPropertyInfo
================================================

HierarchicalPropertyInfo
├── PropertyName: string
├── PropertyValue: object
├── PropertyType: Type
├── SubProperties: Dictionary<string, HierarchicalPropertyInfo>
├── FullPropertyPath: string (dot-separated path)
├── IsClass: bool (indicates if this is a class type)
├── IsEditable: bool
├── NewValue: object (for tracking changes)
└── ValueSetCallback: EventCallback

HIERARCHY BUILD PROCESS:
========================

MapPropertiesOfDataContext(root object)
├── Create ROOT HierarchicalPropertyInfo
├── For each Public Property:
│   ├── IF: Simple Type (not class or not System namespace)
│   │   └── Add to SubProperties as leaf node (IsClass=false)
│   │
│   └── IF: Nested Class (class type, not System namespace)
│       └── Recursively call MapPropertiesOfDataContext
│           └── Add to SubProperties with nested tree (IsClass=true)
│
└── Return complete tree structure


INTERACTIVITY:
==============

Edit Mode Toggle:
├── ToggleEditButton() → IsEditingAllowed = !IsEditingAllowed
└── SetEditFlagRecursive() → walks entire tree setting IsEditable on all nodes

Value Changes:
├── SetValue() called on input change
├── Handles type conversion (enums, numbers, dates, etc.)
├── Updates PropertyValue immediately for UI reflection
└── Invokes ValueSetCallback → OnValueSetCallback()

Property Change Callback:
└── PropertySetValueCallback emits PropertyChangedInfoNotificationInfoPayload with:
    ├── FieldName
    ├── FullPropertyPath
    ├── Value
    └── ValueType (text, boolean, number, date, enum)

Expand/Collapse:
└── ToggleExpandButton() → JavaScript: blazorPropertyGrid.toggleExpandButton()
    └── Toggles Bootstrap collapse class on nested div

๐Ÿ”ง Component Descriptions and Roles

1. PropertyGridComponent (PropertyGridComponent.razor + PropertyGridComponent.razor.cs)

Role: Root container and orchestrator for the entire property grid UI

Primary Responsibility:

Accepts a data object and transforms it into a hierarchical property structure

Key Functions:

  • OnParametersSet() - Initializes the component when parameters are passed
  • MapPropertiesOfDataContext() - Recursively walks the object graph and builds the HierarchicalPropertyInfo tree
  • IsNestedProperty() - Determines if a property should be expanded (nested classes)
  • ToggleEditButton() - Handles the edit mode button click (pencil icon in header)
  • SetEditFlag() / SetEditFlagRecursive() - Propagates the IsEditable flag through the entire tree
  • OnValueSetCallback() - Listens for value changes and emits PropertySetValueCallback events to the parent component

Parameters:

  • DataContext (object) - The root object to display
  • ObjectTitle (string) - Display title for the grid
  • IsEditingAllowed (bool) - Whether fields are editable
  • PropertySetValueCallback (EventCallback) - Callback when a property value changes

Rendering:

Header table with Property/Value columns and edit button. Body table containing rows for top-level properties (delegates nested properties to PropertyRowComponent)

2. PropertyRowComponent (PropertyRowComponent.razor + PropertyRowComponent.razor.cs)

Role: Recursive component that renders individual property rows and handles nested object expansion

Primary Responsibility:

Display a single level of properties and recursively display nested levels

Key Functions:

  • SetValue() - Handles input change events, performs type conversion, and invokes callbacks
  • ToggleExpandButton() - JS interop to toggle Bootstrap collapse classes for expand/collapse UI
  • Property type detection - Conditionally renders different input controls based on property type

Type Support:

  • DateTime → InputDate (HTML5 datetime-local)
  • bool → InputCheckbox
  • int → InputNumber
  • double, decimal, float → InputText with type="number"
  • string → InputText with type="text"
  • Enum → HTML select dropdown with enum values
  • Nested Classes → Recursive PropertyRowComponent call with Depth+1
  • Unknown Types → Raw span display

Parameters:

  • PropertyInfoAtLevel (HierarchicalPropertyInfo) - The current property node to render
  • Depth (int) - Nesting depth for styling and collapse behavior
  • DisplayedFullPropertyPaths (List<string>) - Tracks which paths have been rendered (prevents duplication)

Features:

  • Editable/Read-only modes based on IsEditable flag
  • Collapse/expand functionality for nested objects
  • Visual styling (beige background for read-only values)
  • Full property path as tooltip for clarity

3. HierarchicalPropertyInfo (HierarchicalPropertyInfo.cs)

Role: Data structure representing a single property node in the object hierarchy

Primary Responsibility:

Act as a node in a tree structure that mirrors the original object's property graph

Properties:

  • PropertyName (string) - Name of the property
  • PropertyValue (object) - Current value of the property
  • PropertyType (Type) - CLR type of the property
  • SubProperties (Dictionary<string, HierarchicalPropertyInfo>) - Child properties (for nested objects)
  • FullPropertyPath (string) - Dot-separated path from root (e.g., "Customer.Address.Street")
  • IsClass (bool) - Whether this represents a class type (true = nested object, false = leaf value)
  • IsEditable (bool) - Whether the property can be edited in the current UI state
  • NewValue (object) - Tracks modified value before submission
  • ValueSetCallback (EventCallback) - Callback when this property's value changes

4. PropertyChangedInfoNotificationInfoPayload (PropertyChangedInfoNotificationInfoPayload.cs)

Role: Payload object that carries change notification information back to the parent

Primary Responsibility:

Communicate property change events with detailed context

Properties:

  • FieldName (string) - Name of the property
  • FullPropertyPath (string) - Complete path to the property
  • Value (object) - New value
  • ValueType (string) - Type of value ("text", "number", "boolean", "date", "enum")

๐Ÿ”„ Component Interaction Flow

User interacts with PropertyGrid
    ↓
PropertyGridComponent receives DataContext
    ↓
MapPropertiesOfDataContext() builds tree of HierarchicalPropertyInfo
    ↓
Renders PropertyRowComponent for each top-level property
    ↓
PropertyRowComponent renders based on type:
  ├─ Simple types → InputControl (text, number, checkbox, date, select)
  └─ Nested classes → Recursive PropertyRowComponent
    ↓
User edits a value
    ↓
PropertyRowComponent.SetValue() processes input
    ↓
ValueSetCallback invoked
    ↓
OnValueSetCallback() determines value type and emits PropertySetValueCallback
    ↓
Parent component receives PropertyChangedInfoNotificationInfoPayload

๐ŸŽฏ Key Design Patterns

1. Recursive Composition

PropertyRowComponent calls itself recursively for nested objects, allowing unlimited nesting depth.

2. Tree Structure

HierarchicalPropertyInfo forms a tree that mirrors the object graph, enabling efficient traversal and state management.

3. Event Cascading

Value changes propagate up through callbacks, maintaining separation of concerns between components.

4. Type-Driven Rendering

PropertyRowComponent dynamically renders different input controls based on CLR type, supporting datetime, enum, numeric, boolean, and string types.

5. Bootstrap Collapse Integration

Nested objects use Bootstrap's collapse classes for expand/collapse functionality, toggled via JavaScript interop.


๐Ÿ“Š Data Flow Summary

DataContext (Object)
    ↓
MapPropertiesOfDataContext() [Reflection-based tree building]
    ↓
HierarchicalPropertyInfo Tree
    ↓
PropertyGridComponent → PropertyRowComponent Chain [Rendering]
    ↓
HTML Tables + Form Controls
    ↓
[User edits value]
    ↓
ValueSetCallback Events [Bubbling up]
    ↓
PropertyChangedInfoNotificationInfoPayload [Event payload]
    ↓
Parent Component [Handles business logic]

Sunday, 25 January 2026

Rendering Blazor components using HtmlRenderer

Rendering Blazor Components Dynamically with HtmlRenderer in .NET 8

๐Ÿš€ Rendering Blazor Components Dynamically with HtmlRenderer in .NET 8

A practical example using a generic component renderer

Source code available here:
๐Ÿ‘‰ https://github.com/toreaurstadboss/BlazorIntroCourse/tree/main/Components/Demos/TemplatedComponents

Blazor has always been about component‑driven UI, but until .NET 8, components were tightly coupled to the Blazor runtime — either WebAssembly or Server. With the introduction of HtmlRenderer, that boundary disappears.

You can now render any .razor component into pure HTML on the server, without a browser, without WebAssembly, and without a running Blazor app. This opens up a whole new world of scenarios:

  • Rendering components inside MVC, Razor Pages, or Minimal APIs
  • Generating HTML for emails, PDFs, reports, or static site generation
  • Using Blazor components as server‑side templates
  • Building dynamic component renderers that choose components at runtime
  • Running components in background services or unit tests

In this post, I’ll walk through a practical example: A generic Blazor component that uses HtmlRenderer to render any component dynamically, and a simple Bootstrap‑style Alert component to demonstrate how it works.


๐ŸŽฏ Why HtmlRenderer Matters

Here’s the short version — arguments you can inform other developers why HtmlRenderer opens up so many possibilities for using Blazor components many places:

  • Render Blazor components anywhere — MVC, Razor Pages, Minimal APIs, background jobs.
  • Generate static HTML — perfect for emails, PDFs, SEO, caching, and static sites.
  • Use Blazor components as templates — no need for Razor Views or TagHelpers.
  • Full component lifecycle — DI, parameters, cascading values, child content all work.
  • No browser required — everything runs server‑side.
  • Dynamic component composition — choose components at runtime using generics.
  • Great for testing — render components without a browser or JS runtime.

๐Ÿ–ผ️ Diagram: How HtmlRenderer Works

Your Blazor Component (e.g., <Alert>) Generic Renderer (TComponent + parameters) HtmlRenderer .NET 8 server-side renderer HTML Output (string / MarkupString)

๐Ÿ“ฆSample Blazor component used | Alert.razor — A Simple Bootstrap‑Style Alert Component


<div 
    class=@($"alert {(IsDismissable ? "alert-dismissible fade show" : "")} alert-{AlertType.ToString().ToLower()}")
    role="alert">
    @if (IsDismissable)
    {
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    }
    @ChildContent
</div>

@code {

    [Parameter]
    public required RenderFragment ChildContent { get; set; } = @<b>Default message</b>;

    [Parameter]
    public bool IsDismissable { get; set; }

    [Parameter]
    public AlertTypeEnum AlertType { get; set; } = AlertTypeEnum.Success;

}


๐Ÿงฉ The alert type enum | AlertTypeEnum.cs


namespace DependencyInjectionDemo.Components.Demos.TemplatedComponents
{
    public enum AlertTypeEnum
    {
        Primary,
        Secondary,
        Success,
        Danger,
        Warning,
        Info,
        Light,
        Dark
    }
}


๐Ÿง  The Generic HTML Renderer Component | GenericHtmlRenderer.razor


@((MarkupString)RenderedHtml)

@typeparam TComponent

@code {
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public Dictionary<string, object?> Parameters { get; set; } = new();

    [Inject] IServiceProvider ServiceProvider { get; set; } = default!;
    [Inject] ILoggerFactory LoggerFactory { get; set; } = default!;

    private string RenderedHtml { get; set; } = string.Empty;

    protected override async Task OnInitializedAsync()
    {
        if (ChildContent != null)
        {
            Parameters["ChildContent"] = ChildContent;
        }

        RenderedHtml = await RenderComponentAsync(Parameters);
    }

    private async Task<string> RenderComponentAsync(Dictionary<string, object?> parameters)
    {
        if (!typeof(IComponent).IsAssignableFrom(typeof(TComponent)))
        {
            throw new InvalidOperationException($"{typeof(TComponent).Name} is not a valid Blazor component.");
        }

        using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);
        var parameterView = ParameterView.FromDictionary(parameters);

        var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
        {
            var result = await htmlRenderer.RenderComponentAsync(typeof(TComponent), parameterView);
            return result.ToHtmlString();
        });

        return html;
    }
}

Note that we must also check if the parameters sent in here actually are present in the component. That means the parameter is a public property with attribute Parameter. The revised code of the method RenderComponentAsync is shown below.

private async Task<string> RenderComponentAsync(Dictionary<string, object?> parameters)
{

    if (!typeof(IComponent).IsAssignableFrom(typeof(TComponent)))
    {
        throw new InvalidOperationException($"{typeof(TComponent).Name} is not a valid Blazor component.");
    }

    using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);

    var filteredParameters = GetValidParameters(parameters);

    var parameterView = ParameterView.FromDictionary(filteredParameters);

    var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
    {
        var result = await htmlRenderer.RenderComponentAsync(typeof(TComponent), parameterView);
        return result.ToHtmlString();
    });

    return html;
}

private Dictionary<string, object?> GetValidParameters(Dictionary<string, object?> parameters)
{
    var validnames = typeof(TComponent).GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Where(p => p.IsDefined(typeof(ParameterAttribute), true))
        .Select(p => p.Name)
        .ToHashSet(StringComparer.OrdinalIgnoreCase);   
    validnames.Add("ChildContent");

    var filteredParameters = parameters
        .Where(kvp => validnames.Contains(kvp.Key))
        .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

    var unknownKeys = parameters.Keys.Except(filteredParameters.Keys, StringComparer.OrdinalIgnoreCase).ToList();
    if (unknownKeys.Count > 0)
    {
        var logger = LoggerFactory.CreateLogger("GenericHtmlRenderer");
        string warningmsg = $"Dropped unknown component parameters for {typeof(TComponent).FullName}: {string.Join(",", unknownKeys)}";
        logger.LogWarning(warningmsg);
        Console.WriteLine(warningmsg);
    }

    return filteredParameters;
}

This adjustment makes the code more resilient in case a non-existent parameter is sent in. Please note that an invalid parameter value will still give a runtime exception, so further validation could be added here, such as using TypeConverter to check if we can convert the parameter value sent in to the parameter in the component. The revised code is shown below.

@using System.Reflection
@using System.ComponentModel
@using Microsoft.AspNetCore.Components
@((MarkupString)RenderedHtml)

@typeparam TComponent

@code {
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter] public Dictionary<string, object?> Parameters { get; set; } = new();

    [Inject] IServiceProvider ServiceProvider { get; set; } = default!;
    [Inject] ILoggerFactory LoggerFactory { get; set; } = default!;

    private string RenderedHtml { get; set; } = string.Empty;

    protected override async Task OnInitializedAsync()
    {
        if (ChildContent != null)
        {
            Parameters["ChildContent"] = ChildContent;
        }

        RenderedHtml = await RenderComponentAsync(Parameters);
    }

    private async Task<string> RenderComponentAsync(Dictionary<string, object?> parameters)
    {

        if (!typeof(Microsoft.AspNetCore.Components.IComponent).IsAssignableFrom(typeof(TComponent)))
        {
            throw new InvalidOperationException($"{typeof(TComponent).Name} is not a valid Blazor component.");
        }

        using var htmlRenderer = new HtmlRenderer(ServiceProvider, LoggerFactory);

        var filteredParameters = GetValidParameters(parameters);

        var parameterView = ParameterView.FromDictionary(filteredParameters);

        var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
        {
            var result = await htmlRenderer.RenderComponentAsync(typeof(TComponent), parameterView);
            return result.ToHtmlString();
        });

        return html;
    }


    private Dictionary<string, object?> GetValidParameters(Dictionary<string, object?> parameters)
    {
        var componentType = typeof(TComponent);

        // Get valid property names and their PropertyInfo
        var validProperties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.IsDefined(typeof(ParameterAttribute), true))
            .ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);

        // Add ChildContent as a special case
        validProperties["ChildContent"] = null!;

        var filteredParameters = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);

        foreach (var kvp in parameters)
        {
            if (validProperties.ContainsKey(kvp.Key))
            {
                var propertyInfo = validProperties[kvp.Key];

                if (propertyInfo != null)
                {
                    var targetType = propertyInfo.PropertyType;
                    var value = kvp.Value;

                    // Check if value is null or already assignable
                    if (value == null || targetType.IsInstanceOfType(value))
                    {
                        filteredParameters[kvp.Key] = value;
                    }
                    else
                    {
                        // Use TypeDescriptor to check conversion
                        var converter = TypeDescriptor.GetConverter(targetType);
                        if (converter != null && converter.CanConvertFrom(value.GetType()))
                        {
                            try
                            {
                                filteredParameters[kvp.Key] = converter.ConvertFrom(value);
                            }
                            catch
                            {
                                LogConversionWarning(kvp.Key, value);
                            }
                        }
                        else
                        {
                            LogConversionWarning(kvp.Key, value);
                        }
                    }



๐Ÿงช Demo Page — Rendering Alerts Dynamically | GenericHtmlRendererDemo.razor


@page "/GenericHtmlRendererDemo"
@using BlazorIntroCourse.Components.Demos.TemplatedComponents

<h3>GenericHtmlRendererDemo - Dynamic Alert rendering</h3>

<h6>Here the parameters are set in code</h6>
<GenericHtmlRenderer TComponent="Alert"
                     ChildContent="alertContent"
                     Parameters="alternateAlertParameters" />

<h6>Here we use the ChildContent element to enter in the html</h6>
<GenericHtmlRenderer TComponent="Alert"
                     Parameters="alertParameters">
    <ChildContent>
        <text>This is the second alert</text>
    </ChildContent>                 
</GenericHtmlRenderer>

<h6>Here we use inline object initializer for parameters</h6>
<GenericHtmlRenderer TComponent="Alert"
                     Parameters="@(new Dictionary<string, object>{
    [ "AlertType" ] = AlertTypeEnum.Danger,
    [ "IsDismissable" ] = true
})">
    <ChildContent>
        <text>This is the third alert</text>
    </ChildContent>  
</GenericHtmlRenderer>

@code {

    private RenderFragment alertContent = @<text>This is the first Alert</text>;
    Dictionary<string, object?> alertParameters = new()
    {
        { "AlertType", AlertTypeEnum.Info}, 
        { "IsDismissable", true }
    };

    Dictionary<string, object?> alternateAlertParameters = new()
    {
        { "AlertType", AlertTypeEnum.Success }, 
        { "IsDismissable", true }
    };

}


๐Ÿ“˜ Diagram: Where You Can Use HtmlRenderer

HtmlRenderer API .NET 8 MVC Razor Pages Minimal APIs Background Jobs (emails, PDFs, reports) Static HTML Generation

๐Ÿ Wrapping Up

HtmlRenderer in .NET 8 is one of those features that quietly unlocks a huge amount of flexibility. It turns Blazor components into universal UI building blocks that can be rendered:

  • in a browser
  • on the server
  • inside MVC
  • inside Razor Pages
  • inside Minimal APIs
  • inside background services
  • inside unit tests
  • or even at build time

The generic component renderer shown here is a concrete example of how powerful this can be — dynamic component rendering, runtime composition, and server‑side HTML generation all in one.

If you’re curious, you can explore the full source code here: (part of a larger repo I am looking into currently as a playground for misc Blazor functionality):
๐Ÿ‘‰ https://github.com/toreaurstadboss/BlazorIntroCourse/tree/main/Components/Demos/TemplatedComponents

Sunday, 21 December 2025

Finding out file extension from byte inspection

Consider a byte array stored in a column in a table in a database column. How can we identify the file extension of the byte array by inspecting the byte array itself?
Note that byte arrays could be saved many places, also within files or similar.
The extension of a file can be discovered by inspect the File header. This is the first bytes, usually the first tens or hundreds of bytes of the byte array and constitute the file header. Some extensions got multiple file headers. A best effort to identity byte contents of a column in a database.

Let's use Powershell to inspect a file on disk, a sample JPEG file (.jpg). Lets run the following little script:

format-hex .\Stavkyrkje_Rรธldal.jpg | Select-Object -First 16 The first few bytes are FF D8 FF
I have added a sample Github repo with utility code to check well-known file types for their file extensions.

https://github.com/toreaurstadboss/FileHeaderUtil

The following screenshot shows the application in use. It found out that a byte array seems to be a PDF file by looking at the file header and file trailer. A good match was found :


In fact, a very good match, since both the header and the trailer fully agrees. Note that the 0A bytes are just padding bytes at the end of files and ignored in this util. See the method NormalizeHex presented further below.

Using Gary Kessler`s assembled lists of known file headers and trailers for well-known file types

The util class below shows the helper methods that inspects a byte array and evalues the file header and file trailer against a list of known such headers and trailers.

It bases a compilation of known file headers and file trailers known as "Magic Numbers", compiled by Gary Kessler during the years. In all, 600+ known file types are checked against to classify the matching file extension. Please note that there are cases where multiple matches exists of file header and file trailers matching the given byte array. The matches are sorted by number of matching bytes. The assembled list is very helpful. Thanks, Gary !

Using the file header and also possibly the last bytes of a byte array, the file trailer, we can classify the file type we have in the byte array, i.e. file extension is also implied here by recognizing the file array.

Of course, if one is allowing byte array to be uploaded from a public site for example, it still would be possible to inject malicious bytes, but being able to detect the kind of file is useful both concerning security policies and also determine if the bytes should be handled by an external application or provide information to the end-user what kind of file we have provided a path to for this util.

The curated list of file headers is based upon the list of signatures gathered by Gary Kessler and published on his website here (license of that file is not stated and considered public as it is publicly available information on his website not marked with a license):

https://www.garykessler.net/library/file_sigs.html

This list contains about 650 file types and should cover most of the wellknown formats, including formats not being used so often anymore. If you want to augment the list, check other sources such as Wikipedia if there is information about the given file extension's file header and/or file trailer, so-called "Magic number".

The curated list was updated 3rd June 2023 and contains most well-known file types.

The program uses the file signatures (Json format) to identity the file types of a byte array. Most usually, this is judged by looking at the first few bytes of the file (the so-called "magic numbers"). Sometimes, the file signature may also include bytes from the end of the file (the "trailer").


FileSignatureUtil.cs



using System;
using System.Collections.Generic;
using System.Text;

namespace FileHeaderUtil;


public static class FileSignatureUtil
{

    static FileSignature[] _fileSignatures = [];

    static FileSignatureUtil()
    {
        string json = File.ReadAllText("file_Sigs.json");
        var fileSignaturesRoot = System.Text.Json.JsonSerializer.Deserialize<FileSignatureRootElement>(json, new System.Text.Json.JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });
        _fileSignatures = fileSignaturesRoot?.FileSigs?.ToArray()!;
    }

    /// <summary>
    /// Scans the specified file and returns a list of file signatures that match the file's header and, if applicable,
    /// file's trailer.
    /// </summary>
    /// <remarks>Only file signatures with a defined header are considered for matching. Trailer matching is
    /// performed if both the file and the signature define a trailer. A header and trailer of 64 bytes is evaluted to also 
    /// detect file types / extensions with longer headers and trailers.</remarks>
    /// <param name="targetFile">The path to the file to be analyzed. Cannot be null or empty.</param>
    /// <param name="byteCount">The number of bytes to read from the file for signature matching. Defaults to 64.</param>
    /// <param name="offset">The byte offset at which to begin reading the file for signature matching. Defaults to 0.</param>
    /// <param name="origin">Specifies the reference point used to obtain the offset. Defaults to <see cref="SeekOrigin.Begin"/>.</param>
    /// <returns>A list of <see cref="FileSignature"/> objects that match the file's header and trailer. The list is empty if no
    /// signatures match.</returns>

    public static List<FileSignature> GetMatchingFileSignatures(string targetFile, int byteCount = 64, int offset = 0, SeekOrigin origin = SeekOrigin.Begin)
    {
        static string NormalizeHex(string? hex, bool trimPadding)
        {
            if (string.IsNullOrWhiteSpace(hex))
            {
                return string.Empty;
            }           

            var parts = hex.Replace("-", " ").Split(new[] { ' ', }, StringSplitOptions.RemoveEmptyEntries)
                           .Select(h => h.ToUpperInvariant())
                           .ToList();

            if (trimPadding)
            {
                while (parts.Count > 0 && (parts.Last() == "0A" || parts.Last() == "0D" || parts.Last() == "00"))
                {
                    parts.RemoveAt(parts.Count - 1);
                }
            }

            return string.Join(" ", parts);
        }

        var matches = new List<(FileSignature Sig, int Score)>();

        string fileHeader = NormalizeHex(FileUtil.ShowHeader(targetFile, offset: 0), trimPadding: false);
        string fileTrailer = NormalizeHex(FileUtil.ShowTrailer(targetFile), trimPadding: true);

        foreach (var signature in _fileSignatures)
        {
            if (string.IsNullOrWhiteSpace(signature?.HeaderHex) || signature.HeaderHex == "(NULL)")
                continue;

            string sigHeader = NormalizeHex(signature.HeaderHex, trimPadding: false);
            string sigTrailer = NormalizeHex(signature.TrailerHex, trimPadding: true);

            if (!fileHeader.StartsWith(sigHeader, StringComparison.OrdinalIgnoreCase))
                continue;

            // Trailer check if defined
            if (!string.IsNullOrWhiteSpace(sigTrailer) && sigTrailer != "(NULL)")
            {
                if (!fileTrailer.EndsWith(sigTrailer, StringComparison.OrdinalIgnoreCase))
                    continue;
            }

            // Compute match score (# of matching bytes in header and trailer of file)
            int headerScore = CountMatchingPrefix(fileHeader, sigHeader);
            int trailerScore = CountMatchingSuffix(fileTrailer, sigTrailer);
            int scoreMeasuredAsMatchingByteCount = headerScore + trailerScore;
            signature.MatchingBytesCount = scoreMeasuredAsMatchingByteCount;
            signature.MatchingTrailerBytesCount = trailerScore;
            signature.MatchingHeaderBytesCount = headerScore;
            matches.Add((signature, scoreMeasuredAsMatchingByteCount));
        }

        return matches.OrderByDescending(m => m.Score).Select(m => m.Sig).ToList();
    }

    // Helpers
    private static int CountMatchingPrefix(string source, string pattern)
    {
        var srcParts = source.Split(' ');
        var patParts = pattern.Split(' ');
        int count = 0;
        for (int i = 0; i < Math.Min(srcParts.Length, patParts.Length); i++)
        {
            if (srcParts[i].Equals(patParts[i], StringComparison.OrdinalIgnoreCase))
                count++;
            else break;
        }
        return count;
    }

    private static int CountMatchingSuffix(string source, string pattern)
    {
        if (string.IsNullOrWhiteSpace(pattern)) return 0;
        var srcParts = source.Split(' ');
        var patParts = pattern.Split(' ');
        int count = 0;
        for (int i = 0; i < Math.Min(srcParts.Length, patParts.Length); i++)
        {
            if (srcParts[srcParts.Length - 1 - i].Equals(patParts[patParts.Length - 1 - i], StringComparison.OrdinalIgnoreCase))
                count++;
            else break;
        }
        return count;
    }

}





As we can see in the source code of NormalizeHex, ending padding chars are removed at the end, since in some cases, byte arrays (files or byte columns in databases for examples) are padded with certain bytes. Also, upper-case is applied and '-' is replaced by space ' '.

In the example below, a PDF file is scanned with the console app and the PDF file header and trailer is recognized. In this case, we also peel of trailing bytes at the end, as the specific PDF file had trailing bytes of pad bytes, more specifically : 0A.

FileUtil.cs

The util class here is used to load a file header or file trailer, a smaller byte array usually. 64 bytes is default evaluated here and should cover most file types file headers and file trailers, actually most file types only has 8 bytes or even less as a file header or file trailer.


namespace FileHeaderUtil
{

    /// <summary>
    /// Helper class for file operations
    /// </summary>
    public static class FileUtil
    {

        /// <summary>
        /// Prints the file header HEX representation
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="byteCount">Read the first n bytes. Defaults to 64 bytes.</param>
        /// <returns></returns>
        public static string? ShowHeader(string filePath, int byteCount = 64, int offset = 0)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            byte[] header = ReadBytes(filePath, byteCount, offset, SeekOrigin.Begin);
            if (header == null)
            {
                return null;
            }
            return BitConverter.ToString(header);
        }

        /// <summary>
        /// Prints the file trailer HEX representation
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="byteCount">Read the last n bytes. Defaults to 64 bytes.</param>
        /// <returns></returns>
        public static string? ShowTrailer(string filePath, int byteCount = 64, int offset = 0)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            byte[] header = ReadBytes(filePath, byteCount, offset, SeekOrigin.End);
            if (header == null)
            {
                return null;
            }
            return BitConverter.ToString(header);
        }

        /// <summary>
        /// Reads the n bytes of a byte array. Either from the start or the end of the byte array.
        /// </summary>
        /// <param name="filePath">File path of target file to read the byets</param>
        /// <param name="byteCount">The number of bytes to read</param>
        /// <param name="offset">Offset - number of bytes</param>
        /// <param name="origin">Origin to seek from. Can be either SeekOrigin.Begin, SeekOrigin.Current or SeekOrigin.End</param>
        /// <returns></returns>
        private static byte[] ReadBytes(string filePath, int byteCount, int offset = 0, SeekOrigin origin = SeekOrigin.Begin)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            if (byteCount < 1)
            {
                return Array.Empty<byte>();
            }
            byte[] buffer = new byte[byteCount];
            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            if (origin == SeekOrigin.Begin && offset > 0)
            {
                fileStream.Seek(offset, origin);
            }
            else if (origin == SeekOrigin.End)
            {
                fileStream.Seek(-1 * Math.Abs(offset+byteCount), origin);
            }
            else
            {
                //origin must be Current - offset is expected from the current position, just like SeekOrigin.Begin
                fileStream.Seek(offset, origin); 
            }

            int bytesRead = fileStream.Read(buffer, 0, byteCount); 
            if (bytesRead < byteCount)
            {
                Array.Resize(ref buffer, bytesRead);
            }
            return buffer;
        }        
        
    }

}


This console app will only consider max three matching file headers/trailers in cases where multiple such byte array pairs matches a given byte array of a file. To adjust this, see in Program.cs and adjust the Take parameter. Matches are ordered by number of bytes matching.

Tuesday, 9 December 2025

Enable font ligatures in Visual Studio 2026

Font ligatures are a cognitive boost for developers when reading code inside an IDE.

What are font ligatures

Font ligatures are special glyphs that combine multiple characters into a single, elegant symbol. For example, =>, ===, or != can appear as smooth connected symbols instead of separate characters. They don’t change your code—just make it more readable and visually appealing.

  • ๐ŸŽจ Aesthetic boost – Makes your code look clean and modern without changing functionality.
  • ๐Ÿ‘️ Better readability – Reduces visual clutter, making code easier on the eyes.
  • ๐Ÿ” Clearer syntax – Turns multi-character operators like => or === into neat symbols for quick recognition.
  • Faster comprehension – Helps spot patterns and logic flow at a glance.


In case you want to enable Font ligatures inside VS 2026, Visual Studio 2026, you actually have to resort to running a Powershell script or similar to alter the registry a bit.

EnableFonLigatures.ps1 | Powershell





# Enable Font Ligatures for Visual 2026 (18.x)
$basePath = "HKCU:\Software\Microsoft\VisualStudio"
$targetPrefixes = @("18.0_")
foreach ($prefix in $targetPrefixes) {
    $vsKeys = Get-ChildItem -Path $basePath | Where-Object { $_.PSChildName -like "$prefix*" }
    if ($vsKeys.Count -eq 0) {
        Write-Host "No keys found for prefix $prefix. Open Visual Studio and change Fonts & Colors once, then rerun."
    } else {
        foreach ($key in $vsKeys) {
            $fontColorsPath = Join-Path $key.PSPath "FontAndColors\Text Editor"
            
            # Create the path if missing
            if (-not (Test-Path $fontColorsPath)) {
                Write-Host "Creating missing path: $fontColorsPath"
                New-Item -Path $fontColorsPath -Force | Out-Null
            }
            # Set EnableFontLigatures to 1
            Set-ItemProperty -Path $fontColorsPath -Name "EnableFontLigatures" -Value 1 -Type DWord
            Write-Host "Ligatures enabled for: $fontColorsPath"
        }
    }
}


The following screenshot shows two ligatures symbols. Note the special symbols for => ('goes to') and != ('not equals') that are combined into one elegant symbol, which is more readable for the reader.

Monday, 24 November 2025

Exploring Extension Blocks and Constants in C# 14

Extension blocks - Extension properties

Extension blocks and an important new feature - extension properties can be made in C#14. This is available with .NET 10.

It is not possible to define a generic extension block to add extension properties / members (yet, as of C#14 anyways -maybe for future version of C#..)

Consider this example of some well-known constants from entry-level Calculus using extension properties.


using System.Numerics;

namespace Csharp14NewFeatures
{
    /// <summary>
    /// Provides well-known mathematical constants for any numeric type using generic math.
    /// </summary>
    /// <typeparam name="T">A numeric type implementing INumber<T> (e.g., double, decimal, float).</typeparam>
    public static class MathConstants<T> where T : INumber<T>
    {
        /// <summary>ฯ€ (Pi), ratio of a circle's circumference to its diameter.</summary>
        public static T Pi => T.CreateChecked(Math.PI);

        /// <summary>ฯ„ (Tau), equal to 2ฯ€. Represents one full turn in radians.</summary>
        public static T Tau => T.CreateChecked(2 * Math.PI);

        /// <summary>e (Euler's number), base of the natural logarithm.</summary>
        public static T E => T.CreateChecked(Math.E);

        /// <summary>ฯ† (Phi), the golden ratio (1 + √5) / 2.</summary>
        public static T Phi => T.CreateChecked((1 + Math.Sqrt(5)) / 2);

        /// <summary>√2, square root of 2. Appears in geometry and trigonometry.</summary>
        public static T Sqrt2 => T.CreateChecked(Math.Sqrt(2));

        /// <summary>√3, square root of 3. Common in triangle geometry.</summary>
        public static T Sqrt3 => T.CreateChecked(Math.Sqrt(3));

        /// <summary>ln(2), natural logarithm of 2.</summary>
        public static T Ln2 => T.CreateChecked(Math.Log(2));

        /// <summary>ln(10), natural logarithm of 10.</summary>
        public static T Ln10 => T.CreateChecked(Math.Log(10));

        /// <summary>Degrees-to-radians conversion factor (ฯ€ / 180).</summary>
        public static T Deg2Rad => T.CreateChecked(Math.PI / 180.0);

        /// <summary>Radians-to-degrees conversion factor (180 / ฯ€).</summary>
        public static T Rad2Deg => T.CreateChecked(180.0 / Math.PI);
    }

    /// <summary>
    /// Extension blocks exposing math constants as properties for common numeric types.
    /// </summary>
    public static class MathExtensions
    {
        extension(double source)
        {
            /// <inheritdoc cref="MathConstants{T}.Pi"/>
            public double Pi => MathConstants<double>.Pi;
            public double Tau => MathConstants<double>.Tau;
            public double E => MathConstants<double>.E;
            public double Phi => MathConstants<double>.Phi;
            public double Sqrt2 => MathConstants<double>.Sqrt2;
            public double Sqrt3 => MathConstants<double>.Sqrt3;
            public double Ln2 => MathConstants<double>.Ln2;
            public double Ln10 => MathConstants<double>.Ln10;
            public double Deg2Rad => MathConstants<double>.Deg2Rad;
            public double Rad2Deg => MathConstants<double>.Rad2Deg;
        }

        extension(decimal source)
        {
            public decimal Pi => MathConstants<decimal>.Pi;
            public decimal Tau => MathConstants<decimal>.Tau;
            public decimal E => MathConstants<decimal>.E;
            public decimal Phi => MathConstants<decimal>.Phi;
            public decimal Sqrt2 => MathConstants<decimal>.Sqrt2;
            public decimal Sqrt3 => MathConstants<decimal>.Sqrt3;
            public decimal Ln2 => MathConstants<decimal>.Ln2;
            public decimal Ln10 => MathConstants<decimal>.Ln10;
            public decimal Deg2Rad => MathConstants<decimal>.Deg2Rad;
            public decimal Rad2Deg => MathConstants<decimal>.Rad2Deg;
        }

        extension(float source)
        {
            public float Pi => MathConstants<float>.Pi;
            public float Tau => MathConstants<float>.Tau;
            public float E => MathConstants<float>.E;
            public float Phi => MathConstants<float>.Phi;
            public float Sqrt2 => MathConstants<float>.Sqrt2;
            public float Sqrt3 => MathConstants<float>.Sqrt3;
            public float Ln2 => MathConstants<float>.Ln2;
            public float Ln10 => MathConstants<float>.Ln10;
            public float Deg2Rad => MathConstants<float>.Deg2Rad;
            public float Rad2Deg => MathConstants<float>.Rad2Deg;
        }
    }
}

We must define extension blocks per type here.

If we move over to extension methods, we still must use a non-generic class. However, we can use for example generic math, show below. This allows to reuse code accross multiple types, supporting INumber<T> in this case.


namespace Csharp14NewFeatures
{
    using System;
    using System.Numerics;

    namespace Csharp14NewFeatures
    {
        /// <summary>
        /// Provides generic mathematical constants via extension methods for numeric types.
        /// </summary>
        public static class MathConstantExtensions
        {
            public static T GetPi<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.PI);

            public static T GetTau<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(2 * Math.PI);

            public static T GetEuler<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.E);

            public static T GetPhi<T>(this T _) where T : INumber<T> =>
                T.CreateChecked((1 + Math.Sqrt(5)) / 2);

            public static T GetSqrt2<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Sqrt(2));

            public static T GetSqrt3<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Sqrt(3));

            public static T GetLn2<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Log(2));

            public static T GetLn10<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Log(10));

            public static T GetDeg2Rad<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.PI / 180.0);

            public static T GetRad2Deg<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(180.0 / Math.PI);
        }
    }
}

Example usage of the code above :


  #region Extension metmbers using block syntax - Math

  //Extension properties 
  double radians = double.Pi / 3.0; // Pi/3 radians = 60 degrees (1 * Pi = 180 degrees) 
  double degrees = radians * radians.Rad2Deg; // Using the extension method Rad2Deg

  Console.WriteLine($"Radians: {radians:F6}"); //outputs 1.04719..
  Console.WriteLine($"Degrees: {degrees:F6}"); //outputs 60

  //Using Extension methods 

    //Using Extension methods 

  double radiansV2 = 1.0.GetPi() / 3.0;
  double degreesV2 = radians * 1.0.GetRad2Deg();

  Console.WriteLine($"Radians: {radiansV2:F6}");
  Console.WriteLine($"Degrees: {degreesV2:F6}");

Output of the code usage above:


Radians: 1,047198
Degrees: 60,000000
Radians: 1,047198
Degrees: 60,000000

So to sum up, if you use extension blocks in C#, you can use them together with generics, but the extension block must be defined to a concrete type, not using generics. This will result in some cases in lengthier code, as we cannot use generics as much as extension methods allows. Note that extension methods also must be defined inside a non-generic class.

Saturday, 22 November 2025

C# 14 - Null-conditional assignments

What's new in C#

With .NET 10 released in November 2025, new features of C# is available.

Null-conditional assignment

In C# 14, Null-conditional assignment allows using the null-conditional member access operator on the left and side of assignment.

This allows more compact code, but also at the same time allow the code to become a bit more indeterministic since the code will not be run if the object on the left side of the assignment is null.

Consider this simple class :


public class AnotherClass
{
  public ShipmentService ShipmentService = new ShipmentService();
  public Order? CurrentOrder { get; set; }  
  public int? Counter { get; set; } = 0;   
}


public class ShipmentService
{
    public Order? GetCurrentOrder()
    {
        // Simulate fetching the current order, which may return null
        return null; // or return new Order { OrderId = 1234 };
    }
}

public class Order
{
    public int OrderId { get; set; }
}

We do a null check on the instance of <em>AnotherClass</em> ShipmentService here on the left side.


//Demonstrate AnotherClass using null-conditional assignment 
AnotherClass? anotherClass = null;
anotherClass?.CurrentOrder = anotherClass?.ShipmentService.GetCurrentOrder();
Console.WriteLine($"Current order retrieved using null-conditional assignment: {anotherClass?.CurrentOrder?.OrderId} Current order id is NULL? {anotherClass?.CurrentOrder?.OrderId is null}");

It is also possible to use the null check of the null-conditional assignment with compound assignment operators. The compound operators are += and -= . Note that this must be done with member access and cannot be used with values such as integers for example.


//anotherClass still NULL
anotherClass?.Counter += 2;
Console.WriteLine($"anotherClass.Counter = {anotherClass?.Counter}. Is anotherClass.Counter NULL ? {anotherClass?.Counter is null} : outputs NULL since anotherClass is still null");
anotherClass = new AnotherClass();
anotherClass?.Counter -= 15;
Console.WriteLine($"anotherClass.Counter = {anotherClass?.Counter} : outputs -15 since anotherClass is not null"); 
Output of the code above:

Current order retrieved using null-conditional assignment:  Current order id is NULL? True
anotherClass.Counter = . Is anotherClass.Counter NULL ? True : outputs NULL since anotherClass is still null
anotherClass.Counter = -15 : outputs -15 since anotherClass is not null

Saturday, 1 November 2025

Metadata retrieval and debugging MCP servers

This article will show how one can retrieve metadata and debug MCP servers. We will obtain two kinds of information:
  • Display Json-Rpc formatted metadata of the MCP server. This will be callable via Swagger, contacting a Web Api controller.
  • Browse documentation and use the tools of the MCP server via a freely available tool called ModelInspector.
In this case, I will use the previous article's demo of a Weather client that connects to a server running with MCP. The Github repo for my DEMO MCP server is available here :

https://github.com/toreaurstadboss/WeatherMCPDemo

Let's first see now we can set up Swagger and set up a MVC Web Api controller to expose metadata about the MCP server.

Exposing metadata about the MCP server


Inside Program.cs of our project WeatherServer.Web.Http, we set up Swagger like this:
Program.cs


   public static void Main(string[] args)
   {
       var builder = WebApplication.CreateBuilder(args);
       
       //more code

       //Add swagger support
       builder.Services.AddControllers();
       builder.Services.AddEndpointsApiExplorer();
       builder.Services.AddSwaggerGen();
       
       // some more code
       
       app.UseSwagger();
       app.UseSwaggerUI();


Swagger and its UI is provided via Nuget packages :
WeatherServer.Web.Http.csproj

	<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
	<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />

Next up, let's see how the metadata about the MCP server can be exposed via an MVC controller.
ToolsController.cs


using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;

namespace WeatherServer.Web.Http.Controllers
{

    [ApiController]
    [Route("[controller]")]
    public class ToolsController : ControllerBase
    {
        private readonly IMcpClient _client;
        private readonly IOptions<ModelContextProtocol.Protocol.Implementation> _mcpServerOptions;

        public ToolsController(IMcpClient client, IOptions<ModelContextProtocol.Protocol.Implementation> mcpServerOptions)
        {
            _client = client;
            _mcpServerOptions = mcpServerOptions; 
        }

        [HttpGet(Name = "Overview")]
        [Produces("application/json")]
        public async Task<IActionResult> GetOverview()
        {
            var rpcRequest = new
            {
                jsonrpc = "2.0",
                method = "tools/list",
                id = 1
            };

            var tools = await _client.ListToolsAsync();
            //var prompts = await _client.ListPromptsAsync();
            //var resources = await _client.ListResourcesAsync();
            return Ok(new
            {
                ServerName = _mcpServerOptions.Value.Title,
                Version = _mcpServerOptions.Value.Version,
                Tools = tools,
                //Prompts = prompts,
                // Resources = resources
            });
        }

    }

}


We will list the tools on the MCP server. In our server, we have added connection ability to the MCP server itself to achiveve this. Since my MCP demo server only got some tools and has not added prompts or resources, only this will be exposed. Please note that an anonymous object is created with

  jsonrpc = "2.0",
  method = "tools/list"
  id = 1

These "magic" fields with their values instructs MCP to deliver Json RCP data.

Getting the Json-Rpc metadata

The Json-Rpc metadata is then easily obtained from Swagger UI. The following screenshot shows this.

Download the entire .json document. You can use an online Json browser to browse through the Json document. For example the web site JsonCrack offers a powerful tool to browse larger Json document. Url to JsonCrack:

https://jsoncrack.com/editor

Screenshot showing it displaying Json-Rpc document in JsonCrack : Next up, let's see how ModelInspector can be used to discover metadata about the MCP server. Node must be installed on the PC, at least Node version that supports modern ES modules and fetch API. I use Node version 25 when I tested this. Use npx (Node must be installed and setup with PATH environment variable) to have npx available.

  npx @modelcontextprotocol/inspector --startup-url "https://localhost:7145/mcp"

In case you get SSL troubles and want to bypass SSL security in case you for example rely on self-signed certificates and browser are giving you difficulties, you can use the following in LocalDEV from commandline:

  $env:NODE_TLS_REJECT_UNAUTHORIZED=0
  npx @modelcontextprotocol/inspector --startup-url "https://localhost:7145/mcp"

This will temporarily skip TLS errors and allow you to bypass troubles with self-signed certificates. In production environments, you of course will not use this 'trick'. Once inside ModelContextInspector, choose - Transport Type set to : Streamable HTTP - URL set to: https://localhost:7145/sse - Connection Type set to: Via Proxy Once ready, enter the button Connect : Connect It should say Connected with a green diode indicator. Note that Program.cs sets up the MCP endpoint to SSE :

    app.MapMcp("/sse"); // This exposes the SSE endpoint at /sse

Screenshot showing the tool in use : Hit the button List Tools to list the tools in the MCP demo. You will get the description of each tool and by selecting a tool, you can provide its input parameters and also see Description / Instruction usage. You can then invoke the tool by running the button 'Run Tool' and also see additional information such as description of the tool and also the description of each method. This is even more readable than the raw Json-Rpc document we downloaded using Swagger described above. For more advanced MCP servers, you can inspect additional information from Resources, Prompts and additional information. This makes it possible to debug your MCP servers without having to test via a client or chat via for example a Swagger endpoint. The following Nuget packages are used for the server project of the DEMO.


	<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.2" /<
	<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
	<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
	<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.2" />
	<PackageReference Include="Anthropic.SDK" Version="5.5.1" />
	<PackageReference Include="Microsoft.Extensions.AI" Version="9.9.0" />
	<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.6.25358.103" />


To sum up, to inspect your MCP server in greater detail and if you use technology similar to the Nugets shown here, you can:
  • Add Swagger and Swagger UI to the serverside and expose an endpoint that lists the Json-Rpc metadata for the MCP server such as listing the tools of the MCP server. Or Prompts and Resources, if they have been added to the MCP server.
  • Use the free tool Model Inspector to inspect the MCP server and invoke for example the tools of the MCP server.