Q:

Проблема с маршалингом fixed-полей структуры

Есть некая структура, преобразовывается в массив байт, далее необходимо сделать покрытие тестами данные упаковки и распаковки на локальной машине. Все бы ничего, но иногда случается так, что необходимо полностью сверить массив fixed byte UserData[4192] на факт повреждения данных при передачи через сеть. Тут случаются следующие проблемы, хотелось бы сравнить полностью все байты содержащиеся в пакете с данными. Попытки обратиться к byte* через [] не дают такого сделать, описывая это тем что необходимо использовать выражение fixed. При попытке копировать указатель в выражении fixed:
for (int i = 0; i < endPacket.PacketSize; i++)
{
    fixed (byte* id = endPacket.UserData)
    {
        fixed (byte* d = _packet.UserData)
        {
            Assert.AreEqual(id[i], d[i]);
        }
    }
}

error CS0213: Получить адрес фиксированного выражения с помощью
  оператора fixed невозможно.

Что само себе противоречит выдавая ошибки если его не использовать, т.е. обращаться так:
for (int i = 0; i < endPacket.PacketSize; i++)
{
    Assert.AreEqual(_packet.UserData[i], endPacket.UserData[i]);
}

При этом, если поместить указатель на структуру в выражение fixed вот так:
fixed (Network.Packet* ptr = &_packet)
{
    for (int i = 0; i < endPacket.PacketSize; i++)
    {
        Assert.AreEqual(ptr->UserData[i], endPacket.UserData[i]);
    }
}

то все прекрасно работает.
Почему так происходит, и почему только так можно обращаться к данным содержащимся в структуре?
Структура:
public enum PacketType
{
    Voice,
    Text,
    Command,
    Exit
}

public unsafe struct Packet
{
    public PacketType PacketType;
    public bool IsPartial;
    public fixed byte UserData[MaxBufferSize];
    public readonly int PacketSize;
    public const int MaxBufferSize = 4192;

    public Packet(PacketType packetType, byte[] userMessage, out int bufferOutSize)
    {
        int userMessageSize = userMessage.Length;

        PacketSize = userMessageSize > MaxBufferSize ? MaxBufferSize : userMessageSize;
        bufferOutSize = PacketSize;
        IsPartial = userMessageSize > MaxBufferSize;
        PacketType = packetType;

        fixed (Packet* a = &this)
        {
            for (int i = 0; i < PacketSize; i++)
            {
                a->UserData[i] = userMessage[i];
            }
        }

#if DEBUG // Для теста на случай провала
        for (int i = 0; i < PacketSize; i++)
        {
            Debug.WriteLine($"UserData[{i}] = {UserData[i]}");
        }
#endif
    }
}

Upd: Имеется решение костылем, для упаковки распаковки структуры, т.к. Marshal.PtrToStruct а так же Marshal.StructToPtr не дали необходимых результатов:
Структура: 
public enum PacketType
{
    Voice,
    Text,
    Command,
    Exit
}

[StructLayout(LayoutKind.Explicit)]
public unsafe struct Packet
{
    [FieldOffset(0)] private readonly int _packetType;
    [FieldOffset(4)] private readonly bool _isPartial;
    [FieldOffset(8)] private readonly int _packetSize;
    [FieldOffset(12)] private fixed byte _userData[4192];

    /// <exception cref="IndexOutOfRangeException"></exception>
    internal byte this[int i]
    {
        get
        {
            if (i > _packetSize)
            {
                throw new IndexOutOfRangeException(
                    $"Max value of index cannot be greater than value of PacketSize. Current value is {PacketSize}");
            }

            byte data;

            fixed (void* self = &this)
            {
                data = Marshal.ReadByte(new IntPtr(self), 12 + i);
            }
            return data;
        }
        set
        {
            fixed (void* self = &this)
            {
                Marshal.WriteByte(new IntPtr(self), 12 + i, value);
            }
        }
    }

    public PacketType Type
    {
        get
        {
            PacketType result;
            fixed (void* self = &this)
            {
                result = Marshal.ReadInt32(new IntPtr(self), 0).To<PacketType>();
            }
            return result;
        }

        internal set
        {
            fixed (void* self = &this)
            {
                Marshal.WriteInt32(new IntPtr(self), 0, (int) value);
            }
        }
    }

    public int PacketSize
    {
        get
        {
            int result;
            fixed (void* self = &this)
            {
                result = Marshal.ReadInt32(new IntPtr(self), 8);
            }
            return result;
        }

        internal set
        {
            fixed (int* data = &_packetSize)
            {
                *data = value;
            }
        }
    }

    public bool IsPartial
    {
        get
        {
            bool result;
            fixed (void* self = &this)
            {
                result = Marshal.ReadInt32(new IntPtr(self), 4) == 1;
            }

            return result;
        }

        internal set
        {
            fixed (bool* data = &_isPartial)
            {
                *data = value;
            }
        }
    }

    public Packet(PacketType packetType, byte[] userMessage, out int bufferOutSize)
    {
        int userMessageSize = userMessage.Length;
        _packetSize = userMessageSize > 4192 ? 4192 : userMessageSize;
        bufferOutSize = _packetSize;
        _isPartial = userMessageSize > 4192;
        _packetType = packetType.To<int>();

        for (int i = 0; i < _packetSize; i++)
        {
            this[i] = userMessage[i];
        }
    }
}

Да, выглядит сумбурно и странно, но такая работа с памятью гарантирует мне что упаковка и распаковка данных произойдет корректно.
Расширения упаковки/распаковки структуры:
public static class Extensions
{
    public static T To<T>(this object self)
    {
        return (T) self;
    }

    public static unsafe byte[] Zip(ref this Packet obj)
    {
        const int size = 4 + 4 + 4 + 4192;
        byte[] bytes = new byte[size];
        IntPtr ptr = Marshal.AllocHGlobal(size);

        try
        {
            Marshal.WriteInt32(ptr, 0, obj.Type.To<int>());
            Marshal.WriteInt32(ptr, 4, obj.IsPartial ? 1 : 0);
            Marshal.WriteInt32(ptr, 8, obj.PacketSize);

            for (int i = 0; i < obj.PacketSize; i++)
            {
                Marshal.WriteByte(ptr, 12 + i, obj[i]);
            }

            Marshal.Copy(ptr, bytes, 0, size);
        }
        catch (Exception exception)
        {
            Marshal.FreeHGlobal(ptr);
            throw exception;
        }

        Marshal.FreeHGlobal(ptr);
        return bytes;
    }

    public static Packet Unzip(this byte[] bytes)
    {
        GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
        IntPtr ptr = handle.AddrOfPinnedObject();
        try
        {
            Packet theStructure = new Packet
            {
                Type = Marshal.ReadInt32(ptr, 0).To<PacketType>(),
                IsPartial = Marshal.ReadInt32(ptr, 4) == 1,
                PacketSize = Marshal.ReadInt32(ptr, 8)
            };

            for (int i = 0; i < theStructure.PacketSize; i++)
            {
                theStructure[i] = Marshal.ReadByte(ptr, 12 + i);
            }

            handle.Free();
            return theStructure;
        }
        catch (Exception exception)
        {
            handle.Free();
            throw exception;
        }
    }
}

Надеюсь есть решения таких задач попроще :)

A:

Итак, я таки решил Вашу проблему
Корнем зла как раз и являлся fixed)
Но об этом чуть позже, для начала разберемся с остальными вопросами!

Почему так происходит, и почему только так можно обращаться к данным, содержащимся в структуре?

Дело в том, что когда Вы получаете доступ к фиксированным полям извне, они уже закреплены в стеке, так что попытка повторной фиксации вызывает ошибку компиляции CS0213.
Да и про контекст fixed, начиная с C# 7.3, вообще можно забыть, ибо теперь индексирование закрепленных полей нигде не требует дополнительной фиксации. 
Пример:
public unsafe struct TestFixed
{
    public fixed byte FixedField[1];

    // Инициализация фиксированного выражения с помощью 2 разных записей
    // Контекс unsafe обязателен!
    public unsafe TestFixed(byte[] Arr)
    {
        // Без создания новой переменной
        FixedField[0] = Arr[0];

        // C# 7.2- style
        fixed (byte* field = FixedField)
            field[0] = Arr[0];
    }
}

...

TestFixed test = new TestFixed(new byte[] { 1 });
byte getByte = test.FixedField[0]; // 1

fixed (byte* _field = test.FixedField)
    getByte = _field[0]; // CS0213, так как объект уже закреплен

byte* field = test.FixedField;
getByte = field[0]; // 1

У Вас же ошибка записи структуры в неуправляемый блок памяти посредством Marshal.StructureToPtr вызвана именно наличием фиксированного поля UserData 
Marshal.StructureToPtr как-то странно и через раз их обрабатывает: то считает все до бита по размеру структуры, то считает только "первый элемент" (т.е. N байтов с начала указателя, где N = sizeof(используемый фиксированным полем тип))
Так что лучше все таки использовать обычные "безопасные" структуры, управляя тем, как они хранятся в памяти и маршалируются, с помощью атрибутов
Я переписал Вашу структуру следующим образом:
// Укажем, что члены должны располагаться так, как мы их описали
// А также явно укажем размер структуры (в байтах):
//
// 4 на PacketType (int)
// 4 на IsPartial (пусть bool и занимает всего 1 бит => 1 байт в целых числах,
//      однако по умолчанию структуры пакуются таким образом для более быстрого доступа к элементам)
// 4 на PacketSize
// MaxBufferSize на UserData
[StructLayout(LayoutKind.Sequential, Size = 12 + MaxBufferSize)]
public struct Packet
{
    public PacketType PacketType;

    public bool IsPartial;

    public readonly int PacketSize;

    // Благодаря данному атрибуту мы можем указать, что массив является
    // частью структуры, так что на его месте в памяти будет не ссылка,
    // а все элементы данного массива
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = MaxBufferSize)]
    public byte[] UserData;

    public const int MaxBufferSize = 4192;

    public Packet(PacketType packetType, byte[] userMessage, out int bufferOutSize)
    {
        int userMessageSize = userMessage.Length;
        PacketSize = userMessageSize > MaxBufferSize ? MaxBufferSize : userMessageSize;
        bufferOutSize = PacketSize;
        IsPartial = userMessageSize > MaxBufferSize;
        PacketType = packetType;
        UserData = new byte[MaxBufferSize];
        // И никаких "мучений" с fixed)
        Array.Copy(userMessage, UserData, Math.Min(userMessage.Length, MaxBufferSize));
    }

    public override int GetHashCode() => UserData.Sum(x => x);
    // Проверим полное совпадение данных пакетов
    public override bool Equals(object obj) => obj is Packet packet && packet.PacketType == PacketType && packet.IsPartial == IsPartial && packet.PacketSize == PacketSize && EqualsData(packet);

    // Совпадает ли UserData у пакетов
    public unsafe bool EqualsData(Packet Packet) => Packet.UserData.SequenceEqual(UserData);
}

Проведем тест:
// Готовим данные
PacketType packetType = PacketType.Text;
byte[] userMessage = Enumerable.Range(0, Packet.MaxBufferSize).Select(x => (byte)x).ToArray();

// Инициализируем
Packet packet = new Packet(packetType, userMessage, out _);

// Получаем массив байт, а из него - новую структуру
Packet endPacket = packet.Zip().Unzip<Packet>();

// Проверяем:
bool isOk = packet.Equals(endPacket); // true

Как видите, теперь никаких проблем с копированием в/из памяти не возникло и все данные были обработаны корректно! 
Если же посмотреть размер, занимаемый Вашей структурой в памяти, то мы получим искомые 4204 == 12 + MaxBufferSize!
P.S. - в коде использовались методы из более раннего вопроса по этой теме!

На этом все, проблема решена)
Опять же в качестве совета я хотел предложить следующую вещь: не надо загонять все типы пакетов в один
Если Вы можете настроить сервис на прием сообщений разной длины (сначала передается длина последующего пакета, а потом уже он сам), то создайте разные структуры под пакеты разных типов. Пусть сначала идет как раз тип пакета, чтобы Вы, считав первые 4 байта, могли знать, во что Вам десериализовать последующий массив байт. Тем самым некоторые вещи (к примеру, передачу строк) можно сильно упростить

Надеюсь, все Ваши проблемы наконец-то иссякли и Вы сможете идти дальше в Ваших свершениях!
Удачи Вам!
Если проблемы возникнут вновь - буду рад Вам помочь!

