Introduction
There are many uses for console projects that range from learning to code, utility and real applications. A standard console project requires a decent amount of code to collect information and to present information with colors.
The information here will assist with collecting user input, confirmation, presenting a generic list, colorizing json and exceptions. Bogus will be used to generate mock-up data and FluentValidation for validation of data to be inserted into an SQLite database using Dapper.
The focus will be on Spectre.Console which for most of the code has been placed into a class project. Within the class project the class SpectreConsoleHelpers has been setup as a partial class to separate various methods with useful names.
Spectre.Console documentation
Either before or after reviewing and running provided code, take time to view the documentation and clone the samples repository to better understand what is covered and not covered here.
Recommendations
Take time to review all code and if new to writing code, set breakpoints to step through code using the local window to examine values.
Also, no matter your level of experience there will be something in the provided source code to learn from.
Environment
- Microsoft Visual Studio 2022 - 17.11.4
- NET 8
- C#
- GlobalUsings.cs is used in both projects.
How ask a question
To ask for confirmation before continuing an operation code such as the following is used taken from this post.
bool confirmed = false;
string Key;
do {
Console.Write("Please enter a login key: ");
Key = Console.ReadLine();
Console.WriteLine("You entered, " + Key + " as your login key!");
ConsoleKey response;
do
{
Console.Write("Are you sure you want to choose this as your login key? [y/n] ");
response = Console.ReadKey(false).Key; // true is intercept key (dont show), false is show
if (response != ConsoleKey.Enter)
Console.WriteLine();
} while (response != ConsoleKey.Y && response != ConsoleKey.N);
confirmed = response == ConsoleKey.Y;
} while (!confirmed);
Console.WriteLine("You chose {0}!", Key);
Console.ReadLine();
With Spectre.Console, there is a method for asking a user for confirmation which in the sample below the method is wrapped in a reusable method that has colors set and a property for displaying a message when y/n or Y/N is not used. Also, the characters can be set to say ½ rather than y/n.
public static bool Question(string questionText)
{
ConfirmationPrompt prompt = new($"[{Color.Yellow}]{questionText}[/]")
{
DefaultValueStyle = new(Color.Cyan1, Color.Black, Decoration.None),
ChoicesStyle = new(Color.Yellow, Color.Black, Decoration.None),
InvalidChoiceMessage = $"[{Color.Red}]Invalid choice[/]. Please select a Y or N.",
};
return prompt.Show(AnsiConsole.Console);
}
This overload provides parameters to dynamically set colors.
public static bool Question(string questionText, Color foreGround, Color backGround)
{
ConfirmationPrompt prompt = new($"[{foreGround} on {backGround}]{questionText}[/]")
{
DefaultValueStyle = new(foreGround, backGround, Decoration.None),
ChoicesStyle = new(foreGround, backGround, Decoration.None),
InvalidChoiceMessage = $"[{Color.Red}]Invalid choice[/]. Please select a Y or N."
};
return prompt.Show(AnsiConsole.Console);
}
Usage
public static void PlainAskQuestion()
{
if (Question("Continue with backing up database?"))
{
// backup database
}
else
{
// do nothing
}
}
Exiting an application
Usually done with Console.ReadLine();
, with Spectre.Console the following can be using the following.
public static void ExitPrompt()
{
Console.WriteLine();
Render(new Rule($"[yellow]Press[/] [cyan]ENTER[/] [yellow]to exit the demo[/]")
.RuleStyle(Style.Parse("silver")).LeftJustified());
Console.ReadLine();
}
private static void Render(Rule rule)
{
AnsiConsole.Write(rule);
AnsiConsole.WriteLine();
}
Usage
using static SpectreConsoleLibrary.SpectreConsoleHelpers;
internal partial class Program
{
static void Main(string[] args)
{
ExitPrompt();
}
}
Getting user information
Many novice developers will ask for information as follows.
string firstName = Console.ReadLine();
string lastName = Console.ReadLine();
DateOnly birthDate = DateOnly.Parse(Console.ReadLine()!);
The code is fine if the user enters the correct information but lets say the birthdate is not a DateOnly the program crashes. Next a developer may try using the following but that creates a new problem, how to ask for the date again cleanly?
string firstName = Console.ReadLine();
string lastName = Console.ReadLine();
string birthDate = Console.ReadLine();
if (DateOnly.TryParse(birthDate, out var bd))
{
// date is valid
}
else
{
// date is not valid
}
With Spectre.Console, the following colorizes the prompt using private fields, is for returning a DateOnly and provides validation, in this case the year entered can’t before 1900. Note the validation is optional. If input is incorrect, a error message appears and re-ask for the DateOnly.
public static DateOnly GetBirthDate(string prompt = "Enter your birth date")
{
/*
* doubtful there is a birthday for the current person
* but if checking say a parent or grandparent this will not allow before 1900
*/
const int minYear = 1900;
return AnsiConsole.Prompt(
new TextPrompt<DateOnly>($"[{_promptColor}]{prompt}[/]:")
.PromptStyle(_promptStyle)
.ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Please enter a valid date or press ENTER to not enter a date[/]")
.Validate(dt => dt.Year switch
{
<= minYear => ValidationResult.Error($"[{_errorForeGround} on {_errorBackGround}]Year must be greater than {minYear}[/]"),
_ => ValidationResult.Success(),
})
.AllowEmpty());
}
Usage
Shown with two methods to collect string values
var firstName = SpectreConsoleHelpers.FirstName("First name");
var lastName = SpectreConsoleHelpers.LastName("Last name");
var birthDate = SpectreConsoleHelpers.GetBirthDate("Birth date");
Person person = new()
{
FirstName = firstName,
LastName = lastName,
BirthDate = birthDate
};
For FirstName, uses same colors as GetBirthDate and for demonstration does minor validation and has a custom error message for failing validation and on failing will re-ask for the first name.
public static string FirstName(string prompt = "First name") =>
AnsiConsole.Prompt(
new TextPrompt<string>($"[{_promptColor}]{prompt}[/]")
.PromptStyle(_promptStyle)
.Validate(value => value.Length switch
{
< 3 => ValidationResult.Error("[red]Must have at least three characters[/]"),
_ => ValidationResult.Success(),
})
.ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Please enter your first name[/]"));
The method to get last name validation is simple, the user must not leave the value empty.
public static string LastName(string prompt = "Last name") =>
AnsiConsole.Prompt(
new TextPrompt<string>($"[{_promptColor}]{prompt}[/]?")
.PromptStyle(_promptStyle)
.ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Please enter your last name[/]"));
Simple inputs
Here is an example to get an int were the method name is generic for demonstration only.
public static int GetInt(string prompt) =>
AnsiConsole.Prompt(
new TextPrompt<int>($"[{_promptColor}]{prompt}[/]")
.PromptStyle(_promptStyle)
.DefaultValue(1)
.DefaultValueStyle(new(_promptColor, Color.Black, Decoration.None)));
Generic inputs
We can use generics that T represents the type, a prompt to present the user with and a default value.
public static T Get<T>(string prompt, T defaultValue) =>
AnsiConsole.Prompt(
new TextPrompt<T>($"[{_promptColor}]{prompt}[/]")
.PromptStyle(_promptStyle)
.DefaultValueStyle(new(_promptStyle))
.DefaultValue(defaultValue)
.ValidationErrorMessage($"[{_errorForeGround} on {_errorBackGround}]Invalid entry![/]"));
Usage
string firstName = SpectreConsoleHelpers.Get<string?>("First name", null);
var lastName = SpectreConsoleHelpers.Get<string?>("Last name", null);
var birthDate = SpectreConsoleHelpers.Get<DateOnly?>("Birth date", null);
Colorizing runtime exceptions
With Spectre.Console, AnsiConsole.WriteException writes an exception to the console. If a developer does not care for the default colors they can be customized as shown below done in a language extension.
public static class ExceptionHelpers
{
/// <summary>
/// Provides colorful exception messages in cyan and fuchsia
/// </summary>
/// <param name="exception"><see cref="Exception"/></param>
public static void ColorWithCyanFuchsia(this Exception exception)
{
AnsiConsole.WriteException(exception, new ExceptionSettings
{
Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
Style = new ExceptionStyle
{
Exception = new Style().Foreground(Color.Grey),
Message = new Style().Foreground(Color.DarkSeaGreen),
NonEmphasized = new Style().Foreground(Color.Cornsilk1),
Parenthesis = new Style().Foreground(Color.Cornsilk1),
Method = new Style().Foreground(Color.Fuchsia),
ParameterName = new Style().Foreground(Color.Cornsilk1),
ParameterType = new Style().Foreground(Color.Aqua),
Path = new Style().Foreground(Color.Red),
LineNumber = new Style().Foreground(Color.Cornsilk1),
}
});
}
/// <summary>
/// Provides a colorful exception message
/// </summary>
/// <param name="exception"><see cref="Exception"/></param>
public static void ColorStandard(this Exception exception)
{
AnsiConsole.WriteException(exception, new ExceptionSettings
{
Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
Style = new ExceptionStyle
{
Exception = new Style().Foreground(Color.Grey),
Message = new Style().Foreground(Color.White),
NonEmphasized = new Style().Foreground(Color.Cornsilk1),
Parenthesis = new Style().Foreground(Color.GreenYellow),
Method = new Style().Foreground(Color.DarkOrange),
ParameterName = new Style().Foreground(Color.Cornsilk1),
ParameterType = new Style().Foreground(Color.Aqua),
Path = new Style().Foreground(Color.White),
LineNumber = new Style().Foreground(Color.Cornsilk1),
}
});
}
}
Usage
public static void ShowExceptionExample()
{
AnsiConsole.Clear();
SpectreConsoleHelpers.PrintHeader();
try
{
// Create the InvalidOperationException with the inner exception
throw new InvalidOperationException("Operation cannot be performed",
new ArgumentException("Invalid argument"));
}
catch (Exception ex)
{
ex.ColorWithCyanFuchsia();
}
}
Colorized json
This is done with the following method which in this case overrides the default colors.
Requires Spectre.Console.Json NuGet package.
public partial class SpectreConsoleHelpers
{
public static void WriteOutJson(string json)
{
AnsiConsole.Write(
new JsonText(json)
.BracesColor(Color.Red)
.BracketColor(Color.Green)
.ColonColor(Color.Blue)
.CommaColor(Color.Red)
.StringColor(Color.Green)
.NumberColor(Color.Blue)
.BooleanColor(Color.Red)
.MemberColor(Color.Wheat1)
.NullColor(Color.Green));
}
}
Usage with mocked json
static void DisplayJsonExample()
{
AnsiConsole.Clear();
SpectreConsoleHelpers.PrintHeader();
SpectreConsoleHelpers.WriteOutJson(Json);
}
public static string Json =>
/*lang=json*/
"""
[
{
"FirstName": "Jose",
"LastName": "Fernandez",
"BirthDate": "1985-01-01"
},
{
"FirstName": "Miguel",
"LastName": "Lopez",
"BirthDate": "1970-12-04"
},
{
"FirstName": "Angel",
"LastName": "Perez",
"BirthDate": "1980-09-11"
}
]
""";
Showing progress
Using the Status class provides a way to show progress of one or more operations.
Stock example
private static void SpinnerSample()
{
AnsiConsole.Status()
.Start("Thinking...", ctx =>
{
// Simulate some work
AnsiConsole.MarkupLine("Doing some work...");
Thread.Sleep(1000);
// Update the status and spinner
ctx.Status("Thinking some more");
ctx.Spinner(Spinner.Known.Star);
ctx.SpinnerStyle(Style.Parse("green"));
// Simulate some work
AnsiConsole.MarkupLine("Doing some more work...");
Thread.Sleep(2000);
});
}
A realistic example using EF Core to recreate a database.
public static class EntityExtensions
{
/// <summary>
/// Recreate database with spinner
/// </summary>
public static void CleanStart(this DbContext context)
{
AnsiConsole.Status()
.Start("Recreating database...", ctx =>
{
Thread.Sleep(500);
ctx.Spinner(Spinner.Known.Star);
ctx.SpinnerStyle(Style.Parse("cyan"));
ctx.Status("Removing");
context.Database.EnsureDeleted();
ctx.Status("Creating");
context.Database.EnsureCreated();
});
}
}
Recording actions
To record information entered by users start with AnsiConsole.Record()
and save to HTML file via await File.WriteAllTextAsync("recorded.html", AnsiConsole.ExportHtml());
which can be helpful in figuring out an issue while creating an application.
Other options
- AnsiConsole.ExportText()
- AnsiConsole.ExportCustom()
Summary
With information and provide code sample for Spectre.Console a developer can write more robust console projects.