• Version 1.16.0
Show / Hide Table of Contents

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);
    }
}

In this article
Back to top Copyright © Onix Solutions.
Generated by DocFX