Console applications in C#
Tags: csharp, beginners, codenewbie, dotnet
Published: 2023-01-01 20:01:04

Console applications have been around for a long time and still hold a place in todays world were web applications are predominate type of applications.

They are good for
- Learning how to code
- Create useful utilities
- Used as [CLI tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools-how-to-create)

Full [source code](https://github.com/karenpayneoregon/console-apps) with 84 projects.

For the novice developer they tend to struggle with basic input and creation of menus let alone working with the file system and databases.

Suppose a developer wants to obtain a number from the user, the following is generally used.

```csharp
int input = 0;
try
{
    input = Convert.ToInt32(Console.ReadLine());
}
catch
{
    //code here
}
```
They do not consider what if a user enters a non-numeric value. This means assertion is required, in this case using int.TryParse or int.TryParse etc.

```csharp
var userInput = Console.ReadLine();
if (int.TryParse(userInput, out var input))
{
    Console.WriteLine($"Value from ReadLine: {input}");
}
else
{
    Console.WriteLine($"'{userInput}' can not represent a int");
}
```
Let's look at an even better way to capture numeric input using a open source library, [Spectre.Console](https://spectreconsole.net/).

```csharp
public static int GetInt() =>
    AnsiConsole.Prompt(
        new TextPrompt<int>("Enter a [green]number[/] between [b]1[/] and [b]10[/]")
            .PromptStyle("green"));
```
Usage

```csharp
int result = Prompts.GetInt();
```
If the wrong type is entered a standard message appears and prompts for a value again. Suppose the number must be in a range, we can add validation as per below with a range of 1-10.

```csharp
public static int GetInt() =>
    AnsiConsole.Prompt(
        new TextPrompt<int>("Enter a [green]number[/] between [b]1[/] and [b]10[/]")
            .PromptStyle("green")
            .ValidationErrorMessage("[red]That's not a valid age[/]")
            .Validate(age => age switch
            {
                <= 0 => ValidationResult.Error("[red]1 is min value[/]"),
                >= 10 => ValidationResult.Error("[red]10 is max value[/]"),
                _ => ValidationResult.Success(),
            }));
```
What about DateTime? The following uses the same pattern as above but typed to DateTime with specific validation along with allowing the user not to enter a date.

```csharp
public static DateTime? GetBirthDate() =>
    AnsiConsole.Prompt(
        new TextPrompt<DateTime>("What is your [white]birth date[/]?")
            .PromptStyle("yellow")
            .ValidationErrorMessage("[red]Please enter a valid date or press ENTER to not enter a date[/]")
            .Validate(dateTime => dateTime.Year switch
            {
                  >= 2001 => ValidationResult.Error("[red]Must be less than 2001[/]"),
                _ => ValidationResult.Success(),
            })
            .AllowEmpty());
```
To kept code consistent use a similar method for strings.

```csharp
public static string GetFirstName() =>
    AnsiConsole.Prompt(
        new TextPrompt<string>("[white]First name[/]?")
            .PromptStyle("yellow")
            .AllowEmpty()
            .Validate(value => value.Trim().Length switch
            {
                < 3 => ValidateFirstName(),
                _ => ValidationResult.Success(),
            })
            .ValidationErrorMessage("[red]Please enter your first name[/]"));
```
**Screen shot**

![example screenshot for inputs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tkj50i9710fgf3ladkcl.png)

Menu systems are more of a challenge when using standard code, Spectre.Console to the rescue.

Simple example


![example menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/982uep42w92rx776z7n9.png)

A model for creating a menu

```csharp
public class MenuItem
{
    public int Id { get; set; }
    public string Text { get; set; }
    public override string ToString() => Text;
}
```
Create the menu

```csharp
public class MenuOperations
{
    private static Style HighLightStyle => new(
        Color.LightGreen,
        Color.Black,
        Decoration.None);

    public static SelectionPrompt<MenuItem> MainMenu()
    {

        SelectionPrompt<MenuItem> menu = new()
        {
            HighlightStyle = HighLightStyle
        };

        menu.Title("Select an [B]option[/]");
        menu.AddChoices(new List<MenuItem>()
        {
            new() { Id =  0,  Text = "List employees"},
            new() { Id =  1,  Text = "Add manager"},
            new() { Id =  2,  Text = "Add engineer"},
            new() { Id =  3,  Text = "Add employee"},
            new() { Id =  4,  Text = "Edit an employee "},
            new() { Id =  5,  Text = "Delete an employee "},
            new() { Id =  6,  Text = "Save all"},
            new() { Id = -1, Text = "Exit"},
        });

        return menu;
    }

    public static SelectionPrompt<Employee> RemoveMenu(List<Employee> list)
    {

        SelectionPrompt<Employee> menu = new()
        {
            HighlightStyle = HighLightStyle
        };

        menu.Title("Select an employee to [B]remove[/] or select [B]return to menu[/] to abort.");
        menu.AddChoices(list);

        return menu;
    }
}
```
Then in Main method of the project setup the menu in a loop.

```csharp
using MenuConsoleAppBasic.Classes;
using MenuConsoleAppBasic.Models;
using Spectre.Console;

namespace MenuConsoleAppBasic;

partial class Program
{
    static void Main(string[] args)
    {
        MenuItem menuItem = new MenuItem();

        List<Employee> employeesList = Operations.ReadEmployees();

        while (menuItem.Id > -1)
        {
            AnsiConsole.Clear();
            AnsiConsole.Write(
                new Panel(new Text("Some fictitious company HR").Centered())
                    .Expand()
                    .SquareBorder()
                    .BorderStyle(new Style(Color.Cornsilk1))
                    .Header("[LightGreen]About[/]")
                    .HeaderAlignment(Justify.Center));
            menuItem = AnsiConsole.Prompt(MenuOperations.MainMenu());
            Selection(menuItem, employeesList);
        }

    }
}
```
Displaying data
Suppose there is a need to display data, Spectre.Console has the ability to display tubular data.

![displaying tubular data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6bd7b87t9femngh69ma.png)

# My NuGet package

[SpectreConsoleLibrary](https://www.nuget.org/packages/SpectreConsoleLibrary/) contains useful methods for working with console projects. [GitHub repository](https://github.com/karenpayneoregon/console-apps/tree/master/SpectreConsoleLibrary)

# Summary
By using open source library like Spectre.Console creating useful console applications easy. Spectre.Console also makes it easy to create dotnet tools, see [documentation](https://spectreconsole.net/cli/getting-started) and check out their [GitHub repository](https://github.com/spectreconsole/spectre.console). 

All source code for what has been presented and more can be [found here](https://github.com/karenpayneoregon/console-apps).

**Note** The tile image source code is provided in the [source code](https://github.com/karenpayneoregon/console-apps)