Up-to-date Version of this question: Binary protocol variability V3.0
Summary of the problem: Parsing an incoming stream of events from a binary communication protocol, if we have some variations in devices to support and would not like to have one huge switch to include everything.
So, we use it this way:
    static void Main(string[] args)
    {
        using (var inputStream = FakeInputDataStream())
        using (var binaryReader = new BinaryReader(inputStream))
        using (var tokenizer = new DeviceTokenizer(binaryReader).AsLoggable())
        using (var reader = new EventReader(tokenizer))
            foreach (dynamic e in reader.ReadAll())
                Handle(e);
    }
    static void Handle(SessionStart e) =>
        Console.WriteLine("SessionStart, Hardware Version = " + e.Hardware);
    static void Handle(EnvironmentalReport e) =>
        Console.WriteLine("EnvironmentalReport, Temperature = " + e.Temperature);
Solution looks like this:
Company.Hardware
Base event class:
public abstract class Event
{
}
And EventReader:
public class EventReader : IDisposable
{
    public EventReader(ITokenizer tokenizer)
    {
        Tokenizer = tokenizer;
    }
    public void Dispose() => Tokenizer.Dispose();
    public IEnumerable<Event> ReadAll()
    {
        Event e = null;
        while (TryRead(out e))
            yield return e;
    }
    bool TryRead(out Event e)
    {
        try
        {
            var type = Tokenizer.Read<Type>("discriminator");
            e = (Event)Activator.CreateInstance(type, Tokenizer);
            return true;
        }
        catch (ObjectDisposedException)
        {
            e = null;
            return false;
        }
    }
    ITokenizer Tokenizer { get; }
}
Where:
public interface ITokenizer : IDisposable
{        
    T Read<T>(string token);
}
With base implementation:
public abstract class Tokenizer : ITokenizer,
    IRead<Type>
{
    protected Tokenizer(BinaryReader reader)
    {
        Reader = reader;
    }
    public void Dispose() => Reader.Dispose();
    public T Read<T>(string token)
    {
        if (this is IRead<T>)
            return (this as IRead<T>).Read();
        else
            throw new NotSupportedException();
    }
    Type IRead<Type>.Read() => Protocol[Reader.ReadInt32()];
    protected virtual Protocol Protocol => Protocol.Undefined;
    protected BinaryReader Reader { get; }
}
public interface IRead<T>
{
    T Read();
}
The idea is to allow processing of additional tokens by implementing IRead<T> on the subtype.
Protocol class here maps discriminators to Types and uses System.Collections.Immutable from NuGet:
public class Protocol
{
    public static readonly Protocol Undefined = 
        new Protocol(ImmutableDictionary<int, Type>.Empty);
    Protocol(IImmutableDictionary<int, Type> types)
    {
        Types = types;
    }
    public Protocol Support<TEvent>(int discriminator)
        where TEvent : Event =>
        new Protocol(Types.Add(discriminator, typeof(TEvent)));
    public Type this[int discriminator]
    {
        get
        {
            if (Types.ContainsKey(discriminator))
                return Types[discriminator];
            else
                throw new NotSupportedException();
        }
    }
    IImmutableDictionary<int, Type> Types { get; }
} 
As for the logging:
public interface ILog : IDisposable
{
    void Write(string line);
}
With one implementation:
class TextLog : ILog
{
    public TextLog(TextWriter writer)
    {
        Writer = writer;
    }
    public void Dispose() =>
        Writer.Dispose();
    public void Write(string line) =>
        Writer.WriteLine(line);
    TextWriter Writer { get; }
}
And consumer:
class LoggingTokenizer : ITokenizer
{
    public LoggingTokenizer(ITokenizer parent, ILog log)
    {
        Parent = parent;
        Log = log;
    }
    public void Dispose()
    {
        Parent.Dispose();
        Log.Dispose();
    }
    public T Read<T>(string name)
    {
        var value = Parent.Read<T>(name);
        Log.Write($"{name}={value}");
        return value;
    }
    ITokenizer Parent { get; }
    ILog Log { get; }
}
We would use it through:
public static class TokenizerLogging
{
    public static ITokenizer AsLoggable(this ITokenizer tokenizer) =>
        tokenizer.AsLoggable(Console.Out);
    public static ITokenizer AsLoggable(this ITokenizer tokenizer, TextWriter writer) =>
        tokenizer.AsLoggable(new TextLog(writer));
    public static ITokenizer AsLoggable(this ITokenizer tokenizer, ILog log) =>
        new LoggingTokenizer(tokenizer, log);
}
Company.Hardware.Controller
Let’s say we have an updated tokenizer:
public class ControllerTokenizer : Tokenizer,
    IRead<DateTime>, IRead<Version>
{
    public ControllerTokenizer(BinaryReader reader) 
        : base(reader)
    {
    }
    protected override Protocol Protocol => base.Protocol
        .Support<SessionStart>(1);
    DateTime IRead<DateTime>.Read() => new DateTime(Reader.ReadInt32());
    Version IRead<Version>.Read() => new Version(Reader.ReadInt32(), Reader.ReadInt32());
}
And an event:
public class SessionStart : Event
{
    public SessionStart(ITokenizer tokenizer)
        : this(
              tokenizer.Read<Version>("hardware-version"),
              tokenizer.Read<Version>("firmware-version"),
              tokenizer.Read<DateTime>("clock-time"))
    {
    }
    public SessionStart(Version hardware, Version firmware, DateTime clock)
    {
        Hardware = hardware;
        Firmware = firmware;
        Clock = clock;
    }
    public Version Hardware { get; }
    public Version Firmware { get; }
    public DateTime Clock { get; }
}
Company.Hardware.Controller.Device
EnvironmentalReport event:
public class EnvironmentalReport : Event
{
    public EnvironmentalReport(ITokenizer tokenizer)
        :this(
             tokenizer.Read<float>("temperature"),
             tokenizer.Read<float>("humidity"))
    {
    }
    public EnvironmentalReport(float temperature, float humidity)
    {
        Temperature = temperature;
        Humidity = humidity;
    }
    // actually, I use non-primitive types
    public float Temperature { get; } 
    public float Humidity { get; }
}
Which requires:
public class DeviceTokenizer : ControllerTokenizer,
    IRead<float>
{
    public DeviceTokenizer(BinaryReader reader) 
        : base(reader)
    {
    }
    protected override Protocol Protocol => base.Protocol
        .Support<EnvironmentalReport>(2);
    float IRead<float>.Read() => Reader.ReadSingle();
}
P.S. Parameter validation is omitted for brevity.
Big/Little endian handling:
I am planning to define this helper class:
public static class BigEndian
{
    public static int ReadInt32BE(this BinaryReader reader) =>
        BitConverter.ToInt32(reader.ReadValue(4), 0);
    public static uint ReadUInt32BE(this BinaryReader reader) =>
        BitConverter.ToUInt32(reader.ReadValue(4), 0);
    public static short ReadInt16BE(this BinaryReader reader) =>
        BitConverter.ToInt16(reader.ReadValue(2), 0);
    public static ushort ReadUInt16BE(this BinaryReader reader) =>
        BitConverter.ToUInt16(reader.ReadValue(2), 0);
    public static long ReadInt64BE(this BinaryReader reader) =>
        BitConverter.ToInt64(reader.ReadValue(8), 0);
    static byte[] ReadValue(this BinaryReader reader, int size)
    {
        var bytes = reader.ReadBytes(size);
        if (BitConverter.IsLittleEndian)
            Array.Reverse(bytes);
        return bytes;
    }
}
And at the controller level of tokenizer inheritance it will look like the following:
// big-endian controller
public abstract class FreescaleTokenizer : Tokenizer,
    IRead<Type>, IRead<int>, IRead<uint>, IRead<ErrorCode>
{
    protected FreescaleTokenizer(BinaryReader reader) 
        : base(reader)
    {
    }
    Type IRead<Type>.Read() => Protocol[Reader.ReadInt32BE()];
    uint IRead<uint>.Read() => Reader.ReadUInt32BE();
    int IRead<int>.Read() => Reader.ReadInt32BE();
    ErrorCode IRead<ErrorCode>.Read() => (ErrorCode)Reader.ReadInt32BE();
}
public enum ErrorCode: int
{
    OK = 0,
    FailedGoingCanada = 1
}
Are there any potential troubles?
