A FIX 4.4 TagValue message is a sequence of tag=value fields, each ended by the SOH byte (0x01). You can build and parse that envelope in C# with byte-level code and no FIX engine. Write BeginString (8), BodyLength (9) and MsgType (35) first, compute BodyLength from the bytes that follow it, and append CheckSum (10) computed over everything before it. That gives you a small, checkable framing layer. It does not make an order valid. A valid New Order Single (35=D) depends on the field rules in the FIX 4.4 application specification, which this article does not reproduce in full, and a successful envelope parse only proves framing and checksum.
Start from the FIX 4.4 errata release
The FIX Trading Community identifies FIX 4.4 Specification with Errata 20030618 as the current FIX 4.4 specification and recommends that implementers use that errata release. The download is a ZIP archive containing seven volumes and release notes. FIX 4.4 also appears among the legacy application-message versions the standards organization lists as supported.
As an Amazon Associate I earn from qualifying purchases.
Keep two sources open while you work. The BodyLength rule quoted below comes from the FIX Session Layer technical standard (June 2020), while message-level field requirements such as the New Order Single definition come from the FIX 4.4 application volumes in the errata download.
Three layers of a FIX message
Most confusion in from-scratch FIX code comes from mixing three separate concerns. Keep them apart in your code:
#1 Best Overall
- Message envelope: tags 8, 9 and 35 open the message, and tag 10 closes it. This is what the code below builds and checks.
- Application schema: the MsgType decides which fields a message must and may carry. A 35=D order is validated against its own definition, not against the envelope rules.
- Stream and session framing: a socket delivers bytes, not messages. Finding message boundaries in that byte stream is a separate problem, covered later in this article.
Byte rules for BodyLength and CheckSum
The Session Layer standard states the BodyLength rule this way:
The length must be calculated by counting the number of octets in the message following the end of field delimiter (<SOH>) of BodyLength(9), up to and including the end of field delimiter (<SOH>) of the field immediately preceding the CheckSum(10) field.
FIX Trading Community, FIX Session Layer technical standard (June 2020)
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
In practice, the calculation runs in this order:
- Build the body as bytes: MsgType (35) first, then every other field, each written as
tag=valuefollowed by SOH. - Set BodyLength to the number of bytes in that buffer. Use the array length, not
string.Length, which counts UTF-16 code units rather than octets. - Write the header as
8=FIX.4.4SOH9=the BodyLength digits SOH, then the body bytes. - Compute CheckSum over every byte written so far, from the first byte of
8=through the SOH that precedes10=. Add the byte values, take the result modulo 256, and write it as three decimal digits. Confirm this arithmetic against the CheckSum definition in the errata volumes before relying on it in production; this article does not reproduce that passage. - Append
10=, the three CheckSum digits and a final SOH.
Use one byte-oriented encoding throughout. The code below uses strict US-ASCII rather than Encoding.ASCII, because the default ASCII encoder replaces unsupported characters with ?. That silent substitution would change both the length and the checksum, and you would not see an error.
Build the message in C#
This builder takes the body fields as an ordered list, writes them as bytes, and generates tags 8, 9 and 10 itself. It rejects any value that contains SOH, because a stray delimiter would corrupt the field structure.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
public static class FixCodec
{
public const byte Soh = 0x01;
// Throws on any byte above 0x7F instead of substituting '?'.
private static readonly Encoding Strict = Encoding.GetEncoding(
"us-ascii",
EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
public static byte[] Build(string beginString, IReadOnlyList<KeyValuePair<int, string>> body)
{
if (body.Count == 0 || body[0].Key != 35)
throw new ArgumentException("The first body field must be MsgType (35).");
using var bodyStream = new MemoryStream();
foreach (var field in body)
{
if (field.Key == 8 || field.Key == 9 || field.Key == 10)
throw new ArgumentException("Tags 8, 9 and 10 are generated by Build.");
if (field.Value.IndexOf((char)Soh) >= 0)
throw new ArgumentException("Tag " + field.Key + " contains SOH.");
WriteAscii(bodyStream, field.Key + "=" + field.Value);
bodyStream.WriteByte(Soh);
}
byte[] bodyBytes = bodyStream.ToArray();
using var pre = new MemoryStream();
WriteAscii(pre, "8=" + beginString);
pre.WriteByte(Soh);
WriteAscii(pre, "9=" + bodyBytes.Length.ToString(CultureInfo.InvariantCulture));
pre.WriteByte(Soh);
pre.Write(bodyBytes, 0, bodyBytes.Length);
byte[] preBytes = pre.ToArray();
int checksum = ComputeChecksum(preBytes, preBytes.Length);
using var msg = new MemoryStream();
msg.Write(preBytes, 0, preBytes.Length);
WriteAscii(msg, "10=" + checksum.ToString("D3", CultureInfo.InvariantCulture));
msg.WriteByte(Soh);
return msg.ToArray();
}
private static int ComputeChecksum(byte[] data, int count)
{
int sum = 0;
for (int i = 0; i < count; i++) sum += data[i];
return sum % 256;
}
private static void WriteAscii(Stream stream, string text)
{
byte[] bytes = Strict.GetBytes(text);
stream.Write(bytes, 0, bytes.Length);
}
}
A sample call uses session header fields and illustrative order fields. It is not a validated New Order Single: the required and conditional fields for 35=D must be checked against the FIX 4.4 definition.
var fields = new List<KeyValuePair<int, string>>
{
new KeyValuePair<int, string>(35, "D"),
new KeyValuePair<int, string>(49, "SENDER"),
new KeyValuePair<int, string>(56, "TARGET"),
new KeyValuePair<int, string>(34, "1"),
new KeyValuePair<int, string>(52, "20260101-12:00:00.000"),
new KeyValuePair<int, string>(11, "ORDER-0001"),
new KeyValuePair<int, string>(55, "MSFT"),
new KeyValuePair<int, string>(54, "1"),
new KeyValuePair<int, string>(38, "100"),
new KeyValuePair<int, string>(40, "1"),
new KeyValuePair<int, string>(60, "20260101-12:00:00.000"),
};
byte[] raw = FixCodec.Build("FIX.4.4", fields);
// Display only: make SOH visible. Never feed this text back into Parse.
Console.WriteLine(Encoding.ASCII.GetString(raw).Replace((char)1, '|'));
When you display a message with | in place of SOH, that substitution exists only in the text you print. Keep the original byte array for every calculation and transmission.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Parse a complete message
The parser works on a byte array that already holds one whole message. It checks the envelope before it trusts any field. Run these checks in order and stop at the first failure:
- The message starts with tag 8 and its BeginString is terminated by SOH.
- Tag 9 is the second field and holds a non-negative decimal length.
- BodyLength places the start of the trailer inside the buffer, and the byte before that offset is SOH.
- The bytes at that offset read
10=, followed by three digits and SOH, and the SOH is the last byte of the buffer. - The recomputed checksum over the bytes before the trailer matches the declared value.
- Tag 35 is the first field after BodyLength.
public static IReadOnlyList<KeyValuePair<int, string>> Parse(byte[] msg)
{
int firstSoh = Array.IndexOf(msg, Soh);
if (firstSoh < 0) throw new FormatException("BeginString is not terminated by SOH.");
string begin = Strict.GetString(msg, 0, firstSoh);
if (!begin.StartsWith("8=", StringComparison.Ordinal))
throw new FormatException("Message must start with tag 8.");
int lenStart = firstSoh + 1;
int lenEnd = Array.IndexOf(msg, Soh, lenStart);
if (lenEnd < 0) throw new FormatException("BodyLength is not terminated by SOH.");
string lenField = Strict.GetString(msg, lenStart, lenEnd - lenStart);
if (!lenField.StartsWith("9=", StringComparison.Ordinal) ||
!int.TryParse(lenField.Substring(2), NumberStyles.None, CultureInfo.InvariantCulture, out int bodyLength))
throw new FormatException("Tag 9 must follow tag 8 and hold a decimal length.");
int bodyStart = lenEnd + 1;
long expectedTrailer = (long)bodyStart + bodyLength;
if (bodyLength < 0 || expectedTrailer + 7 > msg.Length)
throw new FormatException("BodyLength points past the end of the buffer.");
int checksumStart = (int)expectedTrailer;
if (msg[checksumStart - 1] != Soh)
throw new FormatException("The field before tag 10 must end with SOH.");
if (msg[checksumStart] != (byte)'1' || msg[checksumStart + 1] != (byte)'0' || msg[checksumStart + 2] != (byte)'=')
throw new FormatException("Tag 10 is not where BodyLength predicts.");
if (msg[checksumStart + 6] != Soh || checksumStart + 7 != msg.Length)
throw new FormatException("CheckSum must be 10=nnn plus SOH, ending the message.");
string csText = Strict.GetString(msg, checksumStart + 3, 3);
if (!int.TryParse(csText, NumberStyles.None, CultureInfo.InvariantCulture, out int declared) || declared > 255)
throw new FormatException("CheckSum must be three decimal digits, 000 to 255.");
if (ComputeChecksum(msg, checksumStart) != declared)
throw new FormatException("CheckSum does not match the message bytes.");
var body = SplitFields(msg, bodyStart, checksumStart);
if (body.Count == 0 || body[0].Key != 35)
throw new FormatException("Tag 35 must be the first field after BodyLength.");
var result = new List<KeyValuePair<int, string>>
{
new KeyValuePair<int, string>(8, begin.Substring(2)),
new KeyValuePair<int, string>(9, lenField.Substring(2)),
};
result.AddRange(body);
result.Add(new KeyValuePair<int, string>(10, csText));
return result;
}
private static List<KeyValuePair<int, string>> SplitFields(byte[] msg, int start, int end)
{
var result = new List<KeyValuePair<int, string>>();
int pos = start;
while (pos < end)
{
int soh = Array.IndexOf(msg, Soh, pos, end - pos);
if (soh < 0) throw new FormatException("Field is not terminated by SOH.");
string field = Strict.GetString(msg, pos, soh - pos);
int eq = field.IndexOf('=');
if (eq <= 0 || !int.TryParse(field.Substring(0, eq), NumberStyles.None, CultureInfo.InvariantCulture, out int tag))
throw new FormatException("Malformed field: " + field);
result.Add(new KeyValuePair<int, string>(tag, field.Substring(eq + 1)));
pos = soh + 1;
}
return result;
}
Calling FixCodec.Parse(raw) on the output of the builder returns the fields in wire order, including tags 8, 9 and 10. Each check above is a separate failure mode, so a malformed or tampered message fails with a specific exception rather than producing partial output. Treat the code as a starting point and add tests built from your own fixtures before using it on live traffic.
Rank #4
Validating an order
The parser above accepts any well-formed envelope, whatever its MsgType. It does not know whether a 35=D message has the fields it needs. Build that check from the New Order Single definition in the FIX 4.4 application specification:
- List the fields the definition marks as required for 35=D, and check each one is present exactly once.
- Implement any conditional rules, where the requiredness of one field depends on the value of another. Some of these are easy to miss, so take each one directly from the specification rather than from memory.
- Check value domains, such as allowed enumerations, against the same definition.
Keep this validator separate from Parse, so a framing error and an invalid order produce different errors.
Framing a live stream
A socket delivers arbitrary chunks of bytes. A chunk can hold half a message, one message, or several messages back to back. Parse assumes the buffer already holds one complete message, so a receiver needs a separate step to find those boundaries. Using BodyLength, the logic is:
Best Value
- Append each received chunk to a buffer.
- Find the first two SOH bytes. If they are not both present, wait for more bytes.
- Read the tag 9 value. If tag 9 is not the second field, treat the stream as out of sync and apply your session recovery policy.
- Compute the end of the message as the body start plus BodyLength plus the seven-byte trailer
10=nnnand SOH. If the buffer is shorter than that, wait. - Copy exactly that many bytes out, pass them to
Parse, remove them from the buffer, and repeat from step 2.
This sketch covers only the BodyLength path. The FIX Session Layer standard defines the wider rules for session-level stream handling, including recovery after a framing error. Read it before you depend on a particular recovery behavior.
| Concern | Complete buffer (Parse) | Continuous stream |
|---|---|---|
| Where the length comes from | Tag 9 in the bytes you already hold | Tag 9 read from the first bytes received, before the rest of the message has arrived |
| Message boundary | End of the buffer | Body start plus BodyLength plus the seven-byte trailer |
| Partial input | Not applicable; malformed input is rejected | Wait for more bytes; never parse a truncated slice |
| Bytes after the trailer | Rejected by the parser above | Start the next message at the byte after the trailer’s SOH |
| Framing source | BodyLength in tag 9 | BodyLength in tag 9, or SOFH if your counterparty wraps messages in SOFH |
SOFH is a separate framing standard that supplies a message length and encoding type for messages in a continuous stream. It is not part of every raw FIX 4.4 TagValue message. Check with your counterparty whether it uses SOFH before you add any SOFH handling.
Where a from-scratch parser stops
This parser is well suited to learning the encoding, building controlled examples, and testing a counterparty’s sample messages. A production connection usually needs more than envelope handling. Logon, heartbeats, sequence-number tracking and resend requests are session-layer concerns that this code does not cover. Teams that move to a live connection often adopt a FIX engine or implementation support at that point. No specific product or service is recommended or verified here, so compare options against the session requirements of your counterparty.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




