Parse without exceptions
Beginner · C#. TryParse turns a read, path, or limit failure into false and the exception the throwing form would raise; cancellation and argument errors still throw, and a stream is left where it was.
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 -- try-parse
The runner checks TryParse succeeding on six bytes and failing on three with a null value, the failure form handing over the short-read exception, a stream back at its origin, cancellation passing through, and the runtime's TryReadValue, TryGet, and GetOrDefault. Success includes PASS try-parse.
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 void Main()
{
TryParseForms();
Console.WriteLine("PASS try-parse");
}
private static void TryParseForms()
{
byte[] bytes = [0x02, 0x00, 0x06, 0x00, 0x00, 0x00];
// TryParse returns false instead of throwing for a read, path, or limit failure; the value is null then.
True(Wire.TryParse(bytes, out Wire.Header? header) && header.Length == 6, "six bytes are a header");
True(!Wire.TryParse(bytes.AsSpan(0, 3), out Wire.Header? missing) && missing is null, "three bytes are not");
// The second form hands over the failure the throwing form would have raised, so a log line can say why.
True(!Wire.TryParse(bytes.AsSpan(0, 3), out _, out CStructException? failure), "the same outcome");
True(failure is CStructReadException, "a short read: the kind was read, the length needs four more bytes");
True(failure!.Message.Contains("needed 4, available 1", StringComparison.Ordinal), failure.Message);
// A stream is left at its origin after a failure, so the caller can try another layout at the same place.
using var stream = new MemoryStream([0xFF, 0x02, 0x00, 0x06]) { Position = 1 };
True(!Wire.TryParse(stream, out _), "three bytes remain");
Equal(1L, stream.Position);
// Cancellation is not a failure: it throws through TryParse as it does through Parse.
using var cancelled = new CancellationTokenSource();
cancelled.Cancel();
Throws<OperationCanceledException>(() => Wire.TryParse(bytes, out _, new ReadOptions { CancellationToken = cancelled.Token }));
// The runtime's TryReadValue<T> is the same idea for a path; TryGet and GetOrDefault are its members' twins.
True(Wire.Layout.TryReadValue(bytes, "header.length", out uint length) && length == 6, "typed path read");
StructValue parsed = Wire.Layout.Parse(bytes, "header");
True(!parsed.TryGet("flags", out byte _, out CStructException? absent) && absent is CStructPathException, "no such member");
Equal((byte)0, parsed.GetOrDefault("flags", (byte)0));
}
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);
}
}
private static void Throws<TException>(Action action)
where TException : Exception
{
try
{
action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException($"Expected {typeof(TException).Name}.");
}
// 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
Ask TryParse for the header from a five-byte slice.
Answer: It fails with 'needed 4, available 3': the two-byte kind was read and the four-byte length needs one more byte. 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.