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

Understanding C# grammar and vocabulary

To learn simple C# language features, you can use .NET Interactive Notebooks, which remove the need to create an application of any kind.

To learn some other C# language features, you will need to create an application. The simplest type of application is a console application.

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

Showing the compiler version

We will start by writing code that shows the compiler version:

  1. If you've completed Chapter 1, Hello, C#! Welcome, .NET!, then you will already have a Code folder. If not, then you'll need to create it.
  2. Use your preferred code editor to create a new console app, as defined in the following list:
    1. Project template: Console Application [C#] / console
    2. Workspace/solution file and folder: Chapter02
    3. Project file and folder: Vocabulary

      Good Practice: If you have forgotten how, or did not complete the previous chapter, then step-by-step instructions for creating a workspace/solution with multiple projects are given in Chapter 1, Hello, C#! Welcome, .NET!.

  1. Open the Program.cs file, and at the top of the file, under the comment, add a statement to show the C# version as an error, as shown in the following code:
    #error version
    
  2. Run the console application:
    1. In Visual Studio Code, in a terminal, enter the command dotnet run.
    2. In Visual Studio, navigate to Debug | Start Without Debugging. When prompted to continue and run the last successful build, click No.
  3. Note the compiler version and language version appear as a compiler error message number CS8304, as shown in Figure 2.1:

    Figure 2.1: A compiler error that shows the C# language version

  4. The error message in the Visual Studio Code PROBLEMS window or Visual Studio Error List window says Compiler version: '4.0.0...' with language version 10.0.
  5. Comment out the statement that causes the error, as shown in the following code:
    // #error version
    
  6. Note that the compiler error messages disappear.

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.

Statements

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 variables and expressions. For example, in the following statement, totalPrice is a variable and subtotal + salesTax is an expression:

var 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.

Comments

When writing your code, you're able to add comments to explain your code using a double slash, //. By inserting // 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.
*/

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 for Windows: Navigate to Edit | Advanced | Comment Selection or Uncomment Selection
  • Visual Studio Code: Navigate to Edit | Toggle Line Comment or Toggle 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.

Blocks

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.

Examples of statements and blocks

In the project template for console apps when targeting .NET 5.0, note that examples of the grammar of C# have been written for you by the project template. 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

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 include using, namespace, class, static, int, string, double, bool, if, switch, break, while, do, for, foreach, and, or, not, record, and init.

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.

However, that still means that there are only about 100 actual C# keywords in 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 namespace and class. 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: https://youtu.be/dkO8cdwf6v8.

Changing the color scheme for C# syntax

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

  1. In Visual Studio Code, navigate to Code | Preferences | Color Theme (it is on the File menu on Windows).
  2. Select a color theme. For reference, I'll use the Light+ (default light) color theme so that the screenshots look good in a printed book.
  3. In Visual Studio, navigate to Tools | Options.
  4. In the Options dialog box, select Fonts and Colors, and then select the display items that you would like to customize.

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 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 Code's C# extension and Visual Studio help you write C# code by highlighting spelling mistakes, such as the method name should 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, similar to 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, or in Visual Studio navigate to View | Error List, and note that a red squiggle appears under the code mistakes and details are shown, as you can see in Figure 2.2:

    Figure 2.2: The Error List window showing two compile errors

  4. 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 Application 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 and will be seen in IntelliSense while you write code.

.NET Interactive notebooks have most namespaces imported automatically.

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 introduces some new features that simplify importing namespaces.

First, the global using statement means you only need to import a namespace in one .cs file and it will be available throughout all .cs files. 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 or GlobalNamespaces.cs, 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 standard.

Second, any projects that target .NET 6.0 and therefore use the C# 10 compiler generate a.cs file in the obj 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 the following table:

SDK

Implicitly imported namespaces

Microsoft.NET.Sdk

System

System.Collections.Generic

System.IO

System.Linq

System.Net.Http

System.Threading

System.Threading.Tasks

Microsoft.NET.Sdk.Web

Same as Microsoft.NET.Sdk and:

System.Net.Http.Json

Microsoft.AspNetCore.Builder

Microsoft.AspNetCore.Hosting

Microsoft.AspNetCore.Http

Microsoft.AspNetCore.Routing

Microsoft.Extensions.Configuration

Microsoft.Extensions.DependencyInjection

Microsoft.Extensions.Hosting

Microsoft.Extensions.Logging

Microsoft.NET.Sdk.Worker

Same as Microsoft.NET.Sdk and:

Microsoft.Extensions.Configuration

Microsoft.Extensions.DependencyInjection

Microsoft.Extensions.Hosting

Microsoft.Extensions.Logging

Let's see the current auto-generated implicit imports file:

  1. In Solution Explorer, select the Vocabulary project, toggle on the Show All Files button, and note the compiler-generated bin and obj folders are visible.
  2. Expand the obj folder, expand the Debug folder, expand the net6.0 folder, and open the file named Vocabulary.GlobalUsings.g.cs.
  3. Note this file is automatically created by the compiler for projects that target .NET 6.0, 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;
    
  4. Close the Vocabulary.GlobalUsings.g.cs file.
  5. In Solution Explorer, select the project, 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">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
      <ItemGroup>
        <Using Remove="System.Threading" />
        <Using Include="System.Numerics" />
      </ItemGroup>
    </Project>
    
  6. Save the changes to the project file.
  7. Expand the obj folder, expand the Debug folder, expand the net6.0 folder, and open the file named Vocabulary.GlobalUsings.g.cs.
  8. Note this file now imports System.Numerics instead of System.Threading, 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.Threading.Tasks;
    global using global::System.Numerics;
    
  9. Close the Vocabulary.GlobalUsings.g.cs file.

You can disable the implicitly imported namespaces feature for all SDKs by removing an entry in the project file, as shown in the following markup:

<ImplicitUsings>enable</ImplicitUsings>

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 string
// by default, this is a carriage-return and line feed
Console.WriteLine();
// outputs the greeting and the current line terminator string
Console.WriteLine("Hello Ahmed");
// outputs a formatted number and date and the current line terminator string
Console.WriteLine("Temperature on {0:D} is {1}°C.", 
  DateTime.Today, 23.4);

A different analogy is that some words are spelled the same but have different meanings depending on the context.

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 in order for 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; nouns that belong to Animal and Car.
  • Fido and Bob are variables; 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 examples, 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 application.

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

  1. We'll start by importing the System.Reflection namespace at the top of the Program.cs file, as shown in the following code:
    using System.Reflection;
    
  2. Delete the statement that writes Hello World! and replace it with the following code:
    Assembly? assembly = Assembly.GetEntryAssembly();
    if (assembly == null) return;
    // loop through the assemblies that this app references
    foreach (AssemblyName name in assembly.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 methods
        methodCount += t.GetMethods().Count();
      }
      // output the count of types and their methods
      Console.WriteLine(
        "{0:N0} types with {1:N0} methods in {2} assembly.",
        arg0: a.DefinedTypes.Count(),
        arg1: methodCount, arg2: name.Name);
    }
    
  3. Run the code. You will see the actual number of types and methods that are available to you in the simplest application when running on your OS. The number of types and methods displayed will be different depending on the operating system that you are using, as shown in the following outputs:
    // Output on Windows
    0 types with 0 methods in System.Runtime assembly.
    106 types with 1,126 methods in System.Linq assembly.
    44 types with 645 methods in System.Console assembly.
    // Output on macOS
    0 types with 0 methods in System.Runtime assembly.
    103 types with 1,094 methods in System.Linq assembly.
    57 types with 701 methods in System.Console 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.

  4. Add statements to the top of the file after importing the namespace to declare some variables, as shown highlighted in the following code:
    using System.Reflection;
    // declare some unused variables using types
    // in additional assemblies
    System.Data.DataSet ds;
    HttpClient client;
    

    By declaring variables that use types in other assemblies, those assemblies are loaded with our application, which allows our code to see all the types and methods in them. The compiler will warn you that you have unused variables but that won't stop your code from running.

  5. Run the console application again and view the results, which should look similar to the following outputs:
    // Output on Windows
    0 types with 0 methods in System.Runtime assembly.
    383 types with 6,854 methods in System.Data.Common assembly.
    456 types with 4,590 methods in System.Net.Http assembly.
    106 types with 1,126 methods in System.Linq assembly.
    44 types with 645 methods in System.Console assembly.
    // Output on macOS
    0 types with 0 methods in System.Runtime assembly.
    376 types with 6,763 methods in System.Data.Common assembly.
    522 types with 5,141 methods in System.Net.Http assembly.
    103 types with 1,094 methods in System.Linq assembly.
    57 types with 701 methods in System.Console 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!