Passing CLI Arguments in a .NET Core Project

Sun Nov 04 2018

by alex knight on unsplash

Passing arguments with the dotnet CLI is pretty useful when it comes to passing in configurations while in your development environment. One pretty good use case is when developing a single page application where the back end can be restarted on a watch separately from the front end.

We can walk through a simple example.

Let's start a .NET Core project with the CLI and generate an Angular/.NET application. (If you want to see how to update the Angular to a newer version than the one that comes out-of-the box with dotnet, then see my other post here.)

dotnet new angular -o my-app
cd my-app

Let's open up the project in your favorite editor. I'll use VS Code (not Visual Studio). We should also run the development server to see what we have here.

code .
dotnet run watch

The run will probably take longer than usual since if it's the first time you run your new project, dotnet run will also run npm install for you in your ClientApp directory.

So there you have it, a nice SPA for you to get started with.

Also, by running watch, when the files change when we are programming the browser will refresh automatically to reflect the change. But wait, I just said that the browser refreshes, right? Yes, in this setup, the entire application reloads even if only the back end changes.

We can just stop there, but obviously we want to improve our back end development process since it's pretty jarring when the screen flickers and you have to wait several seconds for your app to load again. This is especially harsh if you're working on some UI changes or perhaps getting to a specific state in your app is especially painful.

Well, there's an answer for this (spoiler alert, just read the title).

We can modify our Program.cs and Startup.cs files to retrieve your custom command-line argument(s).

Program.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

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

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
      WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>();
  }
}

Inside of CreateWebHostBuilder when we invoke WebHost.CreateDefaultBuilder(args), we can also chain on ConfigureAppConfiguration to add our command-line arguments to our app configuration.

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
  WebHost.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((_, config) => config.AddCommandLine(args))
    .UseStartup<Startup>();

The first argument of the lambda is the WebHostBuilderContext, but we won't need that, so I'll name it _. All I'm doing here is adding the arguments to config so that we will have access to them in Startup.cs.

Now we can work on our Startup.cs file. Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace my_app
{
  public class Startup
  {
    public Startup(IConfiguration configuration)
    {
      Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

      // In production, the Angular files will be served from this directory
      services.AddSpaStaticFiles(configuration =>
        {
          configuration.RootPath = "ClientApp/dist";
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
      }
      else
      {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();
      app.UseSpaStaticFiles();

      app.UseMvc(routes =>
        {
          routes.MapRoute(
            name: "default",
            template: "{controller}/{action=Index}/{id?}");
        });

      app.UseSpa(spa =>
        {
          // To learn more about options for serving an Angular SPA from ASP.NET Core,
          // see https://go.microsoft.com/fwlink/?linkid=864501

          spa.Options.SourcePath = "ClientApp";

          if (env.IsDevelopment())
          {
            spa.UseAngularCliServer(npmScript: "start");
          }
        });
    }
  }
}

If you are looking at all that code, don't worry, we only need to concern ourselves with the section dealing with our SPA: app.UseSpa.

Before we touch any code in this file, let's first think of what we want to pass in as an argument. I know I don't want to have dotnet run running my npm start script. For simplicity sake (and hopefully any other developer coming across this or our future selves will find this intuitive) let's call our flag no_npm. Yup, naming things is hard.

So now we know we want to call dotnet run like dotnet run --no_npm true, we just have to modify our Startup.cs file to reflect our new superbly named argument.

We still want our .NET application to start our Angular client app by default, so we should put that in our else block. We can specify which port our .NET application needs to proxy with spa.UseProxyToSpaDevelopmentServer like so:

  ...
  spa.Options.SourcePath = "ClientApp";

  if (env.IsDevelopment())
  {
    if (Configuration["no_npm"] == "true")
    {
      spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
    }
    else
    {
      spa.UseAngularCliServer(npmScript: "start");
    }
  }
});

Angular CLI development server uses port 4200 by default.

Okay, so now we're ready to see if everything works. Let's go back to the project base directly and start up dotnet run in a new terminal window.

Now listening on: https://localhost:5001
Now listening on: http://localhost:5000

Remember when we first ran the unmodified project? Immediately after this, npm start was kicked off and we could see that our Angular server was being run over some strange port, not port 4200.

** NG Live Development Server is listening on localhost:60456, open your browser on http://localhost:60456/ **

We now don't have that message, and since we already know which port ng serve defaults to and modified Startup.cs to account for that, let's switch over to the ClientApp directory and start up our server.

We can either type ng serve or:

npm start
** NG Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

Let's open up a browser tab to http://localhost:5000. Yes, the same one we navigated in the beginning.

But the best benefit of having this setup is that any changes to the API won't wait for Angular CLI to rebuild the front end application every time any file changes.

If you found the post helpful and would like this same content delivered to you,

Subscribe to my newsletter

;