loading...
Cover image for Creating a .NET Tool - Part 1: Feeding the Dragon

Creating a .NET Tool - Part 1: Feeding the Dragon

marcusturewicz profile image Marcus Turewicz ・4 min read

Creating a .NET Tool (3 Part Series)

1) Creating a .NET Tool - Part 1: Feeding the Dragon 2) Creating a .NET Tool - Part 2: Packaging 3) Creating a .NET Tool - Part 3: CI/CD

I've been using .NET Tools more and more recently; once you have .NET on your system, they're so easy to install with just one command line call and are immediately available on the path. But how does one actually create a tool and make it available? I thought I'd do a series on how to authour a tool and make it available for public installation.

Let's jump right in!

Requirements

The tool I'm creating is a simple JSON validator: you point it to a JSON file on disk and it returns whether the file is valid JSON or not. You should also be able to allow comments with a toggle option. Simple!

Project setup

Tools are typically live in a console app, so let's create one:

$ dotnet new console -o src/jsonv -n JsonValidate

Let's now add a library to make parsing command line arguments a breeze:

$ cd src/jsonv
$ dotnet add package System.CommandLine.DragonFruit --version 0.3.0-alpha.20214.1

Seriously: if you haven't seen or heard of System.CommandLine or System.CommandLine.DragonFruit yet, go check it out. It's a new command line parser aimed at unifying multiple unsuccessful previous efforts by various parties at creating command line parsers, bringing a really nice syntax and a helpful set of features. For instance, the DragonFruit variant allows you to supply aguments through the main method! See below.

Now our project is setup, we can configure the command line argument parsing.

Feeding the dragon

As mentioned, with DragonFruit, we can supply command line arguments via the main method. This really minimises the amount of boiler plate code required and works well for small parsers.

using System;

namespace JsonValidate
{
    class Program
    {
        /// <param name="filePath">The absolute path to the JSON file</param>
        /// <param name="allowComments">Option to allow comments in the file</param>
        static void Main(string filePath, bool allowComments = false)
        {
            Console.WriteLine($"File path is: {filePath}, allowing comments: {allowComments}");
        }
    }
}

Here, I've added two arguments along with their XML documentation. DragonFruit will automatically parse the arguments into the main method and will also bundle the XML documentation into a super useful help option. For instance, running the help command gives:

$ dotnet run -- -h
Usage:
  jsonv [options]

Options:
  --file-path <file-path>    filePath
  --allow-comments           allowComments
  --version                  Show version information
  -?, -h, --help             Show help and usage information

And running the program with some arguments:

$ dotnet run -- --file-path /src/jsonv/Program.cs --allow-comments
File path is: /src/jsonv/Program.cs, allowing comments: True

How good is that?! With a few lines of code, we already have a fully documented help command and command line parsing done. Let's continue with the rest of the program.

Eating the fruit

Now the command line arguments are being parsed, we can add the code to validate the file. Let's use System.Text.Json for this:

Seriously: if you haven't seen or heard of System.Text.Json yet, go check it out. It's a new JSON de/serialiser aimed at high performance and is now the default JSON serialiser in ASP.NET Core.

using System;
using System.IO;
using System.Text.Json;

namespace JsonValidate
{
    class Program
    {
        /// <param name="filePath">The absolute path to the JSON file</param>
        /// <param name="allowComments">Option to allow comments in the file</param>
        static void Main(string filePath, bool allowComments = false)
        {
            if (!File.Exists(filePath))
            {
                Console.WriteLine("Error: file does not exist!");
                return;
            }

            Console.WriteLine($"File path is: {filePath}, allowing comments: {allowComments}");                

            try
            {
                var serializerOptions = new JsonSerializerOptions
                {
                    ReadCommentHandling = allowComments ? JsonCommentHandling.Skip : JsonCommentHandling.Disallow
                };
                JsonSerializer.Deserialize<object>(File.ReadAllBytes(filePath), serializerOptions);
                Console.WriteLine("File is valid JSON!");

            }
            catch (Exception e)
            {
                Console.WriteLine("File is NOT valid JSON!");
                Console.WriteLine($"Error: {e.Message}");
            }
        }
    }
}

Yes: this may not be the most efficient way to read the file but it's just a demo.

Running this now on the project file gives:

$ dotnet run -- --file-path /src/jsonv/Program.cs
File path is: /src/jsonv/Program.cs, allowing comments: False
File is NOT valid JSON!
Error: '0xEF' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.

Great, the file was not valid since it is not JSON and we get an error message.

Now, given this JSON file:

{
  // This is a comment
  "sdk": {
    "version": "3.1.201"
  }
}

We can run the following commands to check the JSON handling:

$ dotnet run -- --file-path /global.json
File path is: /global.json, allowing comments: False
File is NOT valid JSON!
Error: '/' is invalid after a value. Expected either ',', '}', or ']'. Path: $ | LineNumber: 1 | BytePositionInLine: 2.

Great, the file was not valid due to not allowing comments.

$ dotnet run -- --file-path /global.json --allow-comments
File path is: /global.json, allowing comments: True
File is valid JSON!

Great, the file was valid due to allowing comments.

Summary

In this post, we created a simple JSON validator console app with help from System.CommandLine.DragonFruit. This library gets you from zero to complete command line app in minutes. I would really recommend giving it a go, and also it's more stable sibling System.CommandLine. In the next post, we'll look at how to package up this console app as a .NET Tool and publish it to NuGet. Stay tuned!

Resources

Creating a .NET Tool (3 Part Series)

1) Creating a .NET Tool - Part 1: Feeding the Dragon 2) Creating a .NET Tool - Part 2: Packaging 3) Creating a .NET Tool - Part 3: CI/CD

Posted on by:

marcusturewicz profile

Marcus Turewicz

@marcusturewicz

Machine Learning Engineer, bassist, soccer player, .NET fan, native of the cloud and renewable energy advocate.

Discussion

markdown guide