Skip to content

1 - CLI

.NET implementation

A .NET app is developed for one or more implementations of .NET. Implementations of .NET include .NET Framework, .NET 5+ (and .NET Core), and Mono [1].

Each implementation of .NET includes the following components:

  • Runtime(s)
  • Class library - e.g., the .NET 8 Base Class Library
  • Application Frameworks (e.g., ASP.NET, Windows Forms)

Currently there are three main .NET implementations:

  • .NET (Core)
  • .NET Framework
  • Mono

We will create several .NET applications in the following weeks. The .NET 10.0 LTS (Long Term Support) is available on the workstations in the cabinet, and the same version will be used in automatic testing (project evaluation).

Calculator

Our first application should be a simple calculator that takes its inputs from the console and prints the results.

  • Input: expression in OP ARG1 ARG2 format
  • OP: +, -, *, /
  • ARG: integers, floats and new expressions are accepted (e.g., * 3 + 4 5.5 means result = 3 * (4 + 5.5))

Project creation

First, a new project is needed:

Create a new project
Create a new project.

The IDE offers several project templates in the next stage, based on the installed modules. Now we would like to create a Console App. The tags show that the project template uses C# language and would run on Windows, Linux and MacOS (not all project templates share the same cross-platform compatibility, a Windows workstation might be helpful through the semester).

Project Templates
Project Templates.

The project name, the desired location and the solution name can be set in the next stage.

A solution is simply a container that Visual Studio uses to organize one or more related projects. When you open a solution, Visual Studio automatically loads all the projects that the solution contains [4]. We will exploit this functionality from Practice 2.

Project Name, Location, and Solution Name
Project Name, Location, and Solution Name.

Some additional parameters can be set in the last stage.

The installed .NET versions are available for the project Framework. Only those versions are listed here that are compatible with the current project, i.e., the "old" .NET Framework versions are not listed for a cross-platform template.

Do not use top-level statements checkbox: if checked, the generated source file would include the namespace, the class and the Main function. Top-level statements allow us to write executable code directly at the root of a file, eliminating the need for wrapping the code in a class or method. In this case, the compiler generates a Program class with an entry point method for the application. The name of the generated method isn't Main, it's an implementation detail that the code can't reference directly [14].

Additional Information
Additional Information.

Command-line interface

dotnet new - Creates a new project, configuration file, or solution based on the specified template [15].

1
2
3
4
5
6
dotnet new <TEMPLATE> [--dry-run] [--force] [-lang|--language {"C#"|"F#"|VB}]
    [-n|--name <OUTPUT_NAME>] [-f|--framework <FRAMEWORK>] [--no-update-check]
    [-o|--output <OUTPUT_DIRECTORY>] [--project <PROJECT_PATH>]
    [-d|--diagnostics] [--verbosity <LEVEL>] [Template options]

dotnet new -h|--help

First, an empty directory is needed where the project can be created.

1
2
3
cd /path/to/somewhere
mkdir Calculator
cd Calculator

Then the project can be created with the dotnet new <TEMPLATE> command:

1
2
3
4
5
6
7
8
$ dotnet new console --use-program-main
The template "Console App" was created successfully.

Processing post-creation actions...
Restoring /path/to/somewhere/Calculator/Calculator.csproj:
  Determining projects to restore...
  Restored /path/to/somewhere/Calculator/Calculator.csproj (in 32 ms).
Restore succeeded.

The --use-program-main is the equivalent of the Do not use top-level statements checkbox.

Now the build and run commands are interesting. The build compiles the project, while the run first compiles then executes the program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ dotnet build
  Determining projects to restore...
  All projects are up-to-date for restore.
  Calculator -> /path/to/somewhere/Calculator/bin/Debug/net<VERSION>/Calculator.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.48

$ dotnet run
Hello, World!

Structure

When the project is created, the Solution Explorer is on the right-hand side by default. The Copilot tab might cover that in the recent versions of Visual Studio, switching tabs can be done in the bottom of that panel. If closed the Solution Explorer accidentally, View -> Solution Explorer menu can help.

Project Structure
Solution Explorer: Project Structure.

The explorer shows the Solution 'Calculator' (1 of 1 project). A Solution can contain multiple projects and each project can be in loaded or unloaded state. If a project is not loaded, then its code is not reachable.

The Calculator project has some Dependencies and a source file called Program.cs.

Program.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace Calculator
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

The syntax should be familiar from previous courses, the notes will go through all of the keywords in the following weeks.

Build and Run

The easiest way to build a project is to right-click to its name in the Solution Explorer and Build (first option). The process will show the compiler errors (if any) or the shows success:

Build output
1
2
3
4
5
Build started at 14:06...
1>------ Build started: Project: Calculator, Configuration: Debug Any CPU ------
1>  Calculator -> C:\Users\<user>\source\repos\Calculator\Calculator\bin\Debug\net10.0\Calculator.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
========== Build completed at 14:06 and took 07,739 seconds ==========

There is a green arrow next to the project's name below the menu bar, that button runs the project. Even more easier solution is to hit the "F5" button. First the project and its dependencies are compiled then started.

Run a project
Run a project.

Since the current project is a console application, and the requirements says that it should handle CLI arguments, we need to set them inside the IDE. First, the debug properties pop-up needs to be found:

Project Debug Properties
Project Debug Properties.

Several useful variables can be set in this window called Launch Profiles:

  • Command line arguments: texts (separated by space or newline) written here will be the contents of the args array,
  • Working directory: path to the directory where the process will be started,
  • Use remote machine: whether the debugger should attach to a process on a remote machine,
  • Environmental variables: key value pairs set prior to running the application.

Paste the * 3 + 4 5.5 text to the Command line arguments field.

Launch Profiles
Launch Profiles.

If any of these variables are set, a new configuration file will be created in Properties folder, called launchSettings.json

launchSettings.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "profiles": {
        "Calculator": {
            "commandName": "Project",
            "commandLineArgs": "* 3 + 4 5.5\r\n",
            "environmentVariables" {
                "TEST": "24"
            }
        }
    }
}

Also the dotnet run allows passing CLI arguments to the program:

1
dotnet run * 3 + 4 5.5

Command Line Arguments

After we know how to run the application and how to pass arguments to it, it's the time to implement the functionality of the Calculator project.

Program.cs
1
2
3
4
5
static void Main(string[] args) {
    for (int i = 0; i < args.Length; i++)
        Console.Write(args[i] + " ");
}
// output: * 3 + 4 5,5

It would be useful if the user can get some feedback if something is wrong (e.g., wrong number of parameters). Then, a function should evaluate the expressions if this check is fulfilled.

Program.cs
1
2
3
4
5
6
7
8
9
static void Main(string[] args) {
    if (args.Length == 0) {
        Console.WriteLine("Usage: dotnet run <expression>");
        Console.WriteLine("Example: dotnet run * 3 + 4 5.5");
        return;
    }

    Console.WriteLine(Evaluate(args));
}

Expressions

Evaluate function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Member in Program class
static int index = 0;

static float Evaluate(string[] args) {
    if (index >= args.Length)
        throw new ArgumentException("Incomplete expression.");

    string token = args[index++];

    // Check if the token is an operator
    switch (token) {
        case "+":
            return Evaluate(args) + Evaluate(args);
        case "-":
            return Evaluate(args) - Evaluate(args);
        case "*":
            return Evaluate(args) * Evaluate(args);
        case "/":
            return Evaluate(args) / Evaluate(args);
        default:
            // If it's not an operator, assume it's a number
            return float.Parse(token);
    }
}

static keyword

Use the static modifier to declare a static member, which belongs to the type itself rather than to a specific object. The static modifier can be used to declare static classes.

In classes, interfaces, and structs, you may add the static modifier to fields, methods, properties, operators, events, and constructors. The static modifier can't be used with indexers or finalizers. You can add the static modifier to a local function. A static local function can't capture local variables or instance state. [7]

Debugging

The user of the application might make mistakes, therefore, the applications should handle the edge cases. What happens when the expressions are not correct (* 3 + 4) or some numbers are in wrong localization (. vs ,) or replaced by letters (* 3 + 4 S.S)? Exceptions are thrown. It is not so graceful if an application exits with an Exception, they should be handled:

  • with fixing the code, the algorithms and the input handling, or
  • with try-catch blocks
Format Exception
Argument Exception
Possible Exceptions.

TryParse method

Several numeric types have a method called TryParse. It converts the string representation of a number to its number equivalent, and the return value indicates whether the conversion succeeded or failed [8].

Currently floats are used, we need the float.TryParse method.

TryParse
1
2
3
4
5
// string token = "3.14";
if (float.TryParse(token, out float result))
{
    return result;
}

When the method returns, the result variable contains the result of successfully parsing token or an undefined value on failure.

out and ref keywords

out can be used as a parameter modifier, which lets you pass an argument to a method by reference rather than by value. The out keyword is especially useful when a method needs to return more than one value since more than one out parameter can be used.

ref: In a method signature and in a method call, to pass an argument to a method by reference.

TryParse
1
2
3
4
public void Method(ref int refParameter)
{
    refParameter += 42;
}
  • out vs ref: ref needs that the variable must be initialized before it passed to the method, while out parameter doesn't require the variables to be initialized beforehand. But before it returns a value to the calling method, the variable must be initialized in the called one,
  • ref argument values can be changed inside the method,
  • out parameters are not allowed to use in asynchronous methods,
  • out parameters are not allowed to use in iterator methods,
  • There can be more than one out parameter in a method,
  • At the time of method call, out parameter can be declared inline and the inline out parameters can be accessed in the same block of code where it calls,
  • Method overloading can also be done using out parameters,
  • Properties cannot be passed as out parameters as these are not variables. (see later) [11]

With the knowledge of TryParse, the return float.Parse(token) line can be replaced with this more sophisticated technique.

TryParse in Calculator
1
2
3
4
5
6
7
default:
    // If it's not an operator, assume it's a number
    if (float.TryParse(token, out float number))
    {
        return number;
    }
    throw new ArgumentException($"Invalid token: {token}");

string interpolation using $

The $ character identifies a string literal as an interpolated string. An interpolated string is a string literal that might contain interpolation expressions. When an interpolated string is resolved to a result string, the compiler replaces items with interpolation expressions by the string representations of the expression results. String interpolation provides a more readable, convenient syntax to format strings. [12]

String iterpolation
1
2
3
4
5
6
7
    int X = 2;
    int Y = 3;

    var pointMessage = $"""The point "{X}, {Y}" is {Math.Sqrt(X * X + Y * Y):F3} from the origin""";

    Console.WriteLine(pointMessage);
    // The point "2, 3" is 3.606 from the origin

Our next error handling method might be a try-catch around the core logic. We have already got two exceptions from the Evaluate method (ArgumentException). If one of them happens, the exit code is 134 the output looks like as follows:

Unhandled Exception
1
2
3
4
5
Unhandled exception. System.ArgumentException: Invalid token: S
   at Calculator.Program.Evaluate(String[] args) in <path>/Program.cs:line 51
   at Calculator.Program.Evaluate(String[] args) in <path>/Program.cs:line 38
   at Calculator.Program.Evaluate(String[] args) in <path>/Program.cs:line 42
   at Calculator.Program.Main(String[] args) in <path>/Program.cs:line 16

Not so secure to expose the internal structure to the users, the program should exit gracefully without any backtraces. To manage this, the following try-catch structure is needed (the code works the same as in Java and C++ from previous courses).

Try-Catch around the Evaluate(args)
1
2
3
4
5
6
7
8
9
try
{
    Console.WriteLine(Evaluate(args));
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    Environment.Exit(1);
}

The exit code becomes 1 (stating that there was an error) and the error message is Error: Invalid token: S. That's enough right now.

When you debug your app, it usually means that you're running your application with the debugger attached. When you do this task, the debugger provides many ways to see what your code is doing while it runs. You can step through your code and look at the values stored in variables, you can set watches on variables to see when values change, you can examine the execution path of your code, see whether a branch of code is running, and so on. [13]

To start the debugger, run the project as discussed above. Breakpoints are an essential feature of reliable debugging. You can set breakpoints where you want Visual Studio to pause your running code so you can look at the values of variables or the behavior of memory, or know whether or not a branch of code is getting run. The app starts and the debugger runs to the line of code where you set the breakpoint.

Breakpoint
Breakpoint.

The yellow arrow points to the statement on which the debugger paused. App execution is paused at the same point, with the statement not yet executed.

When the app isn't running, F5 starts the debugger, which runs the app until it reaches the first breakpoint. If the app is paused at a breakpoint, then F5 will continue running the app until it reaches the next breakpoint.

Hover over the args variable to see a data tip showing the array size and element type. Expand the args variable to view all its elements and their values.

Paused Execution
Paused Execution.

To advance the debugger to the next statement, press "F10", or choose the Step Over button in the Debug toolbar. Step Over advances the debugger without stepping into function or methods, although their code still executes.

If the execution paused on a function call, press "F11", or choose the Step Into button from the Debug toolbar to continue the execution in the first expression in that function. It helps you examine the execution flow of your code in more depth. To step into a method from a method call, select F11. By default, the debugger skips stepping into nonuser methods.

To leave a method, press "Shift+F11", or choose the Step Out button in the Debug toolbar. Step Out resumes app execution and advances the debugger until the current method or function returns.

Debug Toolbar
Debug Toolbar.
Click to expand the full implementation of Program.cs
Program.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
namespace Calculator
{
    internal class Program
    {
        static int index = 0;

        static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                Console.WriteLine("Usage: dotnet run <expression>");
                Console.WriteLine("Example: dotnet run + 5 * 2 4");
                return;
            }

            try
            {
                Console.WriteLine(Evaluate(args));
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
                Environment.Exit(1);
            }
        }

        static float Evaluate(string[] args)
        {
            if (index >= args.Length)
                throw new ArgumentException("Incomplete expression.");

            string token = args[index++];

            switch (token)
            {
                case "+":
                    return Evaluate(args) + Evaluate(args);
                case "-":
                    return Evaluate(args) - Evaluate(args);
                case "*":
                    return Evaluate(args) * Evaluate(args);
                case "/":
                    return Evaluate(args) / Evaluate(args);
                default:
                    if (float.TryParse(token, out float number))
                    {
                        return number;
                    }
                    throw new ArgumentException($"Invalid token: {token}");
            }
        }
    }
}

References

  1. .NET implementations
  2. .NET CLI overview
  3. Tutorial: Create a .NET console application using Visual Studio
  4. Introduction to projects and solutions
  5. Declare namespaces to organize types
  6. The namespace keyword
  7. static (C# Reference)
  8. Single.TryParse Method
  9. out (C# Reference)
  10. The ref keyword
  11. Out Parameter With Examples in C#
  12. String interpolation using $
  13. Tutorial: Learn to debug C# code using Visual Studio
  14. Top-level statements - programs without Main methods
  15. dotnet new