'SignalR Core (.NET6) on a Linux service

I have a library to connect to several websites and download data from some socket connections those websites expose in their APIs. The library does everything, from connecting to the websites, to handle the data received from the sockets, to their manipulation up to the storage into a MariaDB 10 remote database. I use this library embedded in some small console applications, one for each specific task i need to accomplish on such web sites.

Now, I am trying to convert one of these small console apps into a linux service to have it running 24/7 on a small QNAP server (running busybox which is in turn using Systemd to manage services).

So I added a SignalR hub to convert all the methods of the several classes that take part in the library to allow my clients (the console apps) to activate the sockets, start the download and store everything in the db or doing whatever elase manipulation at client side may be necessary.

So I am moving from this pattern: Web <---> MyLibrary <---> Console App

To the following schema

Web <---> MyLibrary + SignalR hub + linux service <--->  My library + SignalR client <---> Console App

I am still writing code so I am far from the first test. Anyway some big questions turn up in my head (see below please). My library does not have an http server at all (except for the socket management which is in turn done by another NuGet library [cryptoexchange.net]. I don’t need any web interface in my software so I am not hosting the whole server side software in a Asp Core standard framework.I am instead going for a IHostBuilder instance.

This is my program.cs file (server side)

public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateWebHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
#if SYSTEMD
           .UseSystemd()
#else
                //.UseSystemd()
#endif
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    string url =
                    MondayServiceStrings.HTTP_BASE + "*" + ":" + MondayServiceStrings.PORT;
                    // url shall be "https://*:9999"
                    webBuilder.UseUrls(url);
                });         
    }

And the relevant startup class

    /// <summary>
    /// Startup class for the server side signalr service
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// This method gets called by the runtime. Use this method to add services to the container.
        /// </summary>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSignalRCore();
            services.AddHostedService<MondayService>();
        }

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        /// <param name="app">application</param>
        /// <param name="env">Host environment</param>
        public void Configure(IApplicationBuilder app, Microsoft.Extensions.Hosting.IHostEnvironment env)
        {
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<MondayHub>(MondayServiceStrings.URL);
            });
        }
    }

Then the hub

    public interface IMondayServer
    {
        public Parser.Parser Parser { get; set; }
        public DBManager.DBManager DBManager { get; set; }
    }
}
    /// <summary>
    ///  Wrapper interface for the client side of the signalR connection
    /// </summary>
    public interface IMondayHubClient : IMondayClientDBManagerAware, IMondayClientParserAware, IMondaySocketEventAwareClient
    {
    /// <summary>
    /// Main hub to expose DB manager and Parser services to clients
    /// </summary>
    public partial class MondayHub : Hub<IMondayHubClient>
    {
        /// <summary>
        /// Constructor of the hub to expose MOnday services to the outside.
        /// </summary>
        public MondayHub(IMondayServer referrer)
        {
            this._referrer = referrer;
        }

        /// <summary>
        /// Referral to call surrounding server service mathods and act on parser and db manager.
        /// </summary>
        private readonly IMondayServer _referrer;
    }
// public implementation for the database manager interface
    public partial class MondayHub : IMondayClientDBManagerAware //: database frontend interface
    {
        public Task<bool> ConnectToParserSocket()
        {
           return new Task<bool>(() => _referrer.DBManager.ConnectToParserSocket());
        }

        public Task<bool>  DisconnectFromParserSocket()
        {
            return new Task<bool>(() => _referrer.DBManager.DisconnectFromParserSocket());
        }

        public Task<int> ExportToCsv_SummariesAsync(DateTime from, DateTime to, string market, string exchange)
        {
            return  _referrer.DBManager.ExportToCsv_SummariesAsync(from,to,market,exchange);
        }

        public Task<int> ExportToCsv_TickersAsync(DateTime from, DateTime to, string market, string exchange)
        { return  _referrer.DBManager.ExportToCsv_TickersAsync(from,to,market,exchange);
        }

        public Task<List<Summary>> GetSummariesAsync(string exchange, string market, bool isMondayMarket, DateTime from, DateTime to)
        {
            return _referrer.DBManager.GetSummariesAsync(exchange, market, isMondayMarket, from, to);
        }

        public Task<List<Ticker>> GetTickersAsync(string exchange, string market, bool isMondayMarket, DateTime from, DateTime to)
        {
            return _referrer.DBManager.GetTickersAsync(exchange, market, isMondayMarket, from, to);
        }

        public Task<int> ProduceMissingSummariesReport(string alreadySortedData = null)
        {
            return new Task<int>(() => _referrer.DBManager.ProduceMissingSummariesReport(alreadySortedData));
        }

        public Task<int> ProduceMissingTickersReport(string alreadySortedData = null)
        {
            return new Task<int>(() => _referrer.DBManager.ProduceMissingTickersReport(alreadySortedData));
        }

        public Task<int> UploadSummariesAsync(List<Summary> summaries)
        {
            return  _referrer.DBManager.UploadSummariesAsync(summaries);
        }

        public Task<int> UploadTickersAsync(List<Ticker> tickers)
        {
            return _referrer.DBManager.UploadTickersAsync(tickers);
        }
    }

The server side class for the service:

    /// <summary>
    /// Main Worker class for managing the service inside the Systemd
    /// </summary>
    public partial class MondayService : BackgroundService, IMondayServer
    {


        /// <summary>
        /// A database manager instance to manipulate the DB within the service.
        /// </summary>
        public DBManager DbManager { get { return _dbManager; } internal set { _dbManager = value; } }


        /// <summary>
        /// A full featured parser to get data from and to the web exchanges from within the service.
        /// </summary>
        public Parser Parser { get { return _parser; } internal set { _parser = value; } }



        /// <summary>
        /// Std ctr.
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="mondayhub"></param>
        public MondayService(ILogger<MondayService> logger, IHubContext<MondayHub, IMondayHubClient> mondayhub)
        {
            _logger = logger;
            _mondayHub = mondayhub;
            /*
            _parser = new Parser(MondayConfiguration.Credentials);
            _parser.OnSocketDataArrived += BroadCastSocketData;
            _dbManager = new DBManager(_parser);
            _dbManager.ConnectToParserSocket();
            */
        }



        /// <summary>
        /// Core loop of the service. Here all the assets are instantiated and managed up to final
        /// disposal. This istantiates the SignalR service and manages it.
        /// </summary>
        /// <param name="stoppingToken"></param>
        /// <returns></returns>
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Monday DB Manager starting at {now}", DateTimeOffset.Now);
            //AllocateAssets(); check if the allocation of dbmanager and parser shall fall here
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Monday Service ready at: {time}", DateTimeOffset.Now);
                await Task.Delay(TimeSpan.FromMinutes(2), stoppingToken);
            }
            //DisposeAssets(); // incase allocation is done in the execute async here there should be disposal.
            _logger.LogInformation("Monday DB Manager stopping at {now}", DateTimeOffset.Now);
        }        
    }

To give you the big picture I also post the client side code yet not complete:

        /// <summary>
        /// Full client to the Monday Service Hub
        /// </summary>
        public partial class MondayHubClient : IAsyncDisposable
        {
    
    
            /// <summary>
            /// Stadard ctr for a MOndayService client.
            /// </summary>
            public MondayHubClient()
            {
                _hubConnection = new HubConnectionBuilder()
               .WithUrl(new Uri($"TODO define what to put here"))
               .WithAutomaticReconnect()
               .Build();
            }
    
           
    
            /// <summary>
            /// Initiate a connection with the Server hub.
            /// </summary>
            /// <returns></returns>
            public Task StartNotificationConnectionAsync() => _hubConnection.StartAsync();
    
            /// <summary>
            /// Checks whether the connection with the hub is alive.
            /// </summary>
            public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
    
            /// <summary>
            /// Dispose the client.
            /// </summary>
            /// <returns></returns>
            public async ValueTask DisposeAsync()
            {
                if (_hubConnection is not null)
                {
                    await _hubConnection.DisposeAsync();
                    _hubConnection = null;
                }
            }
            public async Task Connect(string serverIp, int port)
            {
                if (_hubConnection == null)
                {
                    _hubConnection = new HubConnectionBuilder()
                        .WithUrl($"{MondayServiceStrings.HTTP_BASE}{MondayServiceStrings.SERVER_IP}:{MondayServiceStrings.PORT}{MondayServiceStrings.URL}")
                        .Build();
    
                    _hubConnection.On<>
    
                    _hubConnection.Closed += async (error) =>
                    {
                        await Task.Delay(new Random().Next(0, 5) * 1000);
                        await _hubConnection.StartAsync();
                    };
    
                    _hubConnection.On<string, string>("?....???", (user, message) =>
                    {
                        string fullMessage = $"{user}: {message}";
                        MessageEvent?.Invoke(this, fullMessage);
                    });
                }
                try
                {
                    await _hubConnection.StartAsync();
                }
                catch (Exception ex)
                {
                    _hubConnection?.Invoke(this, $"{ex.Message}; base Exception: {ex.GetBaseException().Message}");
                    await Task.Delay(new Random().Next(0, 5) * 1000);
                    await Connect(serverIp, port);
                }
            }
    
    
            public Task<bool> ConnectToParserSocket()
            {
                return _hubConnection.InvokeCoreAsync<bool>(MondayServiceStrings.IDBManager.Methods.ConnectToParserSocket, new object[] { }, default);
            }
    
            public Task<bool> DisconnectFromParserSocket()
            {
                return _hubConnection.InvokeCoreAsync<bool>(MondayServiceStrings.IDBManager.Methods.DisconnectFromParserSocket, new object[] { }, default);
            }
    
    
    
            public Task<int> ExportToCsv_SummariesAsync(DateTime from, DateTime to, string market, string exchange)
            {
                return _hubConnection.InvokeCoreAsync<int>(MondayServiceStrings.IDBManager.Methods.ExportToCsv_SummariesAsync, new object[] { from, to, market, exchange }, default);
    
            }
    
            public Task<int> ExportToCsv_TickersAsync(DateTime from, DateTime to, string market, string exchange)
            {
                return _hubConnection.InvokeCoreAsync<int>(MondayServiceStrings.IDBManager.Methods.ExportToCsv_TickersAsync, new object[] { from, to, market, exchange }, default);
            }
    
    
            public Task<List<Summary>> GetSummariesAsync(string exchange, string market, bool isMondayMarket, DateTime from, DateTime to)
            {
                return _hubConnection.InvokeCoreAsync<List<Summary>>(MondayServiceStrings.IDBManager.Methods.GetSummariesAsync, new object[] { exchange, market,isMondayMarket, from, to }, default);
            }
    
            public Task<List<Ticker>> GetTickersAsync(string exchange, string market, bool isMondayMarket, DateTime from, DateTime to)
            {
                return _hubConnection.InvokeCoreAsync<List<Ticker>>(MondayServiceStrings.IDBManager.Methods.GetTickersAsync, new object[] { exchange, market, isMondayMarket, from, to }, default);
                       }
    
            public Task<int> ProduceMissingSummariesReport(string alreadySortedData = null)
            {
                 return _hubConnection.InvokeAsync<int>(MondayServiceStrings.IDBManager.Methods.ProduceMissingSummariesReport, new[] { alreadySortedData }, default);
            }

… and a lot of other interface callable methods to match the same interface at hub’s side….

Now the questions:

a) Shall I use a _referrer (as I am doing in the hub) to use my library at server side? Or shall I use a hubContext from the MondayService class?

b) Where to I instantiate the classes of my library (like the parser and the dbManager)? I do it on the service class (that should be a singleton as far as both parser and database manager store state in themselves so cannot be refreshed so often).

c) Shall I only implement ExecuteAsync at the server side in order to launch the service hosted in a windows (or linux system) service? No need for StartAsync or StopAsync?

d) The restart command in Systemd (but I suppose the same holds in windows) is the sequential sum of a stop plus a startAsync calls?

e) Can I use hubs and SignalR infrestucture in general without any web boilerplate? I have a ping in my ExecuteAsync ( I took it from How do I add SignalR to a .NET Core Windows Service? And I thank Tim a lot). If the loop would be simply a delay with no action would it break something? I thought that that was necessary as a keepalive, but I really don’t need it. I need only to have the hub open to client setup calls and start forwarding any data arriving to the server sockets to the client side code to let the client process it again.

Thanks a lot for reading all this, hope some expert of asp, signalR and linux has a sleepless night to help me!

Alex



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source