Skip to content
ConsoleMonster

Compare several objects

Compare two or more objects against a baseline, property by property, with drift highlighted and noise normalised away.

Compare-ConsoleObject -View Baseline puts two or more objects side by side: one row per property (nested properties by their path), one column per object, and a status for every row. One object is the baseline; every cell that differs from it is highlighted, and a property an object doesn't have says (missing) instead of looking empty. I use it for servers that should be configured alike, such as the domain controllers of one forest.

Try it

Install-Module ConsoleMonster -Scope CurrentUser
PowerShellobject-drift.ps1
# Four domain controllers that should be configured alike.
$controllers = @(
    [pscustomobject]@{
        Name = 'dc01'; Site = 'HQ'; LdapPort = 389; GlobalCatalog = $true; DnsEnabled = $true
        Spn = 'ldap/dc01.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
    }
    [pscustomobject]@{
        Name = 'dc02'; Site = 'HQ'; LdapPort = 636; GlobalCatalog = $true; DnsEnabled = $true
        Spn = 'ldap/dc02.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
    }
    [pscustomobject]@{
        Name = 'dc03'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $false; DnsEnabled = $true
        Spn = 'ldap/dc03.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Negotiate'; ChannelBinding = 'Always' }
    }
    [pscustomobject]@{
        Name = 'dc04'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $true
        Spn = 'ldap/dc04.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Never' }
    }
)

# dc01 is the reference; each SPN is compared without its host name, so only real drift remains.
$controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Baseline dc01 -Ignore Name `
    -Normalize @{ Spn = { $_ -replace '/[^.]+', '/HOST' } } -Title 'Domain controller drift'
Full program
Import-Module ConsoleMonster

# Four domain controllers that should be configured alike.
$controllers = @(
    [pscustomobject]@{
        Name = 'dc01'; Site = 'HQ'; LdapPort = 389; GlobalCatalog = $true; DnsEnabled = $true
        Spn = 'ldap/dc01.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
    }
    [pscustomobject]@{
        Name = 'dc02'; Site = 'HQ'; LdapPort = 636; GlobalCatalog = $true; DnsEnabled = $true
        Spn = 'ldap/dc02.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Always' }
    }
    [pscustomobject]@{
        Name = 'dc03'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $false; DnsEnabled = $true
        Spn = 'ldap/dc03.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Negotiate'; ChannelBinding = 'Always' }
    }
    [pscustomobject]@{
        Name = 'dc04'; Site = 'Branch'; LdapPort = 389; GlobalCatalog = $true
        Spn = 'ldap/dc04.ad.evotec.xyz'
        Security = [ordered]@{ Signing = 'Required'; ChannelBinding = 'Never' }
    }
)

# dc01 is the reference; each SPN is compared without its host name, so only real drift remains.
$controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Baseline dc01 -Ignore Name `
    -Normalize @{ Spn = { $_ -replace '/[^.]+', '/HOST' } } -Title 'Domain controller drift'

Run: ./object-drift.ps1

Terminal
dotnet new console -n ObjectDrift
cd ObjectDrift
dotnet add package ConsoleMonster
C#Program.cs
// Four domain controllers that should be configured alike.
object[] controllers = {
    new {
        Name = "dc01", Site = "HQ", LdapPort = 389, GlobalCatalog = true, DnsEnabled = true,
        Spn = "ldap/dc01.ad.evotec.xyz",
        Security = new { Signing = "Required", ChannelBinding = "Always" }
    },
    new {
        Name = "dc02", Site = "HQ", LdapPort = 636, GlobalCatalog = true, DnsEnabled = true,
        Spn = "ldap/dc02.ad.evotec.xyz",
        Security = new { Signing = "Required", ChannelBinding = "Always" }
    },
    new {
        Name = "dc03", Site = "Branch", LdapPort = 389, GlobalCatalog = false, DnsEnabled = true,
        Spn = "ldap/dc03.ad.evotec.xyz",
        Security = new { Signing = "Negotiate", ChannelBinding = "Always" }
    },
    new {
        Name = "dc04", Site = "Branch", LdapPort = 389, GlobalCatalog = true,
        Spn = "ldap/dc04.ad.evotec.xyz",
        Security = new { Signing = "Required", ChannelBinding = "Never" }
    }
};

// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");

BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
Terminal.Write(new BaselineView(comparison) { Title = "Domain controller drift" });
Full program
using System.Text.RegularExpressions;
using ConsoleMonster;
using ConsoleMonster.Changes;

// Four domain controllers that should be configured alike.
object[] controllers = {
    new {
        Name = "dc01", Site = "HQ", LdapPort = 389, GlobalCatalog = true, DnsEnabled = true,
        Spn = "ldap/dc01.ad.evotec.xyz",
        Security = new { Signing = "Required", ChannelBinding = "Always" }
    },
    new {
        Name = "dc02", Site = "HQ", LdapPort = 636, GlobalCatalog = true, DnsEnabled = true,
        Spn = "ldap/dc02.ad.evotec.xyz",
        Security = new { Signing = "Required", ChannelBinding = "Always" }
    },
    new {
        Name = "dc03", Site = "Branch", LdapPort = 389, GlobalCatalog = false, DnsEnabled = true,
        Spn = "ldap/dc03.ad.evotec.xyz",
        Security = new { Signing = "Negotiate", ChannelBinding = "Always" }
    },
    new {
        Name = "dc04", Site = "Branch", LdapPort = 389, GlobalCatalog = true,
        Spn = "ldap/dc04.ad.evotec.xyz",
        Security = new { Signing = "Required", ChannelBinding = "Never" }
    }
};

// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");

BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
Terminal.Write(new BaselineView(comparison) { Title = "Domain controller drift" });

Run: dotnet run

Terminal
dotnet new console -n ObjectDrift
cd ObjectDrift
dotnet add package ConsoleMonster
C# fluentProgram.cs
// Four domain controllers that should be configured alike.
object[] controllers =
{
    new
    {
        Name = "dc01",
        Site = "HQ",
        LdapPort = 389,
        GlobalCatalog = true,
        DnsEnabled = true,
        Spn = "ldap/dc01.ad.evotec.xyz",
        Security = new
        {
            Signing = "Required",
            ChannelBinding = "Always"
        }
    },
    new
    {
        Name = "dc02",
        Site = "HQ",
        LdapPort = 636,
        GlobalCatalog = true,
        DnsEnabled = true,
        Spn = "ldap/dc02.ad.evotec.xyz",
        Security = new
        {
            Signing = "Required",
            ChannelBinding = "Always"
        }
    },
    new
    {
        Name = "dc03",
        Site = "Branch",
        LdapPort = 389,
        GlobalCatalog = false,
        DnsEnabled = true,
        Spn = "ldap/dc03.ad.evotec.xyz",
        Security = new
        {
            Signing = "Negotiate",
            ChannelBinding = "Always"
        }
    },
    new
    {
        Name = "dc04",
        Site = "Branch",
        LdapPort = 389,
        GlobalCatalog = true,
        Spn = "ldap/dc04.ad.evotec.xyz",
        Security = new
        {
            Signing = "Required",
            ChannelBinding = "Never"
        }
    }
};

// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");
BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
new BaselineView(comparison).Title("Domain controller drift").Show();
Full program
using System.Text.RegularExpressions;
using ConsoleMonster;
using ConsoleMonster.Changes;

// Four domain controllers that should be configured alike.
object[] controllers =
{
    new
    {
        Name = "dc01",
        Site = "HQ",
        LdapPort = 389,
        GlobalCatalog = true,
        DnsEnabled = true,
        Spn = "ldap/dc01.ad.evotec.xyz",
        Security = new
        {
            Signing = "Required",
            ChannelBinding = "Always"
        }
    },
    new
    {
        Name = "dc02",
        Site = "HQ",
        LdapPort = 636,
        GlobalCatalog = true,
        DnsEnabled = true,
        Spn = "ldap/dc02.ad.evotec.xyz",
        Security = new
        {
            Signing = "Required",
            ChannelBinding = "Always"
        }
    },
    new
    {
        Name = "dc03",
        Site = "Branch",
        LdapPort = 389,
        GlobalCatalog = false,
        DnsEnabled = true,
        Spn = "ldap/dc03.ad.evotec.xyz",
        Security = new
        {
            Signing = "Negotiate",
            ChannelBinding = "Always"
        }
    },
    new
    {
        Name = "dc04",
        Site = "Branch",
        LdapPort = 389,
        GlobalCatalog = true,
        Spn = "ldap/dc04.ad.evotec.xyz",
        Security = new
        {
            Signing = "Required",
            ChannelBinding = "Never"
        }
    }
};

// dc01, the first object, is the reference; each SPN is compared without its host name, so only real drift remains.
var options = new DiffOptions();
options.Ignore.Add("Name");
options.Normalize["Spn"] = value => Regex.Replace(value?.ToString() ?? "", "/[^.]+", "/HOST");
BaselineComparison comparison = ObjectDiff.CompareBaseline(controllers, options);
new BaselineView(comparison).Title("Domain controller drift").Show();

Run: dotnet run

Compare-ConsoleObject -View Baseline 120×1224-bitFull

Four domain controllers compared with dc01. Only the properties that drift are listed; the SPNs differ only by host name, so they don't count.

How it works

Pass the objects through the pipeline or -InputObject, or to ObjectDiff.CompareBaseline in C#, and render the result with BaselineView:

$controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Baseline dc01 -Ignore Name `
    -Normalize @{ Spn = { $_ -replace '/[^.]+', '/HOST' } }
  • Columns. Each column is named by -NameProperty, by -Name with one name per object, or by the usual key properties (Name, Id and similar). The first object is the baseline unless -Baseline names another column or gives its zero-based index.
  • Rows. Properties are compared to -Depth levels, so Security.Signing is a row of its own. Matching rows are left out; -IncludeEqual shows them too. -Property and -Ignore choose the paths.
  • Case. Strings compare case-insensitively, as Compare-Object does, and -CaseSensitive (DiffOptions.CaseSensitive) changes both values and names. A difference only in case is still shown, with its own status, so it never disappears silently.
  • Noise. -Normalize maps a property path to a script block (DiffOptions.Normalize takes a delegate) whose result is compared instead of the value. The matrix still shows the original values.
  • Narrow windows. When the columns don't fit, each property becomes a small card listing every object's value and the status, like other ConsoleMonster tables.
Compare-ConsoleObject -View Baseline 60×4424-bitFull
The same comparison at 60 columns: one card per property.

Two sets of objects

Without -View Baseline, Compare-ConsoleObject -ReferenceObject $before -DifferenceObject $after matches two collections by key and lists what was added, removed and changed, with -View Summary, Table, SideBySide or Unified. The same case rule, missing marker and -Normalize apply there, and -PassThru returns the change records instead of drawing them.

In a layout or a report

-AsWidget returns the comparison as a widget instead of writing it, so it goes into Show-ConsoleApp layouts and Export-ConsoleFrame exports:

$matrix = $controllers | Compare-ConsoleObject -View Baseline -NameProperty Name -Ignore Name -AsWidget
$matrix | Export-ConsoleFrame -Format Html -Width 120 -Path ./controller-drift.html

In C#, BaselineView is an ordinary widget; BaselineComparison.Rows gives each property's cells, presence and status for your own checks.

The Compare-ConsoleObject reference lists every parameter, Compare text and files compares strings and files line by line, Interactive apps shows layouts the matrix can sit in, and Recording and replay covers HTML exports.

Edit this page on GitHub