System.Resources.ResourceReader is unsuitable for deserializing resources in the ILSpy context, because it tries to deserialize custom resource types, which fails if the types are in a non-GAC assembly. So we are instead using our own "class ResourcesFile", which is based on the .NET Core ResourceReader implementation. struct ResourcesFileFormat { int32 magicNum; [check == ResourceManager.MagicNumber] // ResourceManager header: int32 resMgrHeaderVersion; [check >= 0] int32 numBytesToSkip; [check >= 0] if (resMgrHeaderVersion <= 1) { string readerType; string resourceSetType; } else { byte _[numBytesToSkip]; } // RuntimeResourceSet header: int32 version; [check in (1, 2)] int32 numResources; [check >=0] int32 numTypes; [check >=0] string typeName[numTypes]; .align 8; int32 nameHashes[numResources]; int32 namePositions[numResources]; [check >= 0] int32 dataSectionOffset; [check >= current position in file] byte remainderOfFile[]; } // normal strings in this file format are stored as: struct string { compressedint len; byte value[len]; // interpret as UTF-8 } // NameEntry #i is stored starting at remainderOfFile[namePositions[i]] // (that is, namePositions is interpreted relative to the start of the remainderOfFile array) struct NameEntry { compressedint len; byte name[len]; // interpret as UTF-16 int32 dataOffset; [check >= 0] } // Found at position ResourcesFileFormat.dataSectionOffset+NameEntry.dataOffset in the file. struct ValueEntry { if (version == 1) { compressedint typeIndex; if (typeIndex == -1) { // no value stored; value is implicitly null } else { switch (typeName[typeIndex]) { case string: case int, uint, long, ulong, sbyte, byte, short, ushort: case float: case double: T value; // value directly stored case DateTime: int64 value; // new DateTime(_store.ReadInt64()) case TimeSpan: int64 value; // new TimeSpan(_store.ReadInt64()) case decimal: int32 value[4]; default: byte data[...]; // BinaryFormatter-serialized data using typeName[typeIndex] } } } else if (version == 2) { compressedint typeCode; // note: unlike v1, no lookup into the typeName array! switch (typeCode) { case null: // no value stored; value is implicitly null case string: case bool: case int, uint, long, ulong, sbyte, byte, short, ushort: T value; case char: uint16 value; case float: case double: case decimal: T value; case DateTime: int64 value; // DateTime.FromBinary(_store.ReadInt64()) case TimeSpan: int64 value; // new TimeSpan(_store.ReadInt64()) case ResourceTypeCode.ByteArray: int32 len; byte value[len]; case ResourceTypeCode.Stream: int32 len; byte value[len]; case >= ResourceTypeCode.StartOfUserTypes: byte data[...]; // BinaryFormatter-serialized data using typeName[typeCode - ResourceTypeCode.StartOfUserTypes] } } }