Book Image

C# 10 and .NET 6 – Modern Cross-Platform Development - Sixth Edition

By : Mark J. Price
5 (1)
Book Image

C# 10 and .NET 6 – Modern Cross-Platform Development - Sixth Edition

5 (1)
By: Mark J. Price

Overview of this book

Extensively revised to accommodate all the latest features that come with C# 10 and .NET 6, this latest edition of our comprehensive guide will get you coding in C# with confidence. You’ll learn object-oriented programming, writing, testing, and debugging functions, implementing interfaces, and inheriting classes. The book covers the .NET APIs for performing tasks like managing and querying data, monitoring and improving performance, and working with the filesystem, async streams, and serialization. You’ll build and deploy cross-platform apps, such as websites and services using ASP.NET Core. Instead of distracting you with unnecessary application code, the first twelve chapters will teach you about C# language constructs and many of the .NET libraries through simple console applications. In later chapters, having mastered the basics, you’ll then build practical applications and services using ASP.NET Core, the Model-View-Controller (MVC) pattern, and Blazor.
Table of Contents (20 chapters)
19
Index

Exploring more about console applications

We have already created and used basic console applications, but we're now at a stage where we should delve into them more deeply.

Console applications are text-based and are run at the command line. They typically perform simple tasks that need to be scripted, such as compiling a file or encrypting a section of a configuration file.

Equally, they can also have arguments passed to them to control their behavior.

An example of this would be to create a new console app using the F# language with a specified name instead of using the name of the current folder, as shown in the following command line:

dotnet new console -lang "F#" --name "ExploringConsole"

Displaying output to the user

The two most common tasks that a console application performs are writing and reading data. We have already been using the WriteLine method to output, but if we didn't want a carriage return at the end of the lines, we could have used the Write method.

Formatting using numbered positional arguments

One way of generating formatted strings is to use numbered positional arguments.

This feature is supported by methods like Write and WriteLine, and for methods that do not support the feature, the string parameter can be formatted using the Format method of string.

The first few code examples in this section will work with a .NET Interactive notebook because they are about outputting to the console. Later in this section, you will learn about getting input via the console and sadly notebooks do not support this.

Let's begin formatting:

  1. Use your preferred code editor to add a new Console Application to the Chapter02 workspace/solution named Formatting.
  2. In Visual Studio Code, select Formatting as the active OmniSharp project.
  3. In Program.cs, type statements to declare some number variables and write them to the console, as shown in the following code:
    int numberOfApples = 12; 
    decimal pricePerApple = 0.35M;
    Console.WriteLine(
      format: "{0} apples costs {1:C}", 
      arg0: numberOfApples,
      arg1: pricePerApple * numberOfApples);
    string formatted = string.Format(
      format: "{0} apples costs {1:C}",
      arg0: numberOfApples,
      arg1: pricePerApple * numberOfApples);
    //WriteToFile(formatted); // writes the string into a file
    

The WriteToFile method is a nonexistent method used to illustrate the idea.

Good Practice: Once you become more comfortable with formatting strings, you should stop naming the parameters, for example, stop using format:, arg0:, and arg1:. The preceding code uses a non-canonical style to show where the 0 and 1 came from while you are learning.

Formatting using interpolated strings

C# 6.0 and later have a handy feature named interpolated strings. A string prefixed with $ can use curly braces around the name of a variable or expression to output the current value of that variable or expression at that position in the string, as the following shows:

  1. Enter a statement at the bottom of the Program.cs file, as shown in the following code:
    Console.WriteLine($"{numberOfApples} apples costs {pricePerApple * numberOfApples:C}");
    
  2. Run the code and view the result, as shown in the following partial output:
     12 apples costs £4.20
    

For short, formatted string values, an interpolated string can be easier for people to read. But for code examples in a book, where lines need to wrap over multiple lines, this can be tricky. For many of the code examples in this book, I will use numbered positional arguments.

Another reason to avoid interpolated strings is that they can't be read from resource files to be localized.

Before C# 10, string constants could only be combined by using concatenation, as shown in the following code:

private const string firstname = "Omar";
private const string lastname = "Rudberg";
private const string fullname = firstname + " " + lastname;

With C# 10, interpolated strings can now be used, as shown in the following code:

private const string fullname = $"{firstname} {lastname}";

This only works for combining string constant values. It cannot work with other types like numbers that would require runtime data type conversions.

Understanding format strings

A variable or expression can be formatted using a format string after a comma or colon.

An N0 format string means a number with a thousand separators and no decimal places, while a C format string means currency. The currency format will be determined by the current thread.

For instance, if you run this code on a PC in the UK, you'll get pounds sterling with commas as the thousand separators, but if you run this code on a PC in Germany, you will get euros with dots as the thousand separators.

The full syntax of a format item is:

{ index [, alignment ] [ : formatString ] }

Each format item can have an alignment, which is useful when outputting tables of values, some of which might need to be left- or right-aligned within a width of characters. Alignment values are integers. Positive integers mean right-aligned and negative integers mean left-aligned.

For example, to output a table of fruit and how many of each there are, we might want to left-align the names within a column of 10 characters and right-align the counts formatted as numbers with zero decimal places within a column of six characters:

  1. At the bottom of Program.cs, enter the following statements:
    string applesText = "Apples"; 
    int applesCount = 1234;
    string bananasText = "Bananas"; 
    int bananasCount = 56789;
    Console.WriteLine(
      format: "{0,-10} {1,6}",
      arg0: "Name",
      arg1: "Count");
    Console.WriteLine(
      format: "{0,-10} {1,6:N0}",
      arg0: applesText,
      arg1: applesCount);
    Console.WriteLine(
      format: "{0,-10} {1,6:N0}",
      arg0: bananasText,
      arg1: bananasCount);
    
  2. Run the code and note the effect of the alignment and number format, as shown in the following output:
    Name          Count
    Apples        1,234
    Bananas      56,789
    

Getting text input from the user

We can get text input from the user using the ReadLine method. This method waits for the user to type some text, then as soon as the user presses Enter, whatever the user has typed is returned as a string value.

Good Practice: If you are using a .NET Interactive notebook for this section, then note that it does not support reading input from the console using Console.ReadLine(). Instead, you must set literal values, as shown in the following code: string? firstName = "Gary";. This is often quicker to experiment with because you can simply change the literal string value and click the Execute Cell button instead of having to restart a console app each time you want to enter a different string value.

Let's get input from the user:

  1. Type statements to ask the user for their name and age and then output what they entered, as shown in the following code:
    Console.Write("Type your first name and press ENTER: "); 
    string? firstName = Console.ReadLine();
    Console.Write("Type your age and press ENTER: "); 
    string? age = Console.ReadLine();
    Console.WriteLine(
      $"Hello {firstName}, you look good for {age}.");
    
  2. Run the code, and then enter a name and age, as shown in the following output:
    Type your name and press ENTER: Gary 
    Type your age and press ENTER: 34 
    Hello Gary, you look good for 34.
    

The question marks at the end of the string? data type declaration indicate that we acknowledge that a null (empty) value could be returned from the call to ReadLine. You will learn more about this in Chapter 6, Implementing Interfaces and Inheriting Classes.

Simplifying the usage of the console

In C# 6.0 and later, the using statement can be used not only to import a namespace but also to further simplify our code by importing a static class. Then, we won't need to enter the Console type name throughout our code. You can use your code editor's find and replace feature to remove the times we have previously written Console:

  1. At the top of the Program.cs file, add a statement to statically import the System.Console class, as shown in the following code:
    using static System.Console;
    
  2. Select the first Console. in your code, ensuring that you select the dot after the word Console too.
  3. In Visual Studio, navigate to Edit | Find and Replace | Quick Replace, or in Visual Studio Code, navigate to Edit | Replace, and note that an overlay dialog appears ready for you to enter what you would like to replace Console. with, as shown in Figure 2.5:

    Figure 2.5: Using the Replace feature in Visual Studio to simplify your code

  4. Leave the replace box empty, click on the Replace all button (the second of the two buttons to the right of the replace box), and then close the replace box by clicking on the cross in its top-right corner.

Getting key input from the user

We can get key input from the user using the ReadKey method. This method waits for the user to press a key or key combination that is then returned as a ConsoleKeyInfo value.

You will not be able to execute the call to the ReadKey method using a .NET Interactive notebook, but if you have created a console application, then let's explore reading key presses:

  1. Type statements to ask the user to press any key combination and then output information about it, as shown in the following code:
    Write("Press any key combination: "); 
    ConsoleKeyInfo key = ReadKey(); 
    WriteLine();
    WriteLine("Key: {0}, Char: {1}, Modifiers: {2}",
      arg0: key.Key, 
      arg1: key.KeyChar,
      arg2: key.Modifiers);
    
  2. Run the code, press the K key, and note the result, as shown in the following output:
    Press any key combination: k 
    Key: K, Char: k, Modifiers: 0
    
  3. Run the code, hold down Shift and press the K key, and note the result, as shown in the following output:
    Press any key combination: K  
    Key: K, Char: K, Modifiers: Shift
    
  4. Run the code, press the F12 key, and note the result, as shown in the following output:
    Press any key combination: 
    Key: F12, Char: , Modifiers: 0
    

When running a console application in a terminal within Visual Studio Code, some keyboard combinations will be captured by the code editor or operating system before they can be processed by your app.

Passing arguments to a console app

You might have been wondering how to get any arguments that might be passed to a console application.

In every version of .NET prior to version 6.0, the console application project template made it obvious, as shown in the following code:

using System;
namespace Arguments
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello World!");
    }
  }
}

The string[] args arguments are declared and passed in the Main method of the Program class. They're an array used to pass arguments into a console application. But in top-level programs, as used by the console application project template in .NET 6.0 and later, the Program class and its Main method are hidden, along with the declaration of the args string array. The trick is that you must know it still exists.

Command-line arguments are separated by spaces. Other characters like hyphens and colons are treated as part of an argument value.

To include spaces in an argument value, enclose the argument value in single or double quotes.

Imagine that we want to be able to enter the names of some colors for the foreground and background, and the dimensions of the terminal window at the command line. We would be able to read the colors and numbers by reading them from the args array, which is always passed into the Main method aka the entry point of a console application:

  1. Use your preferred code editor to add a new Console Application to the Chapter02 workspace/solution named Arguments. You will not be able to use a .NET Interactive notebook because you cannot pass arguments to a notebook.
  2. In Visual Studio Code, select Arguments as the active OmniSharp project.
  3. Add a statement to statically import the System.Console type and a statement to output the number of arguments passed to the application, as shown in the following code:
    using static System.Console;
    WriteLine($"There are {args.Length} arguments.");
    

    Good Practice: Remember to statically import the System.Console type in all future projects to simplify your code, as these instructions will not be repeated every time.

  4. Run the code and view the result, as shown in the following output:
    There are 0 arguments.
    
  5. If you are using Visual Studio, then navigate to Project | Arguments Properties, select the Debug tab, and in the Application arguments box, enter some arguments, save the changes, and then run the console application, as shown in Figure 2.6:
    Graphical user interface, text, application

Description automatically generated

    Figure 2.6: Entering application arguments in Visual Studio project properties

  6. If you are using Visual Studio Code, then in a terminal, enter some arguments after the dotnet run command, as shown in the following command line:
    dotnet run firstarg second-arg third:arg "fourth arg"
    
  7. Note the result indicates four arguments, as shown in the following output:
    There are 4 arguments.
    
  8. To enumerate or iterate (that is, loop through) the values of those four arguments, add the following statements after outputting the length of the array:
    foreach (string arg in args)
    {
      WriteLine(arg);
    }
    
  9. Run the code again and note the result shows the details of the four arguments, as shown in the following output:
    There are 4 arguments. 
    firstarg
    second-arg 
    third:arg 
    fourth arg
    

Setting options with arguments

We will now use these arguments to allow the user to pick a color for the background, foreground, and cursor size of the output window. The cursor size can be an integer value from 1, meaning a line at the bottom of the cursor cell, up to 100, meaning a percentage of the height of the cursor cell.

The System namespace is already imported so that the compiler knows about the ConsoleColor and Enum types:

  1. Add statements to warn the user if they do not enter three arguments and then parse those arguments and use them to set the color and dimensions of the console window, as shown in the following code:
    if (args.Length < 3)
    {
      WriteLine("You must specify two colors and cursor size, e.g.");
      WriteLine("dotnet run red yellow 50");
      return; // stop running
    }
    ForegroundColor = (ConsoleColor)Enum.Parse(
      enumType: typeof(ConsoleColor),
      value: args[0],
      ignoreCase: true);
    BackgroundColor = (ConsoleColor)Enum.Parse(
      enumType: typeof(ConsoleColor),
      value: args[1],
      ignoreCase: true);
    CursorSize = int.Parse(args[2]);
    

    Setting the CursorSize is only supported on Windows.

  2. In Visual Studio, navigate to Project | Arguments Properties, and change the arguments to: red yellow 50, run the console app, and note the cursor is half the size and the colors have changed in the window, as shown in Figure 2.7:
    Graphical user interface, application, website

Description automatically generated

    Figure 2.7: Setting colors and cursor size on Windows

  3. In Visual Studio Code, run the code with arguments to set the foreground color to red, the background color to yellow, and the cursor size to 50%, as shown in the following command:
    dotnet run red yellow 50
    

    On macOS, you'll see an unhandled exception, as shown in Figure 2.8:

    Graphical user interface, text, application

Description automatically generated

Figure 2.8: An unhandled exception on unsupported macOS

Although the compiler did not give an error or warning, at runtime some API calls may fail on some platforms. Although a console application running on Windows can change its cursor size, on macOS, it cannot, and complains if you try.

Handling platforms that do not support an API

So how do we solve this problem? We can solve this by using an exception handler. You will learn more details about the try-catch statement in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions, so for now, just enter the code:

  1. Modify the code to wrap the lines that change the cursor size in a try statement, as shown in the following code:
    try
    {
      CursorSize = int.Parse(args[2]);
    }
    catch (PlatformNotSupportedException)
    {
      WriteLine("The current platform does not support changing the size of the cursor.");
    }
    
  2. If you were to run the code on macOS then you would see the exception is caught, and a friendlier message is shown to the user.

Another way to handle differences in operating systems is to use the OperatingSystem class in the System namespace, as shown in the following code:

if (OperatingSystem.IsWindows())
{
  // execute code that only works on Windows
}
else if (OperatingSystem.IsWindowsVersionAtLeast(major: 10))
{
  // execute code that only works on Windows 10 or later
}
else if (OperatingSystem.IsIOSVersionAtLeast(major: 14, minor: 5))
{
  // execute code that only works on iOS 14.5 or later
}
else if (OperatingSystem.IsBrowser())
{
  // execute code that only works in the browser with Blazor
}

The OperatingSystem class has equivalent methods for other common operating systems like Android, iOS, Linux, macOS, and even the browser, which is useful for Blazor web components.

A third way to handle different platforms is to use conditional compilation statements.

There are four preprocessor directives that control conditional compilation: #if, #elif, #else, and #endif.

You define symbols using #define, as shown in the following code:

#define MYSYMBOL

Many symbols are automatically defined for you, as shown in the following table:

Target Framework

Symbols

.NET Standard

NETSTANDARD2_0, NETSTANDARD2_1, and so on

Modern .NET

NET6_0, NET6_0_ANDROID, NET6_0_IOS, NET6_0_WINDOWS, and so on

You can then write statements that will compile only for the specified platforms, as shown in the following code:

#if NET6_0_ANDROID
// compile statements that only works on Android
#elif NET6_0_IOS
// compile statements that only works on iOS
#else
// compile statements that work everywhere else
#endif