loading...

.NET Core Console Apps - A Better Way?

shawnwildermuth profile image Shawn Wildermuth Originally published at wildermuth.com on ・3 min read

ConsoleI was at a customer site last week and a lot of their integration code is a set of console apps that are run on timers to import and export data. This isn't an uncommon use-case.

I've got a couple of these lying around myself. I realized that I didn't know of a good exemplar of doing simple console apps using .NET Core in a way that is closer to ASP.NET Core (e.g. dependency injection, lifetime management, cancellation, etc.). So I decided to re-write an old console app I have.

Welcome to Pinger. Pinger was a simple console app I wrote ages ago to ping a range of addresses. I am sure this exists in other places, but sometimes you just want to write it. So I took the core logic of the old, .NET version and ported it to .NET Core, but with some additional services to make the code simple.

Dependency Injection was one of my main wants so I started simple and just created my own service collection:

ServiceCollection coll = new ServiceCollection();
coll.AddSingleton(options);
coll.AddTransient<PingerService>();

var provider = coll.BuildServiceProvider();

var svc = provider.GetService<PingerService>();
svc.Run();

This was fine, but I decided that I wanted more features. Luckily, the hosting in .NET Core 3.1 supports this. If you add a reference to the Microsoft.Extensions.Hosting assembly, you can use the default hosting:

> dotnet add package Microsoft.Extensions.Hosting


Host.CreateDefaultBuilder()
  .RunConsoleAsync();

By default, we get dependency injection, support for configuration files, and logging. Note that the RunConsoleAsync is on the builder, not after "Build()" method. Not sure why, but this is what works. But how does the code actually run? We need to setup our services first:

Host.CreateDefaultBuilder()
  .ConfigureServices((b, c) =>
  {
    c.AddHostedService<PingerService>();
  })
  .RunConsoleAsync();

By adding a hosted service (which has to derive from IHostedService), you get start and stop methods to handle the code that your project is actually doing:

  internal class PingerService : IHostedService
  {
    private readonly ILogger<PingerService> _logger;

    public PingerService(ILogger<PingerService> logger)
    {
      _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
      return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
      return Task.CompletedTask;
    }
}

Because we're using RunConsoleAsync it will close the console app after all hosted services are complete. The rest of this is just plumbing that you don't have to worry about. But I wasn't finished. I wanted to handle the console command parsing too. To do this, I'm using the CommandLineParser:

> dotnet add package CommandLineParser 

With the parser, you can just use the parser to take the arguments and turn them into the options. This starts by creating an object that describes the options (I'm using it for a very simple case):

public class Options
{
  [Value(0, MetaName = "first", Required = true, HelpText = "Starting IP Address (or DNS Name)")]
  public string FirstAddress { get; set; }

  [Value(1, MetaName = "last", Required = false, HelpText = "Ending IP Address (or DNS Name)")]
  public string LastAddress { get; set; }

  [Option('r', "repeats", Required = false, HelpText = "Number of times to repeat the pings")]
  public int Repeats { get; set; } = 1;
}

In this case, I just want a start and ending IP address and optionally specify if we should ping each IP multiple times. Then I can use the parser in the program.cs:

Parser.Default.ParseArguments<Options>(args)
  .WithParsed(options =>
  {
      Host.CreateDefaultBuilder()
        .ConfigureServices((b, c) =>
        {
          c.AddSingleton(options);
          c.AddHostedService<PingerService>();
        })
        .RunConsoleAsync();
  });

This just parses the options and if it's not a good set of options, it just displays the help text. Otherwise, it executes the WithParsed lambda function to do the other work. I'm adding the options object as a singleton so that the PingerService (or anything that uses it) can ask for the Options object as a dependency.

That's really it. The example is a little bit more complex than that. You can view all the code (or fork it) at:

https://github.com/shawnwildermuth/pinger

If you think there is something I'm missing, please reply in the comments. I want to have a good example to show students.

Creative Commons License

      This work by [Shawn Wildermuth](http://wildermuth.com) is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License](http://creativecommons.org/licenses/by-nc-nd/3.0/).  
      Based on a work at [wildermuth.com](http://wildermuth.com).

If you liked this article, see Shawn's courses on Pluralsight.

Posted on by:

shawnwildermuth profile

Shawn Wildermuth

@shawnwildermuth

Shawn Wildermuth has been tinkering with computers and software since he got a Vic-20 back in the early ‘80s. He has been a Microsoft MVP, Pluralsight Author, and filmmaker.

Discussion

markdown guide