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.