FIX Proxy sample
This sample demonstrates how to build a FIX Protocol proxy. The proxy accepts incoming FIX client connections and establishes corresponding sessions with a back-end FIX engine. It seamlessly routes FIX messages between clients and the back-end, ensuring reliable communication.
If the back-end engine is temporarily unavailable, client messages are stored in the session’s message store and automatically resent once the connection is reestablished, using the FIX protocol’s built-in message resending capabilities. The same behaviour applies when a client session disconnects and reconnects.
The proxy can also read an optional configuration file that defines FIX sessions to be proactively initiated with the back-end FIX engine according to a predefined schedule. This ensures that when a client FIX session connects, the corresponding back-end connection is already established, eliminating any delay in message forwarding.
Configuration
To configure the proxy, edit the appsetting.json
and Scheduler.config
files.
© Onix Solutions
Source code
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sample;
public record PredefinedSession(string Sender, string Target, string Version);
class Program
{
static void Main()
{
Console.WriteLine("FIX Proxy sample\n");
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var sessions = config.GetSection("Sessions").Get<List<PredefinedSession>>();
int listenPort = config.GetValue<int>("ListenPort");
string schedulerConfigFile = config.GetValue<string>("SchedulerConfigFile", "Scheduler.config");
string licenseStore = config.GetValue<string>("LicenseStore", ".");
Console.WriteLine($"listenPort: {listenPort}");
Console.WriteLine($"schedulerConfigFile: {schedulerConfigFile}");
Console.WriteLine("Predefined sessions: " + string.Join(", ", (sessions ?? []).Select(s => $"({s.Sender}, {s.Target}, {s.Version})")));
Console.WriteLine($"licenseStore: {licenseStore}");
try
{
using FixProxy proxy = new(listenPort, schedulerConfigFile, sessions, licenseStore);
while (true)
{
Console.Write("Press 'X' to quit");
ConsoleKeyInfo cki = Console.ReadKey(true);
Console.WriteLine($" Key pressed: {cki.Key}\n");
if (cki.Key == ConsoleKey.X)
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"Fatal error: {ex.Message}");
}
}
}
using NLog;
using NLog.Extensions.Logging;
using OnixS.Fix;
using OnixS.Fix.Fix44;
using OnixS.Fix.Scheduling;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
namespace Sample;
public class FixProxyException(string message) : Exception(message) { }
public class LogicalException(string message) : FixProxyException($"LogicalError: {message}") { }
public class FixProxy : IDisposable
{
private bool disposedValue;
private readonly Engine engine;
private readonly Scheduler scheduler;
private readonly ConcurrentDictionary<Session, Session> client2backupSession = new();
private readonly ConcurrentDictionary<Session, Session> backup2clientSession = new();
private readonly Logger logger = LogManager.GetLogger("FixProxyLogger");
public FixProxy(int listenPort, string schedulerConfigFile, List<PredefinedSession> predefinedSessions, string licenseStore)
{
var settings = new EngineSettings()
{
SendLogoutOnException = true,
SendLogoutOnInvalidLogon = true, // E.g. to send a Logout when the sequence number of the incoming Logon (A) message is less than expected.
LicenseStore = GetLicenseStoreFolder(licenseStore),
LoggerProvider = new NLogLoggerProvider()
};
settings.ListenPorts.Add(listenPort);
engine = Engine.Init(settings);
engine.Error += (object sender, EngineErrorEventArgs args) => logger.Error("Engine Error: " + args.ToString());
engine.Warning += (object sender, EngineWarningEventArgs args) => logger.Warn("Engine Warning: " + args.ToString());
scheduler = new(schedulerConfigFile);
scheduler.Error += (object scheduler, OnixS.Fix.Scheduling.SessionErrorEventArgs args) => logger.Error($"Scheduler reported error for session {args.Session}: {args.Reason}");
scheduler.Warning += (object scheduler, OnixS.Fix.Scheduling.SessionWarningEventArgs args) => logger.Warn($"Scheduler reported warning for session {args.Session}: {args.Reason}");
// See https://ref.onixs.biz/net-core-fix-engine-guide/articles/accepting-fix-session-without-a-prior-creation-of-session-object.html
Engine.Instance.UnknownIncomingConnection += (object sender, UnknownIncomingConnectionEventArgs e) =>
{
var logon = e.IncomingLogonMessage;
Session clientSession = CreateSessionPair(logon[Tag.TargetCompID], logon[Tag.SenderCompID], logon.Version);
logger.Info($"Accepted unknown incoming connection: {clientSession}");
};
if(null != predefinedSessions)
{
foreach (var session in predefinedSessions)
{
var version = ProtocolVersion.Parse<ProtocolVersion>(session.Version);
Session clientSession = CreateSessionPair(session.Sender, session.Target, version);
}
}
logger.Info("Awaiting incoming FIX Connections on port " + listenPort + " ...");
}
private Session CreateSessionPair(string senderCompId, string targetCompId, ProtocolVersion version)
{
Session clientSession = new(senderCompId, targetCompId, version)
{
MessageMode = MessageMode.FlatMessage,
ReuseEventArguments = true,
ReuseInboundMessage = true,
};
clientSession.InboundApplicationMessage += (object sender, InboundMessageEventArgs e) =>
{
Session session = (Session)sender!;
if (client2backupSession.TryGetValue(session, out var backupSession))
{
backupSession.Send(e.FlatMessage);
}
else
{
throw new LogicalException($"Cannot find the backup session for client session ({session})");
}
};
clientSession.MessageResending += OnMessageResending;
scheduler.Register(clientSession, "Frontend", "Frontend");
Session backupSession = new (targetCompId, senderCompId, version)
{
MessageMode = MessageMode.FlatMessage,
ReuseEventArguments = true,
ReuseInboundMessage = true,
};
if (!client2backupSession.TryAdd(clientSession, backupSession))
{
throw new LogicalException($"Cannot add the mapping between client ({clientSession}) and backup ({backupSession}) sessions");
}
if (!backup2clientSession.TryAdd(backupSession, clientSession))
{
throw new LogicalException($"Cannot add the mapping between backup ({backupSession}) and client ({clientSession}) sessions");
}
backupSession.MessageResending += OnMessageResending;
backupSession.InboundApplicationMessage += (object sender, InboundMessageEventArgs e) =>
{
Session session = (Session)sender!;
if (backup2clientSession.TryGetValue(session, out var clientSession))
{
clientSession.Send(e.FlatMessage);
}
else
{
throw new LogicalException($"Cannot find the client session for backup session ({session})");
}
};
scheduler.Register(backupSession, "Backend", "Backend");
return clientSession;
}
private void OnMessageResending(object sender, MessageResendingEventArgs e)
{
e.AllowResending = true;
}
private static string GetLicenseStoreFolder(string licenseStore)
{
string path = Path.Join(AppContext.BaseDirectory, licenseStore);
if (!Directory.Exists(path))
{
throw new FixProxyException($"License store folder '{path}' does not exist");
}
return path;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
scheduler.Dispose();
Engine.Shutdown();
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}