Tuesday, 31 March 2026

Generic EqualityComparer for classes in C#

GenericEqualityComparer

Creating support for Equality comparison of classes in C# can become repititive. In this article, we will look at a Generic equality comparer that can be used for classes to do equality comparison. Please note that we are meaning here value comparison. Structs and records support via built-in functionality such a value equality comparison. For classes, it depends on what you mean by equality comparison. Usually it means the public properties, but additional state such as private properties and fields can also be considered.

Github repo with this source code

https://github.com/toreaurstadboss/GenericEqualityComparer

A reflection-based IEqualityComparer<T> that compares two objects by their member values instead of by reference. Useful for plain C# classes that don't override Equals and GetHashCode themselves.

1 — The problem it solves

In C#, class instances are compared by reference by default. Two objects with identical data are not equal unless they are the same object in memory.

var car1 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };

Console.WriteLine(car1 == car2);        // False — different object references
Console.WriteLine(car1.Equals(car2));   // False — same reason

GenericEqualityComparer<T> solves this without touching the class itself. It uses reflection to compare each property (and optionally each field) value by value. But it also uses member expressions compiled into delegates to provide fast member value lookups. This makes the generic equality comparer used here possible to use inside collections with many items.

Lets first look at the GenericEqualityComparer source code. It is a generic class.

GenericEqualityComparer.cs


using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;

namespace GenericEqualityComparer.Lib
{

    /// <summary>
    /// A reflection-based <see cref="IEqualityComparer{T}"/> that compares instances of
    /// <typeparamref name="T"/> by their members rather than by reference.
    /// This is intended for use with classes that do not implement their own value-based equality semantics, and is not recommended for performance-sensitive scenarios.
    /// Types such as structs and records already have built-in value equality semantics and should not require this comparer.
    /// </summary>
    /// <typeparam name="T">The type to compare. Must be a reference type.</typeparam>
    /// <remarks>
    /// By default only public instance properties are compared. Pass the constructor flags to
    /// also include private properties and/or fields (public or private).
    /// </remarks>
    public class GenericEqualityComparer<T> : IEqualityComparer<T> where T : class
    {

        private List<Func<T, object>> _propertyGetters = new List<Func<T, object>>(); // Cache of compiled delegates for accessing the configured properties of T, used to avoid the performance overhead of reflection during comparisons.

        private List<Func<T, object>> _fieldGetters = new List<Func<T, object>>(); // Cache of compiled delegates for accessing the configured fields of T, used to avoid the performance overhead of reflection during comparisons.

        /// <summary>
        /// Initialises the comparer and builds the member accessor cache.
        /// </summary>
        /// <param name="includeFields">When <see langword="true"/>, public instance fields are included in the comparison.</param>
        /// <param name="includePrivateProperties">When <see langword="true"/>, private instance properties are included in the comparison.</param>
        /// <param name="includePrivateFields">When <see langword="true"/>, private instance fields are included in the comparison. Also enables public field comparison.</param>
        public GenericEqualityComparer(bool includeFields = false, bool includePrivateProperties = false, bool includePrivateFields = false)
        {
            CreatePropertyGetters(includePrivateProperties);
            if (includeFields || includePrivateFields)
            {
                CreateFieldGetters(includePrivateFields);
            }
        }

        private void CreatePropertyGetters(bool includePrivateProperties)
        {
            var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
            if (includePrivateProperties)
            {
                bindingFlags |= BindingFlags.NonPublic;
            }

            var props = typeof(T).GetProperties(bindingFlags).Where(m => m.GetMethod != null).ToList();

            foreach (var prop in props)
            {

                //Builds the Expression<Func<T, object>> for the property getter and compiles it into a Func<T, object> delegate, which is cached for later use.
                ParameterExpression parameter = Expression.Parameter(typeof(T), "p");
                MemberExpression propertyExpression = Expression.Property(parameter, prop.Name);
                Expression boxedPropertyExpression = Expression.Convert(propertyExpression, typeof(object));
                Expression<Func<T, object>> propertyGetter = Expression.Lambda<Func<T, object>>(boxedPropertyExpression, parameter);
                _propertyGetters.Add(propertyGetter.Compile());
            }
        }

        private void CreateFieldGetters(bool includePrivateFields)
        {
            var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
            if (includePrivateFields)
            {
                bindingFlags |= BindingFlags.NonPublic;
            }

            var fields = typeof(T).GetFields(bindingFlags).ToList();

            foreach (var field in fields)
            {
                // Builds the Expression<Func<T, object>> for the field getter and compiles it into a Func<T, object> delegate, which is cached for later use.
                ParameterExpression parameter = Expression.Parameter(typeof(T), "f");
                MemberExpression fieldExpression = Expression.Field(parameter, field.Name);
                Expression boxedPropertyExpression = Expression.Convert(fieldExpression, typeof(object));
                Expression<Func<T, object>> fieldGetter = Expression.Lambda<Func<T, object>>(boxedPropertyExpression, parameter);
                _fieldGetters.Add(fieldGetter.Compile());
            }

        }

        /// <summary>
        /// Determines whether <paramref name="x"/> and <paramref name="y"/> are equal by comparing
        /// each configured member in turn.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// <see langword="true"/> when all configured members are equal;
        /// <see langword="false"/> when any member differs, or either argument is <see langword="null"/>.
        /// </returns>
        public bool Equals(T? x, T? y)
        {
            if (x == null || y == null)
            {
                return false;
            }
            if (ReferenceEquals(x, y))
            {
                return true;
            }
            if (x.GetType() != y.GetType())
            {
                return false;
            }

            foreach (var propAccessor in _propertyGetters)
            {
                var xv = propAccessor(x);
                var yv = propAccessor(y);
                if (!xv.Equals(yv))
                {
                    return false;
                }
            }

            foreach (var fieldAccessor in _fieldGetters)
            {
                var xv = fieldAccessor(x);
                var yv = fieldAccessor(y);
                if (!xv.Equals(yv))
                {
                    return false;
                }

            }


            return true;
        }

        /// <summary>
        /// Returns an <see cref="EqualityWrapper{T}"/> for <paramref name="value"/> so that
        /// <c>==</c> and <c>!=</c> use this comparer's configured equality semantics.
        /// </summary>
        /// <param name="value">The value to wrap.</param>
        /// <returns>An <see cref="EqualityWrapper{T}"/> bound to this comparer instance.</returns>
        public EqualityWrapper<T> For(T value) => new EqualityWrapper<T>(value, this);

        /// <summary>
        /// Returns a hash code for <paramref name="obj"/> derived from the same configured members
        /// used by <see cref="Equals(T, T)"/>.
        /// </summary>
        /// <param name="obj">The object to hash.</param>
        /// <returns>A hash code consistent with the configured equality semantics.</returns>
        public int GetHashCode([DisallowNull] T obj)
        {
            int hash = 0;

            var propertyValues = _propertyGetters.Select(p => p(obj)).ToList();

            for (int i = 0; i < propertyValues.Count; i += 8)
            {
                hash = HashCode.Combine(hash,
                    propertyValues.ElementAtOrDefault(i),
                    propertyValues.ElementAtOrDefault(i + 1),
                    propertyValues.ElementAtOrDefault(i + 2),
                    propertyValues.ElementAtOrDefault(i + 3),
                    propertyValues.ElementAtOrDefault(i + 4),
                    propertyValues.ElementAtOrDefault(i + 5),
                    propertyValues.ElementAtOrDefault(i + 6));
            }

            if (_fieldGetters.Any())
            {
                var fieldValues = _fieldGetters.Select(f => f(obj)).ToList();
                for (int i = 0; i < fieldValues.Count; i += 8)
                {
                    hash = HashCode.Combine(hash,
                        fieldValues.ElementAtOrDefault(i),
                        fieldValues.ElementAtOrDefault(i + 1),
                        fieldValues.ElementAtOrDefault(i + 2),
                        fieldValues.ElementAtOrDefault(i + 3),
                        fieldValues.ElementAtOrDefault(i + 4),
                        fieldValues.ElementAtOrDefault(i + 5),
                        fieldValues.ElementAtOrDefault(i + 6));
                }
            }

            return hash;
        }

    }

}


The method For accepts a object instance of type T and returns a EqualityWrapper struct that allows the usage of operator == and !=

EqualityWrapper.cs



namespace GenericEqualityComparer.Lib;

/// <summary>
/// Pairs a value of type <typeparamref name="T"/> with a <see cref="GenericEqualityComparer{T}"/>
/// so that <c>==</c> and <c>!=</c> use the comparer's configured equality semantics instead of
/// reference equality.
/// </summary>
/// <typeparam name="T">The type of the wrapped value. Must be a reference type.</typeparam>
/// <remarks>
/// Obtain an instance via <see cref="GenericEqualityComparer{T}.For"/>:
/// <code>comparer.For(car1) == comparer.For(car2)</code>
/// </remarks>
public readonly struct EqualityWrapper<T> where T : class
{
    private readonly T _value;
    private readonly GenericEqualityComparer<T> _comparer;

    internal EqualityWrapper(T value, GenericEqualityComparer<T> comparer)
    {
        _value = value;
        _comparer = comparer;
    }

    /// <summary>
    /// Returns <see langword="true"/> when <paramref name="left"/> and <paramref name="right"/>
    /// are considered equal by their shared comparer.
    /// </summary>
    public static bool operator ==(EqualityWrapper<T> left, EqualityWrapper<T> right)
        => left._comparer.Equals(left._value, right._value);

    /// <summary>
    /// Returns <see langword="true"/> when <paramref name="left"/> and <paramref name="right"/>
    /// are not considered equal by their shared comparer.
    /// </summary>
    public static bool operator !=(EqualityWrapper<T> left, EqualityWrapper<T> right)
        => !(left == right);

    /// <inheritdoc/>
    public override bool Equals(object? obj)
        => obj is EqualityWrapper<T> other && this == other;

    /// <inheritdoc/>
    public override int GetHashCode()
        => _comparer.GetHashCode(_value);
}



2 — Quick start

2.1 Compare public properties

using GenericEqualityComparer.Lib;

var comparer = new GenericEqualityComparer<Car>();

var car1 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car3 = new Car { Make = "Toyota", Model = "Corolla", Year = 2020 };

Console.WriteLine(comparer.Equals(car1, car2));  // True  — all properties match
Console.WriteLine(comparer.Equals(car1, car3));  // False — Model differs

2.2 Use it with LINQ or collections

Because GenericEqualityComparer<T> implements IEqualityComparer<T> you can pass it directly to LINQ methods and collection APIs that accept one.

var cars = new List<Car>
{
    new Car { Make = "Toyota", Model = "Camry",   Year = 2020 },
    new Car { Make = "Toyota", Model = "Camry",   Year = 2020 }, // duplicate
    new Car { Make = "Toyota", Model = "Corolla", Year = 2021 },
};

var comparer = new GenericEqualityComparer<Car>();

// Distinct by value
var unique = cars.Distinct(comparer).ToList();  // 2 items

// GroupBy by value
var grouped = cars.GroupBy(c => c, comparer);

3 — Constructor options

The constructor accepts three optional boolean flags. All default to false.

Parameter Type What it includes
includeFields bool Public instance fields
includePrivateProperties bool Private instance properties
includePrivateFields bool Private instance fields (also enables public fields)

3.1 Include private fields

Imagine a Car class that stores a secret assembly number in a private field:

public class Car
{
    public string Make { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
    public int Year { get; set; }

    // private — not visible to external code
    private string _secretAssemblyNumber = string.Empty;
    public void SetSecretAssemblyNumber(string number) => _secretAssemblyNumber = number;
}
var ford1 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
var ford2 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
ford1.SetSecretAssemblyNumber("ASM-001");
ford2.SetSecretAssemblyNumber("ASM-999");  // intentionally different

// Default comparer — only sees public properties, ignores the private field
var defaultComparer = new GenericEqualityComparer<Car>();
Console.WriteLine(defaultComparer.Equals(ford1, ford2));  // True (field ignored)

// Include private fields — now the hidden difference is detected
var deepComparer = new GenericEqualityComparer<Car>(includePrivateFields: true);
Console.WriteLine(deepComparer.Equals(ford1, ford2));     // False

3.2 Include private properties

The same idea applies when a class uses a private property as an internal identifier:

public class Bicycle
{
    public string Brand { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;

    private string FrameSerialNumber { get; set; } = string.Empty;
    public void SetFrameSerialNumber(string sn) => FrameSerialNumber = sn;
}
var bike1 = new Bicycle { Brand = "Trek", Model = "FX3" };
var bike2 = new Bicycle { Brand = "Trek", Model = "FX3" };
bike1.SetFrameSerialNumber("SN-001");
bike2.SetFrameSerialNumber("SN-999");

var defaultComparer = new GenericEqualityComparer<Bicycle>();
Console.WriteLine(defaultComparer.Equals(bike1, bike2));  // True

var deepComparer = new GenericEqualityComparer<Bicycle>(includePrivateProperties: true);
Console.WriteLine(deepComparer.Equals(bike1, bike2));     // False

4 — EqualityWrapper<T> and the == / != operators

C# doesn't allow overloading == and != on a generic type parameter T in an external comparer class. As a workaround, GenericEqualityComparer<T> exposes a For(value) method that returns an EqualityWrapper<T>. The wrapper carries both the value and the comparer, so its == and != operators delegate to the comparer instead of defaulting to reference equality.

4.1 Basic operator usage

var comparer = new GenericEqualityComparer<Car>();

var car1 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car3 = new Car { Make = "Toyota", Model = "Corolla", Year = 2020 };

bool same      = comparer.For(car1) == comparer.For(car2);  // True
bool different = comparer.For(car1) != comparer.For(car3);  // True

4.2 With private member detection

var deepComparer = new GenericEqualityComparer<Car>(includePrivateFields: true);

var ford1 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
var ford2 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
ford1.SetSecretAssemblyNumber("ASM-001");
ford2.SetSecretAssemblyNumber("ASM-999");

if (deepComparer.For(ford1) != deepComparer.For(ford2))
{
    Console.WriteLine("Cars differ (private field detected)");
}

4.3 Consistent hashing

EqualityWrapper<T> also overrides GetHashCode() so it stays consistent with ==. This means wrapped values can be used safely as dictionary keys or in hash sets.

var comparer = new GenericEqualityComparer<Car>();
var car1     = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2     = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };

int hash1 = comparer.For(car1).GetHashCode();
int hash2 = comparer.For(car2).GetHashCode();

Console.WriteLine(hash1 == hash2);  // True — equal objects, equal hashes

5 — When not to use it

Performance: The comparer uses reflection to discover members at construction time (compiled to delegates for speed), but it is still a little slower than a hand-written Equals. Avoid it in tight loops or hot paths.
  • Records — C# records already have value equality built in. Use == directly.
  • Structs — Same as records; value equality is the default.
  • Classes you own — Prefer overriding Equals / GetHashCode or implementing IEquatable<T> for production code (due to performance). Use this comparer for tests, prototyping, or third-party types you can't modify. Or if you just would like a simple way of providing value based equality checks, but in that case you should
    really
    consider a specific implementation.
In case you work with generated code or for got a large number of POCO classes (Data transfer objects) and want to avoid using inheritance or adding value equality of your existing code, this code allows you adding value based equality, this code shown here should have you covered with a generic util class.

6 - Supported Frameworks

Please note that since we use HashCode here, supported target frameworks are netstandard 2.1 and .netcore 2.1 or later. In case you use .NET Framework 4.8 or earlier for example, you can provide a GetHashCode implementation like the following:

GetHashCode that avoids using HashCode.Combine

We can instead use two selected prime numbers and multipliers to calculate a hash of the object's propertis and fields like the following:


public int GetHashCode([DisallowNull] T obj)
{
    int hash = 0;

    var propertyValues = _propertyGetters.Select(p => p(obj)).ToList();

    for (int i = 0; i < propertyValues.Count; i += 8)
    {
        hash = Combine(hash,
            propertyValues.ElementAtOrDefault(i),
            propertyValues.ElementAtOrDefault(i + 1),
            propertyValues.ElementAtOrDefault(i + 2),
            propertyValues.ElementAtOrDefault(i + 3),
            propertyValues.ElementAtOrDefault(i + 4),
            propertyValues.ElementAtOrDefault(i + 5),
            propertyValues.ElementAtOrDefault(i + 6));
    }

    if (_fieldGetters.Any())
    {
        var fieldValues = _fieldGetters.Select(f => f(obj)).ToList();
        for (int i = 0; i < fieldValues.Count; i += 8)
        {
            hash = Combine(hash,
                fieldValues.ElementAtOrDefault(i),
                fieldValues.ElementAtOrDefault(i + 1),
                fieldValues.ElementAtOrDefault(i + 2),
                fieldValues.ElementAtOrDefault(i + 3),
                fieldValues.ElementAtOrDefault(i + 4),
                fieldValues.ElementAtOrDefault(i + 5),
                fieldValues.ElementAtOrDefault(i + 6));
        }
    }

    return hash;
}

private static int Combine(params object[] values)
{
    unchecked
    {
        int hash = 17;
        foreach (var v in values)
        {
            int h = v?.GetHashCode() ?? 0;
            hash = hash * 31 + h;
        }
        return hash;
    }
}


The strange selection of two prime numbers 17 and factor of 31 is to provide diffusion to avoid hash collisions and avoid also trouble with objects with symmetric values (a,b) equaling (b,a) is avoided
using this way of summing the hashes from each property. The HashCode.Combine allows us to avoid this.

7 — Summary

The article has presented a way to do value equality checks for instances of classes in a generic manner supporting an arbitrary number of public (and private) properties, possibly also including fields (and private fields). If you want an easy way of adding value equality checks in classes and performance allows using the expression compiled delegates shown here with a little overhead initially, you should be able to consider the code here for some scenarios. The Github Repo of mine for this source code contains a lot of tests, so the code is tested.
What you wantHow
Compare public properties new GenericEqualityComparer<T>()
Also include public fields new GenericEqualityComparer<T>(includeFields: true)
Also include private properties new GenericEqualityComparer<T>(includePrivateProperties: true)
Also include private fields new GenericEqualityComparer<T>(includePrivateFields: true)
Use == / != operators comparer.For(a) == comparer.For(b)
Use with LINQ list.Distinct(comparer), list.GroupBy(x => x, comparer)

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.