Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProwlEngine/Prowl.Echo/llms.txt

Use this file to discover all available pages before exploring further.

Prowl.Echo has dedicated format handlers for every major .NET collection type. You call Serializer.Serialize and Serializer.Deserialize the same way you would for a plain object — the library automatically routes each collection to the correct handler based on the runtime type.

Supported Collection Types

List<T>

Serialized as EchoType.List — an ordered sequence of EchoObject entries.

T[] / T[,] / T[][]

Single-dimensional, multi-dimensional, and jagged arrays. Stored as a compound with an "array" or "dimensions" + "elements" layout.

Dictionary<TKey, TValue>

String keys → flat compound. Non-string keys → compound with an "entries" list of {key, value} pairs.

HashSet, Stack, Queue, LinkedList

Each has a dedicated format handler (HashSetFormat, StackFormat, QueueFormat, LinkedListFormat). Other generic ICollection&lt;T&gt; types (such as ObservableCollection&lt;T&gt; and SortedSet&lt;T&gt;) are handled by CollectionFormat.

Lists

List<T> is serialized as an EchoObject with TagType == EchoType.List. Each element is serialized using the declared element type, so polymorphic elements automatically receive a $type tag when needed.
var scores = new List<int> { 100, 250, 375 };
EchoObject echo = Serializer.Serialize(scores);
List<int>? restored = Serializer.Deserialize<List<int>>(echo);
// restored: [100, 250, 375]
// List of complex objects
var items = new List<TestObject>
{
    new TestObject { Id = 1, Name = "One" },
    new TestObject { Id = 2, Name = "Two" },
};
EchoObject echo = Serializer.Serialize(items);
List<TestObject>? restored = Serializer.Deserialize<List<TestObject>>(echo);

Nested Lists

Lists of lists work without any special configuration:
var matrix = new List<List<int>>
{
    new List<int> { 1, 2 },
    new List<int> { 3, 4, 5 },
};
EchoObject echo = Serializer.Serialize(matrix);
List<List<int>>? restored = Serializer.Deserialize<List<List<int>>>(echo);

Arrays

Single-Dimensional Arrays

Stored as a compound containing a "array" key whose value is an EchoType.List of the elements:
int[] nums = { 1, 2, 3, 4, 5 };
EchoObject echo = Serializer.Serialize(nums);
int[]? restored = Serializer.Deserialize<int[]>(echo);
// restored: [1, 2, 3, 4, 5]

Jagged Arrays

Jagged arrays (T[][]) are supported. Each inner array is itself serialized as a compound with an "array" key:
int[][] jagged =
{
    new int[] { 1, 2 },
    new int[] { 3, 4, 5 },
};
EchoObject echo = Serializer.Serialize(jagged);
int[][]? restored = Serializer.Deserialize<int[][]>(echo);

Multi-Dimensional Arrays

Multi-dimensional arrays (T[,], T[,,], etc.) are stored as a compound with a "dimensions" entry (a serialized int[] of lengths) and an "elements" list of all values in row-major order:
int[,] grid = { { 1, 2 }, { 3, 4 } };
EchoObject echo = Serializer.Serialize(grid);
int[,]? restored = Serializer.Deserialize<int[,]>(echo);
// restored[0,0]=1, restored[0,1]=2, restored[1,0]=3, restored[1,1]=4

Dictionaries

String Keys — Flat Compound Layout

When the key type is string, Echo serializes the dictionary as a flat EchoType.Compound where each entry becomes a named tag. This produces the most compact and human-readable output:
var config = new Dictionary<string, float>
{
    { "volume",    0.8f  },
    { "brightness", 1.0f },
};
EchoObject echo = Serializer.Serialize(config);
Dictionary<string, float>? restored = Serializer.Deserialize<Dictionary<string, float>>(echo);
The resulting EchoObject looks like:
Compound {
  "volume":     Float(0.8)
  "brightness": Float(1.0)
}

Non-String Keys — Entry-List Layout

When keys are not strings (e.g., int, Guid, enum, or a custom class), Echo wraps all entries in an "entries" list, where each element is a compound with "key" and "value" sub-tags:
var idMap = new Dictionary<int, string>
{
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
};
EchoObject echo = Serializer.Serialize(idMap);
Dictionary<int, string>? restored = Serializer.Deserialize<Dictionary<int, string>>(echo);
// Enum keys
var dayMap = new Dictionary<DayOfWeek, int>
{
    { DayOfWeek.Monday,    1 },
    { DayOfWeek.Wednesday, 3 },
    { DayOfWeek.Friday,    5 },
};
EchoObject echo = Serializer.Serialize(dayMap);
Dictionary<DayOfWeek, int>? restored = Serializer.Deserialize<Dictionary<DayOfWeek, int>>(echo);
// Guid keys
var guidMap = new Dictionary<Guid, string>
{
    { Guid.NewGuid(), "first"  },
    { Guid.NewGuid(), "second" },
};
EchoObject echo = Serializer.Serialize(guidMap);
Dictionary<Guid, string>? restored = Serializer.Deserialize<Dictionary<Guid, string>>(echo);

Nested Dictionaries

Arbitrary nesting is fully supported:
var nested = new Dictionary<int, Dictionary<string, bool>>
{
    {
        1, new Dictionary<string, bool>
        {
            { "enabled", true },
            { "visible", false },
        }
    },
};
EchoObject echo = Serializer.Serialize(nested);
var restored = Serializer.Deserialize<Dictionary<int, Dictionary<string, bool>>>(echo);

HashSet

HashSet<T> is handled by HashSetFormat, which serializes the set as an EchoType.List. Other set-like types such as SortedSet<T> implement ICollection<T> and are routed through CollectionFormat instead:
var tags = new HashSet<int> { 1, 2, 3 };
EchoObject echo = Serializer.Serialize(tags);
HashSet<int>? restored = Serializer.Deserialize<HashSet<int>>(echo);
// SetEquals(new[] { 1, 2, 3 }) == true
Null elements in HashSet<string?> are preserved:
var set = new HashSet<string?> { "alpha", null, "gamma" };
EchoObject echo = Serializer.Serialize(set);
HashSet<string?>? restored = Serializer.Deserialize<HashSet<string?>>(echo);

Queue

Queue<T> is serialized as an EchoType.List, preserving FIFO order. Elements dequeue in the same order after deserialization:
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue("third");

EchoObject echo = Serializer.Serialize(queue);
Queue<string>? restored = Serializer.Deserialize<Queue<string>>(echo);
// restored.Dequeue() == "first", then "second", then "third"

Stack

Stack<T> is serialized as an EchoType.List. Pop order is preserved across the round-trip:
var stack = new Stack<double>();
stack.Push(1.1);
stack.Push(2.2);
stack.Push(3.3);

EchoObject echo = Serializer.Serialize(stack);
Stack<double>? restored = Serializer.Deserialize<Stack<double>>(echo);
// restored.Pop() == 3.3, then 2.2, then 1.1

LinkedList

LinkedList<T> is serialized as an EchoType.List in node order from First to Last:
var list = new LinkedList<int>();
list.AddLast(1);
list.AddLast(2);
list.AddLast(3);

EchoObject echo = Serializer.Serialize(list);
LinkedList<int>? restored = Serializer.Deserialize<LinkedList<int>>(echo);
// restored.First.Value == 1

Arbitrary ICollection<T> Implementations

Any generic type that implements ICollection<T> (and is not a Dictionary) is handled by CollectionFormat. This includes ObservableCollection<T>, Collection<T>, and similar BCL types:
var obs = new System.Collections.ObjectModel.ObservableCollection<int> { 1, 2, 3 };
EchoObject echo = Serializer.Serialize(obs);
var restored = Serializer.Deserialize<System.Collections.ObjectModel.ObservableCollection<int>>(echo);

Deeply Nested and Mixed Collections

Echo handles arbitrary nesting of heterogeneous collection types:
var complex = new Queue<List<Stack<int>>>();

var stack1 = new Stack<int>();
stack1.Push(1);
stack1.Push(2);

var stack2 = new Stack<int>();
stack2.Push(3);
stack2.Push(4);

complex.Enqueue(new List<Stack<int>> { stack1 });
complex.Enqueue(new List<Stack<int>> { stack2 });

EchoObject echo = Serializer.Serialize(complex);
var restored = Serializer.Deserialize<Queue<List<Stack<int>>>>(echo);

Type Preservation in Collection Elements

When a collection’s element type is abstract, an interface, or object, Echo automatically embeds $type tags for concrete elements so they survive deserialization:
object[] mixed =
{
    42,
    "hello",
    new TestObject { Id = 3, Name = "Three" },
};
EchoObject echo = Serializer.Serialize(mixed);
object[]? restored = Serializer.Deserialize<object[]>(echo);
// restored[2] is TestObject with Id=3
Empty collections round-trip correctly. An empty List<int>, Queue<string>, Stack<double>, or LinkedList<int> serializes and deserializes back to an empty instance of the same type.

Build docs developers (and LLMs) love