/src/lib/FixNet.Core/MessageBuilder.cs
C# | 275 lines | 173 code | 38 blank | 64 comment | 33 complexity | dc01fde320375daf438fbf170a4e2db4 MD5 | raw file
- namespace FixNet.Core
- {
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- using Fields;
- using Exceptions;
- using Extensions;
- using Internals;
- using Parsing;
- using Specification;
- using Messages;
- /// <summary>
- /// A message builder, which parses raw FIX text message into <see cref="Message"/>
- /// </summary>
- public sealed class MessageBuilder
- {
- private readonly TraceSource log = new TraceSource(nameof(MessageBuilder));
- private struct MessageFields
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="MessageFields"/> struct using specified list of fields.
- /// </summary>
- /// <param name="fields">A list of extracted fields</param>
- /// <exception cref="MessageParsingException">Thrown when either <see cref="BeginString"/> or <see cref="MsgType"/> is missing</exception>
- public MessageFields(IList<Field> fields)
- {
- Fields = fields;
- var fixVer = fields.FirstOrDefault(f => f.Tag == BeginString.Tag);
- if (fixVer == null)
- {
- throw new MessageParsingException("BeginString is missing.");
- }
- var msgType = fields.FirstOrDefault(f => f.Tag == MsgType.Tag);
- if (msgType == null)
- {
- throw new MessageParsingException("MsgType is missing.");
- }
- FixVersion = new BeginString(((Field.String)fixVer).Value);
- MessageType = new MsgType(((Field.String)msgType).Value);
- }
- /// <summary>
- /// Gets the list of all fields.
- /// </summary>
- public IList<Field> Fields { get; }
- /// <summary>
- /// Gets the FIX version of the message
- /// </summary>
- public BeginString FixVersion { get; }
- /// <summary>
- /// Gets the type of the message
- /// </summary>
- public MsgType MessageType { get; }
- }
- /// <summary>
- /// Message verification options
- /// </summary>
- [Flags]
- public enum VerificationOptions
- {
- /// <summary>
- /// No verification will be done.
- /// </summary>
- None,
- /// <summary>
- /// Indicates whether to check the header field order.
- /// </summary>
- CheckHeaderFieldOrder = 1 << 0,
- /// <summary>
- /// Indicates whether to check if the required fields are missing in either header, trailer or the message body.
- /// </summary>
- CheckRequiredFields = 1 << 1,
- /// <summary>
- /// Performs full verification check.
- /// </summary>
- Full = CheckHeaderFieldOrder | CheckRequiredFields
- }
- private readonly SpecificationData specificationData;
- private readonly VerificationOptions options;
- private readonly Parser parser;
- /// <summary>
- /// Initializes a new instance of the <see cref="MessageBuilder"/> class.
- /// </summary>
- /// <param name="specificationData">FIX specification data</param>
- /// <param name="options">Verification options. Set to <c>null</c> to skip verification.</param>
- /// <param name="customFieldMapping">Maping for custom fields between their types and parsing funcions</param>
- public MessageBuilder(
- SpecificationData specificationData,
- VerificationOptions options,
- IDictionary<FieldType, CustomFieldParser> customFieldMapping)
- {
- this.specificationData = specificationData;
- this.options = options;
- parser = new Parser(specificationData, customFieldMapping);
- }
- public MessageBuilder() : this(SpecificationData.Empty, VerificationOptions.None, new Dictionary<FieldType, CustomFieldParser>())
- {
- }
- /// <summary>
- /// Builds a FIX message from text.
- /// </summary>
- /// <param name="value">Text representation of the FIX message</param>
- /// <exception cref="MessageParsingException">Thrown when either <see cref="BeginString"/> or <see cref="MsgType"/> are missing in the message.</exception>
- /// <exception cref="MessageVerificationException">Thrown when the verification for the message fails.</exception>
- /// <exception cref="VerificationPreconditionException">Thrown when the <see cref="VerificationOptions"/> are set, but preconditions are not met. For example, when the specification data is not available.</exception>
- /// <returns>An appropriate instance of <see cref="Message"/></returns>
- public Message Build(string value)
- {
- var extractedFields = ExtractFields(value);
- var message = new Message();
- var messageSpec = GetSpecification(extractedFields.MessageType);
- if (options != VerificationOptions.None)
- {
- if (specificationData.Header == null)
- {
- throw new VerificationPreconditionException(
- "Verification options are set, but header specification couldn't be extracted from XML file.");
- }
- if (specificationData.Trailer == null)
- {
- throw new VerificationPreconditionException(
- "Verification options are set, but trailer specification couldn't be extracted from XML file.");
- }
- }
- foreach (var field in extractedFields.Fields)
- {
- if ((specificationData.Header == null && message.Header.OrderedFieldTags.Contains(field.Tag))
- || (specificationData.Header?.ContainsField(field.Tag) ?? false))
- {
- message.Header.Set(field);
- }
- else if ((specificationData.Trailer == null && message.Trailer.OrderedFieldTags.Contains(field.Tag)) ||
- (specificationData.Trailer?.ContainsField(field.Tag) ?? false))
- {
- message.Trailer.Set(field);
- }
- else
- {
- message.Set(field);
- }
- }
- Verify(extractedFields.Fields, message, messageSpec);
- return message;
- }
- /// <summary>
- /// Extracts all fields from the message, including the <see cref="BeginString"/> and <see cref="MsgType"/>.
- /// </summary>
- /// <param name="message">Message from which the fields should be extracted</param>
- /// <returns>An instance of <see cref="MessageFields"/></returns>
- private MessageFields ExtractFields(string message)
- {
- var allFields = Parser.ExtractAllFields(message).Select(parser.MakeTyped).ToList();
- if (!allFields.Any())
- {
- log.TraceEvent(TraceEventType.Warning, 601, "Couldn't extract any fields from message: {0}", message);
- }
- else if (allFields.Count < 5)
- {
- log.TraceEvent(
- TraceEventType.Warning,
- 602,
- "Suspiciously low number of fields extracted ({0}) from the message: {1}",
- allFields.Count,
- message);
- }
- return new MessageFields(allFields);
- }
- /// <summary>
- /// Retrieves the message specification from the specification data container.
- /// </summary>
- /// <param name="msgType">Message type for which the spec should be retrieved</param>
- /// <exception cref="VerificationPreconditionException">Thrown when the verification options are set, but the message specification is missing.</exception>
- /// <returns>An instance of <see cref="SpecElement"/> or <c>null</c>, if no specification for message is available.</returns>
- private SpecElement GetSpecification(MsgType msgType)
- {
- SpecElement messageSpec;
- if (!specificationData.Messages.TryGetValue(msgType, out messageSpec) && options != VerificationOptions.None)
- {
- throw new VerificationPreconditionException(
- $"Verification options are set, but message with MsgType '{msgType.Value}' is not listed in the spec file. Either add it to the XML specification file, or turn off the message verification by setting VerificationOptions to null.");
- }
- return messageSpec;
- }
- /// <summary>
- /// Uses the <see cref="options"/> to verify the fields and message using the message specification.
- /// </summary>
- /// <param name="allFields">All fields in the message</param>
- /// <param name="message">The message itself</param>
- /// <param name="messageSpec">Specification for the message</param>
- /// <exception cref="MessageVerificationException">Thrown when the message does not conform to the specification.</exception>
- private void Verify(IList<Field> allFields, Message message, SpecElement messageSpec)
- {
- if (options.HasFlag(VerificationOptions.CheckRequiredFields))
- {
- foreach (var requiredHeaderTag in specificationData.Header?.RequiredTags ?? Enumerable.Empty<int>())
- {
- if (!message.Header.Contains(requiredHeaderTag))
- {
- throw new MessageVerificationException(
- $"Required tag {requiredHeaderTag} is missing from the header.");
- }
- }
- foreach (var requiredTrailerTag in specificationData.Trailer?.RequiredTags ?? Enumerable.Empty<int>())
- {
- if (!message.Trailer.Contains(requiredTrailerTag))
- {
- throw new MessageVerificationException(
- $"Required tag {requiredTrailerTag} is missing from the trailer.");
- }
- }
- foreach (var requiredBodyTag in messageSpec?.RequiredTags ?? Enumerable.Empty<int>())
- {
- if (!message.Contains(requiredBodyTag))
- {
- throw new MessageVerificationException(
- $"Required tag {requiredBodyTag} is missing from the message body.");
- }
- }
- }
- if (options.HasFlag(VerificationOptions.CheckHeaderFieldOrder))
- {
- foreach (
- var tuple in
- allFields.Zip(message.Header.OrderedFieldTags,
- (field, i) => new ValueTuple<int, int>(field.Tag, i)))
- {
- if (tuple.First != tuple.Second)
- {
- throw new MessageVerificationException(
- $"Header tags out of order: expected tag {tuple.Second}, got {tuple.First}.");
- }
- }
- }
- }
- }
- }