Book Image

C# 12 and .NET 8 – Modern Cross-Platform Development Fundamentals - Eighth Edition

By : Mark J. Price
4.6 (14)
Book Image

C# 12 and .NET 8 – Modern Cross-Platform Development Fundamentals - Eighth Edition

4.6 (14)
By: Mark J. Price

Overview of this book

This latest edition of the bestselling Packt series will give you a solid foundation to start building projects using modern C# and .NET with confidence. You'll learn about object-oriented programming; writing, testing, and debugging functions; and implementing interfaces. You'll take on .NET APIs for managing and querying data, working with the fi lesystem, and serialization. As you progress, you'll explore examples of cross-platform projects you can build and deploy, such as websites and services using ASP.NET Core. This latest edition integrates .NET 8 enhancements into its examples: type aliasing and primary constructors for concise and expressive code. You'll handle errors robustly through the new built-in guard clauses and explore a simplified implementation of caching in ASP.NET Core 8. If that's not enough, you'll also see how native ahead-of-time (AOT) compiler publish lets web services reduce memory use and run faster. You'll work with the seamless new HTTP editor in Visual Studio 2022 to enhance the testing and debugging process. You'll even get introduced to Blazor Full Stack with its new unified hosting model for unparalleled web development flexibility.
Table of Contents (18 chapters)

Understanding C# grammar and vocabulary

Let’s start by looking at the basics of the grammar and vocabulary of C#. Throughout this chapter, you will create multiple console apps, with each one showing related features of the C# language.

Understanding C# grammar

The grammar of C# includes statements and blocks. To document your code, you can use comments.

Good Practice: Comments should not be the only way that you document your code. Choosing sensible names for variables and functions, writing unit tests, and creating actual documents are other ways to document your code.


In English, we indicate the end of a sentence with a full stop. A sentence can be composed of multiple words and phrases, with the order of words being part of the grammar. For example, in English, we say “the black cat.”

The adjective, black, comes before the noun, cat. Whereas French grammar has a different order; the adjective comes after the noun: “le chat noir.” What’s important to take away from this is that the order matters.

C# indicates the end of a statement with a semicolon. A statement can be composed of multiple types, variables, and expressions made up of tokens. Each token is separated by white space or some other recognizably different token, like an operator, for example, = or +.

For example, in the following statement, decimal is a type, totalPrice is a variable, and subtotal + salesTax is an expression:

decimal totalPrice = subtotal + salesTax;

The expression is made up of an operand named subtotal, an operator +, and another operand named salesTax. The order of operands and operators matters because the order affects the meaning and result.


Comments are the primary method of documenting your code to increase an understanding of how it works, for other developers to read, or for you to read even when you come back to it months later.

In Chapter 4, Writing, Debugging, and Testing Functions, you will learn about XML comments that start with three slashes, ///, and work with a tool to generate web pages to document your code.

You can add comments to explain your code using a double slash, //. The compiler will ignore everything after the // until the end of the line, as shown in the following code:

// Sales tax must be added to the subtotal.
var totalPrice = subtotal + salesTax;

To write a multiline comment, use /* at the beginning and */ at the end of the comment, as shown in the following code:

This is a 
multi-line comment.

Although /* */ is mostly used for multiline comments, it is also useful for commenting in the middle of a statement, as shown in the following code:

decimal totalPrice = subtotal /* for this item */ + salesTax;

Good Practice: Well-designed code, including function signatures with well-named parameters and class encapsulation, can be somewhat self-documenting. When you find yourself putting too many comments and explanations in your code, ask yourself: can I rewrite, aka refactor, this code to make it more understandable without long comments?

Your code editor has commands to make it easier to add and remove comment characters, as shown in the following list:

  • Visual Studio 2022: Navigate to Edit | Advanced | Comment Selection or Uncomment Selection.
  • Visual Studio Code: Navigate to Edit | Toggle Line Comment or Toggle Block Comment.
  • JetBrains Rider: Navigate to Code | Comment with Line Comment or Comment with Block Comment.

Good Practice: You comment code by adding descriptive text above or after code statements. You comment out code by adding comment characters before or around statements to make them inactive. Uncommenting means removing the comment characters.


In English, we indicate a new paragraph by starting a new line. C# indicates a block of code with the use of curly brackets, { }.

Blocks start with a declaration to indicate what is being defined. For example, a block can define the start and end of many language constructs, including namespaces, classes, methods, or statements like foreach.

You will learn more about namespaces, classes, and methods later in this chapter and subsequent chapters, but to briefly introduce some of those concepts now:

  • A namespace contains types like classes to group them together.
  • A class contains the members of an object, including methods.
  • A method contains statements that implement an action that an object can take.

Code editors like Visual Studio 2022 and Visual Studio Code provide a handy feature to collapse and expand blocks by toggling the [-] or [+] or an arrow symbol pointing down or right when you move your mouse cursor over the left margin of the code, as shown in Figure 2.2:

Figure 2.2: Code editors with expanded and collapsed blocks


You can define your own labeled regions around any statements you want and then most code editors will allow you to collapse and expand them in the same way as blocks, as shown in the following code:

#region Three variables that store the number 2 million.
int decimalNotation = 2_000_000;
int binaryNotation = 0b_0001_1110_1000_0100_1000_0000; 
int hexadecimalNotation = 0x_001E_8480;

In this way, regions can be treated as commented blocks that can be collapsed to show a summary of what the block does.

I will use #region blocks throughout the solution code in the GitHub repository, especially for the early chapters before we start defining functions that act as natural collapsible regions, but I won’t show them in the print book, to save space. Use your own judgment to decide if you want to use regions in your own code.

Examples of statements and blocks

In a simple console app that does not use the top-level program feature, I’ve added some comments to the statements and blocks, as shown in the following code:

using System; // A semicolon indicates the end of a statement.
namespace Basics
{ // An open brace indicates the start of a block.
  class Program
    static void Main(string[] args)
      Console.WriteLine("Hello World!"); // A statement.
} // A close brace indicates the end of a block.

Note that C# uses a brace style where both the open and close braces are on their own line and are at the same indentation level, as shown in the following code:

if (x < 3)
  // Do something if x is less than 3.

Other languages like JavaScript use curly braces but format them differently. They put the open curly brace at the end of the declaration statement, as shown in the following code:

if (x < 3) {
  // Do something if x is less than 3.

You can use whatever style you prefer because the compiler does not care.

Sometimes, to save vertical space in a print book, I use the JavaScript brace style, but mostly I stick with the C# brace style. I use two spaces instead of the more common four spaces for indenting because my code will be printed in a book and therefore has narrow width available.

More Information: The official coding style conventions can be found at the following link:

Regardless of any official guidelines, I recommend that you conform to whatever standards have been adopted by your development team unless you are a solo developer, in which case as long as your code compiles, you can use any conventions you like. Be kind to your future self though by being consistent one way or the other!

Good Practice: The brace style used in the Microsoft official documentation is the most commonly used for C#. For example, see the for statement, as found at the following link:

Formatting code using white space

White space includes the space, tab, and newline characters. You can use white space to format your code however you like because extra white space has no effect on the compiler.

The following four statements are all equivalent:

int sum = 1 + 2; // Most developers would prefer this format.
2; // One statement over three lines.
int       sum=    1    +2;int sum=1+2; // Two statements on one line.

The only white space character required in the preceding statements is one between int and sum to tell the compiler they are separate tokens. Any single white space character, for example a space, tab, or newline would be acceptable.

More Information: You can read the formal definition of C# white space at the following link:

Understanding C# vocabulary

The C# vocabulary is made up of keywords, symbol characters, and types.

Some of the predefined, reserved keywords that you will see in this book and use frequently include using, namespace, class, static, int, string, double, bool, if, switch, break, while, do, for, foreach, this, and true.

Some of the symbol characters that you will see include ", ', +, -, *, /, %, @, and $.

There are other contextual keywords that only have a special meaning in a specific context, like and, or, not, record, and init.

However, that still means that there are only about 100 actual C# keywords in the language.

Good Practice: C# keywords use all lowercase. Although you can use all lowercase for your own type names, you should not. With C# 11 and later, the compiler will give a warning if you do, as shown in the following output: Warning CS8981 The type name 'person' only contains lower-cased ascii characters. Such names may become reserved for the language.

Comparing programming languages to human languages

The English language has more than 250,000 distinct words, so how does C# get away with only having about 100 keywords? Moreover, why is C# so difficult to learn if it has only 0.0416% of the number of words in the English language?

One of the key differences between a human language and a programming language is that developers need to be able to define the new “words” with new meanings. Apart from the (about) 100 keywords in the C# language, this book will teach you about some of the hundreds of thousands of “words” that other developers have defined, but you will also learn how to define your own “words.”

Programmers all over the world must learn English because most programming languages use English words such as “if” and “break.” There are programming languages that use other human languages, such as Arabic, but they are rare. If you are interested in learning more, this YouTube video shows a demonstration of an Arabic programming language:

Changing the color scheme for C# syntax

By default, Visual Studio 2022 and Visual Studio Code show C# keywords in blue to make them easier to differentiate from other code, which defaults to black. Both tools allow you to customize the color scheme.

In Visual Studio 2022:

  1. Navigate to Tools | Options.
  2. In the Options dialog box, in the Environment section, select Fonts and Colors, and then select the display items that you would like to customize. You can also search for the section instead of browsing for it.

In Visual Studio Code:

  1. Navigate to File | Preferences | Theme | Color Theme. It is in the Code menu on macOS.
  2. Select a color theme. For reference, I’ll use the Light+ (default light) color theme so that the screenshots look better in a printed book.

In JetBrains Rider:

  1. Navigate to File | Settings | Editor | Color Scheme.

Help for writing correct code

Plain text editors such as Notepad don’t help you write correct English. Likewise, Notepad won’t help you write the correct C# either.

Microsoft Word can help you write English by highlighting spelling mistakes with red squiggles, with Word saying that “icecream” should be ice-cream or ice cream, and grammatical errors with blue squiggles, such as a sentence should have an uppercase first letter.

Similarly, Visual Studio 2022 and Visual Studio Code’s C# extension help you write C# code by highlighting spelling mistakes, such as the method name needing to be WriteLine with an uppercase L, and grammatical errors, such as statements that must end with a semicolon.

The C# extension constantly watches what you type and gives you feedback by highlighting problems with colored squiggly lines, like that of Microsoft Word.

Let’s see it in action:

  1. In Program.cs, change the L in the WriteLine method to lowercase.
  2. Delete the semicolon at the end of the statement.
  3. In Visual Studio Code, navigate to View | Problems; in Visual Studio 2022, navigate to View | Error List; or in JetBrains Rider, navigate to View | Tool Windows | Problems, and note that a red squiggle appears under the code mistakes and details are shown, as you can see in Figure 2.3:
Graphical user interface, text, application, Word  Description automatically generated

Figure 2.3: The Error List window showing two compile errors

  1. Fix the two coding errors.

Importing namespaces

System is a namespace, which is like an address for a type. To refer to someone’s location exactly, you might use Oxford.HighStreet.BobSmith, which tells us to look for a person named Bob Smith on the High Street in the city of Oxford.

System.Console.WriteLine tells the compiler to look for a method named WriteLine in a type named Console in a namespace named System.

To simplify our code, the Console App project template for every version of .NET before 6.0 added a statement at the top of the code file to tell the compiler to always look in the System namespace for types that haven’t been prefixed with their namespace, as shown in the following code:

using System; // Import the System namespace.

We call this importing the namespace. The effect of importing a namespace is that all available types in that namespace will be available to your program without needing to enter the namespace prefix. All available types in that namespace will be seen in IntelliSense while you write code.

Implicitly and globally importing namespaces

Traditionally, every .cs file that needs to import namespaces would have to start with using statements to import those namespaces. Namespaces like System and System.Linq are needed in almost all .cs files, so the first few lines of every .cs file often had at least a few using statements, as shown in the following code:

using System;
using System.Linq;
using System.Collections.Generic;

When creating websites and services using ASP.NET Core, there are often dozens of namespaces that each file would have to import.

C# 10 introduced a new keyword combination and .NET SDK 6 introduced a new project setting that works together to simplify importing common namespaces.

The global using keyword combination means you only need to import a namespace in one .cs file and it will be available throughout all .cs files instead of having to import the namespace at the top of every file that needs it. You could put global using statements in the Program.cs file, but I recommend creating a separate file for those statements named something like GlobalUsings.cs with the contents being all your global using statements, as shown in the following code:

global using System;
global using System.Linq;
global using System.Collections.Generic;

Good Practice: As developers get used to this new C# feature, I expect one naming convention for this file to become the de facto standard. As you are about to see, the related .NET SDK feature uses a similar naming convention.

Any projects that target .NET 6 or later, and therefore use the C# 10 or later compiler, generate a <ProjectName>.GlobalUsings.g.cs file in the obj\Debug\net8.0 folder to implicitly globally import some common namespaces like System. The specific list of implicitly imported namespaces depends on which SDK you target, as shown in Table 2.6:


Implicitly imported namespaces










Same as Microsoft.NET.Sdk and:











Same as Microsoft.NET.Sdk and:





Table 2.6: .NET SDKs and their implicitly imported namespaces

Let’s see the current autogenerated implicit imports file:

  1. In Solution Explorer, toggle on the Show All Files button, and note the compiler-generated bin and obj folders are now visible.
  2. In the Vocabulary project, expand the obj folder, expand the Debug folder, expand the net8.0 folder, and then open the file named Vocabulary.GlobalUsings.g.cs.

    The naming convention for this file is <ProjectName>.GlobalUsings.g.cs. Note the g for generated to differentiate it from developer-written code files.

  1. Remember that this file is automatically created by the compiler for projects that target .NET 6 and later, and that it imports some commonly used namespaces including System.Threading, as shown in the following code:
    // <autogenerated />
    global using global::System;
    global using global::System.Collections.Generic;
    global using global::System.IO;
    global using global::System.Linq;
    global using global::System.Net.Http;
    global using global::System.Threading;
    global using global::System.Threading.Tasks;
  2. Close the Vocabulary.GlobalUsings.g.cs file.
  3. In Solution Explorer, open the Vocabulary.csproj project file, and then add additional entries to the project file to control which namespaces are implicitly imported, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk">
        <Using Remove="System.Threading" />
        <Using Include="System.Numerics" />
        <Using Include="System.Console" Static="true" />
        <Using Include="System.Environment" Alias="Env" />

    Note that <ItemGroup> is different from <ImportGroup>. Be sure to use the correct one! Also, note that the order of elements in a project group or item group does not matter. For example, <Nullable> can be before or after <ImplicitUsings>.

  1. Save the changes to the project file.
  2. Expand the obj folder, expand the Debug folder, expand the net8.0 folder, and open the file named Vocabulary.GlobalUsings.g.cs.
  3. Note this file now imports System.Numerics instead of System.Threading, the Environment class has been imported and aliased to Env, and we have statically imported the Console class, as shown highlighted in the following code:
    // <autogenerated />
    global using global::System;
    global using global::System.Collections.Generic;
    global using global::System.IO;
    global using global::System.Linq;
    global using global::System.Net.Http;
    global using global::System.Numerics;
    global using global::System.Threading.Tasks;
    global using Env = global::System.Environment;
    global using static global::System.Console;
  4. In Program.cs, add a statement to output a message from the computer and note that because we statically imported the Console class, we can call its methods like WriteLine without prefixing them with Console, and we can reference the Environment class using its alias Env, as shown in the following code:
    WriteLine($"Computer named {Env.MachineName} says \"No.\"");
  5. Run the project and note the message, as shown in the following output:
    Computer named DAVROS says "No."

Your computer name will be different unless you name your computers after characters from Doctor Who like I do.

You can disable the implicitly imported namespaces feature for all SDKs by removing the <ImplicitUsings> element completely from the project file, or changing its value to disable, as shown in the following markup:


Good Practice: You might choose to do this if you want to manually create a single file with all the global using statements instead of potentially having one generated automatically and others created manually. But my recommendation is to leave the feature enabled and modify the project file to change what is included in the auto-generated class file in the obj folder hierarchy.

Verbs are methods

In English, verbs are doing or action words, like “run” and “jump.” In C#, doing or action words are called methods. There are hundreds of thousands of methods available to C#. In English, verbs change how they are written based on when in time the action happens. For example, Amir was jumping in the past, Beth jumps in the present, they jumped in the past, and Charlie will jump in the future.

In C#, methods such as WriteLine change how they are called or executed based on the specifics of the action. This is called overloading, which we’ll cover in more detail in Chapter 5, Building Your Own Types with Object-Oriented Programming. But for now, consider the following example:

// Outputs the current line terminator.
// By default, this is a carriage-return and line feed.
// Outputs the greeting and the current line terminator.
Console.WriteLine("Hello Ahmed");
// Outputs a formatted number and date and the current line terminator.
  "Temperature on {0:D} is {1}°C.", DateTime.Today, 23.4);

When I show code snippets without numbered step-by-step instructions, I do not expect you to enter them as code, so they won’t execute out of context.

A different and not quite exact analogy is that some verbs are spelled the same but have different effects depending on the context, for example, you can lose a game, lose your place in a book, or lose your keys.

Nouns are types, variables, fields, and properties

In English, nouns are names that refer to things. For example, Fido is the name of a dog. The word “dog” tells us the type of thing that Fido is, and so to order Fido to fetch a ball, we would use his name.

In C#, their equivalents are types, variables, fields, and properties. For example:

  • Animal and Car are types; they are nouns for categorizing things.
  • Head and Engine might be fields or properties; they are nouns that belong to Animal and Car.
  • Fido and Bob are variables; they are nouns for referring to a specific object.

There are tens of thousands of types available to C#, though have you noticed how I didn’t say, “There are tens of thousands of types in C#”? The difference is subtle but important. The language of C# only has a few keywords for types, such as string and int, and strictly speaking, C# doesn’t define any types. Keywords such as string that look like types are aliases, which represent types provided by the platform on which C# runs.

It’s important to know that C# cannot exist alone; after all, it’s a language that runs on variants of .NET. In theory, someone could write a compiler for C# that uses a different platform, with different underlying types. In practice, the platform for C# is .NET, which provides tens of thousands of types to C#, including System.Int32, which is the C# keyword alias int maps to, as well as many more complex types, such as System.Xml.Linq.XDocument.

It’s worth taking note that the term type is often confused with class. Have you ever played the parlor game Twenty Questions, also known as Animal, Vegetable, or Mineral? In the game, everything can be categorized as an animal, vegetable, or mineral. In C#, every type can be categorized as a class, struct, enum, interface, or delegate. You will learn what these mean in Chapter 6, Implementing Interfaces and Inheriting Classes. As an example, the C# keyword string is a class, but int is a struct. So, it is best to use the term type to refer to both.

Revealing the extent of the C# vocabulary

We know that there are more than 100 keywords in C#, but how many types are there? Let’s write some code to find out how many types (and their methods) are available to C# in our simple console app.

Don’t worry about exactly how this code works for now, but know that it uses a technique called reflection:

  1. Comment out all the existing statements in Program.cs.
  2. We’ll start by importing the System.Reflection namespace at the top of the Program.cs file so that we can use some of the types in that namespace like Assembly and TypeName, as shown in the following code:
    using System.Reflection; // To use Assembly, TypeName, and so on.

    Good Practice: We could use the implicit imports and global using features to import this namespace for all .cs files in this project, but since there is only one file, it is better to import the namespace in the one file in which it is needed.

  1. Write statements to get the compiled console app and loop through all the types that it has access to, outputting the names and number of methods each has, as shown in the following code:
    // Get the assembly that is the entry point for this app.
    Assembly? myApp = Assembly.GetEntryAssembly();
    // If the previous line returned nothing then end the app.
    if (myApp is null) return;
    // Loop through the assemblies that my app references.
    foreach (AssemblyName name in myApp.GetReferencedAssemblies())
      // Load the assembly so we can read its details.
      Assembly a = Assembly.Load(name);
      // Declare a variable to count the number of methods.
      int methodCount = 0;
      // Loop through all the types in the assembly.
      foreach (TypeInfo t in a.DefinedTypes)
        // Add up the counts of all the methods.
        methodCount += t.GetMethods().Length;
      // Output the count of types and their methods.
      WriteLine("{0:N0} types with {1:N0} methods in {2} assembly.",
        arg0: a.DefinedTypes.Count(),
        arg1: methodCount, 
        arg2: name.Name);

    N0 is uppercase N followed by the digit zero. It is not uppercase N followed by uppercase O. It means “format a number (N) with zero (0) decimal places.”

  1. Run the project. You will see the actual number of types and methods that are available to you in the simplest application when running on your operating system (OS). The number of types and methods displayed will be different depending on the OS that you are using, as shown in the following output on Windows:
    0 types with 0 methods in System.Runtime assembly.
    41 types with 639 methods in System.Console assembly.
    112 types with 1,190 methods in System.Linq assembly.

    Why does the System.Runtime assembly contain zero types? This assembly is special because it contains only type-forwarders rather than actual types. A type-forwarder represents a type that has been implemented outside of .NET or for some other advanced reason.

  1. Add statements to the top of the file (after importing the System.Reflection namespace) to declare some variables, as shown in the following code:
    // Declare some unused variables using types in
    // additional assemblies to make them load too.
    System.Data.DataSet ds = new();
    HttpClient client = new();
  2. Run the console app again and view the results, which should look like the following outputs:
    0 types with 0 methods in System.Runtime assembly.
    406 types with 7,139 methods in System.Data.Common assembly.
    425 types with 4,681 methods in System.Net.Http assembly.
    41 types with 639 methods in System.Console assembly.
    112 types with 1,190 methods in System.Linq assembly.

Now, you have a better sense of why learning C# is a challenge – because there are so many types and methods to learn. Methods are only one category of a member that a type can have, and you and other programmers are constantly defining new types and members!

Example of asking ChatGPT to explain code

The code we wrote in the Vocabulary project is a good example of when you might want to ask ChatGPT to explain something. If you enter a prompt of “Explain the following code:” followed by the code, here’s what it responded with at the time of writing in July 2023:

“This C# code performs an analysis of the assemblies (essentially libraries or modules) referenced by the current application. It tallies up the total number of types and methods in each referenced assembly.

Let’s break down the code in more detail:

  1. System.Reflection namespace is used to work with assemblies, modules, types, methods, and so on, at runtime.
  2. The System.Data.DataSet and HttpClient instances are declared but not used. The comment implies that these are declared to ensure their respective assemblies are loaded for the subsequent analysis. Creating instances of classes from certain assemblies can cause those assemblies to be loaded into memory.
  3. Assembly.GetEntryAssembly() gets the assembly that is the entry point of the application.
  4. If myApp is null (meaning there’s no entry assembly, which should be impossible in a normally running .NET app), the application exits.
  5. The foreach loop iterates over each assembly name in the referenced assemblies of the entry point assembly.
  6. Assembly.Load(name) loads an assembly given the assembly name. This allows you to interact with the assembly programmatically.
  7. A counter variable methodCount is declared and initialized to 0. This is used to count the total number of methods in the assembly.
  8. Another nested foreach loop then iterates over each type in the assembly.
  9. For each type, it gets the number of methods (t.GetMethods().Length) and adds it to the methodCount.
  10. Finally, the number of types and their methods in the assembly are output to the console using WriteLine(). The {0:N0} format specifier in the string is a placeholder for a number (0 refers to the argument index, N is for number format, and 0 after N specifies the number of decimal places).

So, in essence, this script counts how many types and methods exist in each assembly referenced by the application. This might be useful to understand the complexity or size of the dependencies the application has.”

And now back to the flesh and blood Mark: That’s a pretty good explanation with no mistakes or hallucinations!

Good Practice: Tools like GitHub Copilot and ChatGPT are especially useful for programmers when they are learning new things. And a good programmer is always learning new things! These tools are not just for writing code for you. They can explain existing code written by others and even suggest improvements.