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 routes all diagnostic output — serialization errors, deserialization warnings, condition evaluation failures, and readonly-field alerts — through a single static logger property. By default that property holds a no-op implementation so Echo is silent out of the box. Swapping in your own logger is a one-line change that gives you full visibility into Echo’s internals without any coupling to a specific logging library.

Serializer.Logger

public static IEchoLogger Logger { get; set; } = new NullEchoLogger();
Serializer.Logger is a static property on the Serializer class. Assign any IEchoLogger implementation to it before your first serialization call. All Echo internals call through this property, so a single assignment covers the entire library.
// Replace the default no-op logger with your own implementation
Serializer.Logger = new MyApplicationLogger();

IEchoLogger Interface

The interface is intentionally minimal — four methods that map to the four standard severity levels:
public interface IEchoLogger
{
    void Debug(string message);
    void Info(string message);
    void Warning(string message);
    void Error(string message, Exception? exception = null);
}

Method Reference

Debug(string message)

Low-level diagnostic messages. Not currently emitted by the core library but available for your own ISerializationFormat or ISerializable implementations to use via Serializer.Logger.Debug(...).

Info(string message)

Informational messages about normal serialization events. Use this level for progress reporting in custom formats.

Warning(string message)

Non-fatal anomalies. Echo emits warnings when, for example, a [SerializeIf] condition member cannot be found on the type, or when a readonly field is about to be set during deserialization.

Error(string message, Exception? exception = null)

Failures that prevented a field from being serialized or deserialized. The exception parameter is optional — some error paths provide it, others do not. Echo does not rethrow after logging; it skips the affected field and continues, so a single bad field does not abort the entire operation.

When Echo Logs

Echo emits log messages in the following situations:
SeveritySituation
WarningA [SerializeIf] condition member was not found or doesn’t return bool
WarningA readonly field is being set during deserialization
ErrorA field failed to serialize (exception caught internally)
ErrorA field failed to deserialize (exception caught internally)
ErrorA type is abstract or an interface and cannot be instantiated
ErrorA type has no parameterless constructor
ErrorActivator.CreateInstance threw for any other reason
ErrorA [SerializeIf] condition evaluation threw an exception

NullEchoLogger — The Default

NullEchoLogger is the built-in no-op implementation. All four methods are empty, which means Echo produces no output unless you replace the logger.
public class NullEchoLogger : IEchoLogger
{
    public void Debug(string message) { }
    public void Info(string message) { }
    public void Warning(string message) { }
    public void Error(string message, Exception? exception = null) { }
}
This design means you pay no logging overhead at all in release builds if you keep the default. There are no string allocations in the hot path unless your logger implementation materializes them.

Implementing a Custom Logger

Implement IEchoLogger and forward each method to your preferred logging sink. Here are three common patterns:

Console Logger

using Echo.Logging;

public class ConsoleEchoLogger : IEchoLogger
{
    public void Debug(string message)
        => Console.WriteLine($"[Echo DEBUG] {message}");

    public void Info(string message)
        => Console.WriteLine($"[Echo INFO]  {message}");

    public void Warning(string message)
        => Console.WriteLine($"[Echo WARN]  {message}");

    public void Error(string message, Exception? exception = null)
    {
        Console.WriteLine($"[Echo ERROR] {message}");
        if (exception != null)
            Console.WriteLine(exception);
    }
}

Microsoft.Extensions.Logging Bridge

using Echo.Logging;
using Microsoft.Extensions.Logging;

public class MicrosoftEchoLogger : IEchoLogger
{
    private readonly ILogger _logger;

    public MicrosoftEchoLogger(ILogger logger) => _logger = logger;

    public void Debug(string message)
        => _logger.LogDebug("{Message}", message);

    public void Info(string message)
        => _logger.LogInformation("{Message}", message);

    public void Warning(string message)
        => _logger.LogWarning("{Message}", message);

    public void Error(string message, Exception? exception = null)
        => _logger.LogError(exception, "{Message}", message);
}

Serilog Bridge

using Echo.Logging;
using Serilog;

public class SerilogEchoLogger : IEchoLogger
{
    private readonly ILogger _log;

    public SerilogEchoLogger(ILogger log) => _log = log.ForContext("Source", "Prowl.Echo");

    public void Debug(string message) => _log.Debug("{Message}", message);
    public void Info(string message)  => _log.Information("{Message}", message);
    public void Warning(string message) => _log.Warning("{Message}", message);

    public void Error(string message, Exception? exception = null)
    {
        if (exception != null)
            _log.Error(exception, "{Message}", message);
        else
            _log.Error("{Message}", message);
    }
}

Assigning the Logger

Assign your logger once, early in your application’s startup sequence, before any serialization takes place:
// At application startup
Serializer.Logger = new MicrosoftEchoLogger(
    loggerFactory.CreateLogger("Prowl.Echo")
);
Assign Serializer.Logger before calling Serializer.RegisterFormat or any serialization method. Because Logger is a static property, a single assignment covers every call site in the process.
Serializer.Logger is a mutable static property with no thread synchronization. If you need to swap loggers at runtime from multiple threads, guard the assignment with a lock or use a thread-safe wrapper that delegates to whatever the current inner logger is.

Using the Logger in Custom Formats

Your own ISerializationFormat and ISerializable implementations can emit through the same logger, keeping all Echo-related output in one place:
public class MyCustomFormat : ISerializationFormat
{
    public bool CanHandle(Type type) => type == typeof(MySpecialType);

    public EchoObject Serialize(Type targetType, object value, SerializationContext context)
    {
        Serializer.Logger.Debug($"Serializing {targetType.Name}");
        try
        {
            // ... custom serialization logic
            return EchoObject.NewCompound();
        }
        catch (Exception ex)
        {
            Serializer.Logger.Error($"Failed to serialize {targetType.Name}", ex);
            return new EchoObject(EchoType.Null, null);
        }
    }

    public object? Deserialize(EchoObject value, Type targetType, SerializationContext context)
    {
        Serializer.Logger.Debug($"Deserializing {targetType.Name}");
        // ... custom deserialization logic
        return null;
    }
}

Build docs developers (and LLMs) love