devo6.SSC.Generators 0.3.0

dotnet add package devo6.SSC.Generators --version 0.3.0
                    
NuGet\Install-Package devo6.SSC.Generators -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="devo6.SSC.Generators" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="devo6.SSC.Generators" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="devo6.SSC.Generators" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add devo6.SSC.Generators --version 0.3.0
                    
#r "nuget: devo6.SSC.Generators, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package devo6.SSC.Generators@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=devo6.SSC.Generators&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=devo6.SSC.Generators&version=0.3.0
                    
Install as a Cake Tool

SSC

PR xUnit Publish NuGet License

Runtime Package NuGet Version NuGet Downloads


Source Generator Package NuGet Version NuGet Downloads

SSC is a .NET library for structural comparison of multiple models.

It compares object graphs by aligning member values into per-model slots, normalizing container members (Dictionary/List/IEnumerable), and returning both comparison data and diagnostic issues.

Status

  • Target framework: .NET 8

NuGet Packages

  • Runtime package:
    • devo6.SSC
  • Source Generator package:
    • devo6.SSC.Generators

Install both packages when you want typed generated projection API (AsGeneratedView()).

Core Concepts

  • ParallelCompareApi.Compare<T>(IReadOnlyList<T> models, CompareConfiguration? configuration = null)
  • Result model:
    • CompareResult<T>.Root : comparison tree root
    • CompareResult<T>.Issues : accumulated diagnostics
    • CompareResult<T>.HasError : whether error-level issues exist
  • Slot model:
    • Parallel<T>[modelIndex] for value access
    • GetState(modelIndex) for slot state access
    • ValueState.Missing : this slot is missing, or comparison target does not exist
    • ValueState.Matched : there are comparison targets and all compared slots are equal
    • ValueState.Mismatched : this slot exists and at least one compared slot is different (including target-side missing)

Container Behavior

  • Dictionary:
    • Union keys across models
    • Build child nodes by key
    • Duplicate-equivalent keys in same model are recorded as DuplicateCompareKeyDetected
  • List / Array / IEnumerable:
    • Element type requires [CompareKey]
    • Missing compare key is recorded as issue (strict mode throws)
    • Duplicate compare key is recorded as issue (strict mode throws)

Strict Mode

  • StrictMode = false (default): collect issues and continue where possible
  • StrictMode = true: throw immediately on error
    • CompareInputException for input errors
    • CompareExecutionException for execution errors

Trace Logging

  • CompareConfiguration.TraceLog に callback を渡すと、compare 実行中の内部 trace を受け取れます
  • 特に container 判定では path, declaredType, container, runtimeType, materializedCount, compareKey などが出力されます
List<string> traceLines = [];

CompareConfiguration configuration = new()
{
    TraceLog = traceLines.Add,
};

CompareResult<ProductModel> result = ParallelCompareApi.Compare(models, configuration);

trace 例:

phase=metadata path=ProductModel.Items declaredType=System.Collections.Generic.List`1[ProductItem] container=List
phase=container path=ProductModel.Items container=List elementType=ProductItem compareKey=Id
phase=container path=ProductModel.Items modelIndex=0 container=List runtimeType=System.Collections.Generic.List`1[ProductItem] materializedCount=2

String Key Policy

  • Default string key comparison is StringComparison.Ordinal
  • StringKeyComparison = StringComparison.OrdinalIgnoreCase is supported
  • Under OrdinalIgnoreCase, diagnostic KeyText is canonicalized to the StringComparer.Ordinal minimum representation among equivalent candidates

Quick Start

dotnet build SSC.sln -c Release
dotnet test SSC.sln -c Release --verbosity minimal

Minimal Example

using System.Collections.Generic;
using SSC;

ProductModel[] models =
{
    new ProductModel
    {
        Items =
        [
            new ProductItem { Id = 1, Price = 100 },
            new ProductItem { Id = 2, Price = 200 },
        ],
    },
    new ProductModel
    {
        Items =
        [
            new ProductItem { Id = 2, Price = 250 },
            new ProductItem { Id = 3, Price = 300 },
        ],
    },
};

CompareResult<ProductModel> result = ParallelCompareApi.Compare(models);
dynamic root = result.AsDynamic()!;

// item keys are normalized as union: 1, 2, 3
decimal? leftPriceAtKey1 = root.Items[0].Price[0];   // 100
decimal? rightPriceAtKey1 = root.Items[0].Price[1];  // null (missing)
decimal? leftPriceAtKey2 = root.Items[1].Price[0];   // 200
decimal? rightPriceAtKey2 = root.Items[1].Price[1];  // 250
ValueState stateAtKey3Left = (ValueState)root.Items[2].GetState(0); // Missing

public sealed class ProductModel
{
    public List<ProductItem> Items { get; init; } = [];
}

public sealed class ProductItem
{
    [CompareKey]
    public int Id { get; init; }

    public decimal Price { get; init; }
}

Source Generator Example

using System.Collections.Generic;
using System.Linq;
using SSC;
using SSC.Generated;

[GenerateParallelView]
public sealed class Dataset
{
    public List<Group> Groups { get; init; } = [];
}

public sealed class Group
{
    [CompareKey]
    public int GroupId { get; init; }

    public List<Item> Items { get; init; } = [];
}

public sealed class Item
{
    [CompareKey]
    public int ItemId { get; init; }

    public double MetricA { get; init; }
}

Dataset[] models =
{
    new Dataset
    {
        Groups =
        [
            new Group
            {
                GroupId = 1,
                Items =
                [
                    new Item { ItemId = 100, MetricA = 1.0 },
                    new Item { ItemId = 200, MetricA = 2.0 },
                ],
            },
            new Group
            {
                GroupId = 2,
                Items =
                [
                    new Item { ItemId = 210, MetricA = 21.0 },
                    new Item { ItemId = 220, MetricA = 22.0 },
                ],
            },
        ],
    },
    new Dataset
    {
        Groups =
        [
            new Group
            {
                GroupId = 1,
                Items =
                [
                    new Item { ItemId = 100, MetricA = 10.0 },
                    new Item { ItemId = 300, MetricA = 30.0 },
                ],
            },
            new Group
            {
                GroupId = 2,
                Items =
                [
                    new Item { ItemId = 210, MetricA = 21.0 },
                    new Item { ItemId = 230, MetricA = 23.0 },
                ],
            },
        ],
    },
    new Dataset
    {
        Groups =
        [
            new Group
            {
                GroupId = 1,
                Items =
                [
                    new Item { ItemId = 100, MetricA = 100.0 },
                    new Item { ItemId = 400, MetricA = 40.0 },
                ],
            },
            new Group
            {
                GroupId = 2,
                Items =
                [
                    new Item { ItemId = 210, MetricA = 21.0 },
                    new Item { ItemId = 240, MetricA = 24.0 },
                ],
            },
        ],
    },
};

CompareResult<Dataset> result = ParallelCompareApi.Compare(models);
double? leftMetricAt100 = result.AsGeneratedView()!.Groups[0].Items[0].MetricA[0];
ValueState rightStateAt200 = result.AsGeneratedView()!.Groups[0].Items[1].MetricA.GetState(1);

int[] groupIds = result.AsGeneratedView()!.Groups
    .Select(group => group.GroupId[0] ?? group.GroupId[1] ?? group.GroupId[2] ?? -1)
    .ToArray();

int[] itemIds = result.AsGeneratedView()!.Groups
    .SelectMany(group => group.Items)
    .Select(item => item.ItemId[0] ?? item.ItemId[1] ?? item.ItemId[2] ?? -1)
    .ToArray();

int[] mismatchedItemIds = result.AsGeneratedView()!.Groups
    .SelectMany(group => group.Items)
    .Where(item => item.MetricA.GetState(0) == ValueState.Mismatched
        || item.MetricA.GetState(1) == ValueState.Mismatched
        || item.MetricA.GetState(2) == ValueState.Mismatched)
    .Select(item => item.ItemId[0] ?? item.ItemId[1] ?? item.ItemId[2] ?? -1)
    .ToArray();

Documentation

  • Design guide: doc/design/
  • Draft notes: doc/draft/
  • Progress tracking: tasks/
  • Work reports: reports/

License

MIT (LICENSE)

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 89 4/21/2026
0.2.7-pre 92 4/21/2026
0.2.6-pre 87 4/21/2026
0.2.5-pre 92 4/21/2026
0.2.4-pre 88 4/21/2026
0.2.3-pre 91 4/21/2026
0.2.2-pre 84 4/21/2026
0.2.1-pre 98 4/5/2026
0.2.0 100 4/5/2026
0.1.4-pre 86 4/5/2026
0.1.3-pre 89 4/5/2026
0.1.2-pre 92 4/4/2026