Table of Contents

Parse tagged records with native branches

Advanced · C#. A runtime tag selects the fields that consume storage. Each array element chooses its own branch, and inactive fields do not appear in results.

Run this example

Prerequisites: the repository's .NET 10 SDK and a checkout of this source. Run from the repository root:

dotnet run --project docs/examples/CStructSharp.Docs.Examples.csproj -c Release -- conditional-records

The runner checks a UTF-8 label, a 24-bit number at offset 7, an inactive-path error, and rejected branch-changing update. Success includes PASS conditional-records.

Try the related browser lesson.

Complete program

The layout, options, input bytes, helper methods, and required types are all included. To adapt it outside the repository, create a .NET 10 console project, add CStructSharp, and replace Program.cs with this complete file. These examples follow the source version; use a matching package when testing a release.

Download the complete C# source.

// Generated from executable documentation examples. Edit the source region, then regenerate.
using System;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Buffers;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using CStructSharp;
using CStructSharp.Codecs;
using CStructSharp.Diagnostics;
using CStructSharp.Introspection;
using CStructSharp.Values;

internal static partial class Program
{
    public static void Main()
    {
        ConditionalRecords();
        Console.WriteLine("PASS conditional-records");
    }

    private static void ConditionalRecords()
    {
        const string definition = """
            struct entry {
                uint8 kind;
                switch (kind) {
                    case 1: { utf8 label[3]; }
                    default: { uint24< number; }
                }
                if (kind == 1) { uint8 flags; }
            };
            struct root { uleb128_32 count; entry items[count]; };
            """;
        var layout = new CStruct(definition, aligned: false);
        byte[] bytes = [2, 1, 226, 130, 172, 7, 2, 42, 0, 0];
        StructValue parsed = layout.Parse(bytes, "root");
        Equal("€", parsed.Get<string>("items[0].label"));
        Equal(42U, parsed.Get<uint>("items[1].number"));
        SequenceEqual(bytes, layout.Serialize("root", parsed));
        using var stream = new MemoryStream(bytes);
        Equal(7L, layout.ResolveAddress(stream, "root.items[1].number"));
        Throws<CStructPathException>(() => layout.ResolveAddress(stream, "root.items[1].label"));
        Throws<CStructWriteException>(() => layout.Update(stream, "root.items[0].kind", 2));
        SequenceEqual(bytes, stream.ToArray());
        // Each item evaluates both groups with its own fields, including a calculation.
        const string decisions = "struct entry { uint8 tag; int8 some_parameter; if (some_parameter * 20 > 10) { uint8 high; } else { uint8 low; } switch (tag) { case 1: { uint8 first; } case 2: { uint8 second; } default: { uint8 other; } } }; struct root { entry items[3]; };";
        var decisionLayout = new CStruct(decisions, aligned: false);
        byte[] items = [1, 0, 10, 11, 2, 1, 20, 21, 3, 255, 30, 31];
        StructValue selected = decisionLayout.Parse(items, "root");
        Equal((byte)10, selected.Get<byte>("items[0].low"));
        Equal((byte)21, selected.Get<byte>("items[1].second"));
        Equal((byte)31, selected.Get<byte>("items[2].other"));
        SequenceEqual(items, decisionLayout.Serialize("root", selected));
        items[0] = 2;
        Equal((byte)11, decisionLayout.Parse(items, "root").Get<byte>("items[0].second"));
        items[0] = 1;
        items[1] = 1;
        Equal((byte)10, decisionLayout.Parse(items, "root").Get<byte>("items[0].high"));

        // A caller's count cannot replace an unread local; a short-circuit guard repairs the example.
        const string scope = "#define count 99\nstruct entry { uint8 tag; if (tag) { uint8 count; } if (count > 0) { uint8 payload[count]; } }; struct root { entry items[2]; };";
        byte[] scopedBytes = [1, 1, 42, 0];
        var variables = new Dictionary<string, int> { ["count"] = 99 };
        var scopedLayout = new CStruct(scope, aligned: false);
        Throws<CStructReadException>(() => scopedLayout.Parse(scopedBytes, "root", variables: variables));
        var guarded = new CStruct(scope.Replace("if (count > 0)", "if (tag != 0 && count > 0)"), aligned: false);
        StructValue scoped = guarded.Parse(scopedBytes, "root", variables: variables);
        Equal(1, scoped.Get<StructValue>("items[1]").Count);
        SequenceEqual(scopedBytes, guarded.Serialize("root", scoped, variables: variables));

        // Inactive nested expressions are skipped, but become errors when reached.
        var nested = new CStruct("struct root { uint8 tag; if (tag) { if (missing > 0) { uint8 value; } } uint8 tail; };", aligned: false);
        Equal((byte)9, nested.Parse(new byte[] { 0, 9 }, "root").Get<byte>("tail"));
        Throws<CStructReadException>(() => nested.Parse(new byte[] { 1, 9 }, "root"));
    }

    private static void Equal<T>(T expected, T actual)
    {
        if (!EqualityComparer<T>.Default.Equals(expected, actual))
        {
            throw new InvalidOperationException($"Expected '{expected}', received '{actual}'.");
        }
    }

    private static void SequenceEqual(byte[] expected, byte[] actual)
    {
        if (!expected.AsSpan().SequenceEqual(actual))
        {
            throw new InvalidOperationException(
                $"Expected {Convert.ToHexString(expected)}, received {Convert.ToHexString(actual)}.");
        }
    }

    private static void Throws<TException>(Action action)
        where TException : Exception
    {
        try
        {
            action();
        }
        catch (TException)
        {
            return;
        }

        throw new InvalidOperationException($"Expected {typeof(TException).Name}.");
    }
}

Try it and diagnose mistakes

Change the first record kind through Update.

Answer: Changing active branches is rejected. Serialize a new record when its layout must change. The program contains assertions for its original inputs. When changing an input intentionally, update the expected assertion too; an unchanged assertion is not evidence that the new value is wrong.

Continue with the related guide or choose another recipe.