Table of Contents

Read records one after another

Intermediate · C#. Records and ParseMany parse one record per step of the loop and advance by the record's size; trailing bytes shorter than one record fail on the step that meets them with the record's index in the path.

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 -- record-sequence

The runner checks three headers through the generated Records, their lengths summed through the allocation-free view enumerator, the same three through ParseMany and RecordsAsync, and two trailing bytes failing at record index 3. Success includes PASS record-sequence.

This example uses the C# API. Browser capabilities and result shapes are described in the browser guide.

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 async Task Main()
    {
        await RecordSequence();
        Console.WriteLine("PASS record-sequence");
    }

    private static async Task RecordSequence()
    {
        // Three headers, one after another, and nothing else: Records reads them lazily, one per step of the loop.
        byte[] bytes = [1, 0, 6, 0, 0, 0, 2, 0, 7, 0, 0, 0, 3, 0, 8, 0, 0, 0];
        var kinds = new List<ushort>();
        foreach (Wire.Header header in Wire.Records(bytes))
        {
            kinds.Add(header.Kind);
        }

        Equal("1,2,3", string.Join(",", kinds));

        // The view enumerator walks the same records without allocating a single object: each step is a view over
        // the next six bytes. The record's size (Sizes.Header) is the stride.
        uint total = 0;
        foreach (Wire.HeaderView view in Wire.HeaderView.Enumerate(bytes))
        {
            total += view.Length;
        }

        Equal(21u, total);

        // The runtime's ParseMany reads the same records as StructValues, from memory or from a stream.
        Equal(3, Wire.Layout.ParseMany(bytes, "header").Count());
        var lengths = new List<uint>();
        await foreach (Wire.Header header in Wire.RecordsAsync(new MemoryStream(bytes)))
        {
            lengths.Add(header.Length);
        }

        Equal("6,7,8", string.Join(",", lengths));

        // Trailing bytes shorter than one record are not ignored: the step that meets them fails, naming the record.
        byte[] trailing = [.. bytes, 9, 9];
        int whole = 0;
        try
        {
            foreach (Wire.Header header in Wire.Records(trailing))
            {
                whole++;
            }

            True(false, "unreachable");
        }
        catch (CStructReadException failure)
        {
            Equal(3, whole);
            True(failure.Message.Contains("remaining 2 bytes are not a whole number of 6-byte elements", StringComparison.Ordinal), failure.Message);
            Equal("[3].header", failure.Path);
        }
    }

    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 True(bool condition, string message)
    {
        if (!condition)
        {
            throw new InvalidOperationException(message);
        }
    }

    // The layout text lives in the attribute; the generator adds the members of this partial class at build time.
    [CStructLayout("struct header { uint16 kind; uint32 length; };")]
    public static partial class Wire
    {
    }
}

Try it and diagnose mistakes

Append six more bytes instead of two.

Answer: A fourth header is read; the loop counts four and no failure occurs. 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.