ApplicationHost Infrastucture (.NET)

ApplicationServer boilerplate once was a rather valuable addition to Remoting SDK for .NET. The entire server application infrastructure became available literally via a single line of code:

public static void Main(string[] args)
{
   new ApplicationServer("Server App Name").Run(args);
}

Out-of-the-box support for command-line, GUI or Windows Service/Daemon run modes, customizable server parameters, transparent access to TLS support and other features allowed developers to focus on the business logic code rather on the boring infrastructure implementation details.

As time passed, .NET Core and .NET 5 brought us developed infrastructure like de-facto built-in Dependency Injection support and new program initialization abstractions. So inevitable at some point ApplicationServer-based code started to look slightly obsolete. Even the recent addition of Dependency Injection containers support did not help ApplicationServer to start feeling native to the new world of .NET Core and .NET 5. To add insult to injury, the internal ApplicationServer infrastructure did not play well with the new BackgroundService class provided by .NET Core / .NET 5.

The Solution

Still every challenge is an opportunity to evolve. So a support for 2 different application hosting packages was added: Microsoft.Extensions.Hosting and Topshelf.

Microsoft.Extensions.Hosting Support

Support for the Microsoft.Extensions.Hosting.* packages family (including ...WindowsService and ...Systemd packages) is provided via the RemObjects.SDK.Extensions.Hosting.Microsoft NuGet package.

The minimal server application startup code that uses this package looks like

‌‌

static class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args)
            .Build();

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args)
    {
        return Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                })
                .UseApplicationServer(configuration =>
                {
                })
                .UseNetworkServer(server =>
                {
                })
                .UseConsoleLifetime();
    }
}

The dependencies used in the application should be set up via the callback method in the ConfigureServices method call.

Then two application server-related methods are called:

  • The UseApplicationServer method call is the place where the main server properties like its name, RODL namespace and self-signed certificate properties (if any) are defined and the main server infrastructure is initialized. This method call is required to make the application work as a Remoting SDK or Data Abstract server. If no custom values are provided then both application name and its RODL namespace will be set to ApplicationServer.

  • Then the UseNetworkServer method should be called. This method sets up the network server and allows to fine-tune its options like the port or server channel type used, or even to define custom network server implementation (see below).

These two method calls initialize the server infrastructure similar to the one provided by the ApplicationServer-based infrastructure.

What's more important is that new methods allow to inject Remoting SDK application server lifecycle as a native part of the .NET Core / .NET 5 application lifecycle. For example it is possible to use Dependency Injection container provided by Microsoft, as well as to use packages that allow the app to run as Windows Service or a Systemd daemon (Microsoft.Extensions.Hosting.WindowsServices and Microsoft.Extensions.Hosting.Systemd respectively).

For example to turn the application into a Windows Service one would need to reference the Microsoft.Extensions.Hosting.WindowsServices package and to replace the UseConsoleLifetime method call with the UseWindowsService method call.

Once the application is built it needs to be installed as a Windows Service. Unfortunately the Microsoft.Extensions.Hosting.WindowsServices package does not provide its own installer so the standard Windows Service installer should be used:

sc create [Service_Name] BinPath=[full_path_to_the_server_app_exe_file]

Also several other commands are available that allow to start, stop or delete the Windows Service:

sc start [Service_Name]

sc stop [Service_Name]

sc delete [Service_Name]

Topshelf Support

Support for the Topshelf framework is provided via the RemObjects.SDK.Extensions.Hosting.Topshelf package.

Topshelf describes itself as such:

Topshelf is a framework for hosting services written using the .NET framework. The creation of services is simplified, allowing developers to create a simple console application that can be installed as a service using Topshelf

More info on this framework can be found on its documentation site.

The minimal application startup code that uses this package looks like this:

‌ ‌‌

static void Main(string[] args)
{
    HostFactory.Run(host =>
    {
        host.ConfigureServices(services =>
        {
        });
        host.UseApplicationServer(configuration =>
        {
        });
        host.UseNetworkServer(server =>
        {
        });

        host.UseHostedService();
    });
}

The ConfigureServices, UseApplicationServer and UseNetworkServer methods have exactly the same purpose as for the RemObjects.SDK.Extensions.Hosting.Microsoft package.

The RemObjects.SDK.Extensions.Hosting.Topshelf package uses Dependency Injection container provided by the Microsoft.Extnsions.DependencyInjection package.

Topshelf framework provides a rich set of command line arguments, including commands to register the Windows Service:

NetServer.exe install -servicename:"NetService"

‌ More commands and their options descriptions can be found here

Extensibility

Custom Network Server implementation

The new infrastructure described in this topic allows to define a custom implementation of pretty much any component used, starting from own ICertificateGenerator implementation up to the custom IAppServerEngine application core implementation. All what is required to do this is to re-register corresponding service in the services collection.

Still the most common task is to use a custom Network Server component implementation.

For example let's take a look at the following task:

Server should expose its services via an additional server channel.

The first step is to implement the custom Network server itself. A very straightforward custom Network Server implementation would look like

‌ ‌‌

public class ExtendedNetworkServer : NetworkServer
{
    private IpHttpServerChannel _serverChannel;

    protected override void InternalStart()
    {
        base.InternalStart();

        this._serverChannel = new IpHttpServerChannel();
        this._serverChannel.Port = this.Port + 1;
        this._serverChannel.Dispatchers.AddRange(
            this.ServerMessages.Select(
               m => new MessageDispatcher(m.DefaultDispatcherName, m)));
        this._serverChannel.Open();
    }

    protected override void InternalStop()
    {
        this._serverChannel.Close();
        base.InternalStop();
    }
}

The next step is to register this class as the one that should be used as a Network Server implementation at runtime. All what is needed to this is to provide the custom Network Server class as a generic parameter in the UseNetworkServer method call:

‌‌‌

...
   .UseNetworkServer<ExtendedNetworkServer>(server =>
...

Custom App Server Engine implementation

AppServerEngine is a 'heart' of an application server. It provides internal services used to load the server configuration (including RODL composition), and to start or stop the NetworkServer instance.

In the rare cases where a very deep customization is required, it is possible to use your own custom implementation of the IAppServerEngine interface.

Here is a sample implementation of a custom App Server Engine:

‌ ‌‌‌

public class ExtendedAppServerEngine : AppServerEngine
{
    public ExtendedAppServerEngine(IServiceProvider serviceProvider, INetworkServer server,
            IAppServerConfiguration configuration, INetworkServerConfigurator networkServerConfigurator,
            ICertificateGenerator certificateGenerator)
    : base(serviceProvider, server, configuration, networkServerConfigurator, certificateGenerator)
    {
    }

    protected override void InternalStart()
    {
        System.Diagnostics.Debug.WriteLine(
                "ExtendedAppServerEngine.InternalStart");

        base.InternalStart();
    }
}

Once defined, this class needs to be registered in the services collection as the implementation of the IAppServerEngine interface, replacing the default implementation registered by the UseApplicationServer method:

‌‌‌‌

static void Main(string[] args)
{
    HostFactory.Run(host =>
    {
        host.ConfigureServices(services =>
        {
        });
        host.UseApplicationServer(configuration =>
        {
        });
        host.UseNetworkServer<ExtendedNetworkServer>(server =>
        {
        });

        // Register custom App Server Engine implementation
        host.ConfigureServices(services =>
        {
            services.Replace(
                ServiceDescriptor.Singleton<IAppServerEngine,
                        ExtendedAppServerEngine>());
        });

            host.UseHostedService();
    });
}

Migration to the new Hosting infrastructure

Migration an existing ApplicationServer-based app to the new Hosting-based packages infrastructure can be done in a few simple steps:

  1. Remove all direct references to Remoting SDK or Data Abstract assemblies.
  2. Reference required NuGet packages ( RemObjects.SDK.Server or RemObjects.DataAbstract.Server accordingly).
  3. Reference the RemObjects.SDK.Extensions.Hosting.Microsoft or RemObjects.SDK.Extensions.Hosting.Topshelf package depending on the hosting framework you want to use.
  4. Comment out existing Program.Main method.
  5. Reintroduce the Program.Main method as follows:
    • For RemObjects.SDK.Extensions.Hosting.Microsoft package: ‌‌‌‌‌

      public static void Main(string[] args)
      {
          var host = CreateHostBuilder(args)
              .Build();
      
          host.Run();
      }
      
      public static IHostBuilder CreateHostBuilder(string[] args)
      {
          return Host.CreateDefaultBuilder(args)
                  .ConfigureServices((hostContext, services) =>
                  {
                  })
                  .UseApplicationServer(configuration =>
                  {
                  })
                  .UseNetworkServer(server =>
                  {
                  })
                  .UseConsoleLifetime();
      }
      

      ‌‌
    • For RemObjects.SDK.Extensions.Hosting.Topshelf package: ‌‌‌‌‌

      static void Main(string[] args)
      {
          HostFactory.Run(host =>
          {
              host.ConfigureServices(services =>
              {
              });
              host.UseApplicationServer(configuration =>
              {
              });
              host.UseNetworkServer(server =>
              {
              });
      
              host.UseHostedService();
          });
      }
      

  6. Define necessary ApplicationServer configuration options using method UseApplicationServer.
  7. Define necessary NetworkServer configuration options using method UseNetworkServer.

Conclusion

The new Hosting infrastructure provides more clean API and is easier to use than the old ApplicationServer boilerplate code. It fits better into the new approaches introduced by .NET Core and .NET 5 runtimes as well.

The ApplicationServer boilerplate is aimed more at the "old" .NET Framework while the new Hosting infrastructure uses benefits of the newer .NET Core and .NET 5 runtimes, like the features provided by the Microsoft.Extensions packages or native support of Dependency Injection.

While ApplicationServer boilerplate won't be removed from Remote SDK or anyhow deprecated, for new projects it is better to consider to use the new Hosting infrastructure instead of the ApplicationServer boilerplate code.