Part of me has always been jealous of the polish and effort applied to the JavaScript community’s offerings. Vue.js provides developers with a unique graphical user interface in the form of Vue CLI. With the installation of an NPM package, @vue/cli, developers can create new projects, manage existing projects, a project dashboard, and more.

In the world of .NET Development, our gateway has always been the IDE. Visual Studio is the Kind of IDEs, but with newcomers in the IDE space like JetBrains Rider, Visual Studio Code, and VS4Mac, the developer experience is becoming more fragmented. What a .NET developer sees first is becoming less likely to be Visual Studio.

Customized experiences become increasingly more important when users are working with new concepts with philosophical and technical foundations. Offerings like static site generators, testing frameworks, debugging tools would benefit significantly from integrated user interfaces.

With the release of .NET 5 and ASP.NET Core’s new hosting model, it has never been easier to host multiple web applications in a side-by-side configuration within one console host. In this short but concise post, let’s do just that.

ASP.NET Core Console Host

Developers working with the latest ASP.NET Core, will likely have seen the contents of the Program.cs file. Here, we have an opportunity to configure the web host.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

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

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
    }
}

Essential elements of the Program include creating the IHostBuilder and the reference to the Startup file. The Startup is where all of our routes are registered.

A subtle element to note here. The call to Run is synchronous. The console application will block, thus not executing any other code until the statement is complete.

Running Two ASP.NET Core Apps

More is always better, right? Well, in this section, we need to solve for running two ASP.NET Core hosts side-by-side. To do that, we need to address the following. problems:

  • Not blocking on the call toRun.
  • Registering a new application startup, presumably for a dashboard.
  • Listening to different ports other than the default ones of 5000, 5001.

The first step is to make the Main method async. Doing so will allow us to run two Task instances in parallel. We’ll also utilize Task.WhenAny to quit if we terminate either web host.

public static async Task Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    var dashboard = CreateDashboardHostBuilder(args).Build();

    await Task.WhenAny(
        host.RunAsync(), 
        dashboard.RunAsync()
    );
}

Great! Now, we need to define our CreateDashboardHostBuilder method. We’ll choose 5002, 5003 for continuity reasons.

public static IHostBuilder CreateDashboardHostBuilder(string [] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder => {
            webBuilder
                .UseUrls("http://*:5002", "https://*:5003")
                .UseStartup<DashboardStartup>();
        });

As you can see, we are using the UseUrls method to tell this instance of the WebHostBuilder to use non-conflicting ports. Putting it all together, our Program.cs should look like this.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace Doubles
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();
            var dashboard = CreateDashboardHostBuilder(args).Build();

            await Task.WhenAny(
                host.RunAsync(), 
                dashboard.RunAsync()
            );
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => {
                    webBuilder.UseStartup<Startup>();
                });
        
        public static IHostBuilder CreateDashboardHostBuilder(string [] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => {
                    webBuilder
                        .UseUrls("http://*:5002", "https://*:5003")
                        .UseStartup<DashboardStartup>();
                });
    }
}

We have more room for configuration in this basic setup. We could choose not to run our secondary host if the application is in production. We can also log to a different place, possibly in-memory, since this is a development experience. The possibilities are limitless.

Running our application, we can see that the application is listening on a total of 4 ports.

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/khalidabuhakmeh/Projects/Dotnet/Doubles/Doubles
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:5002
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://[::]:5003
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

Cool right? Of course, it is!

Conclusion

Providing an out-of-the-box experience for developers can help guide new developers through a project. Development time experiences are fun and can allow for better learning experiences. While hosting two web applications might be an approach package providers want to support, there are also other options. Project maintainers may wish to build global .NET tools that work on a broader level, but the opportunity to host a self-contained solution and toolbox is also lovely.

Let me know what you think and if you need to host two ASP.NET Core applications within one console host. Thank you for reading.