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

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

Copyright © 2023 Packt Publishing

All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.

Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book.

Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.

Early Access Publication: C# 12 and .NET 8 – Modern Cross- Platform Development Fundamentals

Early Access Production Reference: B19586 Published by Packt Publishing Ltd.

Livery Place

35 Livery Street Birmingham

B3 2PB, UK

ISBN: 978-1-83763-587-0

www.packt.com

Table of Contents

image

image

C# 12 and .NET 8 – Modern Cross-Platform Development Fundamentals, Eighth Edition: Start building websites and services with ASP.NET Core 8, Blazor, and EF Core 8

image

1 Hello, C#! Welcome, .NET!

Join our book community on Discord

Introducing this book and its contents

Getting code solutions for this book

.NET terms used in this book

The structure and style of this book

Topics covered by this book

Topics covered by Apps and Services with .NET 8

Setting up your development environment

Choosing the appropriate tool and application type for learning

Deploying cross-platform

Downloading and installing Visual Studio 2022

Downloading and installing Visual Studio Code

Understanding .NET

Understanding .NET support

Understanding .NET support phases

Understanding .NET Runtime and .NET SDK versions

Listing and removing versions of .NET

Understanding intermediate language

Comparing .NET technologies

Managing multiple projects using code editors

Building console apps using Visual Studio 2022

Writing code using Visual Studio 2022

Compiling and running code using Visual Studio

Understanding the compiler-generated folders and files

Understanding top-level programs

Requirements for top-level programs

Implicitly imported namespaces

Revealing the hidden code by throwing an exception

Revealing the namespace for the Program class

Adding a second project using Visual Studio 2022

Building console apps using Visual Studio Code

Writing code using Visual Studio Code

Compiling and running code using the dotnet CLI

Adding a second project using Visual Studio Code

Summary of steps for Visual Studio Code

Summary of other project types used in this book

image

Making good use of the GitHub repository for this book

image

Understanding the solution code on GitHub

Raising issues with the book

Giving me feedback

Avoiding common mistakes

Downloading solution code from the GitHub repository

Using Git with Visual Studio Code and the command prompt

Cloning the book solution code repository

Looking for help

Reading the documentation on Microsoft Learn

Documentation links in this book

Getting help for the dotnet tool

Getting definitions of types and their members

Configuring inline aka inlay hints

Looking for answers on Stack Overflow

Searching for answers using Google

Searching the .NET source code

Subscribing to the official .NET blog

Watching Scott Hanselman's videos

AI tools like ChatGPT and GitHub Copilot

Disabling tools when they get in the way

Practicing and exploring

Exercise 1.1 – Test your knowledge

Exercise 1.2 – Practice C# anywhere with a browser

Exercise 1.3 – Explore topics

Exercise 1.4 – Explore Polyglot Notebooks

Exercise 1.5 – Explore themes of modern .NET

Exercise 1.6 – Free Code Camp and C#

Certification

Exercise 1.7 – Alpha versions of .NET

image

Summary

2 Speaking C#

Join our book community on Discord

Introducing the C# language

C# language versions and features

Understanding C# standards

Discovering your C# compiler version

How to output the SDK version

Enabling a specific language version compiler

Using future C# compiler versions

Switching the C# compiler for .NET 8 to a future version

Showing the compiler version

Understanding C# grammar and vocabulary

Understanding C# grammar

Statements

Comments

Blocks

Regions

Examples of statements and blocks

Formatting code using white space

Understanding C# vocabulary

Comparing programming languages to human languages

Changing the color scheme for C# syntax

Help for writing correct code

Importing namespaces

Implicitly and globally importing namespaces

Verbs are methods

Nouns are types, variables, fields, and properties

Revealing the extent of the C# vocabulary

Example of asking ChatGPT to explain code

Working with variables

Naming things and assigning values

Literal values

Storing text

Storing numbers

Storing real numbers

Storing Booleans

Storing any type of object

Storing dynamic types

Declaring local variables

Getting and setting the default values for types

Exploring more about console apps

Displaying output to the user

Getting text input from the user

Simplifying the usage of the console

Getting key input from the user

Passing arguments to a console app

Setting options with arguments

Handling platforms that do not support an API

Understanding async and await

Improving responsiveness for console apps

Practicing and exploring

Exercise 2.1 – Test your knowledge

Exercise 2.2 – Test your knowledge of number types

Exercise 2.3 – Practice number sizes and ranges

Exercise 2.4 – Explore topics

Exercise 2.5 – Explore Spectre

image

Summary

3 Controlling Flow, Converting Types, and Handling Exceptions

Join our book community on Discord

Operating on variables

Understanding binary operators

Understanding unary operators

Understanding ternary operators

Exploring unary operators

Exploring binary arithmetic operators

Assignment operators

Null-coalescing operators

Exploring logical operators

Exploring conditional logical operators

Exploring bitwise and binary shift operators

Miscellaneous operators

Understanding selection statements

Branching with the if statement

Why you should always use braces with if statements

Pattern matching with the if statement

Branching with the switch statement

Adding a new item to a project using Visual Studio 2022

Pattern matching with the switch statement

Simplifying switch statements with switch expressions

Understanding iteration statements

Looping with the while statement

Looping with the do statement

Looping with the for statement

Looping with the foreach statement

Understanding how foreach works internally

Storing multiple values in an array

Working with single-dimensional arrays

Working with multi-dimensional arrays

Working with jagged arrays

List pattern matching with arrays

Understanding inline arrays

Summarizing arrays

Casting and converting between types

Casting numbers implicitly and explicitly

How negative numbers are represented in binary

Converting with the System.Convert type

Rounding numbers and the default rounding rules

Taking control of rounding rules

Converting from any type to a string

Converting from a binary object to a string

Parsing from strings to numbers or dates and times

Avoiding Parse exceptions by using the TryParse method

Understanding the Try method naming convention

Handling exceptions

Wrapping error-prone code in a try block

Catching all exceptions

Catching specific exceptions

Catching with filters

Checking for overflow

Throwing overflow exceptions with the checked statement

Disabling compiler overflow checks with the unchecked statement

Practicing and exploring

Exercise 3.1 – Test your knowledge

Exercise 3.2 – Explore loops and overflow

Exercise 3.3 – Test your knowledge of operators

Exercise 3.4 – Practice loops and operators

Exercise 3.5 – Practice exception handling

Exercise 3.6 – Explore C# 101 notebooks

Exercise 3.7 – Explore topics

image

Summary

image

4 Writing, Debugging, and Testing Functions

Join our book community on Discord

Writing functions

Exploring top-level programs, functions, and namespaces

Defining a partial Program class with a static function

Times table example

A brief aside about arguments and parameters

Writing a function that returns a value

Converting numbers from cardinal to ordinal

Calculating factorials with recursion

Documenting functions with XML comments

Using lambdas in function implementations

Debugging during development

Creating code with a deliberate bug

Setting a breakpoint and starting debugging

Navigating with the debugging toolbar

Debugging windows

Stepping through code

Using the Visual Studio Code integrated terminal during debugging

Customizing breakpoints

Hot reloading during development

Hot reloading using Visual Studio 2022

Hot reloading using Visual Studio Code and dotnet watch

Logging during development and runtime

Understanding logging options

Instrumenting with Debug and Trace

Configuring trace listeners

Switching trace levels

Logging information about your source code

Unit testing

Understanding types of testing

Creating a class library that needs testing

Writing unit tests

Throwing and catching exceptions in functions

Understanding usage errors and execution errors

Commonly thrown exceptions in functions

Throwing exceptions using guard clauses

Understanding the call stack

Where to catch exceptions

Rethrowing exceptions

Implementing the tester-doer and try patterns

Practicing and exploring

Exercise 4.1 – Test your knowledge

Exercise 4.2 – Practice writing functions with debugging and unit testing

Exercise 4.3 – Explore topics

image

Summary

5 Building Your Own Types with Object-Oriented Programming

Join our book community on Discord

Talking about OOP

Building class libraries

Creating a class library

Understanding file-scoped namespaces

Defining a class in a namespace

Understanding type access modifiers

Understanding members

Importing a namespace to use a type

Instantiating a class

Inheriting from System.Object

Avoiding a namespace conflict with a using alias

Renaming a type with a using alias

Storing data in fields

Defining fields

Types for fields

Member access modifiers

Setting and outputting field values

Setting field values using object initializer syntax

Storing a value using an enum type

Storing multiple values using an enum type

Storing multiple values using collections

Understanding generic collections

Making a field static

Making a field constant

Making a field read-only

Requiring fields to be set during instantiation

Initializing fields with constructors

Defining multiple constructors

Setting required fields with a constructor

Working with methods and tuples

Returning values from methods

Defining and passing parameters to methods

Overloading methods

Passing optional parameters

Naming parameter values when calling methods

Mixing optional and required parameters

Controlling how parameters are passed

Understanding ref returns

Combining multiple returned values using tuples

Naming the fields of a tuple

Aliasing tuples

Deconstructing tuples

Deconstructing other types using tuples

Implementing functionality using local functions

Splitting classes using partial

Controlling access with properties and indexers

Defining read-only properties

Defining settable properties

Limiting flags enum values

Defining indexers

Pattern matching with objects

Pattern-matching flight passengers

Enhancements to pattern matching in C# 9 or later

Working with record types

Init-only properties

Defining record types

Equality of record types

Positional data members in records

Defining a primary constructor for a class

Practicing and exploring

Exercise 5.1 – Test your knowledge

Exercise 5.2 – Practice with access modifiers

Exercise 5.3 – Explore topics

image

Summary

6 Implementing Interfaces and Inheriting Classes

Join our book community on Discord

Setting up a class library and console application

Static methods and overloading operators

Implementing functionality using methods

Implementing functionality using operators

Making types safely reusable with generics

Working with non-generic types

Working with generic types

Raising and handling events

Calling methods using delegates

Examples of delegate use

Status: It's complicated

Defining and handling delegates

Defining and handling events

Implementing interfaces

Common interfaces

Comparing objects when sorting

Comparing objects using a separate class

Implicit and explicit interface implementations

Defining interfaces with default implementations

Managing memory with reference and value types

Understanding stack and heap memory

Defining reference and value types

image

How reference and value types are stored in memory

Understanding boxing

Equality of types

Defining struct types

Defining record struct types

Releasing unmanaged resources

Ensuring that Dispose is called

Working with null values

Making a value type nullable

Understanding null-related initialisms

Understanding nullable reference types

Controlling the nullability warning check feature

Disabling null and other compiler warnings

Declaring non-nullable variables and parameters

Checking for null

Checking for null in method parameters

Inheriting from classes

Extending classes to add functionality

Hiding members

Understanding this and base keywords

Overriding members

Inheriting from abstract classes

Choosing between an interface and an abstract class

Preventing inheritance and overriding

Understanding polymorphism

Casting within inheritance hierarchies

Implicit casting

Explicit casting

Avoiding casting exceptions

Using is to check a type

Using as to cast a type

Inheriting and extending .NET types

Inheriting exceptions

Extending types when you can't inherit

Using static methods to reuse functionality

Using extension methods to reuse functionality

Summarizing custom type choices

Categories of custom type and their capabilities

Mutability and records

Comparing inheritance and implementation

Reviewing illustrative code

Practicing and exploring

Exercise 6.1 – Test your knowledge

image

Exercise 6.2 – Practice creating an inheritance hierarchy

Exercise 6.3 – Writing better code

Exercise 6.4 – Explore topics

image

Summary

7 Packaging and Distributing .NET Types

Join our book community on Discord

The road to .NET 8

image

.NET Core 1.0, June 2016

image

.NET Core 1.1, November 2016

image

.NET Core 2.0, August 2017

image

.NET Core 2.1, May 2018

image

.NET Core 2.2, December 2018

image

.NET Core 3.0, September 2019

viii. .NET 5,

November

2020

ix. .NET 6,

November

2021

x. .NET 7,

November

2022

xi. .NET 8,

November

2023

.NET Core 3.1, December 2019

xii. Checking your .NET SDKs for updates

Understanding .NET components

image

Assemblies, NuGet packages, and namespaces

What is a namespace?

Dependent assemblies

Microsoft .NET project SDKs

Namespaces and types in assemblies

NuGet packages

Understanding frameworks

Importing a namespace to use a type

Relating C# keywords to .NET types

Mapping C# aliases to .NET types

Understanding native-sized integers

Revealing the location of a type

Sharing code with legacy platforms using .NET Standard

Understanding defaults for class libraries with different SDKs

Creating a .NET Standard class library

Controlling the .NET SDK

Mixing SDKs and framework targets

Publishing your code for deployment

Creating a console app to publish

Understanding dotnet commands

Getting information about .NET and its environment

Managing projects using the dotnet CLI

Publishing a self-contained app

Publishing a single-file app

Reducing the size of apps using app trimming

Controlling where build artifacts are created

Native ahead-of-time compilation

Limitations of native AOT

Reflection and native AOT

Requirements for native AOT

Enabling native AOT for a project

Building a native AOT project

Publishing a native AOT project

Decompiling .NET assemblies

Decompiling using the ILSpy extension for Visual Studio 2022

Viewing source links with Visual Studio 2022

image

No, you cannot technically prevent decompilation

Packaging your libraries for NuGet distribution

Referencing a NuGet package

Fixing dependencies

Packaging a library for NuGet

Publishing a package to a public NuGet feed

Publishing a package to a private NuGet feed

Exploring NuGet packages with a tool

Testing your class library package

Working with preview features

Requiring preview features

Enabling preview features

Method interceptors

Practicing and exploring

Exercise 7.1 – Test your knowledge

Exercise 7.2 – Explore topics

Exercise 7.3 – Porting from .NET Framework to modern .NET

Exercise 7.4 – Creating source generators

Exercise 7.5 – Explore PowerShell

Exercise 7.6 – Improving performance in .NET

image

Summary

8 Working with Common .NET Types

Join our book community on Discord

Working with numbers

Working with big integers

Working with complex numbers

Generating random numbers for games and similar apps

Generating GUIDs

Working with text

Getting the length of a string

Getting the characters of a string

Splitting a string

Getting part of a string

Checking a string for content

Comparing string values

Joining, formatting, and other string members

Building strings efficiently

Pattern matching with regular expressions

Checking for digits entered as text

Regular expression performance improvements

Understanding the syntax of a regular expression

Examples of regular expressions

Splitting a complex comma-separated string

Activating regular expression syntax coloring

Improving regular expression performance with source generators

Storing multiple objects in collections

Common features of all collections

Working with lists

Working with dictionaries

image

Sets, stacks, and queues

Collection add and remove methods

Sorting collections

Specialized collections

Read-only, immutable, and frozen collections

Initializing collections using collection expressions

Good practice with collections

Working with spans, indexes, and ranges

Using memory efficiently using spans

Identifying positions with the Index type

Identifying ranges with the Range type

Using indexes, ranges, and spans

Practicing and exploring

Exercise 8.1 – Test your knowledge

Exercise 8.2 – Practice regular expressions

Exercise 8.3 – Practice writing extension methods

Exercise 8.4 – Working with network resources

Exercise 8.5 – Explore topics

image

Summary

9 Working with Files, Streams, and Serialization

Join our book community on Discord

Managing the filesystem

Handling cross-platform environments and filesystems

Managing drives

Managing directories

Managing files

Managing paths

Getting file information

Controlling how you work with files

Reading and writing with streams

Understanding abstract and concrete streams

Understanding storage streams

Understanding function streams

Understanding stream helpers

Building a stream pipeline

Writing to text streams

Writing to XML streams

Simplifying disposal by using the using statement

Compressing streams

Reading and writing with random access handles

Encoding and decoding text

Encoding strings as byte arrays

Encoding and decoding text in files

Serializing object graphs

Serializing as XML

Generating compact XML

Deserializing XML files

Serializing with JSON

High-performance JSON processing

Deserializing JSON files

Controlling JSON processing

Working with environment variables

Reading all environment variables

Expanding, setting, and getting an environment variables

Practicing and exploring

Exercise 9.1 – Test your knowledge

Exercise 9.2 – Practice serializing as XML

Exercise 9.3 – Working with Tar archives

Exercise 9.4 – Migrating from Newtonsoft to new JSON

Exercise 9.5 – Explore topics

image

Summary

10 Working with Data Using Entity Framework Core

Join our book community on Discord

Understanding modern databases

Understanding legacy Entity Framework

Using the legacy Entity Framework 6.3 or later

Understanding Entity Framework Core

Understanding Database First and Code First

Performance improvements in EF Core

Using a sample relational database

Using SQLite

Using SQL Server or other SQL systems

Setting up SQLite for Windows

Setting up SQLite for macOS and Linux

Setting up EF Core in a .NET project

Creating a console app for working with EF Core

Creating the Northwind sample database for SQLite

If you are using Visual Studio 2022

Managing the Northwind sample database with SQLiteStudio

Using the lightweight ADO.NET database providers

Choosing an EF Core database provider

Connecting to a named SQLite database

Defining the Northwind database context class

Defining EF Core models

Using EF Core conventions to define the model

Using EF Core annotation attributes to define the model

Using the EF Core Fluent API to define the model

Understanding data seeding with the Fluent API

Building EF Core models for the Northwind tables

Defining the Category and Product entity classes

Adding tables to the Northwind database context class

Setting up the dotnet-ef tool

Scaffolding models using an existing database

Customizing the reverse engineering templates

Configuring preconvention models

Querying EF Core models

Filtering included entities

Filtering and sorting products

Getting the generated SQL

Logging EF Core

Filtering logs by provider-specific values

Logging with query tags

Getting a single entity

Pattern matching with Like

Generating a random number in queries

Defining global filters

Loading and tracking patterns with EF Core

Eager loading entities using the Include extension method

Enabling lazy loading

Explicit loading entities using the Load method

Controlling the tracking of entities

Three tracking scenarios

Lazy loading for no tracking queries

Summary of tracking

Modifying data with EF Core

Inserting entities

Updating entities

Deleting entities

More efficient updates and deletes

Pooling database contexts

Practicing and exploring

Exercise 10.1 – Test your knowledge

Exercise 10.2 – Exporting data using different serialization formats

Exercise 10.3 – Working with transactions

Exercise 10.4 – Explore a Code First EF Core model

Exercise 10.5 – Explore app secrets

Exercise 10.6 – Explore topics

Exercise 10.7 – Explore NoSQL databases

image

Summary

11 Querying and Manipulating Data Using LINQ

Join our book community on Discord

Writing LINQ expressions

Comparing imperative and declarative language features

LINQ components

Building LINQ expressions with the Enumerable class

LINQ in practice

Understanding deferred execution

Filtering entities using Where

Targeting a named method

Simplifying the code by removing the explicit delegate instantiation

Targeting a lambda expression

Lambda expressions with default parameter values

Sorting and more

Sorting by a single property using OrderBy

Sorting by a subsequent property using ThenBy

Sorting by the item itself

Declaring a query using var or a specified type

Filtering by type

Working with sets and bags

Using LINQ with EF Core

Creating a console app for exploring LINQ to Entities

Building an EF Core model

Filtering and sorting sequences

Projecting sequences into new types

Joining, grouping, and lookups

Joining sequences

Group-joining sequences

Grouping for lookups

Aggregating and paging sequences

Checking for an empty sequence

Be careful with Count!

Paging with LINQ

Sweetening LINQ syntax with syntactic sugar

Practicing and exploring

Exercise 11.1 – Test your knowledge

Exercise 11.2 – Practice querying with LINQ

Exercise 11.3 – Using multiple threads with parallel LINQ

Exercise 11.4 – Working with LINQ to XML

Exercise 11.5 – Creating your own LINQ extension methods

Exercise 11.6 – Explore topics

image

Summary

12 Introducing Web Development Using ASP.NET Core

Join our book community on Discord

Understanding ASP.NET Core

Classic ASP.NET versus modern ASP.NET Core

Building websites using ASP.NET Core

Comparison of file types used in ASP.NET Core

Building websites using a content management system

Building web applications using SPA frameworks

Building web and other services

New features in ASP.NET Core

image

ASP.NET Core 1.0, June 2016

image

ASP.NET Core 1.1, November 2016

image

ASP.NET Core 2.0, August 2017

image

ASP.NET Core 2.1, May 2018

image

ASP.NET Core 2.2, December 2018

image

ASP.NET Core 3.0, September 2019

image

ASP.NET Core 3.1, December 2019

Blazor WebAssembly 3.2, May 2020

ix. ASP.NET Core 5,

November

2020

x. ASP.NET Core 6,

November

2021

xi. ASP.NET Core 7,

November

2022

xii. ASP.NET Core 8,

November

2023

Structuring projects

Structuring projects in a solution

image

Building an entity model for use in the rest of the book

image

Creating the Northwind database

Creating a class library for entity models using SQLite

Creating a class library for a database context using SQLite

Customizing the model and defining an extension method

Registering the scope of a dependency service

Creating class libraries for entity models using SQL Server

Improving the class-to-table mapping

Testing the class libraries

Understanding web development

Understanding Hypertext Transfer Protocol

Understanding the components of a URL

Using Google Chrome to make HTTP requests

Understanding client-side web development technologies

Practicing and exploring

Exercise 12.1 – Test your knowledge

Exercise 12.2 – Know your webbreviations

Exercise 12.3 – Explore topics

image

Summary

13 Building Websites Using ASP.NET Core Razor Pages

Join our book community on Discord

Exploring ASP.NET Core

Creating an empty ASP.NET Core project

Testing and securing the website

Enabling stronger security and redirecting to a secure connection

Controlling the hosting environment

Enabling a website to serve static content

Creating a folder for static files and a web page

Enabling static and default files

Understanding browser requests during development

Exploring ASP.NET Core Razor Pages

Enabling Razor Pages

Adding code to a Razor Page

Using shared layouts with Razor Pages

Temporarily storing data

Using code-behind files with Razor Pages

Configuring files included in an ASP.NET Core project

Project file build actions

Using Entity Framework Core with ASP.NET Core

Configuring Entity Framework Core as a service

Enabling a model to insert entities

Defining a form to insert a new supplier

Injecting a dependency service into a Razor Page

Configuring services and the HTTP request pipeline

Understanding endpoint routing

Configuring endpoint routing

Reviewing the endpoint routing configuration in our project

Setting up the HTTP pipeline

Summarizing key middleware extension methods

Visualizing the HTTP pipeline

Implementing an anonymous inline delegate as middleware

Practicing and exploring

Exercise 13.1 – Test your knowledge

Exercise 13.2 – Using Razor class libraries

Exercise 13.3 – Enabling HTTP/3 and request decompression support

Exercise 13.4 – Practice building a data-driven web page

Exercise 13.5 – Practice building web pages for functions

Exercise 13.6 – Introducing Bootstrap

Exercise 13.7 – Explore topics

image

Summary

14 Building Websites Using the Model-View-Controller Pattern

Join our book community on Discord

Setting up an ASP.NET Core MVC website

Creating an ASP.NET Core MVC website

Creating the authentication database for SQL Server LocalDB

Changing the port numbers and starting the website

Exploring visitor registration

Reviewing an MVC website project structure

Reviewing the ASP.NET Core Identity database

Exploring an ASP.NET Core MVC website

ASP.NET Core MVC initialization

The default MVC route

Controllers and actions

The ControllerBase class

The Controller class

The responsibilities of a controller

The view search path convention

Logging using the dependency service

Using entity and view models

Implementing views

How cache busting with Tag Helpers works

Customizing an ASP.NET Core MVC website

Defining a custom style

Setting up the category images

Razor syntax and expressions

Defining a typed view

Passing parameters using a route value

Disambiguating action methods

Model binders in detail

Passing a route parameter

Passing a form parameter

Defining views with HTML Helper methods

Defining views with Tag Helpers

Cross-functional filters

Using a filter to define a custom route

Improving performance and scalability using caching

Caching HTTP responses

Output caching endpoints

Output caching MVC views

Varying cached data by query string

Disabling caching to avoid confusion

Querying a database and using display templates

Improving scalability using asynchronous tasks

Making controller action methods asynchronous

Practicing and exploring

Exercise 14.1 – Test your knowledge

Exercise 14.2 – Practice implementing MVC by implementing a category detail page

Exercise 14.3 – Practice improving scalability by understanding and implementing async action methods

Exercise 14.4 – Practice unit testing MVC controllers

Exercise 14.5 – Using a filter to control authorization

Exercise 14.6 – Explore topics

image

Summary

15 Building and Consuming Web Services

Join our book community on Discord

Building web services using the ASP.NET Core Web API

Understanding web service acronyms

Understanding HTTP requests and responses for Web APIs

Creating an ASP.NET Core Web API project

Reviewing the web service's functionality

Creating a web service for the Northwind database

Registering dependency services

Creating data repositories with caching for entities

Routing web services

Route constraints

Short-circuit routes in ASP.NET Core 8

Improved route tooling in ASP.NET Core 8

Understanding action method return types

Configuring the customer repository and Web API controller

Specifying problem details

Controlling XML serialization

Documenting and testing web services

Testing GET requests using a browser

Making GET requests using HTTP/REST tools

Making other requests using HTTP/REST tools

Passing environment variables

Understanding Swagger

Testing requests with Swagger UI

Enabling HTTP logging

Support for logging additional request headers in W3CLogger

Consuming web services using HTTP clients

Understanding HttpClient

Configuring HTTP clients using HttpClientFactory

Getting customers as JSON in the controller

Starting multiple projects

Starting the web service and MVC client projects

Practicing and exploring

Exercise 15.1 – Test your knowledge

Exercise 15.2 – Practice creating and deleting customers with HttpClient

Exercise 15.3 – Implementing advanced features for web services

Exercise 15.4 – Building web services using Minimal APIs

Exercise 15.5 – Explore topics

image

Summary

16 Building User Interfaces Using Blazor

Join our book community on Discord

History of Blazor

JavaScript and friends

Silverlight – C# and .NET using a plugin

WebAssembly – a target for Blazor

Blazor hosting models in .NET 7 and earlier

Unification of Blazor hosting models in .NET 8

Understanding Blazor components

What is the difference between Blazor and Razor?

Reviewing the Blazor Web App project template

Creating a Blazor Web App project

Reviewing Blazor routing, layouts, and navigation

How to define a routable page component

How to navigate routes and pass route parameters

How to use the navigation link component with routes

Understanding base component classes

Running the Blazor Web App project template

Building components using Blazor

Defining and testing a simple Blazor component

Using Bootstrap icons

Making the component a routable page component

Getting entities into a component

Abstracting a service for a Blazor component

Enabling streaming rendering

Defining forms using the EditForm component

Building a customer detail component

Building customer create, edit, and delete components

Enabling server-side interactions

Testing the customer components

Enabling client-side execution using WebAssembly

Practicing and exploring

Exercise 16.1 – Test your knowledge

Exercise 16.2 – Practice by creating a times table component

Exercise 16.3 – Practice by creating a country navigation item

Exercise 16.4 – Enhancing Blazor apps

Exercise 16.5 – Leveraging open source Blazor component libraries

Exercise 16.6 – Explore topics

image

Summary

17 Epilogue

Join our book community on Discord

Next steps on your C# and .NET learning journey

Polishing your skills with design guidelines

Companion books to continue your learning journey

Other books to take your learning further

image

The ninth edition, coming November 2024

Good luck!

Appendix: Answers to the Test Your Knowledge Questions

Join our book community on Discord

Chapter 1 – Hello, C#! Welcome, .NET!

Exercise 1.1 – Test your knowledge

Chapter 2 – Speaking C#

Exercise 2.1 – Test your knowledge

Exercise 2.2 – Test your knowledge of number types

Exercise 2.3 – Practice number sizes and ranges

Chapter 3 – Controlling Flow, Converting Types, and Handling Exceptions

Exercise 3.1 – Test your knowledge

Exercise 3.2 – Explore loops and overflow

Exercise 3.3 – Test your knowledge of operators

Exercise 3.4 – Practice loops and operators

Exercise 3.5 – Practice exception handling

Chapter 4 – Writing, Debugging, and Testing Functions

Exercise 4.1 – Test your knowledge

Exercise 4.2 – Practice writing functions with debugging and unit testing

Chapter 5 – Building Your Own Types with Object-

Oriented Programming

Exercise 5.1 – Test your knowledge

Exercise 5.2 – Practice with access modifiers

Chapter 6 – Implementing Interfaces and Inheriting Classes

Exercise 6.1 – Test your knowledge

image

Exercise 6.2 – Practice creating an inheritance hierarchy

Chapter 7 – Packaging and Distributing .NET Types

Exercise 7.1 – Test your knowledge

Chapter 8 – Working with Common .NET Types

Exercise 8.1 – Test your knowledge

Exercise 8.2 – Practice regular expressions

Exercise 8.3 – Practice writing extension methods

Chapter 9 – Working with Files, Streams, and Serialization

Exercise 9.1 – Test your knowledge

Exercise 9.2 – Practice serializing as XML

Chapter 10 – Working with Data Using Entity Framework Core

Exercise 10.1 – Test your knowledge

Exercise 10.2 – Exporting data using different serialization formats

image

Chapter 11 – Querying and Manipulating Data Using LINQ

image

Exercise 11.1 – Test your knowledge

Exercise 11.2 – Practice querying with LINQ

Chapter 12 – Introducing Web Development Using ASP.NET Core

Exercise 12.1 – Test your knowledge

Exercise 12.2 – Know your webbreviations

Chapter 13 – Building Websites Using ASP. NET Core Razor Pages

Exercise 13.1 – Test your knowledge

Exercise 13.4 – Practice building a data-driven web page

Exercise 13.5 – Practice building web pages for functions

Chapter 14 – Building Websites Using the Model-View-

Controller Pattern

Exercise 14.1 – Test your knowledge

Exercise 14.2 – Practice implementing MVC by implementing a category detail page

Exercise 14.4 – Practice unit testing MVC controllers

Chapter 15 – Building and Consuming Web Services

Exercise 15.1 – Test your knowledge

Exercise 15.2 – Practice creating and deleting customers with HttpClient

Chapter 16 – Building User Interfaces Using Blazor

Exercise 16.1 – Test your knowledge

C# 12 and .NET 8 – Modern Cross- Platform Development Fundamentals, Eighth Edition: Start building websites and services with ASP.NET Core 8, Blazor, and EF Core 8‌

Welcome to Packt Early Access. We’re giving you an exclusive preview of this book before it goes on sale. It can take many months to write a book, but our authors have cutting-edge information to share with you today. Early Access gives you an insight into the latest developments by making chapter drafts available. The chapters may be a little rough around the edges right now, but our authors will update them over time.You can dip in and out of this book or follow along from start to finish; Early Access is designed to be flexible. We hope you enjoy getting to know more about the process of writing a Packt book.

Chapter 1: Hello, C#! Welcome, .NET!

Chapter 2: Speaking C#

Chapter 3: Controlling Flow, Converting Types, and Handling Exceptions

Chapter 4: Writing, Debugging, and Testing Functions

Chapter 5: Building Your Own Types with Object-Oriented Programming

Chapter 6: Implementing Interfaces and Inheriting Classes

Chapter 7: Packaging and Distributing .NET Types

Chapter 8: Working with Common .NET Types

Chapter 9: Working with Files, Streams, and Serialization

Chapter 10: Working with Data Using Entity Framework Core

Chapter 11: Querying and Manipulating Data Using LINQ

Chapter 12: Introducing Web Development Using ASP.NET Core

Chapter 13: Building Websites Using ASP.NET Core Razor Pages

Chapter 14: Building Websites Using the Model-View- Controller Pattern

Chapter 15: Building and Consuming Web Services

Chapter 16: Building User Interfaces Using Blazor

Hello, C#! Welcome, .NET!‌‌‌

Join our book community on Discord‌

https://packt.link/EarlyAccess

image

In this first chapter, the goals are setting up your development environment; understanding the similarities and differences between modern .NET, .NET Core, .NET Framework, Mono, Xamarin, and .NET Standard; creating the simplest application possible with C# 12 and .NET 8 using various code editors; and then discovering good places to look for help.This chapter covers the following topics:

image

image

image

Introducing this book and its contents Setting up your development environment Understanding .NET

image

image

Building console apps using Visual Studio 2022 Building console apps using Visual Studio Code

image

image

Making good use of the GitHub repository for this book Looking for help

Introducing this book and its contents

Let's get started by introducing you to the code solutions and structure of this book.

Getting code solutions for this book‌

The GitHub repository for this book has solutions using full application projects for all code tasks and exercises, found at the following link:https://github.com/markjprice/cs12dotnet8After navigating to the GitHub repository in your web browser, press the . (dot) key on your keyboard, or manually change .com to .dev in the link to convert the repository into a live code editor based on Visual Studio Code using GitHub Codespaces, as shown in Figure 1.1:

image

Figure 1.1: GitHub Codespaces live editing the book's GitHub repository

We provide you with a PDF file that has color images of the screenshots and diagrams used in this book. You can download this file from https://static.packt- cdn.com/downloads/???_ColorImages.pdf.

Visual Studio Code in a web browser is great to run alongside your chosen local code editor as you work through the book's coding tasks. You can compare your code to the solution code and easily copy and paste parts if needed.

You do not need to use or know anything about Git to get the solution code of this book. You can download a ZIP file containing all the code solutions by using the following direct link and then extract the ZIP file into your local filesystem: https://github.com/markjprice/cs12dotnet8/archive/refs/heads/main.zip.

.NET terms used in this book‌

Throughout this book, I use the term modern .NET to refer to .NET 8 and its predecessors like .NET 6 that derive from .NET Core. I use the term legacy .NET to refer to .NET Framework, Mono, Xamarin, and .NET Standard. Modern .NET is a unification of those legacy platforms and standards.

The structure and style of this book‌

After this first chapter, the book can be divided into three parts: language, libraries, and web development.First, the grammar and vocabulary of the C# language; second, the types available in the .NET libraries for building app features; and third, the fundamentals of cross-platform websites, services, and browser apps that you can build using C# and

.NET.Most people learn complex topics best by imitation and repetition rather than reading a detailed explanation of the theory; therefore, I will not overload you with detailed explanations of every step throughout this book. The idea is to get you to write some code and see it run.You don't need to know all the nitty-gritty details immediately. That will be something that comes with time as you build your own apps and go beyond what any book can teach you.In the words of Samuel Johnson, author of the English dictionary in 1755, I have committed "a few wild blunders, and risible absurdities, from which no work of such multiplicity is free." I take sole responsibility for these and hope you appreciate the challenge of my attempt to lash the wind by writing this book about rapidly evolving technologies like C# and .NET, and the apps that you can build with them.

If you have a complaint about this book, then please contact me before writing a negative review on Amazon. Authors cannot respond to Amazon reviews so I cannot contact you to resolve the problem and help you or listen to your feedback and try to do better in the next edition. Please ask a question on the Discord channel for this book at https://packt.link/cs12dotnet8, email me at markjprice@gmail.com , or raise an issue in the GitHub repository for the book at the following link: https://github.com/markjprice/cs12dotnet8/issues.

Topics covered by this book‌

The following topics are covered in this book:

image

Language fundamentals: Fundamental features of the C# language, from declaring variables to writing functions and object-oriented programming.

image

Library fundamentals: Fundamental features of the .NET base class library as well as some important optional packages for common tasks like database access.

image

Web development fundamentals: Fundamental features of the ASP.NET Core framework for server-side and client-side website and web service development.

Topics covered by Apps and Services with .NET 8‌

The following topics are available in a companion book, Apps and Services with .NET 8:

image

Data: SQL Server, Azure Cosmos DB.

image

Specialized libraries: Dates, times, time zones, and internationalization; common third-party libraries for image handling, logging, mapping, and generating PDFs; multitasking and concurrency; and many more.

image

Services: Caching, queuing, background services, gRPC, GraphQL, Azure Functions, SignalR, and Minimal APIs.

image

User Interfaces: ASP.NET Core, Blazor, and .NET MAUI.

This book, C# 12 and .NET 8 – Modern Cross-Platform Development Fundamentals, is best read linearly, chapter by chapter, because it builds up fundamental skills and knowledge. The companion book, Apps and Services with .NET 8, can be read more like a cookbook, so if you are especially interested in building gRPC services, then you could read that chapter without the preceding chapters about Minimal API services. To see a list of all books I have published with Packt, you can use the following link:https://subscription.packtpub.com/search?query=mark+j.+priceA similar list is available on Amazon:https://www.amazon.com/Mark-J-Price/e/B071DW3QGN/You can search other book-selling sites for my books too.

Setting up your development environment‌

Before you start programming, you'll need a code editor for C#. Microsoft has a family of code editors and Integrated Development Environments (IDEs), which include:

image

Visual Studio 2022 for Windows

image

image

Visual Studio Code for Windows, Mac, or Linux Visual Studio Code for the Web or GitHub Codespaces

Third parties have created their own C# code editors, for example, JetBrains Rider, which is available for Windows, Mac, or Linux but does have a license cost. JetBrains Rider is popular with more experienced .NET developers.

Warning! Although JetBrains is a fantastic company with great products, both Rider and the ReSharper extension for Visual Studio are software, and all software has bugs and quirky behavior. For example, they might show errors like "Cannot resolve symbol" in your Razor Pages, Razor views, and Blazor components. Yet you can build and run those files because there is no actual problem. If you have installed the Unity Support plugin then it will complain about boxing operations (which are a genuine problem for Unity game developers), but in projects that are not Unity so the warning does not apply.

Choosing the appropriate tool and application type for learning‌

What is the best tool and application type for learning C# and .NET?When learning, the best tool is one that helps you write code and configuration but does not hide what is really happening. IDEs provide graphical user interfaces that are friendly to use, but what are they doing for you underneath? A more basic code editor that is closer to the action while providing help to write your code can be better while you are learning.Having said that, you can make the argument that the best tool is the one you are already familiar with or that you or your team will use as your daily development tool. For that reason, I want you to be free to choose any C# code editor or IDE to complete the coding tasks in this book, including Visual Studio Code, Visual Studio 2022, or even JetBrains Rider.In this book, I give detailed step-by-step instructions in Chapter 1 for how to create multiple projects in both Visual Studio 2022 for Windows and Visual Studio Code. There are also links to online instructions for other code editors, as shown at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/code-editors/README.md.In subsequent chapters, I will only give the names of projects along with general instructions so you can use whichever tool you prefer. The best application type for learning the C# language constructs and many of the .NET libraries is one that does not distract with unnecessary application code. For example, there is no need to create an entire Windows desktop application or a website just to learn how to write a switch statement.For that reason, I believe the best method for learning the C# and .NET topics in Chapters 1 to 11 is to build console apps. Then, in Chapters 12 to 16, you will build websites, services, and web browser apps.

Pros and cons of the Polyglot Notebooks extension

The Polyglot Notebooks extension for Visual Studio Code provides an easy and safe place to write simple code snippets for experimenting and learning. For example, data scientists use them to analyze and visualize data. Students use them to learn how to write small pieces of code for language constructs and to explore APIs. Polyglot Notebooks enables you to create a single notebook file that mixes "cells" of Markdown (richly formatted text) and code using C# and other related languages, such as PowerShell, F#, and SQL (for databases). The extension does this by hosting an instance of the .NET Interactive engine.

The old legacy name for the Polyglot Notebooks extension was the .NET Interactive Notebooks extension but it was renamed because it is not limited to only .NET languages like C# and F#. The extension retains its original identifier,

ms-dotnettools.dotnet-interactive-vscode .

Polyglot Notebooks have some limitations:

image

They cannot be used to create websites, services, and apps.

image

You cannot use Console class methods like ReadLine or ReadKey to get input from the user. (But there are alternative methods that you will learn if you complete the optional online-only exercise at the end of this chapter.)

image

They cannot have arguments passed to them.

image

image

They do not allow you to define your own namespaces. They do not have any debugging tools (yet).

At the end of this chapter, you will have the opportunity to complete an optional exercise to practice using Polyglot Notebooks.

Visual Studio Code for cross-platform development

The most modern and lightweight code editor to choose from, and the only one from Microsoft that is cross-platform, is Visual Studio Code. It can run on all common operating systems, including Windows, macOS, and many varieties of Linux, including Red Hat Enterprise Linux (RHEL) and Ubuntu.Visual Studio Code is a good choice for modern cross-platform development because it has an extensive and growing set of extensions to support many languages beyond C#. The most important extension for C# and .NET developers is the C# Dev Kit that was released in preview in June 2023 because it turns Visual Studio Code from a general-purpose code editor into a tool optimized for C# and .NET developers.

More Information: You can read about the C# Dev Kit extension in the official announcement at the following link: https://devblogs.microsoft.com/visualstudio/announcing-csharp-dev-kit-for-visual-studio- code/.

Being cross-platform and lightweight, Visual Studio Code and its extensions can be installed on all platforms that your apps will be deployed to for quick bug fixes and so on. Choosing Visual Studio Code means a developer can use a cross-platform code editor to develop cross- platform apps. Visual Studio Code is supported on ARM processors so that you can develop on Apple Silicon computers and Raspberry Pi computers.Visual Studio Code has strong support for web development, although it currently has weak support for mobile and desktop development.Visual Studio Code is by far the most popular code editor or IDE, with over 73% of professional developers selecting it in the Stack Overflow 2023 survey that you can read at the following link: https://survey.stackoverflow.co/2023/.

GitHub Codespaces for development in the cloud

GitHub Codespaces is a fully configured development environment based on Visual Studio Code that can be spun up in an environment hosted in the cloud and accessed through any web browser. It supports Git repos, extensions, and a built-in command-line interface so you can edit, run, and test from any device.

More Information: You can learn more about GitHub Codespaces at the following link: https://github.com/features/codespaces.

Visual Studio 2022 for general development

Visual Studio 2022 for Windows can create most types of applications, including console apps, websites, web services, desktop, and mobile apps. Although you can use Visual Studio 2022 for Windows to write a cross-platform mobile app, you still need macOS and Xcode to compile it.It only runs on Windows 10 version 1909 or later, Home, Professional, Education, or Enterprise; or on Windows 11 version 21H2 or later, Home, Pro, Pro Education, Pro for Workstations, Enterprise, or Education. Windows Server 2016 and later are also supported. 32-bit operating systems and Windows S mode are not supported.

Warning! Visual Studio 2022 for Mac does not officially support .NET 8 and it will reach end-of-life in August 2024. If you have been using Visual Studio 2022 for Mac then you should switch to Visual Studio Code for Mac, JetBrains Rider for Mac, or use Visual Studio 2022 for Windows in a virtual machine on your local computer or in the cloud using a technology like Microsoft Dev Box. The retirement announcement can be read here: https://devblogs.microsoft.com/visualstudio/visual-studio-for-mac-retirement- announcement/.

What I used

To write and test the code for this book, I used the following hardware and software:

image

Visual Studio 2022 for Windows on:

image

image

Windows 11 on an HP Spectre (Intel) laptop Visual Studio Code on:

image

image

macOS on an Apple Silicon Mac mini (M1) desktop Windows 11 on an HP Spectre (Intel) laptop

image

JetBrains Rider on:

image

image

macOS on an Apple Silicon Mac mini (M1) desktop Windows 11 on an HP Spectre (Intel) laptop

I hope that you have access to a variety of hardware and software too, because seeing the differences in platforms deepens your understanding of development challenges, although any one of the above combinations is enough to learn the fundamentals of C# and .NET and how to build practical apps and websites.

You can learn how to write code with C# and .NET using a Raspberry Pi 400 with Ubuntu Desktop 64-bit by reading an extra article that I wrote at the following link: https://github.com/markjprice/cs12dotnet8/tree/main/docs/raspberry-pi-ubuntu64.

Deploying cross-platform‌

Your choice of code editor and operating system for development does not limit where your code gets deployed..NET 8 supports the following platforms for deployment:

image

Windows: Windows 10 version 1607 or later. Windows 11 version 22000 or later. Windows Server 2012 R2 SP1 or later. Nano Server version 1809 or later.

image

Mac: macOS Catalina version 10.15 or later and in the Rosetta 2 x64 emulator.

image

Linux: Alpine Linux 3.17 or later. Debian 11 or later. Fedora 37 or later. openSUSE 15 or later. Oracle Linux 8 or later. RHEL 8 or later. SUSE Enterprise Linux 12 SP2 or later. Ubuntu 20.04 or later.

image

Android: API 21 or later.

image

iOS and tvOS: 11.0 or later.

image

MacCatalyst: 10.15 or later. 11.0 or later on Arm64.

Warning! .NET support for Windows 7 and 8.1 ended in January 2023: https://github.com/dotnet/core/issues/7556.

Windows Arm64 support in .NET 5 and later means you can develop on, and deploy to, Windows Arm devices like Microsoft's Windows Dev Kit 2023 (formerly known as Project Volterra) and Surface Pro X.

You can review the latest supported operating systems and versions at the following link: https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md.

Downloading and installing Visual Studio 2022‌

Many professional .NET developers use Visual Studio 2022 for Windows in their day-to-day development work. Even if you choose to use Visual Studio Code to complete the coding tasks in this book, you might want to familiarize yourself with Visual Studio 2022 for Windows too. It is not until you have written a decent amount of code with a tool that you can really judge if it fits your needs.If you do not have a Windows computer, then you can skip this section and continue to the next section where you will download and install Visual Studio Code on macOS or Linux.Since October 2014, Microsoft has made a professional-quality edition of Visual Studio available to students, open-source contributors, and individuals for free. It is called Community Edition. Any of the editions are suitable for this book. If you have not already installed it, let's do so now:

Download Microsoft Visual Studio 2022 version 17.8 or later for Windows from the following link: https://visualstudio.microsoft.com/downloads/.

Run the installer to start the installation.

On the Workloads tab, select the following:

image

ASP.NET and web development.

image

.NET desktop development (because this includes console apps).

image

Desktop development with C++ with all default components (because this enables publishing console apps and web services that start faster and have smaller memory footprints).

Click Install and wait for the installer to acquire the selected software and install it.

When the installation is complete, click Launch.

The first time that you run Visual Studio, you will be prompted to sign in. If you have a Microsoft account, you can use that account. If you don't, then register for a new one at the following link: https://signup.live.com/.

The first time that you run Visual Studio, you will be prompted to configure your environment. For Development Settings, choose Visual C#. For the color theme, I chose Blue, but you can choose whatever tickles your fancy.

If you want to customize your keyboard shortcuts, navigate to Tools | Options…, and then select the Keyboard section.

Keyboard shortcuts for Visual Studio 2022 for Windows

In this book, I will avoid showing keyboard shortcuts since they are often customized. Where they are consistent across code editors and commonly used, I will try to show them. If you want to identify and customize your keyboard shortcuts, then you can, as shown at the following link: https://learn.microsoft.com/en-us/visualstudio/ide/identifying-and- customizing-keyboard-shortcuts-in-visual-studio.

Downloading and installing Visual Studio Code‌

Visual Studio Code has rapidly improved over the past couple of years and has pleasantly surprised Microsoft with its popularity. If you are brave and like to live on the bleeding edge, then there is the Insiders edition, which is a daily build of the next version.Even

if you plan to only use Visual Studio 2022 for Windows for development, I recommend that you download and install Visual Studio Code and try the coding tasks in this chapter using it, and then decide if you want to stick with just using Visual Studio 2022 for the rest of the book.Let's now download and install Visual Studio Code, the .NET SDK, and the C# Dev Kit extension:

Download and install either the Stable build or the Insiders edition of Visual Studio Code from the following link: https://code.visualstudio.com/.

More Information: If you need more help installing Visual Studio Code, you can read the official setup guide at the following link: https://code.visualstudio.com/docs/setup/setup-overview.

Download and install the .NET SDKs for version 8.0 and at least one other version like

6.0 or 7.0 from the following link: https://www.microsoft.com/net/download.

In real life, you are extremely unlikely to only have one .NET SDK version installed on your computer. To learn how to control which .NET SDK version is used to build a project, we need multiple versions installed. .NET 6, .NET 7, and .NET 8 are supported versions at the time of publishing in November 2023. You can safely install multiple SDKs side by side. The most recent SDK will be used to build your projects.

To install the C# Dev Kit extension with a user interface, you must first launch the Visual Studio Code application.

In Visual Studio Code, click the Extensions icon or navigate to View | Extensions.

C# Dev Kit is one of the most popular extensions available, so you should see it at the top of the list, and you can enter C# in the search box.

C# Dev Kit has a dependency on the C# extension version 2.0 or later, so you do not have to install the C# extension separately. Note that C# extension version 2.0 or later no longer uses OmniSharp since it has a new Language Service Protocol (LSP) host. C# Dev Kit also has dependencies on the .NET Install Tool for Extension Authors and IntelliCode for C# Dev Kit extensions so they will be installed too.

Click Install and wait for supporting packages to download and install.

Good Practice: Be sure to read the license agreement for the C# Dev Kit. It has a more restrictive license than the C# extension: https://aka.ms/vs/csdevkit/license.

Installing other extensions

In later chapters of this book, you will use more Visual Studio Code extensions. If you want to install them now, all the extensions that we will use are shown in Table 1.1:

Extension name and identifier Description

C# Dev Kit

ms-dotnettools.csdevkit

Official C# extension from Microsoft. Manage your code with a solution explorer and test your code with integrated unit test discovery and execution.

Includes the C# and IntelliCode for C# Dev Kit extensions.

C# editing support, including syntax

C#

ms-dotnettools.csharp

IntelliCode for C# Dev Kit

ms-dotnettools.vscodeintellicode-csharp

highlighting, IntelliSense, Go To Definition, Find All References, debugging support for

.NET, and support for csproj projects on Windows, macOS, and Linux.

Provides AI-assisted development features for Python, TypeScript/JavaScript, C#, and Java developers.

MSBuild project tools

tintoy.msbuild-project-tools

Provides IntelliSense for MSBuild project files, including autocomplete for

elements.

Polyglot Notebooks

ms-dotnettools.dotnet-interactive-vscode

ilspy-vscode

icsharpcode.ilspy-vscode

This extension adds support for using .NET and other languages in a notebook. It has a dependency on the Jupyter extension (

ms-toolsai.jupyter ), which itself has dependencies.

Decompile MSIL assemblies – support for modern .NET, .NET Framework, .NET Core, and

.NET Standard.

REST Client

humao.rest-client

Send an HTTP request and view the response directly in Visual Studio Code.

Table 1.1: Visual Studio Code extensions used in this book

Managing Visual Studio Code extensions at the command prompt

You can install a Visual Studio Code extension at the command prompt or terminal, as shown in Table 1.2:

Command Description

code --list-extensions List installed extensions.

code --install-extension Install the specified extension. code --uninstall-extension Uninstall the specified extension. Table 1.2: Commands to list, install, and uninstall extensions

For example, to install the C# Dev Kit extension, enter the following at the command prompt:

code --install-extension ms-dotnettools.csdevkit

I have created PowerShell scripts to install and uninstall the Visual Studio Code extensions in the preceding table. You can find them at the following link: https://github.com/markjprice/cs12dotnet8/tree/main/scripts/extension-scripts/.

Understanding Visual Studio Code versions

Microsoft releases a new feature version of Visual Studio Code (almost) every month and bug- fix versions more frequently. For example:

image

image

Version 1.79.0, May 2023 feature release Version 1.79.1, May 2023 bug fix release

The version used in this book is 1.82.1, August 2023 feature release, but the version of Visual Studio Code is less important than the version of the C# Dev Kit or C# extension that you install. I recommend C# extension v2.2.10 or later and C# Dev Kit v0.4.10 or later.While the C# extension is not required, it provides IntelliSense as you type, code navigation, and debugging features, so it's something that's very handy to install and keep updated to support the latest C# language features.

Keyboard shortcuts for Visual Studio Code

If you want to customize your keyboard shortcuts for Visual Studio Code, then you can, as shown at the following link: https://code.visualstudio.com/docs/getstarted/keybindings.I recommend that you download a PDF of Visual Studio Code keyboard shortcuts for your operating system from the following list:

image

image

image

Windows: https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf macOS: https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf Linux: https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf

Understanding .NET‌

.NET, .NET Core, .NET Framework, and Xamarin are related and overlapping platforms for developers used to build applications and services.

“Those who cannot remember the past are condemned to repeat it.” -- George Santayana

If you are not familiar with the history of .NET, then I introduce you to each of these .NET concepts at the following

link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch01-dotnet-history.md

Understanding .NET support‌

.NET versions are either Long Term Support (LTS), Standard Term Support (STS) (formerly known as Current), or Preview, as described in the following list:

image

LTS releases are a good choice for applications that you do not intend to update frequently, although you must update the .NET runtime for your production code monthly. LTS releases are supported by Microsoft for 3 years after General Availability (GA), or

1 year after the next LTS release ships, whichever is longer.

image

STS releases include features that may change based on feedback. These are a good choice for applications that you are actively developing because they provide access to the latest improvements. STS releases are supported by Microsoft for 18 months after GA, or

6 months after the next STS or LTS release ships, whichever is longer.

image

Preview releases are for public testing. These are a good choice for adventurous programmers who want to live on the bleeding edge, or programming book authors who need to have early access to new language features, libraries, and app and service platforms. Preview releases are not usually supported by Microsoft, but some preview or Release Candidate (RC) releases may be declared Go Live, meaning they are supported by Microsoft in production.

STS and LTS releases receive critical patches throughout their lifetime for security and reliability.

Good Practice: You must stay up to date with the latest patches to get support. For example, if a system is running on .NET runtime version 8.0.0 and then version 8.0.1 is released, you must install version 8.0.1 to get support. These updates are released on the second Tuesday of each month, aka Patch Tuesday.

To better understand your choices of STS and LTS releases, it is helpful to see it visually, with 3-year-long black bars for LTS releases, and 1½-year-long gray bars for STS releases, as shown in Figure 1.2:

image

Figure 1.2: Support durations for recent and planned STS and LTS releases

During the lifetime of .NET 8, two older versions will reach end-of-life and two new versions will be released. I have tried to be cognizant that you might choose to use .NET 9 or .NET 10 with this book, although, obviously, the book cannot cover new features of those future versions!

End of support or end-of-life (EOL) means the date after which bug fixes, security updates, or technical assistance are no longer available from Microsoft.

If you need long-term support from Microsoft, then choose .NET 8 today and stick with it even after .NET 9 releases in 2024. This is because .NET 9 will be an STS release, and it will therefore lose support in May 2026, before .NET 8 does in November 2026. As soon as

.NET 10 is released, start upgrading your .NET 8 projects to it. You will have a year to do so before .NET 8 reaches its end-of-life.

Good Practice: Remember that with all releases, you must upgrade to bug-fix releases like .NET runtime 8.0.1 and .NET SDK 8.0.101, which are expected to release in December 2023, because updates are released every month.

At the time of publishing in November 2023, all versions of modern .NET have reached their end of life except those shown in the following list that are ordered by their end-of-life dates:

image

.NET 7 will reach end-of-life on May 14, 2024.

image

.NET 6 will reach end-of-life on November 12, 2024.

image

.NET 8 will reach end-of-life on November 10, 2026.

You can check which .NET versions are currently supported and when they will reach end- of-life at the following link: https://github.com/dotnet/core/blob/main/releases.md.

Understanding .NET support phases‌

The lifetime of a version of .NET passes through several phases, during which they have varying levels of support, as described in the following list:

image

Preview: Not supported. .NET 8 Preview 1 to Preview 7 were in this support phase from February 2023 to August 2023.

image

Go Live: Supported until GA, then becomes immediately unsupported so you must upgrade to the final release version as soon as it is available. .NET 8 Release Candidate 1 and Release Candidate 2 were in this support phase in September and October 2023.

image

image

image

Active: .NET 8 will be in this support phase from November 2023 to May 2026. Maintenance: Supported only with security fixes for the last six months of its lifetime. .NET 8 will be in this support phase from May 2026 to November 2026. End-of-life: Not supported. .NET 8 will reach its end-of-life in November 2026.

Understanding .NET Runtime and .NET SDK versions‌

If you have not built a standalone app, then the .NET Runtime is the minimum needed to install so that an operating system can run a .NET application. The .NET SDK includes the

.NET Runtime as well as the compilers and other tools needed to build .NET code and apps..NET Runtime versioning follows semantic versioning, that is, a major increment indicates breaking changes, minor increments indicate new features, and patch increments indicate bug fixes..NET SDK versioning does not follow semantic versioning. The major and minor version numbers are tied to the runtime version it is matched with. The patch number follows a convention that indicates the major and minor versions of the SDK.You can see an example of this in Table 1.3:

Change Runtime SDK

Initial release

8.0.0

8.0.100

SDK bug fix

8.0.0

8.0.101

Runtime and SDK bug

fix 8.0.1

8.0.102

SDK new feature

8.0.1

8.0.200

Table 1.3: Examples

of changes

and versions for a .NET runtime and SDK

Listing and removing versions of .NET‌

.NET Runtime updates are compatible with a major version such as 8.x, and updated releases of the .NET SDK maintain the ability to build applications that target previous versions of the runtime, which enables the safe removal of older versions.You can see which SDKs and runtimes are currently installed using the following commands:

dotnet --list-sdks dotnet --list-runtimes dotnet --info

Good Practice: To make it easier to enter commands at the command prompt or terminal, the following link lists all commands throughout the book as a single statement that can be easily copied and pasted: https://github.com/markjprice/cs12dotnet8/blob/main/docs/command-lines.md.

On Windows, use the Apps & features section to remove .NET SDKs.On Linux, there is no single mechanism, but you can learn more at the following link:https://learn.microsoft.com/en-us/dotnet/core/install/remove-runtime-sdk-versions? pivots=os-linux

You could use a third-party tool like Dots, the friendly .NET SDK manager, found at the following link: https://johnnys.news/2023/01/Dots-a-dotnet-SDK-manager. At the time of

writing, you must build the app from source on its GitHub repository, so I only recommend that for advanced developers.

Understanding intermediate language‌

The C# compiler (named Roslyn) used by the dotnet CLI tool converts your C# source code into intermediate language (IL) code and stores the IL in an assembly (a DLL or EXE file). IL code statements are like assembly language instructions, which are executed by .NET's virtual machine, known as CoreCLR.At runtime, CoreCLR loads the IL code from the assembly, the just-in-time (JIT) compiler compiles it into native CPU instructions, and then it is executed by the CPU on your machine.The benefit of this two-step compilation process is that Microsoft can create Common Language Runtimes (CLRs) for Linux and macOS, as well as for Windows. The same IL code runs everywhere because of the second compilation step, which generates code for the native operating system and CPU instruction set.Regardless of which language the source code is written in, for example, C#, Visual Basic, or F#, all .NET applications use IL code for their instructions stored in an assembly. Microsoft and others provide disassembler tools that can open an assembly and reveal this IL code, such as the ILSpy .NET Decompiler extension. You will learn more about this in Chapter 7, Packaging and Distributing .NET Types.

Comparing .NET technologies‌

We can summarize and compare .NET technologies today, as shown in Table 1.4:

Technology Description Host operating

systems

Modern

.NET

.NET

Framework

A modern feature set, with full C# 8 to C# 12 language support. It can be used to port existing apps or create new desktop, mobile, and web apps and services. It can target older .NET platforms.

A legacy feature set with limited C# 8 support and no C# 9 or later support. It should be used to maintain existing applications only.

Windows, macOS, Linux, Android, iOS, tvOS, Tizen

Windows only

Xamarin Mobile and desktop apps only. Android, iOS, macOS

Table 1.4: Comparison of .NET technologies

Managing multiple projects using code editors‌

Visual Studio 2022 for Windows, JetBrains Rider, and even Visual Studio Code (with the C# Dev Kit extension installed) all have a concept called a solution that allows you to open and manage multiple projects simultaneously. We will use a solution to manage the two projects that you will create in this chapter.

Building console apps using Visual Studio 2022‌

The goal of this section is to showcase how to build a console app using Visual Studio 2022 for Windows.If you do not have a Windows computer or want to use Visual Studio Code, then you can skip this section since the code will be the same; just the tooling experience is different. But I recommend that you review this section because it does explain some of the code and how top-level programs work, and that information applies to all code editors.This section is also available in the GitHub repository (so it can be updated after publishing if

needed) at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/code- editors/vs4win.mdIf you want to see similar instructions for using JetBrains Rider, they are available in the GitHub repository at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/code-editors/rider.md

Writing code using Visual Studio 2022‌

Let's get started writing code:

Start Visual Studio 2022. You must have installed version 17.8 or later to see the new

Welcome tab that replaces the old modal dialog box.

In the Welcome tab, click New Project, as shown in Figure 1.3:

image

Figure 1.3: Welcome tab with buttons like New Project

In the Create a new project dialog, select the C# language to filter the project templates, and then enter console in the Search for templates box, and select Console App, making sure that you have chosen the cross-platform project template, not the one for .NET Framework, which is Windows-only, and the C# project template rather than another language, such as Visual Basic or TypeScript, so that it is selected as shown in Figure 1.4:

image

Figure 1.4: Selecting the C# Console App project template for modern cross-platform .NET

Click Next.

In the Configure your new project dialog, enter HelloCS for the project name, enter

C:\cs12dotnet8 for the location, and enter Chapter01 for the solution name.

More screenshots of Visual Studio 2022 when creating new projects can be found in the GitHub repository at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch01-project-options.md.

Click Next.

In the Additional information dialog, in the Framework drop-down list, note that your

.NET SDK choices indicate if that version is Standard Term Support, Long Term Support, Preview, or Out of support, and then select .NET 8.0 (Long Term Support).

You can install as many .NET SDK versions as you like. If you are missing a .NET SDK version, then you can install it from the following link: https://dotnet.microsoft.com/en-us/download/dotnet.

Leave the check box labeled Do not use top-level statements cleared. (Later in this chapter, you will create a console app that selects this option so you can see the difference.)

Leave the check box labeled Enable native AOT publish cleared. You will learn what this option does in Chapter 7, Packaging and Distributing .NET Types.

Click Create.

If you cannot see the Solution Explorer, then navigate to View | Solution Explorer.

If code is not shown, then in Solution Explorer, double-click the file named Program.cs to open it, and note that Solution Explorer shows the HelloCS project, as shown in Figure 1.5:

image

Figure 1.5: Editing Program.cs in Visual Studio 2022

In Program.cs , note the code consists of only a comment and a single statement, as shown in the following code:

// See https://aka.ms/new-console-template for more information Console.WriteLine("Hello, World!");

This template uses the top-level program feature introduced in C# 9, which I will explain later in this chapter. As the comment in the code says, you can read more about this template at the following link: https://aka.ms/new-console-template.

1. In Program.cs , modify line 2 so that the text that is being written to the console says

Hello, C#!

All code examples and commands that the reader must review or type are shown in plain text so you will never have to read code or commands from a screenshot like in Figure 1.5, which might be too small or too faint in print.

Compiling and running code using Visual Studio‌

The next task is to compile and run the code:

In Visual Studio, navigate to Debug | Start Without Debugging.

Good Practice: When you start a project in Visual Studio 2022, you can choose to attach a debugger or not. If you do not need to debug, then it is better not to attach one because attaching a debugger requires more resources and slows everything down. Attaching a debugger also limits you to only starting one project. If you want to run more than one project, each with a debugger attached, then you must start multiple instances of Visual Studio. In the toolbar, click the green outline triangle button (to the right of HelloCS in Figure 1.5) to start without debugging instead of the green solid triangle button (to the left of HelloCS in Figure 1.5), unless you need to debug.

The output in the console window will show the result of running your application, as shown in Figure 1.6:

image

Figure 1.6: Running the console app on Windows

Press any key to close the console app window and return to Visual Studio.

Optionally, close the Properties pane to make more vertical space for Solution Explorer.

Double-click the HelloCS project and note the HelloCS.csproj project file shows that this project has its target framework set to net8.0 , as shown in Figure 1.7.

In the Solution Explorer toolbar, toggle on the Show All Files button

image

, and note that the compiler-generated bin and obj folders are visible, as shown in

Figure 1.7:

image

Figure 1.7: Showing the compiler-generated folders and files

Understanding the compiler-generated folders and files‌

Two compiler-generated folders were created, named obj and bin , as described in the following list:

image

The obj folder contains one compiled object file for each source code file. These objects haven't been linked together into a final executable yet.

image

The bin folder contains the binary executable for the application or class library. We will look at this in more detail in Chapter 7, Packaging and Distributing .NET Types.

You do not need to look inside these folders or understand their files yet (but feel free to browse around if you are curious). Just be aware that the compiler needs to create temporary folders and files to do its work. You could delete these folders and their files, and they will be automatically recreated the next time you "build" or run the project. Developers often delete these temporary folders and files to "clean" a project. Visual Studio even has a command on the Build menu named Clean Solution that deletes some of these temporary files for you. The equivalent command with Visual Studio Code is dotnet clean .

Understanding top-level programs‌

If you have seen older .NET projects before, then you might have expected more code, even just to output a simple message. This project has minimal statements because some of the required code is written for you by the compiler when you target .NET 6 or later. If you had created the project with .NET SDK 5 or earlier, or if you had selected the check box labeled Do not use top-level statements, then the Program.cs file would have more statements, as shown in the following code:

using System; namespace HelloCS

{

class Program

{

static void Main(string[] args)

{

Console.WriteLine("Hello, World!");

}

}

}

During compilation with .NET SDK 6 or later, all the boilerplate code to define the Program

class and its Main method is generated and wrapped around the statements you write.This

uses a feature introduced in .NET 5 called top-level programs, but it was not until .NET 6 that Microsoft updated the project template for console apps to use top-level statements by default. Then in .NET 7 or later, Microsoft added options to use the older style if you prefer:

image

If you are using Visual Studio 2022, select the check box labeled Do not use top-level statements.

image

If you are using the dotnet CLI at the command prompt, add a switch:

dotnet new console --use-program-main

Warning! One functional difference is that the auto-generated code does not define a namespace, so the Program class is implicitly defined in an empty namespace with no name instead of a namespace that matches the name of the project.

Requirements for top-level programs‌

Key points to remember about top-level programs include the following:

image

image

There can be only one file like this in a project. Any using statements must be at the top of the file.

image

image

If you declare any classes or other types, they must be at the bottom of the file. Although you should name the method Main if you explicitly define it, the method is named

$ when created by the compiler.

Implicitly imported namespaces‌

The using System; statement at the top of the file imports the System namespace. This enables the Console.WriteLine statement to work. Why do we not have to import it in our project? The trick is that we do still need to import the System namespace, but it is now done for us using a combination of features introduced in C# 10 and .NET 6. Let's see how:

In Solution Explorer, expand the obj folder, expand the Debug folder, expand the

net8.0 folder, and open the file named HelloCS.GlobalUsings.g.cs .

Note that this file is automatically created by the compiler for projects that target

.NET 6 or later, and that it uses a feature introduced in C# 10 called global namespace imports, which imports some commonly used namespaces like System for use in all code files, as shown in the following code:

//

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;

1. In Solution Explorer, click the Show All Files button to hide the bin and obj

folders.

I will explain more about the implicit imports feature in the next chapter. For now, just note that a significant change that happened between .NET 5 and .NET 6 is that many of the project templates, like the one for console apps, use new SDK and language features to hide what is really happening.

Revealing the hidden code by throwing an exception‌

Now let's discover how the hidden code has been written:

1. In Program.cs , after the statement that outputs the message, add a statement to throw a new exception, as shown in the following code:

throw new Exception();

In Visual Studio, navigate to Debug | Start Without Debugging. (Do not start the project with debugging or the exception will be caught by the debugger!)

The output in the console window will show the result of running your application, including that a hidden Program class was defined by the compiler with a method named

$ that has a parameter named args for passing in arguments, as shown in Figure 1.8:

image

Figure 1.8: Throwing an exception to reveal the hidden Program.

$ method

Press any key to close the console app window and return to Visual Studio.

Revealing the namespace for the Program class‌

Now, let's discover what namespace the Program class has been defined within:

1. In Program.cs , before the statement that throws an exception, add statements to get the name of the namespace of the Program class, and then write it to the console, as shown in the following code:

string name = typeof(Program).Namespace ?? "None!"; Console.WriteLine($"Namespace: {name}");

?? is the null-coalescing operator. The first statement means, "If the namespace of Program is null , then return None! ; otherwise, return the name." You will see more explanations of these keywords and operators throughout the book. For now, just enter the code and run it to see what it does.

In Visual Studio, navigate to Debug | Start Without Debugging.

The output in the console window will show the result of running your application, including that the hidden Program class was defined without a namespace, as shown in the following output:

Namespace: None!

1. Press any key to close the console app window and return to Visual Studio.

Adding a second project using Visual Studio 2022‌

Let's add a second project to our solution to explore how to work with multiple projects:

In Visual Studio, navigate to File | Add | New Project….

Warning! The above step adds a new project to the existing solution. Do NOT navigate to File | New | Project…, which instead is meant to be used to create a new project and solution (although the dialog box has a dropdown to choose to add to an existing solution too).

In the Add a new project dialog, in Recent project templates, select Console App [C#]

and then click Next.

In the Configure your new project dialog, for Project name, enter AboutMyEnvironment , leave the location as C:\cs12dotnet8\Chapter01 , and then click Next.

In the Additional information dialog, select .NET 8.0 (Long Term Support) and select the Do not use top-level statements check box.

Warning! Make sure you have selected the Do not use top-level statements check box, so we get to see the older style of Program.cs .

Click Create.

In the AboutMyEnvironment project, in Program.cs , note the statements to define a namespace that matches the project name, an internal class named Program , and a static method named Main with a parameter named args that returns nothing ( void ), as shown in the following code:

namespace AboutMyEnvironment

{

internal class Program

{

static void Main(string[] args)

{

Console.WriteLine("Hello, World!");

}

}

}

1. In Program.cs , in the Main method, replace the existing Console.WriteLine statement with statements to output the current directory, the version of the operating system, and the namespace of the Program class, as shown in the following code:

Console.WriteLine(Environment.CurrentDirectory); Console.WriteLine(Environment.OSVersion.VersionString); Console.WriteLine("Namespace: {0}", typeof(Program).Namespace);

In Solution Explorer, right-click the Chapter01 solution, and then select Configure Startup Projects….

In the Solution 'Chapter01' Property Pages dialog box, set Startup Project to Current selection, and then click OK.

In Solution Explorer, click the AboutMyEnvironment project (or any file or folder within it), and note that Visual Studio indicates that AboutMyEnvironment is now the startup project by making the project name bold.

Good Practice: I recommend this way of setting the startup project because it then makes it very easy to switch startup projects by simply clicking a project (or any file in a project) to make it the startup project. Although you can right-click a project and set it as a startup project, if you then want to run a different

project, you must manually change it again. Simply clicking anywhere in the project is easier. In most chapters, you will only need to run one project at a time. In Chapter 15, Building and Consuming Web Services, I will show you how to configure multiple startup projects.

Navigate to Debug | Start Without Debugging to run the AboutMyEnvironment project, and note the result, as shown in the following output and in Figure 1.9:

C:\cs12dotnet8\Chapter01\AboutMyEnvironment\bin\Debug\net8.0 Microsoft Windows NT 10.0.22621.0

Namespace: AboutMyEnvironment

image

Figure 1.9: Running a console app in a Visual Studio solution with two projects

Windows 11 is just branding. Its official name is Windows NT, and its major version number is still 10! But its patch version is 22000 or higher.

1. Press any key to close the console app window and return to Visual Studio.

When using Visual Studio 2022 for Windows to run a console app, it executes the app from the \bin\Debug\net8.0 folder. It will be important to remember this when we work with the filesystem in later chapters. When using Visual Studio Code, or more accurately, the dotnet CLI, it has different behavior, as you are about to see.

Building console apps using Visual Studio Code‌

The goal of this section is to showcase how to build a console app using Visual Studio Code and the dotnet CLI.If you never want to try Visual Studio Code or the dotnet command-line tool, then please feel free to skip this section, and then continue with the Making good use of the GitHub repository for this book section.Both the instructions and screenshots in this section are for Windows, but the same actions will work with Visual Studio Code on the macOS and Linux variants.The main differences will be native command-line actions such as deleting a file: both the command and the path are likely to be different on Windows or macOS and Linux. Luckily, the dotnet CLI tool itself and its commands are identical on all platforms.

Writing code using Visual Studio Code‌

Let's get started writing code!

Start your favorite tool for working with the filesystem, for example, File Explorer on Windows or Finder on Mac.

Navigate to your C: drive on Windows, your user folder on macOS or Linux (mine are named markjprice and home/markjprice ), or any directory or drive in which you want to save your projects.

Create a new folder named cs12dotnet8 . (If you completed the section for Visual Studio 2022, then this folder will already exist.)

In the cs12dotnet8 folder, create a new folder named Chapter01-vscode .

If you did not complete the section for Visual Studio 2022, then you could name this folder Chapter01 , but I will assume you will want to complete both sections and therefore need to use a non-conflicting name.

In the Chapter01-vscode folder, open the command prompt or terminal. For example, on Windows, right-click in the folder and then select Open in Terminal.

At the command prompt or terminal, use the dotnet CLI to create a new solution named

Chapter01 , as shown in the following command:

dotnet new sln --name Chapter01

You can use either -n or --name as the switch to specify a name. The default would match the name of the folder, for example, Chapter01-vscode .

1. Note the result, as shown in the following output:

The template "Solution File" was created successfully.

1. At the command prompt or terminal, use the dotnet CLI to create a new subfolder and project for a console app named HelloCS , as shown in the following command:

dotnet new console --output HelloCS

You can use either -o or --output as the switch to specify the folder and project name. The dotnet new console command targets your latest .NET SDK version by default. To target a different version, use the -f or --framework switch to specify a target framework. For example, to target .NET 6, as shown in the following command:

dotnet new console -f net6.0

1. At the command prompt or terminal, use the dotnet CLI to add the project to the solution, as shown in the following command:

dotnet sln add HelloCS

1. Note the results, as shown in the following output:

Project HelloCS\HelloCS.csproj added to the solution.

1. At the command prompt or terminal, start Visual Studio Code and open the current folder indicated with a . (dot), as shown in the following command:

code .

If you are prompted, Do you trust the authors of the files in this folder?, then select the Trust the authors of all files in the parent folder 'cs12dotnet8' check box

and then click Yes, I trust the authors.

In Visual Studio Code, in EXPLORER, in the CHAPTER01-VSCODE folder view, expand the HelloCS folder, and you will see that the dotnet command-line tool created two files, HelloCS.csproj and Program.cs , and bin and obj folders, as shown in Figure 1.10:

image

Figure 1.10: EXPLORER shows that two files and a folder have been created

Navigate to View | Output.

In the OUTPUT pane, select C# Dev Kit and note the tool has recognized and processed the solution, as shown in Figure 1.11:

image

Figure 1.11: C# Dev Kit processing a solution file

At the bottom of EXPLORER, note the SOLUTION EXPLORER.

Drag SOLUTION EXPLORER to the top of the EXPLORER pane and expand it.

In SOLUTION EXPLORER, expand the HelloCS project, and then click the file named

Program.cs to open it in the editor window.

In Program.cs , modify line 2 so that the text that is being written to the console says

Hello, C#!

Good Practice: Navigate to File | Auto Save. This toggle will save the annoyance of remembering to save before rebuilding your application each time.

Compiling and running code using the dotnet CLI‌

The next task is to compile and run the code:

In SOLUTION EXPLORER, right-click on any file in the HelloCS project and choose Open In Integrated Terminal.

In TERMINAL, enter the following command: dotnet run .

The output in the TERMINAL window will show the result of running your application, as shown in Figure 1.12:

image

Figure 1.12: The output of running your first console app in Visual Studio Code

In Program.cs , after the statement that outputs the message, add statements to get the name of the namespace of the Program class, write it to the console, and then throw a new exception, as shown in the following code:

string name = typeof(Program).Namespace ?? "None!"; Console.WriteLine($"Namespace: {name}");

throw new Exception();

In TERMINAL, enter the following command: dotnet run .

In TERMINAL, you can press the up and down arrows to loop through previous commands and then press the left and right arrows to edit the command before pressing Enter to run them.

The output in the TERMINAL window will show the result of running your application, including that a hidden Program class was defined by the compiler with a method named

$ that has a parameter named args for passing in arguments, and that it does not have a namespace, as shown in the following output:

Hello, C#!

Namespace: None!

Unhandled exception. System.Exception: Exception of type 'System.Exception' was thrown.

at Program.

$(String[] args) in C:\cs12dotnet8\Chapter01-vscode\HelloCS\Program.cs:line

Adding a second project using Visual Studio Code‌

Let's add a second project to explore how to work with multiple projects:

1. In TERMINAL, change to the Chapter01-vscode directory, as shown in the following command:

cd ..

1. In TERMINAL, enter the command to create a new console app project named

AboutMyEnvironment using the older non-top-level program style, as shown in the

following command:

dotnet new console -o AboutMyEnvironment --use-program-main

Good Practice: Be careful when entering commands in TERMINAL. Be sure that you are in the correct folder before entering potentially destructive commands!

1. In TERMINAL, use the dotnet CLI to add the new project folder to the solution, as shown in the following command:

dotnet sln add AboutMyEnvironment

1. Note the results, as shown in the following output:

Project AboutMyEnvironment\AboutMyEnvironment.csproj added to the solution.

1. In SOLUTION EXPLORER, in the AboutMyEnvironment project, open Program.cs , and then in the Main method, change the existing statement to output the current directory, the operating system version string, and the namespace of the Program class, as shown in the following code:

Console.WriteLine(Environment.CurrentDirectory); Console.WriteLine(Environment.OSVersion.VersionString); Console.WriteLine("Namespace: {0}", typeof(Program).Namespace);

In SOLUTION EXPLORER, right-click on any file in the AboutMyEnvironment project and choose Open In Integrated Terminal.

In TERMINAL, enter the command to run the project, as shown in the following command:

dotnet run .

Note the output in the TERMINAL window, as shown in the following output and in Figure 1.13:

C:\cs12dotnet8\Chapter01-vscode\AboutMyEnvironment Microsoft Windows NT 10.0.22621.0

Namespace: AboutMyEnvironment

image

Figure 1.13: Running a console app in Visual Studio Code with two projects

Once you open multiple terminal windows, you can toggle between them by clicking their names in the panel on the right-hand side of TERMINAL. By default, the name will be one of the common shells like pwsh, powershell, zsh, or bash. Right-click and choose to rename to set something else.

When using Visual Studio Code, or more accurately, the dotnet CLI, to run a console app, it executes the app from the folder. When using Visual Studio 2022 for Windows, it executes the app from the \bin\Debug\net8.0 folder. It will be important to remember this when we work with the filesystem in later chapters.

If you were to run the program on macOS Ventura, the environment operating system would be different, as shown in the following output:

Unix 13.5.2

Good Practice: Although the source code, like the .csproj and .cs files, is identical, the bin and obj folders that are automatically generated by the compiler could have mismatches that give you errors. If you want to open the same project in both Visual Studio 2022 and Visual Studio Code, delete the temporary bin and obj folders before opening the project in the other code editor. This potential problem is why I asked you to create a different folder for the Visual Studio Code projects in this chapter.

Summary of steps for Visual Studio Code‌

Follow these steps to create a solution and projects using Visual Studio Code:

Create a folder for the solution, for example, Chapter01 .

Create a solution file in the folder: dotnet new sln .

Create a folder and project using a template: dotnet new console -o HelloCS .

Add the folder and its project to the solution: dotnet sln add HelloCS .

Repeat steps 3 and 4 to create and add any other projects.

Open the folder containing the solution using Visual Studio Code: code .

Summary of other project types used in this book‌

A Console App / console project is just one type of project template. In this book, you will also create projects using the following project templates, as shown in Table 1.5:

Visual Studio

2022

dotnet new

JetBrains Rider - Type

Console App

console

Console Application

Class Library

classlib

Class Library

xUnit Test Project xunit Unit Test Project - xUnit

ASP.NET Core Empty web ASP.NET Core Web Application - Empty

Razor Class Library razorclasslib ASP.NET Core Web Application - Razor Class

Library

ASP.NET Core Web App (Model-View- Controller)

mvc ASP.NET Core Web Application - Web App (Model-View-Controller)

ASP.NET Core Web API webapi ASP.NET Core Web Application - Web API ASP.NET Core Web API (native AOT) api ASP.NET Core Web Application - Web API

(native AOT)

Blazor Web App blazor ASP.NET Core Web Application - Blazor Web App

Table 1.5: Project template names for various code editors

The steps for adding any type of new project to a solution are the same. Only the type name of the project template differs, and sometimes some command-line switches to control options. I will always specify what those switches and options should be if they differ from the defaults.A summary of project template defaults, options, and switches can be found here: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch01-project-options.md.

Making good use of the GitHub repository for this book‌

Git is a commonly used source code management system. GitHub is a company, website, and desktop application that makes it easier to manage Git. Microsoft purchased GitHub in 2018, so it will continue to get closer integration with Microsoft tools.I created a GitHub repository for this book, and I use it for the following:

image

To store the solution code for the book that can be maintained after the print publication date.

image

To provide extra materials that extend the book, like errata fixes, small improvements, lists of useful links, and optional sections about topics that cannot fit in the printed book.

image

To provide a place for readers to get in touch with me if they have issues with the book.

Good Practice: I strongly recommend that all readers review the errata, improvements, post-publication changes, and common errors pages before attempting any coding task in this book. You can find them at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/errata/errata.md.

Understanding the solution code on GitHub‌

The solution code in the GitHub repository for this book includes folders for each chapter that can be opened with any of the following code editors:

image

image

Visual Studio 2022 or JetBrains Rider: Open the .sln solution file. Visual Studio Code: Open the folder that contains the solution file.

Chapters 1 to 11 each have their own solution file named ChapterXX.sln where XX is the chapter number. Chapters 12 to 16 share a single solution file named PracticalApps.sln .All the code solutions can be found at the following link:https://github.com/markjprice/cs12dotnet8/tree/main/code

Good Practice: If you need to, return to this chapter to remind yourself how to create and manage multiple projects in the code editor of your choice. The GitHub repository has step-by-step instructions for three code editors (Visual Studio 2022, Visual Studio Code, and JetBrains Rider), along with additional screenshots: https://github.com/markjprice/cs12dotnet8/tree/main/docs/code-editors/.

Raising issues with the book‌

If you get stuck following any of the instructions in this book, or if you spot a mistake in the text or the code in the solutions, please raise an issue in the GitHub repository:

Use your favorite browser to navigate to the following link: https://github.com/markjprice/cs12dotnet8/issues.

Click New Issue.

Enter as much detail as possible that will help me to diagnose the issue. For example:

image

The specific section title, page number, and step number.

image

Your code editor, for example, Visual Studio 2022, Visual Studio Code, or something else, including the version number.

image

image

As much of your code and configuration that you feel is relevant and necessary. Description of expected behavior and the behavior experienced.

image

Screenshots (you can drag and drop image files into the Issue box).

The following is less relevant but might be useful:

image

image

Your operating system, for example, Windows 11 64-bit, or macOS Ventura version 13.5.2. Your hardware, for example, Intel, Apple Silicon, or ARM CPU.

I cannot always respond immediately to issues. But I want all my readers to be successful with my book, so if I can help you (and others) without too much trouble, then I will gladly do so.

Giving me feedback‌

If you'd like to give me more general feedback about the book, then either email me at markjprice@gmail.com or ask me a question on Discord in the book channel. You can provide the feedback anonymously, or if you would like a response from me, then you can supply an email address. I will only use this email address to answer your feedback.Please join me and your fellow readers on Discord using this invite: https://packt.link/cs12dotnet8I recommend that you add the preceding link to your favorite bookmarks.I love to hear from my readers about what they like about my book, as well as suggestions for improvements and how they are working with C# and .NET, so don't be shy. Please get in touch!Thank you in advance for your thoughtful and constructive feedback.

Avoiding common mistakes‌

After working through the step-by-step tasks in this book, readers often then strike out on their own and attempt to write similar code, but sometimes they hit problems and either raise an issue in the GitHub repository or post a question to the Discord channel for the book. From these, I have noted some common mistakes, so I maintain a page in the repository to highlight and explain these potential traps and how to fix them:https://github.com/markjprice/cs12dotnet8/blob/main/docs/errata/common-mistakes.md

Downloading solution code from the GitHub repository‌

If you just want to download all the solution files without using Git, click the green Code

button and then select Download ZIP, as shown in Figure 1.14:

image

Figure 1.14: Downloading the repository as a ZIP file

Good Practice: It is best to clone or download the code solutions to a short folder path, like C:\cs12dotnet8\ or C:\book\ , to avoid build-generated files exceeding the maximum path length. You should also avoid special characters like # . For example, do not use a folder name like C:\C# projects\ . That folder name might work for a simple console app project but once you start adding features that automatically generate code, you are likely to have strange issues. Keep your folder names short and simple.

Using Git with Visual Studio Code and the command prompt‌

Visual Studio Code has integrations with Git, but it will use your operating system's Git installation, so you must install Git 2.0 or later first before you get these features.You can install Git from the following link: https://git-scm.com/download.If you like to use a GUI, you can download GitHub Desktop from the following link: https://desktop.github.com.

Cloning the book solution code repository‌

Let's clone the book solution code repository. In the steps that follow, you will use the Visual Studio Code terminal, but you could enter the commands at any command prompt or terminal window:

Create a folder named Repos-vscode in your user or Documents folder, or wherever you want to store your Git repositories.

Open the Repos-vscode folder at the command prompt or terminal, and then enter the following command:

git clone https://github.com/markjprice/cs12dotnet8.git

Note that cloning all the solutions for all the chapters will take a minute or so, so please be patient.

Looking for help‌

This section is all about how to find quality information about programming on the web.

Reading the documentation on Microsoft Learn‌

The definitive resource for getting help with Microsoft developer tools and platforms is in the technical documentation on Microsoft Learn, and you can find it at the following link: https://learn.microsoft.com/en-us/docs.

Documentation links in this book‌

The official Microsoft documentation for .NET needs to cover all versions. The default version shown in the documentation is always the most recent general availability (GA) version.For example, between November 2023 and November 2024, the default version of .NET shown in documentation pages will be 8.0. Between November 2024 and November 2025, the default version of .NET will be 9.0. The following link will automatically direct to the current version depending on the current date:https://learn.microsoft.com/en- us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattributeTo view the documentation page specifically for .NET 7, append ?view=net-7.0 to the end of a link. For example, use the following link:https://learn.microsoft.com/en- us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute?view=net-7.0All documentation links in this book do not specify a version, so after November 2024, they will show the documentation pages for .NET 9.0. If you want to force the documentation to show the version for .NET 8.0, then append ?view=net-8.0 to the end of a link.You can check what versions a .NET feature supports by appending #applies-to to the end of a link, for example:https://learn.microsoft.com/en- us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute#applies-toWe can therefore see that the StringSyntax attribute is only available in .NET 7 or later.

Getting help for the dotnet tool‌

At the command prompt, you can ask the dotnet tool for help with its commands. The syntax is:

dotnet help

This will cause your web browser to open a page in the documentation about the specified command. Common dotnet commands include new , build , run , and many more.

Warning! The dotnet help new command worked with .NET Core 3.1 to .NET 6, but it returns an error with .NET 7 or later:

Specified command 'new' is not a valid SDK command. Specify a valid SDK command. For more info

Hopefully, they will fix that bug soon!

Another type of help is command-line documentation. It follows this syntax:

dotnet -?|-h|--help

For example, dotnet new -? or dotnet new -h or dotnet new --help outputs documentation about the new command at the command prompt.

As you should now expect, dotnet help help opens a web browser for the help command, and dotnet help -h outputs documentation for the help command at the command prompt!

Let's try some examples:

1. To open the official documentation in a web browser window for the dotnet build command, enter the following at the command prompt or in the Visual Studio Code terminal, and note the page opened in your web browser, as shown in Figure 1.15:

dotnet help build

image

Figure 1.15: Web page documentation for the dotnet build command

1. To get help output at the command prompt, use the -? or -h or --help flag, as shown in the following command:

dotnet build -?

1. You will see the following partial output:

Description:

.NET Builder Usage:

dotnet build [...] [options] Arguments:

The project or solution file to operate on. If a file is not specified, Options:

--ucr, --use-current-runtime Use current runtime as the target runtime.

-f, --framework The target framework to build for. The target framework m

...

-?, -h, --help Show command line help.

1. Repeat both types of help request for the following commands: add , help , list , new , and run , remembering that new might not show its web page due to a bug introduced in

.NET 7.

Getting definitions of types and their members‌

One of the most useful features of a code editor is Go To Definition (F12). It is available in Visual Studio Code, Visual Studio 2022, and JetBrains Rider. It will show what the public definition of the type or member looks like by reading the metadata in the compiled assembly.Some tools, such as ILSpy .NET Decompiler, will even reverse-engineer from the metadata and IL code back into C# or another language for you.A similar and related feature

is named Go To Implementation (Ctrl + F12). Instead of reading the metadata or decompiling, this will show the actual source code if that is embedded using the optional source link feature.

Warning! Go To Definition should go to the decompiled metadata for a member or type. But if you have previously viewed source link, then it goes to source link. Go To Implementation should go to the source link implementation for a member or type. But if you have disabled source link, then it goes to the decompiled metadata.

Let's see how to use the Go To Definition feature:

1. In your preferred code editor, open the solution/folder named Chapter01 .

image

image

If you are using Visual Studio 2022: Navigate to Tools | Options.

image

image

In the search box, enter navigation to source . Select Text Editor | C# | Advanced.

image

Clear the Enable navigation to Source Link and Embedded sources check box, and then click OK, as shown in Figure 1.16:

image

Figure 1.16: Disabling Source Link for the Go To Definition feature

Definitions can be either reverse-engineered from metadata or loaded from the original source code if that is enabled. Personally, I find the code from metadata more useful, as you are about to see. At the end of this section, try switching the Source Link option back on to see the difference.

1. In the HelloCS project, at the bottom of Program.cs , enter the following statement to declare an integer variable named z :

int z;

Click on int , right-click on int , and then choose Go To Definition in Visual Studio 2022 or Visual Studio Code. In JetBrains Rider, choose Go to | Go to Declaration or Usages.

In the code window that appears, you can see how the int data type is defined, as shown in Figure 1.17:

image

Figure 1.17: The int data type metadata

You can see that int :

image

image

Is defined using the struct keyword. Is in the System.Runtime assembly.

image

image

Is in the System namespace. Is named Int32 .

image

image

Is therefore an alias for the System.Int32 type. Implements interfaces such as IComparable .

image

image

Has constant values for its maximum and minimum values. Has methods such as Parse . (Not visible in Figure 1.17.)

Right now, the Go To Definition feature is not that useful to you because you do not yet know what all of this information means. By the end of the first part of this book, which consists of Chapters 2 to 6 and teaches you about the C# language, you will know enough for this feature to become very handy.

1. In the code editor window, scroll down to find the Parse method with a single string

parameter, as shown in the following code:

public static Int32 Parse(string s)

1. Expand the code and review the comments that document this method, as shown in Figure 1.18:

image

Figure 1.18: The comments for the Parse method with a single string parameter

In the comments, you will see that Microsoft has documented the following:

image

A summary that describes the method.

image

image

Parameters like the string value that can be passed to the method. The return value of the method, including its data type.

image

Three exceptions that might occur if you call this method, including ArgumentNullException , FormatException , and OverflowException . Now, we know that we could choose to wrap a call to this method in a try statement and which exceptions to catch.

Hopefully, you are getting impatient to learn what all this means!Be patient for a little longer. You are almost at the end of this chapter, and in the next chapter, you will dive into the details of the C# language. But first, let's see where else you can look for help.

Configuring inline aka inlay hints‌

Throughout the code in this book, when calling a method, I often explicitly specify named parameters to help the reader learn what is going on. For example, I have specified the names of the parameters format and arg0 in the following code:

Console.WriteLine(format: "Value is {0}.", arg0: 19.8);

Inline hints aka inlay hints show the names of parameters without you having to type them, as shown in Figure 1.19:

image

Figure 1.19: Configuring inline hints aka inlay hints

Most code editors have this feature that you can enable permanently or only when a key combination like Alt + F1 or Ctrl is held down:

image

In Visual Studio 2022, navigate to Tools | Options, navigate to Text Editor | C# | Advanced, scroll down to the Inline Hints section, and select the Display inline parameter hint names check box, and then click OK.

image

In Visual Studio Code, navigate to File | Preferences | Settings, search for inlay , select the C# filter, and then select the Display inline parameter name hints check box.

image

In JetBrains Rider, in Settings, navigate to Editor | Inlay Hints | C# | Parameter Name Hints.

Looking for answers on Stack Overflow‌

Stack Overflow is the most popular third-party website for getting answers to difficult programming questions. Let's see an example:

Start your favorite web browser.

Navigate to stackoverflow.com ; in the search box, enter securestring and note the search results, which are shown in Figure 1.20:

image

Figure 1.20: Stack Overflow search results for securestring

Searching for answers using Google‌

You can search Google with advanced search options to increase the likelihood of finding what you need:

Navigate to Google at the following link: https://www.google.com/.

Search for information about garbage collection using a simple Google query and note that you will probably see a lot of ads for garbage collection services in your local area before you see the Wikipedia definition of garbage collection in computer science.

Improve the search by restricting it to a useful site such as Stack Overflow, and by removing languages that we might not care about, such as C++, Rust, and Python, or by adding C# and .NET explicitly, as shown in the following search query:

garbage collection site:stackoverflow.com +C# -Java

Searching the .NET source code‌

Sometimes you can learn a lot from seeing how the Microsoft teams have implemented .NET. The source for the entire code base for .NET is available in public GitHub repositories. For example, you might know that there is a built-in attribute to validate an email address.

Let's search the repositories for the word "email" and see if we can find out how it works:

Use your preferred web browser to navigate to: https://github.com/search.

Click advanced search.

In the search box, type email .

In the In these respositories box, type dotnet/runtime . (Other repositories you might want to search include dotnet/core , dotnet/aspnetcore , dotnet/wpf , dotnet/winforms ).

In the Written in this language box, select C#.

In the top right of the page, note how the advanced query has been written for you, click Search, and then click the Code filter and note the results include the

EmailAddressAttribute , as shown in Figure 1.21:

image

Figure 1.21: Advanced search for email in the dotnet/runtime repository

Click the source file and note it implements email validation by checking that the string value contains an @ symbol but not as the first or last character, as shown in the following code:

// only return true if there is only 1 '@' character

// and it is neither the first nor the last character int index = valueAsString.IndexOf('@');

return

index > 0 &&

index != valueAsString.Length - 1 && index == valueAsString.LastIndexOf('@');

1. Close the browser.

image

For your convenience, you can do a quick search for other terms by replacing the search term email in the following link: https://github.com/search? q=%22email%22+repo%3Adotnet%2Fruntime+language%3AC%23&type=code&ref=advsearch.

Subscribing to the official .NET blog‌

To keep up to date with .NET, an excellent blog to subscribe to is the official .NET blog, written by the .NET engineering teams, and you can find it at the following link: https://devblogs.microsoft.com/dotnet/.

Watching Scott Hanselman's videos‌

Scott Hanselman from Microsoft has an excellent YouTube channel about computer stuff they didn't teach you: http://computerstufftheydidntteachyou.com/.I recommend it to everyone working with computers.

AI tools like ChatGPT and GitHub Copilot‌

One of the biggest changes in coding and development in the past year is the emergence of generative artificial intelligence (AI) tools that can help with coding tasks like

completing a code statement, implementing an entire function, writing unit tests, and suggesting debugging fixes for existing code.

You can read what developers say about AI tools in the 2023 Stack Overflow Developer Survey. "44% of them use AI tools in their development process now, and 26% plan to soon.": https://stackoverflow.blog/2023/06/14/hype-or-not-developers-have-something-to- say-about-ai/.

ChatGPT currently has two models: 3.5 (free) and 4.0 ($20 per month). Let's say you need to write a C# function to validate an email address. You might go to ChatGPT and enter the following prompt:

write a c# function to validate an email address

It responds with a complete class with methods, as shown in Figure 1.22:

image

Figure 1.22: ChatGPT writes a function to validate an email address

It then provides an explanation of the code and examples of how to call the function, as shown in the following code:

bool isValid = EmailValidator.IsValidEmail("test@example.com"); Console.WriteLine(isValid ? "Valid" : "Invalid");

But is a general-purpose generative AI like ChatGPT the best partner for a C# programmer? Microsoft has a service specifically for programmers, named GitHub Copilot, that can help autocomplete code directly in your code editor. It is being enhanced with more intelligence using GPT-4. It has plugins for code editors including Visual Studio 2022, Visual Studio Code, and JetBrains IntelliJ-based IDEs.

Personally, I really like the Copilot branding. It makes it clear that you are the pilot. You are ultimately responsible for "flying the plane." But for the easy or boring

bits, you can hand it over to your co-pilot for a bit, while being actively ready to take back control if needed.

GitHub Copilot is free for students, teachers, and some open source project maintainers. For everyone else, it has a 30-day free trial and then it costs $10 per month or $100 per year for individuals. Once you have an account, you can then sign up for waiting lists to get the more advanced experimental GitHub Copilot X features. You should check online for which Copilot features are available for various code editors. As you can imagine, this is a fast-changing world and a lot of what I might write in the book today will be out-of-date by the time you read it: https://github.com/features/copilot.

JetBrains has its own equivalent, named AI Assistant, that you can read about at the following link: https://blog.jetbrains.com/idea/2023/06/ai-assistant-in-jetbrains-ides/.

So, what can GitHub Copilot do for you today?Imagine that you have just added a new class file named Product.cs . You click inside the Product class, press Enter to insert a blank line, and then pause for a second as you think about what you need to type…and GitHub Copilot generates some sample code in gray, as shown in Figure 1.23:

image

Figure 1.23: GitHub Copilot suggesting how to define a Product class

At this point, you can glance over the code and, if it is close to what you want, just press Tab to insert it all, or press Alt + . (dot) to toggle between other suggestions.Sometimes it is too far off what you need, and you'll be better off ignoring its suggestion completely and just writing it yourself. But usually, there's something there that's usable or reminds you of the syntax you need to use. And sometimes, it feels like magic, writing dozens of lines of exactly what you need.Microsoft feeds its AI tools with code from public GitHub repositories, including all the repositories I have created since 2016 for all the editions of this book. This means that it can suggest code completions for the readers of this book that are surprisingly accurate predictions including my frequent use of pop culture references in my code. It's like I, Mark J. Price, am the "ghost in the machine" guiding your coding.It's easy to imagine a custom ChatGPT that has ingested all the official Microsoft .NET documentation, every public blog article written about .NET, and perhaps even hundreds of books about .NET, and having a conversation about it to find a bug or suggest how to solve a programming problem.

You can sign up for GitHub Copilot at the following link: https://github.com/github- copilot/signup/.

Disabling tools when they get in the way‌

Although these tools can be helpful, they can also get in your way, especially when learning, because they sometimes do work for you without telling you. If you do not do that work for yourself at least a few times, you won't learn fully.To configure IntelliSense for C# in Visual Studio 2022:

Navigate to Tools | Options.

In the Options dialog box tree view, navigate to Text Editor | C# | IntelliSense.

Click the ? button in the caption bar to view the documentation.

To configure GitHub Copilot X in Visual Studio 2022:

Navigate to Tools | Options.

In the Options dialog box tree view, navigate to GitHub | Copilot.

Set Enable Globally to True or False, and then click OK.

To disable GitHub Copilot X in Visual Studio Code:

In the status bar, on the right, to the left of the notification icon, click the GitHub Copilot icon.

In the popup, click Disable Globally.

To enable, click the GitHub Copilot icon again and then click Enable Globally.

For help with JetBrains Rider IntelliSense, please see the following link: https://www.jetbrains.com/help/rider/Auto-Completing_Code.xhtml.

Practicing and exploring‌

Let's now test your knowledge and understanding by trying to answer some questions, getting some hands-on practice, and going into the topics covered throughout this chapter in greater detail.

Exercise 1.1 – Test your knowledge‌

Try to answer the following questions, remembering that although most answers can be found in this chapter, you should do some online research or code writing to answer others:

Is Visual Studio 2022 better than Visual Studio Code?

Is .NET 5 and later better than .NET Framework?

What is .NET Standard and why is it still important?

Why can a programmer use different languages, for example, C# and F#, to write applications that run on .NET?

What is a top-level program and how do you access any command-line arguments?

What is the name of the entry point method of a .NET console app and how should it be explicitly declared if you are not using the top-level program feature?

What namespace is the Program class defined in with a top-level program?

Where would you look for help for a C# keyword?

Where would you look first for solutions to common programming problems?

What should you do after getting an AI to write code for you?

Appendix, Answers to the Test Your Knowledge Questions, is available to download from a link in the README in the GitHub repository: https://github.com/markjprice/cs12dotnet8.

Exercise 1.2 – Practice C# anywhere with a browser‌

You don't need to download and install Visual Studio Code or even Visual Studio 2022 to write C#. You can start coding online at any of the following links:

image

image

Visual Studio Code for Web: https://vscode.dev/ SharpLab: https://sharplab.io/

image

C# Online Compiler | .NET Fiddle: https://dotnetfiddle.net/

image

W3Schools C# Online Compiler: https://www.w3schools.com/cs/cs_compiler.php

Exercise 1.3 – Explore topics‌

A book is a curated experience. I have tried to find the right balance of topics to include in the printed book. Other content that I have written can be found in the GitHub repository for this book.I believe that this book covers all the fundamental knowledge and skills a C# and .NET developer should have or be aware of. Some longer examples are best included as links to Microsoft documentation or third-party article authors.Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-1--- hello-c-welcome-net

Exercise 1.4 – Explore Polyglot Notebooks‌

Complete the following online-only section to explore how you can use Polyglot Notebooks with its .NET Interactive engine:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch01-polyglot-notebooks.md

Exercise 1.5 – Explore themes of modern .NET‌

Microsoft has created a website using Blazor that shows the major themes of modern .NET: https://themesof.net/

Exercise 1.6 – Free Code Camp and C# Certification‌

For many years, Microsoft had an exam for C# 5, Exam 70-483: Programming in C#. I taught hundreds of developers the skills needed to get qualified and pass it. Sadly, that exam was retired a few years ago.In August 2023, Microsoft announced a new foundational certification for C# alongside a free 35-hour online course. You can read more about how to qualify for the certification at the following link:https://www.freecodecamp.org/learn/foundational-c- sharp-with-microsoft/

Exercise 1.7 – Alpha versions of .NET‌

You can (but probably shouldn't) download future versions of .NET including alpha versions from the following link:https://github.com/dotnet/installer#tableFor example, in August 2023, you could download .NET SDK 9 alpha, which included an early release candidate of the

.NET 8 runtime, although so few people do that Edge gives a warning and tries to stop you, as shown in Figure 1.24:

image

Figure 1.24: Download page for alpha versions of .NET

Warning! Alpha versions are designed to be used only internally by Microsoft employees. Beta versions (official previews) are designed to be used externally and are publicized in Microsoft blog posts. Personally, I would not download an alpha of .NET 9 until December 2023 when it might have some new features compared to .NET 8. Once official previews of .NET 9 become available in February 2024, I recommend using those instead.

For more about using .NET 9 or 10 with this book, please see the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/dotnet9.md.

Summary‌

In this chapter, we:

image

Set up your development environment.

image

Discussed the similarities and differences between modern .NET, .NET Core, .NET Framework, Xamarin, and .NET Standard in an online article.

image

Used Visual Studio 2022 and Visual Studio Code with the .NET SDK CLI to create a couple of simple console apps grouped in a solution.

image

image

Learned how to download the solution code for this book from its GitHub repository. And, most importantly, we learned how to find help. This could be in the traditional way, by using help command switches, documentation, and articles, or the modern way, by having a conversation with a coding expert AI, or using an AI-based tool to perform "grunt work."

In the next chapter, you will learn how to "speak" C#.

Speaking C#‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is all about the basics of the C# programming language. Over the course of this chapter, you'll learn how to write statements using the grammar of C#, as well as being introduced to some of the common vocabulary that you will use every day. In addition to this, by the end of the chapter, you'll feel confident in knowing how to temporarily store and work with information in your computer's memory.This chapter covers the following topics:

image

image

Introducing the C# language Discovering your C# compiler version

image

image

Understanding C# grammar and vocabulary Working with variables

image

image

Exploring more about console apps Understanding async and await

Introducing the C# language‌

This part of the book is about the C# language—the grammar and vocabulary that you will use every day to write the source code for your applications.Programming languages have many similarities to human languages, except that in programming languages, you can make up your own words, just like Dr. Seuss!In a book written by Dr. Seuss in 1950, If I Ran the Zoo, he states this:

"And then, just to show them, I'll sail to Ka-Troo And Bring Back an It-Kutch, a Preep, and a Proo, A Nerkle, a Nerd, and a Seersucker, too!"

C# language versions and features‌

This part of the book covers the C# programming language and is written primarily for beginners, so it covers the fundamental topics that all developers need to know, including declaring variables, storing data, and how to define your own custom data types.This book covers features of the C# language from version 1 up to the latest version, 12.If you already have some familiarity with older versions of C# and are excited to find out about the new features in the most recent versions of C#, I have made it easier for you to jump around by listing language versions and their important new features below, along with the chapter number and topic title where you can learn about them.

Project COOL

Before the first release of C#, it had the codename C-like Object-Oriented Language

(COOL).

C# 1

C# 1 was released in February 2002 and included all the important features of a statically typed object-oriented modern language, as you will see throughout Chapters 2 to 6.

C# 1.2

C# 1.2, with a few minor improvements, like automatic disposal at the end of foreach

statements, was released with Visual Studio .NET 2003.

C# 2

C# 2 was released in 2005 and focused on enabling strong data typing using generics, to improve code performance and reduce type errors, including the topics listed in Table 2.1:

Feature Chapter Topic

Nullable value types 6 Making a value type nullable

Generics 6 Making types more reusable with generics Table 2.1: Features of C# 2 covered in this book

C# 3

C# 3 was released in 2007 and focused on enabling declarative coding with Language INtegrated Queries (LINQ) and related features like anonymous types and lambda expressions, including the topics listed in Table 2.2:

Feature Chapter Topic

Implicitly typed local variables

2 Inferring the type of a local variable

LINQ 11 All topics in Chapter 11 , Querying and Manipulating Data Using LINQ

Table 2.2: Features of C# 3 covered in this book

C# 4

C# 4 was released in 2010 and focused on improving interoperability with dynamic languages like F# and Python, including the topics listed in Table 2.3:

Feature Chapter Topic

Dynamic types 2 Storing dynamic types

Named/optional arguments 5 Optional parameters and named arguments Table 2.3: Features of C# 3 covered in this book

C# 5

C# 5 was released in 2012 and focused on simplifying asynchronous operation support by automatically implementing complex state machines while writing what looks like synchronous statements, including the topics listed in Table 2.4:

Feature Chapter Topic

Simplified asynchronous tasks 2 Understanding async and await

Table 2.4: Features of C# 5 covered in this book

C# 6

C# 6 was released in 2015 and focused on minor refinements to the language, including the topics listed in Table 2.5:

Feature Chapter Topic

static imports 2 Simplifying the usage of the console Interpolated strings 2 Displaying output to the user Expression-bodied members 5 Defining read-only properties

Table 2.5: Features of C# 6 covered in this book

C# 7

C# 7 was released in March 2017 and focused on adding functional language features like tuples and pattern matching, as well as minor refinements to the language, including the topics listed in Table 2.6:

Feature Chapter Topic

Binary literals and digit separators 2 Storing whole numbers

Pattern matching

3

Pattern matching with the if statement

out variables

5

Controlling how parameters are passed

Tuples

5

Combining multiple values with tuples

Local functions

6

Defining local functions

Table 2.6: Features of C# 7 covered in this book

C# 7.1

C# 7.1 was released in August 2017 and focused on minor refinements to the language, including the topics listed in Table 2.7:

Feature Chapter Topic

async Main 2 Improving responsiveness for console apps Default literal expressions 5 Setting fields with default literals Inferred tuple element names 5 Inferring tuple names

Table 2.7: Features of C# 7.1 covered in this book

C# 7.2

C# 7.2 was released in November 2017 and focused on minor refinements to the language, including the topics listed in Table 2.8:

Feature Chapter Topic

Leading underscores in numeric literals 2 Storing whole numbers

Non-trailing named arguments 5 Optional parameters and named arguments

private protected access modifier 5 Understanding access modifiers You can test == and != with tuple types 5 Comparing tuples

Table 2.8: Features of C# 7.2 covered in this book

C# 7.3

C# 7.3 was released in May 2018 and focused on performance-oriented safe code, which improved ref variables, pointers, and stackalloc . These features are advanced and rarely needed by most developers, so they are not covered in this book.

C# 8

C# 8 was released in September 2019 and focused on a major change to the language related to null handling, including the topics listed in Table 2.9:

Feature Chapter Topic

Switch expressions 3 Simplifying switch statements with switch expressions Nullable reference types 6 Making a reference type nullable

Default interface methods 6 Understanding the default interface methods Table 2.9: Features of C# 8 covered in this book

C# 9

C# 9 was released in November 2020 and focused on record types, refinements to pattern matching, and minimal-code projects, including the topics listed in Table 2.10:

Feature Chapter Topic

Minimal-code console apps 1 Understanding top-level programs

Target-typed new expressions

2 Using target-typed new expressions to instantiate objects

Enhanced pattern matching 5 Pattern matching with objects Records 5 Working with records

Table 2.10: Features of C# 9 covered in this book

C# 10

C# 10 was released in November 2021 and focused on features that minimize the amount of code needed in common scenarios, including the topics listed in Table 2.11:

Feature Chapter Topic

Global namespace imports 2 Importing namespaces

Constant string literals 2 Formatting using interpolated strings File-scoped namespaces 5 Simplifying namespace declarations Record structs 6 Working with record struct types ArgumentNullException.ThrowIfNull 6 Checking for null in method parameters Table 2.11: Features of C# 10 covered in this book

C# 11

C# 11 was released in November 2022 and focused on features that simplify your code, including the topics listed in Table 2.12:

Feature Chapter Topic

Raw string literals 2 Understanding raw string literals

Line breaks in interpolated string expressions

2 Formatting using interpolated strings

Required properties 5 Requiring properties to be set during instantiation

Table 2.12: Features of C# 11 covered in this book

C# 12

C# 12 was released in November 2023 and focused on features that simplify your code and improving performance, including the topics listed in Table 2.13:

Feature Chapter Topic

Inline arrays 3 Understanding inline arrays

Default lambda parameters 4 Lambda expressions with default parameter values New exception guard clauses 4 Throwing exceptions using guard clauses

Aliasing any type 5 Renaming a type with a using alias Primary constructors 5 Defining a primary constructor Interceptors 7 Method interceptors

Table 2.13: Features of C# 12 mentioned in this book

Understanding C# standards‌

Over the years, Microsoft has submitted a few versions of C# to standards bodies, as shown in Table 2.14:

C# version

ECMA standard ISO/IEC

standard

1.0

ECMA-334:2003 ISO/IEC

23270:2003

2.0

ECMA-334:2006 ISO/IEC

23270:2006

5.0

ECMA-334:2017 ISO/IEC

23270:2018

6.0

ECMA-334:2022 ISO/IEC

23270:2022

Table 2.14: ECMA standards for C#

The ECMA standard for C# 7.3 is still a draft. So don't even think about when C# versions 8 to 12 might be ECMA standards! Microsoft made C# open source in 2014. You can read the latest C# standard document at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/specifications.

More practically useful than the ECMA standards are the public GitHub repositories for making the work on C# and related technologies as open as possible, as shown in Table 2.15:

Description Link

C# language design https://github.com/dotnet/csharplang Compiler implementation https://github.com/dotnet/roslyn Standard to describe the language https://github.com/dotnet/csharpstandard Table 2.15: Public GitHub repositories for C#

Discovering your C# compiler version‌

The .NET language compiler for C# and Visual Basic, also known as Roslyn, along with a separate compiler for F#, is distributed as part of the .NET SDK. To use a specific version

of C#, you must have at least that version of the .NET SDK installed, as shown in Table 2.16:

.NET SDK Roslyn compiler Default C# language

1.0.4

2.0-2.2

7.0

1.1.4

2.3-2.4

7.1

2.1.2

2.6-2.7

7.2

2.1.200

2.8-2.10

7.3

3.0

3.0-3.4

8.0

5.0

3.8

9.0

6.0

4.0

10.0

7.0

4.4

11.0

8.0

4.8

12.0

Table 2.16: .NET SDK versions and their C# compiler versions

When you create class libraries, you can choose to target .NET Standard as well as versions of modern .NET. They have default C# language versions, as shown in Table 2.17:

.NET Standard C# 2.0 7.3

2.1 8.0

Table 2.17: .NET Standard versions and their default C# compiler versions

Although you must have a minimum version of the .NET SDK installed to have access to a specific compiler version, the projects that you create can target older versions of

.NET and still use a modern compiler version. For example, if you have the .NET 7 SDK or later installed, then you can use C# 11 language features in a console app that targets

.NET Core 3.0.

How to output the SDK version‌

Let's see what .NET SDK and C# language compiler versions you have available:

On Windows, start Windows Terminal or Command Prompt. On macOS, start Terminal.

To determine which version of the .NET SDK you have available, enter the following command:

dotnet --version

1. Note that the version at the time of publishing is 8.0.100, indicating that it is the initial version of the SDK without any bug fixes or new features yet, as shown in the following output:

8.0.100

Enabling a specific language version compiler‌

Developer tools like Visual Studio and the dotnet command-line interface assume that you want to use the latest major version of a C# language compiler by default. Before C# 8 was released, C# 7 was the latest major version and was used by default. To use the improvements in a C# point release like 7.1, 7.2, or 7.3, you had to add a configuration element to the project file, as shown in the following markup:

7.3

After the release of C# 12 with .NET 8, if Microsoft releases a C# 12.1 compiler and you want to use its new language features, then you will have to add a configuration element to your project file, as shown in the following markup:

12.1

Potential values for the are shown in Table 2.18:

Description

7 , 7.1 , 7.2 ,

7.3 , 8 , 9 , 10

, 11 , 12

Entering a specific version number will use that compiler if it has been installed.

latestmajor Uses the highest major number, for example, 7.0 in August 2019, 8 in October 2019, 9 in November 2020, 10 in November 2021, 11 in November

2022, and 12 in November 2023.

latest Uses the highest major and highest minor number, for example, 7.2 in 2017, 7.3 in 2018, 8 in 2019, and perhaps 12.1 in H1 2024.

preview Uses the highest available preview version, for example, 12.0 in July 2023 with .NET 8 Preview 6 installed.

Table 2.18: LangVersion settings for a project file

Using future C# compiler versions‌

In February 2024, Microsoft is likely to release the first preview of .NET 9 with a C# 13 compiler. You will be able to install its SDK from the following link:https://dotnet.microsoft.com/en-us/download/dotnet/9.0The link will give a

404 Missing resource error until February 2024, so do not bother using it until then!After you've installed a .NET 9 SDK preview, you will be able to use it to create new projects and explore the new language features in C# 13. After creating a new project, you can edit the

.csproj file and add the element set to preview to use the preview C# 13 compiler, as shown highlighted in the following markup:

Exe

net9.0

preview

Switching the C# compiler for .NET 8 to a future version‌

.NET 8 is an LTS release, so Microsoft must support developers who continue to use .NET 8 for three years. But that does not mean that you are stuck with the C# 12 compiler for three years!In November 2024, Microsoft is likely to release .NET 9, including a C# 13 compiler with new features. Although future versions of .NET 8 are likely to include preview versions of the C# 13 compiler, to be properly supported by Microsoft, you should only set

to preview for exploration, not production projects, because it is not supported by Microsoft, and it is more likely to have bugs. Microsoft makes previews available because they want to hear feedback. You can be a part of C#'s development and improvement.Once .NET 9 SDK is made generally available in November 2024, you will be able to get the best of both worlds. You can use the .NET 9 SDK and its C# 13 compiler while your projects continue to target .NET 8. To do so, set the target framework to net8.0 and add a

element set to 13 , as shown highlighted in the following markup:

Exe

net8.0

enable

enable

13

The preceding project targets net8.0 so it is supported until November 2026 when run on a monthly patched version of the .NET 8 runtime. If the preceding project is built using .NET

9 SDK, then it can have the set to 13 meaning C# 13.If you target net9.0 , which new projects will by default if you have installed the .NET 9 SDK, then the default language will be C# 13 so it would not need to be explicitly set.In February 2025, Microsoft is likely to release the first preview of .NET 10, and, in November 2025, it will likely release .NET 10 for general availability in production. You will be able to install its SDK from the following link and explore C# 14 in the same way as described above for C# 13 with

.NET 9:https://dotnet.microsoft.com/en-us/download/dotnet/10.0Again, the preceding link is for future use! It will give a 404 Missing resource error until February 2025, so do not bother using it until then.

Warning! Some C# language features depend on changes in the underlying .NET libraries. Even if you use the latest SDK with the latest compiler, you might not be able to use all the new language features while targeting an older version of .NET. For example, C#

11 introduced the required keyword, but it cannot be used in a project that targets

.NET 6 because that language feature requires new attributes that are only available in

.NET 7. Luckily, the compiler will warn you if you try to use a C# feature that is not supported. Just be prepared for that eventuality.

Showing the compiler version‌

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

If you've completed Chapter 1, Hello, C#! Welcome, .NET!, then you will already have a

cs12dotnet8 folder. If not, then you'll need to create it.

Use your preferred code editor to create a new project, as defined in the following list:

image

Project template: Console App [C#] / console

image

Project file and folder: Vocabulary

image

Solution file and folder: Chapter02

image

Do not use top-level statements: Cleared

image

Enable native AOT publish: Cleared

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

In the Vocabulary project, in Program.cs , after the comment, add a statement to show the C# version as an error, as shown in the following code:

#error version

Run the console app:

image

image

If you are using Visual Studio 2022, then navigate to Debug | Start Without Debugging. When prompted to continue and run the last successful build, click No. If you are using Visual Studio Code, then in a terminal for the Vocabulary folder, enter the command dotnet run . Note that we are expecting a compiler error, so do

not panic when you see it!

Note that the compiler version and the language version appear as a compiler error message number CS8304 , as shown in Figure 2.1:

image

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

The error message in the Visual Studio Code PROBLEMS window or Visual Studio Error List

window says Compiler version: '4.8.0...' with language version default (12.0) .

1. Comment out the statement that causes the error, as shown in the following code:

// #error version

1. Note that the compiler error messages disappear.

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.

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 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‌

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:

image

Visual Studio 2022: Navigate to Edit | Advanced | Comment Selection or Uncomment Selection.

image

image

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.

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:

image

image

A namespace contains types like classes to group them together. A class contains the members of an object, including methods.

image

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 left when you move your mouse cursor over the left margin of the code, as shown in Figure 2.2:

image

Regions‌

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;

#endregion

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: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding- conventions. 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: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/statements/iteration-statements. 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. int sum=1+ 2; // One statement over three lines. int sum= 1 +2;int sum=1+2; // Two statements on one line. The only white space characters 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: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language- specification/lexical-structure#634-white-space. 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 m 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: https://youtu.be/dkO8cdwf6v8. 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: Navigate to Tools | Options. 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: Navigate to File | Preferences | Theme | Color Theme. It is in the Code menu on macOS. 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: In Program.cs , change the L in the WriteLine method to lowercase. Delete the semicolon at the end of the statement. 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: image Figure 2.3: The Error List window showing two compile errors 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 .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.19:

SDK Implicitly imported namespaces

Microsoft.NET.Sdk SystemSystem.Collections.GenericSystem.IOSystem.LinqSystem.Net.HttpSyst

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

Table 2.19: .NET SDKs and their implicitly imported namespaces Let's see the current autogenerated implicit imports file:

In Solution Explorer, toggle on the Show All Files button, and note the compiler- generated bin and obj folders are now visible.

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 .GlobalUsings.g.cs . Note the g

for generated to differentiate it from developer-written code files.

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:

//

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;

Close the Vocabulary.GlobalUsings.g.cs file.

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:

Exe

net8.0

enable

enable

Note that is different from . 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, can be before or after .

Save the changes to the project file.

Expand the obj folder, expand the Debug folder, expand the net8.0 folder, and open the file named Vocabulary.GlobalUsings.g.cs .

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:

//

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;

1. 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.\"");

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

element completely from the project file, or changing its value to

disable , as shown in the following markup:

disable

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. Console.WriteLine();

// Outputs the greeting and the current line terminator. Console.WriteLine("Hello Ahmed");

// Outputs a formatted number and date and the current line terminator.

Console.WriteLine(

"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:

image

Animal and Car are types; they are nouns for categorizing things.

image

Head and Engine might be fields or properties; they are nouns that belong to Animal

and Car .

image

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:

Comment out all the existing statements in Program.cs .

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();

1. 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:

System.Reflection namespace is used to work with assemblies, modules, types, methods, and so on, at runtime.

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.

Assembly.GetEntryAssembly() gets the assembly that is the entry point of the

application.

If myApp is null (meaning there's no entry assembly, which should be impossible in a normally running .NET app), the application exits.

The foreach loop iterates over each assembly name in the referenced assemblies of the entry point assembly.

Assembly.Load(name) loads an assembly given the assembly name. This allows you to interact with the assembly programmatically.

A counter variable methodCount is declared and initialized to 0. This is used to count the total number of methods in the assembly.

Another nested foreach loop then iterates over each type in the assembly.

For each type, it gets the number of methods (t.GetMethods().Length) and adds it to the methodCount.

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.

Working with variables‌

All applications process data. Data comes in, data is processed, and then data goes out.Data usually comes into our program from files, databases, or user input, and it can be put temporarily into variables, which will be stored in the memory of the running program. When the program ends, the data in memory is lost. Data is usually output to files and databases, or to the screen or a printer. When using variables, you should think about, firstly, how much space the variable takes up in the memory, and, secondly, how fast it can be processed.We control this by picking an appropriate type. You can think of simple common types such as int and double as being different-sized storage boxes, where a smaller box would take less memory but may not be as fast at being processed; for example, adding 16-bit numbers might not be processed as quickly as adding 64-bit numbers on a 64-bit operating

system. Some of these boxes may be stacked close by, and some may be thrown into a big heap further away.

Naming things and assigning values‌

There are naming conventions for things, and it is a good practice to follow them, as shown in Table 2.20:

Naming convention Examples Used for

Camel case cost , orderDetail , and

dateOfBirth

Local variables, private fields.

Title case aka Pascal case

String , Int32 , Cost ,

DateOfBirth , and Run

Types, non-private fields, and other members like methods.

Table 2.20: Naming conventions and what they should be used for

Some C# programmers like to prefix the names of private fields with an underscore, for example, _dateOfBirth instead of dateOfBirth . The naming of private members of all kinds is not formally defined because they will not be visible outside the class, so writing them with or without an underscore prefix are both valid.

Good Practice: Following a consistent set of naming conventions will enable your code to be easily understood by other developers (and yourself in the future!).

The following code block shows an example of declaring a named local variable and assigning a value to it with the = symbol. You should note that you can output the name of a variable using a keyword introduced in C# 6, nameof :

// Let the heightInMetres variable become equal to the value 1.88. double heightInMetres = 1.88;

Console.WriteLine($"The variable {nameof(heightInMetres)} has the value

{heightInMetres}.");

Warning! The message in double quotes in the preceding code wraps onto a second line because the width of a printed page is too narrow. When entering a statement like this in your code editor, type it all in a single line.

In C# 12, nameof can now access instance data from a static context. You will learn the difference between instance and static data in Chapter 5, Building Your Own Types with Object-Oriented Programming.

Literal values‌

When you assign to a variable, you often, but not always, assign a literal value. But what is a literal value? A literal is a notation that represents a fixed value. Data types have different notations for their literal values, and over the next few sections, you will see examples of using literal notation to assign values to variables.

More Information: You can read the formal definition of literals in the C# language specification: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/language-specification/lexical-structure#645-literals.

Storing text‌

For text, a single letter, such as an A , is stored as a char type.

Good Practice: Actually, it can be more complicated than that. Egyptian Hieroglyph A002 (U+13001) needs two System.Char values (known as surrogate pairs) to represent it:

\uD80C and \uDC01 . Do not always assume one char equals one letter or you could introduce hard-to-notice bugs into your code.

A char is assigned using single quotes around the literal value, or assigning the return value of a function call, as shown in the following code:

char letter = 'A'; // Assigning literal characters. char digit = '1';

char symbol = '$';

char userChoice = GetChar(); // Assigning from a fictitious function.

For text, multiple letters, such as Bob , are stored as a string type and are assigned using double quotes around the literal value, or by assigning the return value of a function call or constructor, as shown in the following code:

string firstName = "Bob"; // Assigning literal strings. string lastName = "Smith";

string phoneNumber = "(215) 555-4256";

// Assigning a string returned from the string class constructor. string horizontalLine = new('-', count: 74); // 74 hyphens.

// Assigning a string returned from a fictitious function. string address = GetAddressFromDatabase(id: 563);

// Assigning an emoji by converting from Unicode. string grinningEmoji = char.ConvertFromUtf32(0x1F600);

Outputting emojis

To output emojis at a command prompt on Windows, you must use Windows Terminal because Command Prompt does not support emojis and set the output encoding of the console to use UTF-8, as shown in the following code:

Console.OutputEncoding = System.Text.Encoding.UTF8; string grinningEmoji = char.ConvertFromUtf32(0x1F600); Console.WriteLine(grinningEmoji);

Verbatim strings

When storing text in a string variable, you can include escape sequences, which represent special characters like tabs and new lines using a backslash, as shown in the following code:

string fullNameWithTabSeparator = "Bob\tSmith";

But what if you are storing the path to a file on Windows, and one of the folder names starts with a T , as shown in the following code?

string filePath = "C:\televisions\sony\bravia.txt";

The compiler will convert the \t into a tab character and you will get errors!You must prefix it with the @ symbol to use a verbatim literal string , as shown in the following code:

string filePath = @"C:\televisions\sony\bravia.txt";

Raw string literals

Introduced in C# 11, raw string literals are convenient for entering any arbitrary text without needing to escape the contents. They make it easy to define literals containing

other languages like XML, HTML, or JSON.Raw string literals start and end with three or more double-quote characters, as shown in the following code:

string xml = """

Mark

""";

Why three or more double-quote characters? This is for scenarios where the content itself needs to have three double-quote characters; you can then use four double-quote characters to indicate the beginning and end of the content. Where the content needs to have four double-quote characters, you can then use five double-quote characters to indicate the beginning and end of the content. And so on.In the previous code, the XML is indented by 13 spaces. The compiler looks at the indentation of the last three or more double-quote characters, and then automatically removes that level of indentation from all the content inside the raw string literal. The results of the previous code would therefore not be indented as in the defining code, but instead be aligned with the left margin, as shown in the following markup:

Mark

If the end three double-quote characters are aligned with the left margin, as shown in the following code:

string xml = """

Mark

""";

Then the 13-space indentation would not be removed, as shown in the following markup:

Mark

Raw interpolated string literals

You can mix interpolated strings that use curly braces { } with raw string literals. You specify the number of braces that indicates a replaced expression by adding that number of dollar signs to the start of the literal. Any fewer braces than that are treated as raw content.For example, if we want to define some JSON, single braces will be treated as normal braces, but the two dollar symbols tell the compiler that any two curly braces indicate a replaced expression value, as shown in the following code:

var person = new { FirstName = "Alice", Age = 56 }; string json = $$"""

{

"first_name": "{{person.FirstName}}",

"age": {{person.Age}},

"calculation": "{{{ 1 + 2 }}}"

}

""";

Console.WriteLine(json);

The previous code would generate the following JSON document:

{

"first_name": "Alice", "age": 56,

"calculation": "{3}"

}

The number of dollars tells the compiler how many curly braces are needed to become recognized as an interpolated expression.

Summarizing options for storing text

To summarize:

image

Literal string: Characters enclosed in double-quote characters. They can use escape characters like \t for tab. To represent a backslash, use two: \\ .

image

image

Raw string literal: Characters enclosed in three or more double-quote characters. Verbatim string: A literal string prefixed with @ to disable escape characters so that a backslash is a backslash. It also allows the string value to span multiple lines because the whitespace characters are treated as themselves instead of instructions to the compiler.

image

Interpolated string: A literal string prefixed with $ to enable embedded formatted variables. You will learn more about this later in this chapter.

Storing numbers‌

Numbers are data that we want to perform an arithmetic calculation on, for example, multiplying. A telephone number is not a number. To decide whether a variable should be stored as a number or not, ask yourself whether you need to perform arithmetic operations on the number or whether the number includes non-digit characters such as parentheses or hyphens to format the number, such as (414) 555-1234. In this case, the "number" is a sequence of characters, so it should be stored as a string .Numbers can be natural numbers, such as 42, used for counting (also called whole numbers); they can also be negative numbers, such as -42 (called integers); or they can be real numbers, such as 3.9 (with a fractional part), which are called single- or double-precision floating-point numbers in computing.Let's explore numbers:

Use your preferred code editor to add a new Console App / console project named

Numbers to the Chapter02 solution.

image

For Visual Studio 2022, configure the startup project to the current selection.

In Program.cs , delete the existing code, and then type statements to declare some number variables using various data types, as shown in the following code:

// An unsigned integer is a positive whole number or 0. uint naturalNumber = 23;

// An integer is a negative or positive whole number or 0. int integerNumber = -23;

// A float is a single-precision floating-point number.

// The F or f suffix makes the value a float literal.

// The suffix is required to compile. float realNumber = 2.3f;

// A double is a double-precision floating-point number.

// double is the default for a number value with a decimal point. double anotherRealNumber = 2.3; // A double literal value.

Storing whole numbers

You might know that computers store everything as bits. The value of a bit is either 0 or 1 . This is called a binary number system. Humans use a decimal number system.The decimal number system, also known as Base 10, has 10 as its base, meaning there are 10 digits, from

to 9. Although it is the number base most used by human civilizations, other number base systems are popular in science, engineering, and computing. The binary number system, also

known as Base 2, has two as its base, meaning there are two digits, 0 and 1.The following image shows how computers store the decimal number 10. Take note of the bits with the value

in the 8 and 2 columns; 8 + 2 = 10:

image

Figure 2.4: Computers storing the decimal number 10

So, 10 in decimal is 00001010 in a binary byte (8 bits).

Improving legibility by using digit separators

Two of the improvements seen in C# 7 and later are the use of the underscore character _ as a digit separator and support for binary literals.You can insert underscores anywhere into the digits of a number literal, including decimal, binary, or hexadecimal notation, to improve legibility.For example, you could write the value for 1 million in decimal notation, that is, Base 10, as 1_000_000 .You can even use the 2/3 grouping common in India:

10_00_000 .

Using binary or hexadecimal notation

To use binary notation, that is, Base 2, using only 1s and 0s, start the number literal with 0b . To use hexadecimal notation, that is, Base 16, using 0 to 9 and A to F, start the number literal with 0x .

Exploring whole numbers

Let’s enter some code to see some examples:

In the Numbers project, in Program.cs , type statements to declare some number variables using underscore separators, as shown in the following code:

int decimalNotation = 2_000_000;

int binaryNotation = 0b_0001_1110_1000_0100_1000_0000; int hexadecimalNotation = 0x_001E_8480;

// Check the three variables have the same value. Console.WriteLine($"{decimalNotation == binaryNotation}"); Console.WriteLine(

$"{decimalNotation == hexadecimalNotation}");

// Output the variable values in decimal. Console.WriteLine($"{decimalNotation:N0}"); Console.WriteLine($"{binaryNotation:N0}"); Console.WriteLine($"{hexadecimalNotation:N0}");

// Output the variable values in hexadecimal. Console.WriteLine($"{decimalNotation:X}"); Console.WriteLine($"{binaryNotation:X}"); Console.WriteLine($"{hexadecimalNotation:X}");

1. Run the project and note the result is that all three numbers are the same, as shown in the following output:

True True

2,000,000

2,000,000

2,000,000

1E8480

1E8480

1E8480

Computers can always exactly represent integers using the int type or one of its sibling types, such as long and short .

Storing real numbers‌

Computers cannot always represent real, aka decimal or non-integer, numbers precisely. The float and double types store real numbers using single- and double-precision floating points.Most programming languages implement the Institute of Electrical and Electronics Engineers (IEEE) Standard for Floating-Point Arithmetic. IEEE 754 is a technical standard for floating-point arithmetic established in 1985 by the IEEE.The following image shows a simplification of how a computer represents the number 12.75 in binary notation. Note the bits with the value 1 in the 8, 4, ½, and ¼ columns.8 + 4 + ½ + ¼ = 12¾ = 12.75.

image

Figure 2.5: Computer representing number 12.75 in binary notation

So, 12.75 in decimal is 00001100.1100 in binary. As you can see, the number 12.75 can be exactly represented using bits. However, most numbers can’t, which is something that we’ll be exploring shortly.

Writing code to explore number sizes

C# has an operator named sizeof() that returns the number of bytes that a type uses in memory. Some types have members named MinValue and MaxValue , which return the minimum and maximum values that can be stored in a variable of that type. We are now going to use these features to create a console app to explore number types:

1. In Program.cs , at the bottom of the file, type statements to show the size of three number data types, as shown in the following code:

Console.WriteLine($"int uses {sizeof(int)} bytes and can store numbers in the range {int.MinValu Console.WriteLine($"double uses {sizeof(double)} bytes and can store numbers in the range {doubl Console.WriteLine($"decimal uses {sizeof(decimal)} bytes and can store numbers in the range {dec

Warning! The width of the printed pages in this book makes the string values (in double quotes) wrap over multiple lines. You must type them on a single line, or you will get compile errors.

1. Run the code and view the output, as shown in Figure 2.6:

image

Figure 2.6: Size and range information for common number data types

An int variable uses four bytes of memory and can store positive or negative numbers up to about 2 billion. A double variable uses 8 bytes of memory and can store much bigger values! A decimal variable uses 16 bytes of memory and can store big numbers, but not as big as a double type.But you may be asking yourself, why might a double variable be able to store bigger numbers than a decimal variable, yet it’s only using half the space in memory?

Well, let’s now find out!

Comparing double and decimal types

You will now write some code to compare double and decimal values. Although it isn’t hard to follow, don’t worry about understanding the syntax right now:

1. Type statements to declare two double variables, add them together, and compare them to the expected result. Then, write the result to the console, as shown in the following code:

Console.WriteLine("Using doubles:"); double a = 0.1;

double b = 0.2; if (a + b == 0.3)

{

Console.WriteLine($"{a} + {b} equals {0.3}");

}

else

{

Console.WriteLine($"{a} + {b} does NOT equal {0.3}");

}

1. Run the code and view the result, as shown in the following output:

Using doubles:

0.1 + 0.2 does NOT equal 0.3

In cultures that use a comma for the decimal separator, the result will look slightly different, as shown in the following output: 0,1 + 0,2 does NOT equal 0,3 .

The double type is not guaranteed to be accurate because most numbers like 0.1 , 0.2 , and

0.3 literally cannot be exactly represented as floating-point values.If you were to try different values like 0.1 + 0.3 == 0.4 it would happen to return true because with double values, some imprecise values happen to be exactly equal in their current representation even though they might not actually be equal mathematically. So, some numbers can be

directly compared but some cannot. I deliberately picked 0.1 and 0.2 to compare to 0.3 because they cannot be compared, as proven by the result.You could compare real numbers stored in the float type, which is less accurate than the double type, but the comparison would actual appear to be true because of that lower accuracy!

float a = 0.1F; float b = 0.2F;

if (a + b == 0.3F) // True because float is less "accurate" than double.

...

As a rule of thumb, you should only use double when accuracy, especially when comparing the equality of two numbers, is not important. An example of this might be when you're measuring a person's height; you will only compare values using greater than or less than, but never equals.The problem with the preceding code is illustrated by how the computer stores the number 0.1 , or multiples of it. To represent 0.1 in binary, the computer stores 1 in the 1/16 column, 1 in the 1/32 column, 1 in the 1/256 column, 1 in the 1/512 column, and so on.The number 0.1 in decimal is 0.00011001100110011 … in binary, repeating forever:

image

Figure 2.7: Number 0.1 in decimal repeating forever in binary

Good Practice: Never compare double values using == . During the First Gulf War, an American Patriot missile battery used double values in its calculations. The inaccuracy caused it to fail to track and intercept an incoming Iraqi Scud missile, and 28 soldiers were killed; you can read about this at https://www.ima.umn.edu/~arnold/disasters/patriot.xhtml. The Patriot missile system has improved since then. "Forty years after it was brought into service, the Patriot air- defense system is finally doing what it was designed for." "No one was 100% sure that the Patriot was capable of destroying a Kh-47 hypersonic missile," said Col. Serhiy Yaremenko, commander of the 96th Anti-Aircraft Missile Brigade, which defends Kyiv. "Ukrainians proved it.": https://archive.ph/2023.06.11- 132200/https://www.wsj.com/amp/articles/u-s-patriot-missile-is-an-unsung-hero-of- ukraine-war-db6053a0.

Now let's see the same code using the decimal number type:

Copy and paste the statements that you wrote before (which used the double variables).

Modify the statements to use decimal and rename the variables to c and d , as shown in the following code:

Console.WriteLine("Using decimals:");

decimal c = 0.1M; // M suffix means a decimal literal value decimal d = 0.2M;

if (c + d == 0.3M)

{

Console.WriteLine($"{c} + {d} equals {0.3M}");

}

else

{

Console.WriteLine($"{c} + {d} does NOT equal {0.3M}");

}

1. Run the code and view the result, as shown in the following output:

Using decimals:

0.1 + 0.2 equals 0.3

The decimal type is accurate because it stores the number as a large integer and shifts the decimal point. For example, 0.1 is stored as 1 , with a note to shift the decimal point one place to the left. 12.75 is stored as 1275 , with a note to shift the decimal point two places to the left.

Good Practice: Use int for whole numbers. Use double for real numbers that will not be compared for equality to other values; it is okay to compare double values being less than or greater than, and so on. Use decimal for money, CAD drawings, general engineering, and wherever the accuracy of a real number is important.

The float and double types have some useful special values: NaN represents not-a-number (for example, the result of dividing by zero), Epsilon represents the smallest positive number that can be stored in a float or double , and PositiveInfinity and NegativeInfinity represent infinitely large positive and negative values. They also have methods for checking for these special values like IsInfinity and IsNaN .

New number types and unsafe code

The System.Half type was introduced in .NET 5. Like float and double it can store real numbers. It normally uses two bytes of memory. The System.Int128 and System.UInt128 types were introduced in .NET 7. Like int and uint , they can store signed (positive and negative) and unsigned (only zero and positive) integer values. They normally use 16 bytes of memory.For these new number types, the sizeof operator only works in an unsafe code block, and you must compile the project using an option to enable unsafe code. Let's explore how this works:

1. In Program.cs , at the bottom of the file, type statements to show the size of the Half

and Int128 number data types, as shown in the following code:

unsafe

{

Console.WriteLine($"Half uses {sizeof(Half)} bytes and can store numbers in the range {Half.Mi Console.WriteLine($"Int128 uses {sizeof(Int128)} bytes and can store numbers in the range {Int

}

1. In Numbers.csproj , add an element to enable unsafe code, as shown highlighted in the following markup:

Exe

net8.0

enable

enable

True

1. Run the Numbers project and note the sizes of the two new number types, as shown in the following output:

Half uses 2 bytes and can store numbers in the range -65,504 to 65,504.

Int128 uses 16 bytes and can store numbers in the range -170,141,183,460,469,231,731,687,303,715

More Information: The sizeof operator requires an unsafe code block except for the commonly used types like int and byte . You can learn more about sizeof at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/operators/sizeof. Unsafe code cannot have its safety verified. You can learn more about unsafe code blocks at the following link: https://learn.microsoft.com/en- us/dotnet/csharp/language-reference/unsafe-code.

Storing Booleans‌

Booleans can only contain one of the two literal values true or false , as shown in the following code:

bool happy = true; bool sad = false;

They are most used to branch and loop. You don't need to fully understand them yet, as they are covered more in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.

Storing any type of object‌

There is a special type named object that can store any type of data, but its flexibility comes at the cost of messier code and possibly poor performance. Because of those two reasons, you should avoid it whenever possible. The following steps show you how to use object types if you need to use them because you must use a Microsoft or third-party library that uses them:

Use your preferred code editor to add a new Console App / console project named

Variables to the Chapter02 solution.

In Program.cs , delete the existing statements and then type statements to declare and use some variables using the object type, as shown in the following code:

object height = 1.88; // Storing a double in an object. object name = "Amir"; // Storing a string in an object. Console.WriteLine($"{name} is {height} metres tall."); int length1 = name.Length; // This gives a compile error!

int length2 = ((string)name).Length; // Cast name to a string. Console.WriteLine($"{name} has {length2} characters.");

Run the code and note that the fourth statement cannot compile because the data type of the name variable is not known by the compiler, as shown in Figure 2.8:

image

Figure 2.8: The object type does not have a Length property

Add double slashes to the beginning of the statement that cannot compile to comment out the statement, making it inactive.

Run the code again and note that the compiler can access the length of a string if the programmer explicitly tells the compiler that the object variable contains a string by prefixing name variable with a cast expression like (string)name . The results can then successfully be written to the console, as shown in the following output:

Amir is 1.88 meters tall. Amir has 4 characters.

You will learn about cast expressions in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.The object type has been available since the first version of C#, but C# 2 and later have a better alternative called generics, which we will cover in Chapter 6, Implementing Interfaces and Inheriting Classes. This will provide us with the flexibility we want but without the performance overhead.

Storing dynamic types‌

There is another special type named dynamic that can also store any type of data, but even more than object , its flexibility comes at the cost of performance. The dynamic keyword was introduced in C# 4. However, unlike object , the value stored in the variable can have its members invoked without an explicit cast. Let's make use of a dynamic type:

1. Add statements to declare a dynamic variable. Assign a string literal value, and then an integer value, and then an array of integer values. Finally, add a statement to output the length of the dynamic variable, as shown in the following code:

dynamic something;

// Storing an array of int values in a dynamic object.

// An array of any type has a Length property. something = new[] { 3, 5, 7 };

// Storing an int in a dynamic object.

// int does not have a Length property. something = 12;

// Storing a string in a dynamic object.

// string has a Length property. something = "Ahmed";

// This compiles but might throw an exception at run-time. Console.WriteLine($"The length of something is {something.Length}");

// Output the type of the something variable. Console.WriteLine($"something is a {something.GetType()}");

You will learn about arrays in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.

1. Run the code and note it works because a string value does have a Length property, as shown in the following output:

The length of something is 5 something is a System.String

Comment out the statement that assigns a string value to the something variable by prefixing the statement with two slashes // .

Run the code and note the runtime error because the last value assigned to something is an int that does not have a Length property, as shown in the following output:

Unhandled exception. Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'int' does not conta

Comment out the statement that assigns an int to the something variable.

Run the code and note the output because an array of three int values does have a

Length property, as shown in the following output:

The length of something is 3 something is a System.Int32[]

One limitation of dynamic is that code editors cannot show IntelliSense to help you write the code. This is because the compiler cannot check what the type is during build time.

Instead, the CLR checks for the member at runtime and throws an exception if it is missing.Exceptions are a way to indicate that something has gone wrong at runtime. You will learn more about them and how to handle them in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.Dynamic types are most useful when interoperating with

non-.NET systems. For example, you might need to work with a class library written in F#, Python, or some JavaScript. You might also need to interop with technologies like the Component Object Model (COM), for example, when automating Excel or Word.

Declaring local variables‌

Local variables are declared inside methods, and they only exist during the execution of that method. Once the method returns, the memory allocated to any local variables is released.Strictly speaking, value types are released while reference types must wait for garbage collection. You will learn about the difference between value types and reference types and how to make sure that only one garbage collection is needed rather than two when releasing unmanaged resources in Chapter 6, Implementing Interfaces and Inheriting Classes.

Specifying the type of a local variable

Let's explore local variables declared with specific types and using type inference:

image

Type statements to declare and assign values to some local variables using specific types, as shown in the following code:

int population = 67_000_000; // 67 million in UK. double weight = 1.88; // in kilograms.

decimal price = 4.99M; // in pounds sterling.

string fruit = "Apples"; // string values use double-quotes. char letter = 'Z'; // char values use single-quotes.

bool happy = true; // Booleans can only be true or false.

Depending on your code editor and color scheme, it will show green squiggles under each of the variable names and lighten their text color to warn you that the variable is assigned but its value is never used.

Inferring the type of a local variable

You can use the var keyword to declare local variables with C# 3 and later. The compiler will infer the type from the value that you assign after the assignment operator, = . This happens at compile time so using var has no effect on runtime performance.A literal number without a decimal point is inferred as an int variable, that is, unless you add a suffix, as described in the following list:

image

image

L : Compiler infers long UL : Compiler infers ulong

image

image

image

M : Compiler infers decimal D : Compiler infers double F : Compiler infers float

A literal number with a decimal point is inferred as double unless you add the M suffix, in which case the compiler infers a decimal variable, or the F suffix, in which case it infers a float variable.Double quotes indicate a string variable, single quotes indicate a char variable, and the true and false values infer a bool type:

1. Modify the previous statements to use var , as shown in the following code:

var population = 67_000_000; // 67 million in UK. var weight = 1.88; // in kilograms.

var price = 4.99M; // in pounds sterling.

var fruit = "Apples"; // string values use double-quotes. var letter = 'Z'; // char values use single-quotes.

var happy = true; // Booleans can only be true or false.

Hover your mouse over each of the var keywords and note that your code editor shows a tooltip with information about the type that has been inferred.

At the top of Program.cs , import the namespace for working with XML to enable us to declare some variables using types in that namespace, as shown in the following code:

using System.Xml; // To use XmlDocument.

1. At the bottom of Program.cs , add statements to create some new objects, as shown in the following code:

// Good use of var because it avoids the repeated type

// as shown in the more verbose second statement.

var xml1 = new XmlDocument(); // Works with C# 3 and later. XmlDocument xml2 = new XmlDocument(); // Works with all C# versions.

// Bad use of var because we cannot tell the type, so we

// should use a specific type declaration as shown in

// the second statement.

var file1 = File.CreateText("something1.txt"); StreamWriter file2 = File.CreateText("something2.txt");

Good Practice: Although using var is convenient, some developers avoid using it to make it easier for a code reader to understand the types in use. Personally, I use it only when the type is obvious. For example, in the preceding code statements, the first statement is just as clear as the second in stating what the types of the xml variables are, but it is shorter. However, the third statement isn't clear in showing the type of the file variable, so the fourth is better because it shows that the type is StreamWriter . If in doubt, spell it out!

Using target-typed new to instantiate objects

With C# 9, Microsoft introduced another syntax for instantiating objects known as target- typed new. When instantiating an object, you can specify the type first and then use new without repeating the type, as shown in the following code:

XmlDocument xml3 = new(); // Target-typed new in C# 9 or later.

If you have a type with a field or property that needs to be set, then the type can be inferred, as shown in the following code:

// In Program.cs.

Person kim = new();

kim.BirthDate = new(1967, 12, 26); // i.e. new DateTime(1967, 12, 26)

// In a separate Person.cs file or at the bottom of Program.cs. class Person

{

public DateTime BirthDate;

}

This way of instantiating objects is especially useful with arrays and collections because they have multiple objects often of the same type, as shown in the following code:

List people = new() // Instead of: new List()

{

new() { FirstName = "Alice" }, // Instead of: new Person() { ... } new() { FirstName = "Bob" },

new() { FirstName = "Charlie" }

};

You will learn about arrays in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions, and collections in Chapter 8, Working with Common .NET Types.

Good Practice: Use target-typed new to instantiate objects because it requires fewer characters, when reading a statement from left to right, as in English, you immediately know the type of the variable, and it is not limited to local variables like var is.

IMHO, the only reason not to use target-typed new is if you must use a pre-version 9 C# compiler. I do acknowledge that my opinion is not accepted by the whole C# community. I have used target-typed new throughout the remainder of this book. Please let me know if you spot any cases that I missed!

Getting and setting the default values for types‌

Most of the primitive types except string are value types, which means that they must have a value. You can determine the default value of a type by using the default() operator and passing the type as a parameter. You can assign the default value of a type by using the default keyword.The string type is a reference type. This means that string variables contain the memory address of a value, not the value itself. A reference type variable can have a null value, which is a literal that indicates that the variable does not reference anything (yet). null is the default for all reference types.You'll learn more about value types and reference types in Chapter 6, Implementing Interfaces and Inheriting Classes.Let's explore default values:

1. Add statements to show the default values of an int , a bool , a DateTime , and a

string , as shown in the following code:

Console.WriteLine($"default(int) = {default(int)}"); Console.WriteLine($"default(bool) = {default(bool)}"); Console.WriteLine($"default(DateTime) = {default(DateTime)}"); Console.WriteLine($"default(string) = {default(string)}");

1. Run the code and view the result. Note that your output for the date and time might be formatted differently if you are not running it in the UK because date and time values are formatted using the current culture of your computer, and that null values output as an empty string , as shown in the following output:

default(int) = 0 default(bool) = False

default(DateTime) = 01/01/0001 00:00:00 default(string) =

1. Add statements to declare a number, assign a value, and then reset it to its default value, as shown in the following code:

int number = 13;

Console.WriteLine($"number set to: {number}"); number = default;

Console.WriteLine($"number reset to its default: {number}");

1. Run the code and view the result, as shown in the following output:

number set to: 13

number reset to its default: 0

Exploring more about console apps‌

We have already created and used basic console apps, but we're now at a stage where we should delve into them more deeply.Console apps are text-based and are run at the command prompt. 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:

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

Displaying output to the user‌

The two most common tasks that a console app performs are writing and reading data. We have already used the WriteLine method to output, but if we didn't want a carriage return at the end of a line, for example, if we later wanted to continue to write more text at the end of that line, we could have used the Write method.If you want to write three letters to the console without carriage returns after them, then call the Write method, as shown in the following code:

Write("A");

Write("B");

Write("C");

This would write the three characters on a single line and leave the cursor at the end of the line, as shown in the following output:

ABC

If you want to write three letters to the console with carriage returns after them, then call the WriteLine method, as shown in the following code:

WriteLine("A");

WriteLine("B");

WriteLine("C");

This would write three lines and leave the cursor on the fourth line:

A B C

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 . For methods that do not support the feature, the string parameter can be formatted using the Format method of string .Let's begin formatting:

Use your preferred code editor to add a new Console App / console project named

Formatting to the Chapter02 solution.

In Program.cs , delete the existing statements and then 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 cost {1:C}", arg0: numberOfApples,

arg1: pricePerApple * numberOfApples); string formatted = string.Format(

format: "{0} apples cost {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.

The Write , WriteLine , and Format methods can have up to three numbered arguments, named arg0 , arg1 , and arg2 . If you need to pass more than three values, then you cannot name them.

1. In Program.cs , type statements to write three and then five arguments to the console, as shown in the following code:

// Three parameter values can use named arguments. Console.WriteLine("{0} {1} lived in {2}.",

arg0: "Roger", arg1: "Cevung", arg2: "Stockholm");

// Four or more parameter values cannot use named arguments. Console.WriteLine(

"{0} {1} lived in {2} and worked in the {3} team at {4}.", "Roger", "Cevung", "Stockholm", "Education", "Optimizely");

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.

JetBrains Rider and its warnings about boxing

If you use JetBrains Rider and you have installed the Unity Support plugin, then it will complain a lot about boxing. A common scenario when boxing happens is when value types like int and DateTime are passed as positional arguments to string formats. This is a problem for Unity projects because they use a different memory garbage collector to the normal .NET runtime. For non-Unity projects, like all the projects in this book, you can ignore these boxing warnings because they are not relevant. You can read more about this Unity-specific issue at the following link: https://docs.unity3d.com/Manual/performance-garbage-collection- best-practices.xhtml#boxing.

Formatting using interpolated strings

C# 6 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:

// The following statement must be all on one line when using C# 10

// or earlier. If using C# 11 or later, we can include a line break

// in the middle of an expression but not in the string text. Console.WriteLine($"{numberOfApples} apples cost {pricePerApple

* numberOfApples:C}");

1. Run the code and view the result, as shown in the following partial output:

12 apples cost £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 statements 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.The next code example is not meant to be entered in your project.Before C# 10, string constants could only be combined by using concatenation with the + operator, 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 (prefixed with $ ) 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, which would require runtime data type conversions. You cannot enter private const declarations in a top-level program like Program.cs . You will see how to use them in Chapter 5, Building Your Own Types with Object-Oriented Programming.

Good Practice: If you are writing code that will be part of a Unity project then interpolated string formats is an easy way to avoid boxing.

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 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 code that uses the number or currency format on a PC in the UK, you'll get pounds sterling with commas as the thousand separators, but if you run it 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();

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);

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

Custom number formatting

You can take complete control of number formatting using custom format codes, as shown in

Table 2.21:

Format Description code

0 Zero placeholder. Replaces the zero with the corresponding digit if present; otherwise, it uses zero. For example, 0000.00 formatting the value 123.4 would give 0123.40 .

# Digit placeholder. Replaces the hash with the corresponding digit if present; otherwise, it uses nothing. For example, ####.## formatting the value 123.4 would give 123.4 .

. Decimal point. Sets the location of the decimal point in the number. Respects culture formatting, so it is a . (dot) in US English but a , (comma) in French.

, Group separator. Inserts a localized group separator between each group. For example, 0,000 formatting the value 1234567 would give 1,234,567 . Also used to scale a number by dividing by multiples of 1000 for each comma. For example, 0.00,, formatting the value 1234567 would give 1.23 because the two commas mean divide by 1,000 twice.

% Percentage placeholder. Multiples the value by 100 and adds a percentage character.

\ Escape character. Makes the next character a literal instead of a format code. For example, \##,####\# formatting the value 1234 would give #1,234# .

; Section separator. Defines different format strings for positive, negative, and zero numbers. For example, [0];(0);Zero formatting: 13 would give [13] , -13 would give

(13) , and 0 would give Zero .

Others All other characters are shown in the output as is. Table 2.21: Custom numeric format codes

More Information: A full list of custom number format codes can be found at the following link: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom- numeric-format-strings.

You can apply standard number formatting using simpler format codes, like C and N . They support a precision number to indicate how many digits of precision you want. The default is two. The most common, as shown in Table 2.22:

Format Description code

C or

c

N or

n

D or

d

B or

b

X or

x

E or

e

Currency. For example, in US culture, C formatting the value 123.4 gives $123.40 , and C0 formatting the value 123.4 gives $123 .

Number. Integer digits with an optional negative sign and grouping characters. Decimal. Integer digits with an optional negative sign but no grouping characters.

Binary. For example, B formatting the value 13 gives 1101 and B8 formatting the value 13 gives 00001101 .

Hexadecimal. For example, X formatting the value 255 gives FF and X4 formatting the value 255 gives 00FF .

Exponential notation. For example, E formatting the value 1234.567 would give

1.234567000E+003 and E2 formatting the value 1234.567 would give 1.23E+003 .

Table 2.22: Standard numeric format codes

More Information: A full list of standard number format codes can be found at the following link: https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard- numeric-format-strings.

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.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}.");

By default, with .NET 6 and later, nullability checks are enabled, so the C# compiler gives two warnings because the ReadLine method could return a null value instead of a string value. But there is no scenario where this method would actually return null , so instead we will see how to switch off these specific warnings in this scenario.

1. For the firstName variable, append a ? after string , as shown highlighted in the following code:

string? firstName = Console.ReadLine();

This tells the compiler that we are expecting a possible null value, so it does not need to warn us. If the variable is null then when it is later output with WriteLine , it will just be blank, so that works fine in this case. If we were going to access any of the members of the firstName variable, then we would need to handle the case where it is null .

1. For the age variable, append a ! before the semi-colon at the end of the statement, as shown highlighted in the following code:

string age = Console.ReadLine()!;

This is called the null-forgiving operator because it tells the compiler that, in this case, ReadLine will not return null , so it can stop showing the warning. It is now our responsibility to ensure this is the case. Luckily, the Console type's implementation of ReadLine always returns a string even if it is just an empty string value.

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

You have now seen two common ways to handle nullability warnings from the compiler. We will cover nullability and how to handle it in more detail in Chapter 6, Implementing Interfaces and Inheriting Classes.

Simplifying the usage of the console‌

In C# 6 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.

Importing a static type for a single file

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;

Select the first Console. in your code, ensuring that you select the dot after the word

Console too.

In Visual Studio 2022, navigate to Edit | Find and Replace | Quick Replace; in Visual Studio Code, navigate to Edit | Replace; or in JetBrains Rider, navigate to Edit | Find

| 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.9:

image

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

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.

Run the console app and note the behavior is the same as before.

Importing a static type for all code files in a project

Instead of statically importing the Console class just for one code file, it would probably be better to import it globally for all code files in the project:

Delete the statement to statically import System.Console .

Open Formatting.csproj , and after the section, add a new section to globally and statically import System.Console using the implicit using .NET SDK feature, as shown in the following markup:

1. Run the console app and note the behavior is the same as before.

Good Practice: In the future, for all console app projects you create for this book, add the section above to simplify the code you need to write in all C# files to work with the Console class.

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, which is then returned as a ConsoleKeyInfo value.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);

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

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

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

Warning! When running a console app in a terminal within Visual Studio Code, some keyboard combinations will be captured by the code editor before they can be processed by your console app. For example, Ctrl + Shift + X in Visual Studio Code activates the Extensions view in the sidebar. To fully test this console app, open a command prompt or terminal in the project folder and run the console app from there.

Passing arguments to a console app‌

When you run a console app, you often want to change its behavior by passing arguments. For example, with the dotnet command-line tool, you can pass the name of a new project template, as shown in the following commands:

dotnet new console dotnet new mvc

You might have been wondering how to get any arguments that might be passed to a console app.In every version of .NET prior to version 6, the console app 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 app. But in top-level programs, as used by the console app project template in .NET 6 and later, the Program class and its Main method are hidden, along with the declaration of the args 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 app:

Use your preferred code editor to add a new Console App / console project named

Arguments to the Chapter02 solution.

Open Arguments.csproj , and after the section, add a new

section to statically import System.Console for all C# files using the implicit usings

.NET SDK feature, as shown in the following markup:

Good Practice: Remember to use the implicit usings .NET SDK feature to statically import the System.Console type in all future console app projects to simplify your code, as these instructions will not be repeated every time.

1. In Program.cs , delete the existing statements, and then add a statement to output the number of arguments passed to the application, as shown in the following code:

WriteLine($"There are {args.Length} arguments.");

1. Run the console app and view the result, as shown in the following output:

There are 0 arguments.

If you are using Visual Studio 2022 for Windows:

Navigate to Project | Arguments Properties.

Select the Debug tab, click Open debug launch profiles UI, and in the Command line arguments box, enter the following arguments:

firstarg second-arg third:arg "fourth arg" , as shown in Figure 2.10:

image

Figure 2.10: Entering command-line arguments in the Visual Studio project properties on Windows

Close the Launch Profiles window.

In Solution Explorer, in the Properties folder, open the launchSettings.json file and note it defines the command-line arguments when you run the project, as shown

highlighted in the following configuration:

{

"profiles": {

"Arguments": { "commandName": "Project",

"commandLineArgs": "firstarg second-arg third:arg \"fourth arg\""

}

}

}

The launchSettings.json file can also be used by JetBrains Rider. The equivalent for Visual Studio Code is the .vscode/launch.json file.

1. Run the console app project. If you are using JetBrains Rider:

Right-click the Arguments project.

In the popup menu, select More Run/Debug | Modify Run Configuration….

In the Edit Run Configuration: 'Arguments' dialog box, in the Program arguments box, enter firstarg second-arg third:arg "fourth arg" , as shown in Figure 2.11:

image

Figure 2.11: Entering command-line arguments in the JetBrains Rider run configuration

Click OK.

Run the console app.

If you are using Visual Studio Code:

image

In Terminal, enter some arguments after the dotnet run command, as shown in the following command:

dotnet run firstarg second-arg third:arg "fourth arg"

For all code editors:

1. Note the result indicates four arguments, as shown in the following output:

There are 4 arguments.

1. In Program.cs , 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);

}

1. 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.We have statically imported the System.Console class. It has properties like ForegroundColor , BackgroundColor , and CursorSize that we can now set just by using their names without needing to prefix them with Console . 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]); Note the compiler warning that setting the CursorSize is only supported on Windows. For now, do not worry about most of this code like (ConsoleColor) , Enum.Parse , or typeof , as it will all be explained in the next few chapters. image If you are using Visual Studio 2022, 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.12: image Figure 2.13: Setting colors and cursor size on Windows image If you are using Visual Studio Code, then 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 or Linux, you'll see an unhandled exception, as shown in Figure 2.13: image Figure 2.13: 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 app running on Windows can change its cursor size, on macOS, it cannot, and it 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."); } 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 Table 2.23: Target Framework Symbols .NET Standard NETSTANDARD2_0 , NETSTANDARD2_1 , and so on Modern .NET NET7_0 , NET7_0_ANDROID , NET7_0_IOS , NET7_0_WINDOWS , and so on Table 2.23: Predefined compiler symbols You can then write statements that will compile only for the specified platforms, as shown in the following code: #if NET7_0_ANDROID // Compile statements that only work on Android. #elif NET7_0_IOS // Compile statements that only work on iOS. #else // Compile statements that work everywhere else. #endif Understanding async and await‌ C# 5 introduced two C# keywords when working with the Task type that enable easy multithreading. The pair of keywords is especially useful for the following: image image Implementing multitasking for a graphical user interface (GUI). Improving the scalability of web applications and web services. image Preventing blocking calls when interacting with the filesystem, databases, and remote services, all of which tend to take a long time to complete their work. In Chapter 13, Building Websites Using the Model-View-Controller Pattern, we will see how the async and await keywords can improve scalability for websites. But for now, let's see an example of how they can be used in a console app, and then later you will see them used in a more practical example within web projects. Improving responsiveness for console apps‌ One of the limitations with console apps is that you can only use the await keyword inside methods that are marked as async , but C# 7 and earlier do not allow the Main method to be marked as async ! Luckily, a new feature introduced in C# 7.1 was support for async in Main .Let's see it in action: Use your preferred code editor to add a new Console App / console project named AsyncConsole to the Chapter02 solution. Open AsyncConsole.csproj , and after the section, add a new

section to statically import System.Console for all C# files using the implicit usings

.NET SDK feature, as shown in the following markup:

1. In Program.cs , delete the existing statements, and then add statements to create an HttpClient instance, make a request for Apple's home page, and output how many bytes it has, as shown in the following code:

HttpClient client = new(); HttpResponseMessage response =

await client.GetAsync("http://www.apple.com/"); WriteLine("Apple's home page has {0:N0} bytes.",

response.Content.Headers.ContentLength);

Navigate to Build | Build AsyncConsole and note that the project builds successfully.

In .NET 5 and earlier, you would have seen an error message, as shown in the following output:

Program.cs(14,9): error CS4033: The 'await' operator can only be used within an async me

You would have had to add the async keyword to your Main method and change its return type from void to Task . With .NET 6 and later, the console app project template uses the top-level program feature to automatically define the Program class with an asynchronous

$ method for you.

Run the code and view the result, which is likely to have a different number of bytes since Apple changes its home page frequently, as shown in the following output:

Apple's home page has 170,688 bytes.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring the topics covered in this chapter with deeper research.

Exercise 2.1 – Test your knowledge‌

To get the best answer to some of these questions, you will need to do your own research. I want you to "think outside the book," so I have deliberately not provided all the answers in the book.I want to encourage you to get into the good habit of looking for help elsewhere, following the principle of "teach a person to fish."

What statement can you type in a C# file to discover the compiler and language version?

What are the two types of comments in C#?

What is the difference between a verbatim string and an interpolated string?

Why should you be careful when using float and double values?

How can you determine how many bytes a type like double uses in memory?

When should you use the var keyword?

What is the newest syntax to create an instance of a class like XmlDocument ?

Why should you be careful when using the dynamic type?

How do you right-align a format string?

What character separates arguments for a console app?

Appendix, Answers to the Test Your Knowledge Questions, is available to download from a link in the README on the GitHub repository: https://github.com/markjprice/cs12dotnet8.

Exercise 2.2 – Test your knowledge of number types‌

What type would you choose for the following "numbers"?

image

image

A person's telephone number A person's height

image

A person's age

image

image

A person's salary A book's ISBN

image

A book's price

image

image

A book's shipping weight A country's population

image

The number of stars in the universe

image

The number of employees in each of the small or medium businesses in the UK (up to about 50,000 employees per business)

Exercise 2.3 – Practice number sizes and ranges‌

In the Chapter02 solution, create a console app project named Ch02Ex03Numbers that outputs the number of bytes in memory that each of the following number types uses and the minimum and maximum values they can have: sbyte , byte , short , ushort , int , uint , long , ulong , Int128 , UInt128 , Half , float , double , and decimal .The result of running your console app should look something like Figure 2.14:

image

Figure 2.14: The result of outputting number type sizes

Code solutions for all exercises are available to download or clone from the GitHub repository at the following link: https://github.com/markjprice/cs12dotnet8.

Exercise 2.4 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-2--- speaking-c

Exercise 2.5 – Explore Spectre‌

No, not the villainous organization from the James Bond films! Spectre is a package that enhances console apps. You can read about it at the following link: https://spectreconsole.net/ .

Summary‌

In this chapter, you learned how to:

image

Declare variables with a specified or inferred type.

image

image

Use some of the built-in types for numbers, text, and Booleans. Choose between number types.

image

Control output formatting in console apps.

In the next chapter, you will learn about operators, branching, looping, converting between types, and how to handle exceptions.

3 Controlling Flow, Converting Types, and Handling Exceptions‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is all about writing code that performs simple operations on variables, makes decisions, performs pattern matching, and repeats statements or blocks. You will also learn how to work with arrays to store multiple values, how to convert variable or expression values from one type to another, how to handle exceptions, and how to check for overflows in number variables.This chapter covers the following topics:

image

image

image

image

Operating on variables Understanding selection statements Understanding iteration statements Storing multiple values in an array

image

image

Casting and converting between types Handling exceptions

image

Checking for overflow

Operating on variables‌

Operators apply simple operations such as addition and multiplication to operands such as variables and literal values. Operators return a new value that is the result of the operation and can be assigned to a variable, and they can also affect the operands.

Understanding binary operators‌

Most operators are binary, meaning that they work on two operands, as shown in the following pseudocode:

var resultOfOperation = firstOperand operator secondOperand;

Examples of binary operators include adding and multiplying, as shown in the following code:

int x = 5; int y = 3;

int resultOfAdding = x + y;

int resultOfMultiplying = x * y;

Understanding unary operators‌

Some operators are unary, meaning they work on a single operand and can be applied before or after the operand, as shown in the following pseudocode:

var resultOfOperationAfter = onlyOperand operator; var resultOfOperationBefore = operator onlyOperand;

Examples of unary operators include incrementors and retrieving a type or its size in bytes, as shown in the following code:

int x = 5;

int postfixIncrement = x++; int prefixIncrement = ++x;

Type theTypeOfAnInteger = typeof(int); string nameOfVariable = nameof(x);

int howManyBytesInAnInteger = sizeof(int);

Understanding ternary operators‌

A ternary operator works on three operands, as shown in the following pseudocode:

var resultOfOperation = firstOperand firstOperator secondOperand secondOperator thirdOperand;

An example of a ternary operator is the conditional operator ?: , which acts like a simplified if statement. The first operand is a Boolean expression, the second operand is a value to return if it is true , and the third operand is a value to return if it is false , as shown in the following code:

// Syntax of conditional operator.

var result = boolean_expression ? value_if_true : value_if_false;

// Example of conditional operator.

string result = x > 3 ? "Greater than 3" : "Less than or equal to 3";

// Equivalent using an if statement. string result;

if (x > 3)

{

result = "Greater than 3";

}

else

{

result = "Less than or equal to 3";

}

More experienced C# developers adopt ternary operators as much as possible because they are concise and can result in cleaner code once you are used to reading them.

Exploring unary operators‌

Two common unary operators are used to increment, ++ , and decrement, -- , a number. Let us write some example code to show how they work:

If you've completed the previous chapters, then you will already have a cs12dotnet8 folder. If not, then you'll need to create it.

Use your preferred coding tool to create a new solution and project, as defined in the following list:

image

image

image

Project template: Console App / console Project file and folder: Operators Solution file and folder: Chapter03

image

Do not use top-level statements: Cleared

image

Enable native AOT publish: Cleared

In Operators.csproj , add a new section to statically import System.Console for all C# files using the implicit usings .NET SDK feature, as shown in the following markup:

1. In Program.cs , delete the existing statements and then declare two integer variables named a and b , set a to 3 , increment a while assigning the result to b , and then output their values, as shown in the following code:

#region Exploring unary operators int a = 3;

int b = a++;

WriteLine($"a is {a}, b is {b}"); #endregion

Good Practice: I recommend wrapping the statements for each section in #region and #endregion as shown in the preceding code so that you can easily collapse the sections. But I will not show this in future code tasks to save space.

1. Before running the console app, ask yourself a question: what do you think the value of b will be when output? Once you've thought about that, run the code and compare your prediction against the actual result, as shown in the following output:

a is 4, b is 3

The variable b has the value 3 because the ++ operator

executes after the assignment; this is known as a postfix operator. If you need to increment before the assignment, then use the prefix operator.

1. Copy and paste the statements, and then modify them to rename the variables and use the prefix operator, as shown in the following code:

int c = 3;

int d = ++c; // Prefix means increment c before assigning it. WriteLine($"c is {c}, d is {d}");

1. Rerun the code and note the result, as shown in the following output:

a is 4, b is 3

c is 4, d is 4

Good Practice: Due to the confusion between the prefix and postfix for the increment and decrement operators when combined with an assignment, the Swift programming language designers decided to drop support for this operator in version 3. My recommendation for usage in C# is to never combine the use of the ++ and -- operators with an assignment operator, = . Perform the operations as separate statements.

Exploring binary arithmetic operators‌

Increment and decrement are unary arithmetic operators. Other arithmetic operators are usually binary and allow you to perform arithmetic operations on two numbers, as the following shows:

1. Add statements to declare and assign values to two integer variables named e and f , and then apply the five common binary arithmetic operators to the two numbers, as shown in the following code:

int e = 11; int f = 3;

WriteLine($"e is {e}, f is {f}"); WriteLine($"e + f = {e + f}"); WriteLine($"e - f = {e - f}");

WriteLine($"e * f = {e * f}"); WriteLine($"e / f = {e / f}"); WriteLine($"e % f = {e % f}");

1. Run the code and note the result, as shown in the following output:

e is 11, f is 3 e + f = 14

e - f = 8 e * f = 33 e / f = 3 e % f = 2

To understand the divide / and modulo % operators when applied to integers, you need to think back to primary school. Imagine you have eleven sweets and three friends.How can you divide the sweets between your friends? You can give three sweets to each of your friends, and there will be two left over. Those two sweets are the modulus, also known as the remainder after dividing. If you had twelve sweets, then each friend would get four of them, and there would be none left over, so the remainder would be 0.

1. Add statements to declare and assign a value to a double variable named g to show the difference between whole-number and real-number divisions, as shown in the following code:

double g = 11.0;

WriteLine($"g is {g:N1}, f is {f}"); WriteLine($"g / f = {g / f}");

1. Run the code and note the result, as shown in the following output:

g is 11.0, f is 3

g / f = 3.6666666666666665

If the first operand is a floating-point number, such as g with the value 11.0 , then the divide operator returns a floating-point value, such as 3.6666666666665 , rather than a whole number.

Assignment operators‌

You have already been using the most common assignment operator, = .To make your code more concise, you can combine the assignment operator with other operators like arithmetic operators, as shown in the following code:

int p = 6;

p += 3; // Equivalent to: p = p + 3; p -= 3; // Equivalent to: p = p - 3; p *= 3; // Equivalent to: p = p * 3; p /= 3; // Equivalent to: p = p / 3;

Null-coalescing operators‌

Related operators to the assignment operators are the null-coalescing operators. Sometimes, you want to either assign a variable to a result or if the variable is null , then assign an alternative value. You can do this using the null-coalescing operators, ?? or ??= , as shown in the following code:

string? authorName = ReadLine(); // Prompt user to enter an author name.

// The maxLength variable will be the length of authorName if it is

// not null, or 30 if authorName is null. int maxLength = authorName?.Length ?? 30;

// The authorName variable will be "unknown" if authorName was null. authorName ??= "unknown";

Exploring logical operators‌

Logical operators operate on Boolean values, so they return either true or false . Let's explore binary logical operators that operate on two Boolean values, traditionally named p and q in mathematics:

1. In Program.cs , add statements to declare two Boolean variables p and q with values

of true and false , and then output truth tables showing the results of applying AND, OR, and XOR (exclusive OR) logical operators, as shown in the following code:

bool p = true; bool q = false;

WriteLine($"AND | p | q "); WriteLine($"p | {p & p,-5} | {p & q,-5} ");

WriteLine($"q | {q & p,-5} | {q & q,-5} "); WriteLine();

WriteLine($"OR | p | q "); WriteLine($"p | {p | p,-5} | {p | q,-5} ");

WriteLine($"q | {q | p,-5} | {q | q,-5} "); WriteLine();

WriteLine($"XOR | p | q "); WriteLine($"p | {p ^ p,-5} | {p ^ q,-5} ");

WriteLine($"q | {q ^ p,-5} | {q ^ q,-5} ");

Remember that ,-5 means left-align within a five-width column.

1. Run the code and note the results, as shown in the following output:

AND | p | q

| True | False

| False | False OR | p | q

| True | True

| True | False XOR | p | q

| False | True

| True | False

For the AND & logical operator, both operands must be true for the result to

be true . For the OR | logical operator, either operand can be true for the result to be true . For the XOR ^ logical operator, either operand can be true (but not both!) for the result to be true .

Exploring conditional logical operators‌

Conditional logical operators are like logical operators, but you use two symbols instead of one, for example, && instead of & , or || instead of | .In Chapter 4, Writing, Debugging, and Testing Functions, you will learn about functions in more detail, but I need to introduce functions now to explain conditional logical operators, also known as short- circuiting Boolean operators.A function executes statements and then returns a value. That value could be a Boolean value like true that is used in a Boolean operation. Let's make use of conditional logical operators:

1. At the bottom of Program.cs , write statements to declare a function that writes a message to the console and returns true , as shown in the following code:

static bool DoStuff()

{

WriteLine("I am doing some stuff.");

return true;

}

Local functions can be anywhere within the statements in Program.cs that uses the top- level program feature but it is good practice to put them at the bottom of the file.

1. After the previous WriteLine statements, perform an AND & operation on

the p and q variables, and the result of calling the function, as shown in the following code:

WriteLine();

// Note that DoStuff() returns true. WriteLine($"p & DoStuff() = {p & DoStuff()}"); WriteLine($"q & DoStuff() = {q & DoStuff()}");

1. Run the code, view the result, and note that the function was called twice, once for p

and once for q , as shown in the following output:

I am doing some stuff. p & DoStuff() = True

I am doing some stuff. q & DoStuff() = False

1. Copy and paste the three statements and then change the & operators into && operators, as shown in the following code:

WriteLine();

WriteLine($"p && DoStuff() = {p && DoStuff()}"); WriteLine($"q && DoStuff() = {q && DoStuff()}");

1. Run the code, view the result, and note that the function does run when combined with the p variable. It does not run when combined with the q variable because

the q variable is false so the result will be false anyway, so it does not need to execute the function, as shown in the following output:

I am doing some stuff. p && DoStuff() = True

q && DoStuff() = False // DoStuff function was not executed!

Good Practice: Now you can see why the conditional logical operators are described as being short-circuiting. They can make your apps more efficient, but they can also introduce subtle bugs in cases where you assume that the function will always be called. It is safest to avoid them when used in combination with functions that cause side effects.

Exploring bitwise and binary shift operators‌

Bitwise operators compare the bits in the binary representation of a number. Each bit, either the 0 (zero) or 1 (one) value, is compared individually to the bit in the same column.Binary shift operators can perform some common arithmetic calculations much faster than traditional operators, for example, any multiplication by a factor of 2.Let's explore bitwise and binary shift operators:

1. In Program.cs , add statements to declare two integer variables named x and y with values 10 and 6 , and then output the results of applying AND, OR, and XOR bitwise operators, as shown in the following code:

WriteLine(); int x = 10; int y = 6;

WriteLine($"Expression | Decimal | Binary");

WriteLine($" ");

WriteLine($"x | {x,7} | {x:B8}");

WriteLine($"y | {y,7} | {y:B8}");

WriteLine($"x & y | {x & y,7} | {x & y:B8}");

WriteLine($"x | y | {x | y,7} | {x | y:B8}");

WriteLine($"x ^ y | {x ^ y,7} | {x ^ y:B8}");

Remember that ,7 means right-align in a seven-width column and :B8 means format in binary with eight digits.

1. Run the code and note the results, as shown in the following output:

Expression | Decimal | Binary

x

|

10 | 00001010

y

|

6 | 00000110

x & y

|

2 | 00000010

x | y

|

14 | 00001110

x ^ y

|

12 | 00001100

For x & y , only the 2-bit column is set. For x | y , the 8, 4 and 2-bit columns are set. For x ^ y , the 8 and 4 columns are set.

1. In Program.cs , add statements to output the results of applying the left-shift operator to move the bits of the variable a by three columns, multiplying a by 8, and right-shifting the bits of the variable b by one column, as shown in the following code:

// Left-shift x by three bit columns.

WriteLine($"x << 3 | {x << 3,7} | {x << 3:B8}"); // Multiply x by 8. WriteLine($"x * 8 | {x * 8,7} | {x * 8:B8}"); // Right-shift y by one bit column. WriteLine($"y >> 1 | {y >> 1,7} | {y >> 1:B8}");

1. Run the code and note the results, as shown in the following output:

x << 3 | 80 | 01010000 x * 8 | 80 | 01010000 y >> 1 | 3 | 00000011

The 80 result is because the bits in it were shifted three columns to the left, so the 1 bits moved into the 64- and 16-bit columns, and 64 + 16 = 80. This is the equivalent of multiplying by 8, but CPUs can perform a bit-shift faster. The 3 result is because the 1 bits in b were shifted one column into the 2- and 1-bit columns.

Good Practice: Remember that when operating on integer values, the & and | symbols are bitwise operators, and when operating on Boolean values like true and false , the & and | symbols are logical operators.

Miscellaneous operators‌

nameof and sizeof are convenient operators when working with types:

image

nameof returns the short name (without the namespace) of a variable, type, or member as a string value, which is useful when outputting exception messages.

image

sizeof returns the size in bytes of simple types, which is useful for determining the efficiency of data storage. Technically, the sizeof operator requires an unsafe code block but the sizes of value types with a C# alias, like int and double , are hardcoded as constants by the compiler so they do not need an unsafe block.

For example:

int age = 50;

WriteLine($"The {nameof(age)} variable uses {sizeof(int)} bytes of memory.");

There are many other operators; for example, the dot between a variable and its members is called the member access operator and the round brackets at the end of a function or method name are called the invocation operator, as shown in the following code:

int age = 50;

// How many operators in the following statement? char firstDigit = age.ToString()[0];

// There are four operators:

// = is the assignment operator

// . is the member access operator

// () is the invocation operator

// [] is the indexer access operator

Understanding selection statements‌

Every application needs to be able to select from choices and branch along different code paths. The two selection statements in C# are if and switch . You can use if for all your code, but switch can simplify your code in some common scenarios, such as when there is a single variable that can have multiple values that each require different processing.

Branching with the if statement‌

The if statement determines which branch to follow by evaluating a Boolean expression. If the expression is true , then the block executes. The else block is optional, and it executes if the if expression is false . The if statement can be

nested.The if statement can be combined with other if statements as else if branches, as shown in the following code:

if (expression1)

{

// Executes if expression1 is true.

}

else if (expression2)

{

// Executes if expression1 is false and expression2 is true.

}

else if (expression3)

{

// Executes if expression1 and expression2 are false

// and expression3 is true.

}

else

{

// Executes if all expressions are false.

}

Each if statement's Boolean expression is independent of the others and,

unlike switch statements, does not need to reference a single value.Let's write some code to explore selection statements like if :

Use your preferred coding tool to add a new Console App / console project named SelectionStatements to the Chapter03 solution.

Remember to statically import System.Console in your project file. And if you are using Visual Studio 2022, then configure the startup project to be the current selection.

In Program.cs , delete the existing statements and then add statements to check if a password is at least eight characters, as shown in the following code:

string password = "ninja"; if (password.Length < 8) { WriteLine("Your password is too short. Use at least 8 chars."); } else { WriteLine("Your password is strong."); } 1. Run the code and note the result, as shown in the following output: Your password is too short. Use at least 8 chars. Why you should always use braces with if statements‌ As there is only a single statement inside each block, the preceding code could be written without the curly braces, as shown in the following code: if (password.Length < 8) WriteLine("Your password is too short. Use at least 8 chars."); else WriteLine("Your password is strong."); This style of if statement should be avoided because it can introduce serious bugs. An infamous example is the #gotofail bug in Apple's iPhone iOS operating system. For 18 months after Apple's iOS 6 was released, in September 2012, it had a bug due to an if statement without braces in its Secure Sockets Layer (SSL) encryption code. This meant that any user running Safari, the device's web browser, who tried to connect to secure websites, such as their bank, was not properly secure because an important check was being accidentally skipped.Just because you can leave out the curly braces, doesn't mean you should. Your code is not "more efficient" without them; instead, it is harder to read, less maintainable, and potentially more dangerous. Pattern matching with the if statement‌ A feature introduced with C# 7 and later is pattern matching. The if statement can use the is keyword in combination with declaring a local variable to make your code safer: 1. Add statements so that if the value stored in the variable named o is an int , then the value is assigned to the local variable named i , which can then be used inside the if statement. This is safer than using the variable named o because we know for sure that i is an int variable and not something else, as shown in the following code: // Add and remove the "" to change between string and int. object o = "3"; int j = 4; if (o is int i) { WriteLine($"{i} x {j} = {i * j}"); } else { WriteLine("o is not an int so it cannot multiply!"); } 1. Run the code and view the results, as shown in the following output: o is not an int so it cannot multiply! Delete the double-quote characters around the "3" value so that the value stored in the variable named o is an int type instead of a string type. Rerun the code to view the results, as shown in the following output: 3 x 4 = 12 Branching with the switch statement‌ The switch statement is different from the if statement because switch compares a single expression against a list of multiple possible case statements. Every case statement is related to the single expression. Every case section must end with one of the following: image The break keyword (like case 1 in the following code). image image The goto case keywords (like case 2 in the following code). They should have no statements (like case 3 in the following code). image image The goto keyword that references a named label (like case 5 in the following code). The return keyword to leave the current function (not shown in the code). Let's write some code to explore the switch statements: 1. Type the following code for a switch statement. You should note that the penultimate statement is a label that can be jumped to, and the first statement generates a random number between 1 and 6 (the number 7 in the code is an exclusive upper bound). The switch statement branches are based on the value of this random number, as shown in the following code: // Inclusive lower bound but exclusive upper bound. int number = Random.Shared.Next(minValue: 1, maxValue: 7); WriteLine($"My random number is {number}"); switch (number) { case 1: WriteLine("One"); break; // Jumps to end of switch statement. case 2: WriteLine("Two"); goto case 1; case 3: // Multiple case section. case 4: WriteLine("Three or four"); goto case 1; case 5: goto A_label; default: WriteLine("Default"); break; } // End of switch statement. WriteLine("After end of switch"); A_label: WriteLine($"After A_label"); Good Practice: You can use the goto keyword to jump to another case or a label. The goto keyword is frowned upon by most programmers but can be a good solution to code logic in some scenarios. However, you should use it sparingly, if at all. To see image how often Microsoft uses goto in the .NET base class libraries, use the following link: https://github.com/search? q=%22goto%20%22+repo%3Adotnet%2Fruntime+language%3AC%23&type=code&ref=advsearch. 1. Run the code multiple times to see what happens in various cases of random numbers, as shown in the following example output: // First random run. My random number is 4 Three or four One After end of switch After A_label // Second random run. My random number is 2 Two One After end of switch After A_label // Third random run. My random number is 6 Default After end of switch After A_label // Fourth random run. My random number is 1 One After end of switch After A_label // Fifth random run. My random number is 5 After A_label Good Practice: The Random class that we used to generate a random number has a Next method that allows you to specify an inclusive lower bound and an exclusive upper bound and will generate a pseudo-random number. Instead of creating a new instance of Random that is not thread-safe, since .NET 6 you can use a Shared instance that is thread-safe so it can be used concurrently from any thread. Adding a new item to a project using Visual Studio 2022‌ Visual Studio 2022 version 17.6 or later has an optional simplified dialog box for adding a new item to a project. After navigating to Project | Add New Item…, or right-clicking on a project in Solution Explorer and selecting Add | New Item…, you will see the traditional dialog box, as shown in Figure 3.1: image Figure 3.1: Add New Item dialog box in normal view If you click the Show Compact View button, then it switches to a simplified dialog box, as shown in Figure 3.2: image Figure 3.2: Add New Item dialog box in compact view To revert to the normal dialog box, click the Show All Templates button. Pattern matching with the switch statement‌ Like the if statement, the switch statement supports pattern matching in C# 7 and later. The case values no longer need to be literal values; they can be patterns.In C# 7 and later, your code can more concisely branch, based on the subtype of a class, and you can declare and assign a local variable to safely use it. Additionally, case statements can include a when keyword to perform more specific pattern matching.Let's see an example of pattern matching with the switch statement using a custom class hierarchy of animals with different properties: You will learn more details about defining classes in Chapter 5, Building Your Own Types with Object-Oriented Programming. For now, you should be able to get the idea from reading the code. In the SelectionStatements project, add a new class file named Animals.cs : image In Visual Studio 2022, navigate to Project | Add New Item… or press Ctrl + Shift + A, type the name, and then click Add. image In Visual Studio Code, click the New File… button and type the name. image In JetBrains Rider, right-click on the project and select Add | Class/Interface…. In Animals.cs , delete any existing statements, and then define three classes, a base class, Animal , and two inherited classes, Cat and Spider , as shown in the following code: class Animal // This is the base type for all animals. { public string? Name; public DateTime Born; public byte Legs; } class Cat : Animal // This is a subtype of animal. { public bool IsDomestic; } class Spider : Animal // This is another subtype of animal. { public bool IsPoisonous; } 1. In Program.cs , add statements to declare an array of nullable animals, and then show a message based on what type and attributes each animal has, as shown in the following code: var animals = new Animal?[] { new Cat { Name = "Karen", Born = new(year: 2022, month: 8, day: 23), Legs = 4, IsDomestic = true }, null, new Cat { Name = "Mufasa", Born = new(year: 1994, month: 6, day: 12) }, new Spider { Name = "Sid Vicious", Born = DateTime.Today, IsPoisonous = true}, new Spider { Name = "Captain Furry", Born = DateTime.Today } }; foreach (Animal? animal in animals) { string message; switch (animal) { case Cat fourLeggedCat when fourLeggedCat.Legs == 4: message = $"The cat named {fourLeggedCat.Name} has four legs."; break; case Cat wildCat when wildCat.IsDomestic == false: message = $"The non-domestic cat is named {wildCat.Name}."; break; case Cat cat: message = $"The cat is named {cat.Name}."; break; default: // default is always evaluated last. message = $"{animal.Name} is a {animal.GetType().Name}."; break; case Spider spider when spider.IsPoisonous: message = $"The {spider.Name} spider is poisonous. Run!"; break; case null: message = "The animal is null."; break; } WriteLine($"switch statement: {message}"); } The case statement shown in the following code: case Cat fourLeggedCat when fourLeggedCat.Legs == 4: Can also be written using the more concise property pattern-matching syntax, as shown in the following code: case Cat { Legs: 4 } fourLeggedCat: 1. Run the code and note that the array named animals is declared to contain the Animal? type, so it could be any subtype of Animal , such as a Cat or a Spider , or a null value. In this code, we create four instances of Animal of different types with different properties, and one null one, so the result will be five messages that describe each of the animals, as shown in the following output: switch statement: The cat named Karen has four legs. switch statement: The animal is null. switch statement: The non-domestic cat is named Mufasa. switch statement: The Sid Vicious spider is poisonous. Run! switch statement: Captain Furry is a Spider. Simplifying switch statements with switch expressions‌ In C# 8 or later, you can simplify switch statements using switch expressions.Most switch statements are very simple, yet they require a lot of typing. switch expressions are designed to simplify the code you need to type while still expressing the same intent in scenarios where all cases return a value to set a single variable. switch expressions use a lambda, => , to indicate a return value.Let's implement the previous code that used a switch statement using a switch expression so that you can compare the two styles:

1. In Program.cs , at the bottom and inside the foreach loop, add statements to set the message based on what type and attributes the animal has, using a switch expression, as shown in the following code:

message = animal switch

{

Cat fourLeggedCat when fourLeggedCat.Legs == 4

=> $"The cat named {fourLeggedCat.Name} has four legs.", Cat wildCat when wildCat.IsDomestic == false

=> $"The non-domestic cat is named {wildCat.Name}.", Cat cat

=> $"The cat is named {cat.Name}.", Spider spider when spider.IsPoisonous

=> $"The {spider.Name} spider is poisonous. Run!", null

=> "The animal is null.",

_

=> $"{animal.Name} is a {animal.GetType().Name}."

};

WriteLine($"switch expression: {message}");

The main differences are the removal of the case and break keywords. The underscore character _ is used to represent the default return value. It is known as a discard and you can read more about it at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards.

1. Run the code, and note that the result is the same as before, as shown in the following output:

switch statement: The cat named Karen has four legs. switch expression: The cat named Karen has four legs. switch statement: The animal is null.

switch expression: The animal is null.

switch statement: The non-domestic cat is named Mufasa. switch expression: The non-domestic cat is named Mufasa. switch statement: The Sid Vicious spider is poisonous. Run! switch expression: The Sid Vicious spider is poisonous. Run! switch statement: Captain Furry is a Spider.

switch expression: Captain Furry is a Spider.

Understanding iteration statements‌

Iteration statements repeat a block of statements either while a condition is true ( while

and for statements) or for each item in a collection ( foreach statement). The choice

of which statement to use is based on a combination of ease of understanding to solve the logic problem and personal preference.

Looping with the while statement‌

The while statement evaluates a Boolean expression and continues to loop while it is true. Let's explore iteration statements:

Use your preferred coding tool to add a new Console App / console project named IterationStatements to the Chapter03 solution.

In Program.cs , delete the existing statements and then add statements to define

a while statement that loops while an integer variable has a value less than 10, as shown in the following code:

int x = 0; while (x < 10) { WriteLine(x); x++; } 1. Run the code and view the results, which should be the numbers 0 to 9, as shown in the following output: 0 1 2 3 4 5 6 7 8 9 Looping with the do statement‌ The do statement is like while , except the Boolean expression is checked at the bottom of the block instead of the top, which means that the block always executes at least once, as the following shows: 1. Type statements to define a do loop, as shown in the following code: string? actualPassword = "Pa$$w0rd"; string? password; do { Write("Enter your password: "); password = ReadLine(); } while (password != actualPassword); WriteLine("Correct!"); 1. Run the code, and note that you are prompted to enter your password repeatedly until you enter it correctly, as shown in the following output: Enter your password: password Enter your password: 12345678 Enter your password: ninja Enter your password: correct horse battery staple Enter your password: Pa$$w0rd Correct! As an optional challenge, add statements so that the user can only make three attempts before an error message is displayed. At this point, you might want to comment out the code for this section so you do not have to keep entering a password every time you run the console app! Looping with the for statement‌ The for statement is like while , except that it is more succinct. It combines: image An optional initializer expression, which executes once at the start of the loop. image An optional conditional expression, which executes on every iteration at the start of the loop to check whether the looping should continue. If the expression returns true or it is missing, the loop will execute again. image An optional iterator expression, which executes on every loop at the bottom of the statement. This is often used to increment a counter variable. The for statement is commonly used with an integer counter. Let's explore some code: 1. Type a for statement to output the numbers 1 to 10, as shown in the following code: for (int y = 1; y <= 10; y++) { WriteLine(y); } Run the code to view the result, which should be the numbers 1 to 10. Add another for statement to output the numbers 0 to 10, incrementing by 3, as shown in the following code: for (int y = 0; y <= 10; y += 3) { WriteLine(y); } Run the code to view the result, which should be the numbers 0, 3, 6, and 9. Optionally, experiment with changing the initializer expression, conditional expression, or iterator expression to see their effects. Only change one thing at a time so that you can clearly see the effect produced. Looping with the foreach statement‌ The foreach statement is a bit different from the previous three iteration statements.It is used to perform a block of statements on each item in a sequence, for example, an array or collection. Each item is usually read-only, and if the sequence structure is modified during iteration, for example, by adding or removing an item, then an exception will be thrown.Try the following example: 1. Type statements to create an array of string variables and then output the length of each one, as shown in the following code: string[] names = { "Adam", "Barry", "Charlie" }; foreach (string name in names) { WriteLine($"{name} has {name.Length} characters."); } 1. Run the code and view the results, as shown in the following output: Adam has 4 characters. Barry has 5 characters. Charlie has 7 characters. Understanding how foreach works internally‌ A developer who defines a type that represents multiple items, like an array or collection, should make sure that a programmer can use the foreach statement to enumerate through the type's items.Technically, the foreach statement will work on any type that follows these rules: image The type must have a method named GetEnumerator that returns an object. image image The returned object must have a property named Current and a method named MoveNext . The MoveNext method must change the value of Current and return true if there are more items to enumerate through or return false if there are no more items. There are interfaces named IEnumerable and IEnumerable that formally define these rules, but technically the compiler does not require the type to implement these interfaces.The compiler turns the foreach statement in the preceding example into something like the following pseudocode:

IEnumerator e = names.GetEnumerator(); while (e.MoveNext())

{

string name = (string)e.Current; // Current is read-only! WriteLine($"{name} has {name.Length} characters.");

}

Due to the use of an iterator and its read-only Current property, the variable declared in a foreach statement cannot be used to modify the value of the current item.

Storing multiple values in an array‌

When you need to store multiple values of the same type, you can declare an array. For example, you may do this when you need to store four names in a string array.

Working with single-dimensional arrays‌

The code that you will write next will allocate memory for an array for storing four string values. It will then store string values at index positions 0 to 3 (arrays usually have a lower bound of zero, so the index of the last item is one less than the length of the array).We could visualize the array like this:

0 1 2 3

Kate Jack Rebecca Tom

Table 3.1: Visualization of an array of four string values

Good Practice: Do not assume that all arrays count from zero. The most common type of array in .NET is an szArray, a single-dimension zero-indexed array, and these use the normal [] syntax. But .NET also has mdArray, a multi-dimensional array, and they do not have to have a lower bound of zero. These are rarely used, but you should know they exist.

Finally, it will loop through each item in the array using a for statement.Let's look at how to use an array:

Use your preferred code editor to add a new Console App / console project named Arrays

to the Chapter03 solution.

In Program.cs , delete the existing statements and then type statements to declare and use an array of string values, as shown in the following code:

string[] names; // This can reference any size array of strings.

// Allocate memory for four strings in an array. names = new string[4];

// Store items at these index positions.

names[0] = "Kate";

names[1] = "Jack"; names[2] = "Rebecca"; names[3] = "Tom";

// Loop through the names.

for (int i = 0; i < names.Length; i++) { // Output the item at index position i. WriteLine($"{names[i]} is at position {i}."); } 1. Run the code and note the result, as shown in the following output: Kate is at position 0. Jack is at position 1. Rebecca is at position 2. Tom is at position 3. Arrays are always of a fixed size at the time of memory allocation, so you need to decide how many items you want to store before instantiating them.An alternative to defining the array in three steps as above is to use array initializer syntax: 1. Before the for loop, add a statement to declare, allocate memory, and instantiate the values of a similar array, as shown in the following code: // Alternative syntax for creating and initializing an array. string[] names2 = { "Kate", "Jack", "Rebecca", "Tom" }; 1. Change the for loop to use names2 , run the console app, and note that the results are the same. Working with multi-dimensional arrays‌ Instead of a single-dimension array for storing a row of string values (or any other data type), what if we want to store a grid of values? Or a cube? Or even higher dimensions?We could visualize a two-dimensional array, aka grid, of string values like this: 0 1 2 3 0 Alpha Beta Gamma Delta 1 Anne Ben Charlie Doug 2 Aardvark Bear Cat Dog Table 3.2: Visualization of a two-dimensional array Let's look at how to use multi-dimensional arrays: 1. At the bottom of Program.cs , add statements to declare and instantiate a two- dimensional array of string values, as shown in the following code: string[,] grid1 = // Two dimensional array. { { "Alpha", "Beta", "Gamma", "Delta" }, { "Anne", "Ben", "Charlie", "Doug" }, { "Aardvark", "Bear", "Cat", "Dog" } }; 1. We can discover the lower and upper bounds of this array using helpful methods, as shown in the following code: WriteLine($"1st dimension, lower bound: {grid1.GetLowerBound(0)}"); WriteLine($"1st dimension, upper bound: {grid1.GetUpperBound(0)}"); WriteLine($"2nd dimension, lower bound: {grid1.GetLowerBound(1)}"); WriteLine($"2nd dimension, upper bound: {grid1.GetUpperBound(1)}"); 1. Run the code and note the result, as shown in the following output: 1st dimension, lower bound: 0 1st dimension, upper bound: 2 2nd dimension, lower bound: 0 2nd dimension, upper bound: 3 1. We can then use these values in nested for statements to loop through the string values, as shown in the following code: for (int row = 0; row <= grid1.GetUpperBound(0); row++) { for (int col = 0; col <= grid1.GetUpperBound(1); col++) { WriteLine($"Row {row}, Column {col}: {grid1[row, col]}"); } } 1. Run the code and note the result, as shown in the following output: Row 0, Column 0: Alpha Row 0, Column 1: Beta Row 0, Column 2: Gamma Row 0, Column 3: Delta Row 1, Column 0: Anne Row 1, Column 1: Ben Row 1, Column 2: Charlie Row 1, Column 3: Doug Row 2, Column 0: Aardvark Row 2, Column 1: Bear Row 2, Column 2: Cat Row 2, Column 3: Dog You must supply a value for every row and every column when it is instantiated, or you will get compile errors. If you need to indicate a missing string value, then use string.Empty . Or if you declare the array to be nullable string values by using string?[] , then you can also use null for a missing value. If you cannot use the array initialization syntax, perhaps because you are loading values from a file or database, then you can separate the declaration of the array dimension and the allocation of memory from the assigning of values, as shown in the following code: // Alternative syntax for declaring and allocating memory // for a multi-dimensional array. string[,] grid2 = new string[3,4]; // Allocate memory. grid2[0, 0] = "Alpha"; // Assign values. grid2[0, 1] = "Beta"; // And so on. grid2[2, 3] = "Dog"; When declaring the size of the dimensions, you specify the length, not the upper bound. The expression new string[3,4] means the array can have 3 items in its first dimension (0) with an upper bound of 2, and the array can have 4 items in its second dimension (1) with an upper bound of 3. Working with jagged arrays‌ If you need a multi-dimensional array but the number of items stored in each dimension is different, then you can define an array of arrays, aka a jagged array. We could visualize a jagged array as shown in Figure 3.3: image Figure 3.3: Visualization of a jagged array Let's look at how to use a jagged array: 1. At the bottom of Program.cs , add statements to declare and instantiate an array of arrays of string values, as shown in the following code: string[][] jagged = // An array of string arrays. { new[] { "Alpha", "Beta", "Gamma" }, new[] { "Anne", "Ben", "Charlie", "Doug" }, new[] { "Aardvark", "Bear" } }; 1. We can discover the lower and upper bounds of the array of arrays, and then each array with it, as shown in the following code: WriteLine("Upper bound of the array of arrays is: {0}", jagged.GetUpperBound(0)); for (int array = 0; array <= jagged.GetUpperBound(0); array++) { WriteLine("Upper bound of array {0} is: {1}", arg0: array, arg1: jagged[array].GetUpperBound(0)); } 1. Run the code and note the result, as shown in the following output: Upper bound of the array of arrays is: 2 Upper bound of array 0 is: 2 Upper bound of array 1 is: 3 Upper bound of array 2 is: 1 1. We can then use these values in nested for statements to loop through the string values, as shown in the following code: for (int row = 0; row <= jagged.GetUpperBound(0); row++) { for (int col = 0; col <= jagged[row].GetUpperBound(0); col++) { WriteLine($"Row {row}, Column {col}: {jagged[row][col]}"); } } 1. Run the code and note the result, as shown in the following output: Row 0, Column 0: Alpha Row 0, Column 1: Beta Row 0, Column 2: Gamma Row 1, Column 0: Anne Row 1, Column 1: Ben Row 1, Column 2: Charlie Row 1, Column 3: Doug Row 2, Column 0: Aardvark Row 2, Column 1: Bear List pattern matching with arrays‌ Earlier in this chapter, you saw how an individual object supports pattern matching against its type and properties. Pattern matching also works with arrays and collections.Introduced with C# 11, list pattern matching works with any type that has a public Length or Count property and has an indexer using an int or System.Index parameter. You will learn about indexers in Chapter 5, Building Your Own Types with Object-Oriented Programming.When you define multiple list patterns in the same switch expression, you must order them so that the more specific one comes first, or the compiler will complain because a more general pattern will match all the more specific patterns too and make the more specific one unreachable.Table 3.3 shows examples of list pattern matching, assuming a list of int values: Example Description [] Matches an empty array or collection. [..] Matches an array or collection with any number of items, including zero, so [..] must come after [] if you need to switch on both. [_] Matches a list with any single item. Matches a list with any single item and can [int item1] or [var item1] use the value in the return expression by referring to item1 . [7, 2] Matches exactly a list of two items with those values in that order. [_, _] Matches a list with any two items. [var item1, var item2] Matches a list with any two items and can use the values in the return expression by referring to item1 and item2 . [_, _, _] Matches a list with any three items. [var item1, ..] Matches a list with one or more items. Can refer to the value of the first item in its return expression by referring to item1 . [var firstItem, .., var lastItem] Matches a list with two or more items. Can refer to the value of the first and last item in its return expression by referring to firstItem and lastItem . [.., var lastItem] Matches a list with one or more items. Can refer to the value of the last item in its return expression by referring to lastItem . Table 3.3: Examples of list pattern matching Let's see some examples in code: 1. At the bottom of Program.cs , add statements to define some arrays of int values, and then pass them to a method that returns descriptive text depending on the pattern that matches best, as shown in the following code: int[] sequentialNumbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int[] oneTwoNumbers = { 1, 2 }; int[] oneTwoTenNumbers = { 1, 2, 10 }; int[] oneTwoThreeTenNumbers = { 1, 2, 3, 10 }; int[] primeNumbers = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 }; int[] fibonacciNumbers = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; int[] emptyNumbers = { }; // Or use Array.Empty()

int[] threeNumbers = { 9, 7, 5 };

int[] sixNumbers = { 9, 7, 5, 4, 2, 10 }; WriteLine($"{nameof(sequentialNumbers)}: {CheckSwitch(sequentialNumbers)}"); WriteLine($"{nameof(oneTwoNumbers)}: {CheckSwitch(oneTwoNumbers)}"); WriteLine($"{nameof(oneTwoTenNumbers)}: {CheckSwitch(oneTwoTenNumbers)}");

WriteLine($"{nameof(oneTwoThreeTenNumbers)}: {CheckSwitch(oneTwoThreeTenNumbers)}"); WriteLine($"{nameof(primeNumbers)}: {CheckSwitch(primeNumbers)}"); WriteLine($"{nameof(fibonacciNumbers)}: {CheckSwitch(fibonacciNumbers)}"); WriteLine($"{nameof(emptyNumbers)}: {CheckSwitch(emptyNumbers)}"); WriteLine($"{nameof(threeNumbers)}: {CheckSwitch(threeNumbers)}"); WriteLine($"{nameof(sixNumbers)}: {CheckSwitch(sixNumbers)}");

static string CheckSwitch(int[] values) => values switch

{

[] => "Empty array",

[1, 2, _, 10] => "Contains 1, 2, any single number, 10.",

[1, 2, .., 10] => "Contains 1, 2, any range including empty, 10.",

[1, 2] => "Contains 1 then 2.",

[int item1, int item2, int item3] =>

$"Contains {item1} then {item2} then {item3}.", [0, _] => "Starts with 0, then one other number.",

[0, ..] => "Starts with 0, then any range of numbers.",

[2, .. int[] others] => $"Starts with 2, then {others.Length} more numbers.", [..] => "Any items in any order.",

};

1. Run the code and note the result, as shown in the following output:

sequentialNumbers: Contains 1, 2, any range including empty, 10.

oneTwoNumbers: Contains 1 then 2.

oneTwoTenNumbers: Contains 1, 2, any range including empty, 10.

oneTwoThreeTenNumbers: Contains 1, 2, any single number, 10. primeNumbers: Starts with 2, then 9 more numbers. fibonacciNumbers: Starts with 0, then any range of numbers. emptyNumbers: Empty array

threeNumbers: Contains 9 then 7 then 5. sixNumbers: Any items in any order.

You can learn more about list pattern matching at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/operators/patterns#list-patterns.

Understanding inline arrays‌

Inline arrays were introduced with C# 12, and they are an advanced feature used by the .NET runtime team to improve performance. You are unlikely to use them yourself unless you are a public library author, but you will automatically benefit from others use of them.

More Information: You can learn more about inline arrays at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp- 12.0/inline-arrays.

Summarizing arrays‌

We use slightly different syntax to declare different types of arrays, as shown in Table 3.4:

Type of array Declaration syntax

One dimension datatype[] , for example,

string[]

Two dimensions string[,]

Three dimensions string[,,]

Ten dimensions string[,,,,,,,,,]

Array of arrays aka two-dimensional jagged array string[][]

Array of arrays of arrays aka three-dimensional jagged array

Table 3.4: Summary of array declaration syntax

string[][][]

Arrays are useful for temporarily storing multiple items, but collections are a more flexible option when adding and removing items dynamically. You don't need to worry about collections right now, as we will cover them in Chapter 8, Working with Common .NET Types.You can convert any sequence of items into an array using the ToArray extension method, which we will cover in Chapter 11, Querying and Manipulating Data Using LINQ.

Good Practice: If you do not need to dynamically add and remove items, then you should use an array instead of a collection like List because arrays are more efficient in memory use and the items are stored contiguously, which can improve performance.

Casting and converting between types‌

You will often need to convert values of variables between different types. For example, data input is often entered as text in the console, so it is initially stored in a variable of the string type, but it then needs to be converted into a date/time, number, or some other data type, depending on how it should be stored and processed.Sometimes you will need to convert between number types, like between an integer and a floating point, before performing calculations.Converting is also known as casting, and it has two

varieties: implicit and explicit. Implicit casting happens automatically, and it is safe, meaning that you will not lose any information.Explicit casting must be

performed manually because it may lose information, for example, the precision of a number. By explicitly casting, you are telling the C# compiler that you understand and accept the risk.

Casting numbers implicitly and explicitly‌

Implicitly casting an int variable into a double variable is safe because no information can be lost, as the following shows:

Use your preferred coding tool to add a new Console App / console project named CastingConverting to the Chapter03 solution.

In Program.cs , delete the existing statements, then type statements to declare and assign an int variable and a double variable, and then implicitly cast the integer's value when assigning it to the double variable, as shown in the following code:

int a = 10;

double b = a; // An int can be safely cast into a double. WriteLine($"a is {a}, b is {b}");

1. Type statements to declare and assign a double variable and an int variable, and then implicitly cast the double value when assigning it to the int variable, as shown in the following code:

double c = 9.8;

int d = c; // Compiler gives an error if you do not explicitly cast. WriteLine($"c is {c}, d is {d}");

1. Run the code and note the error message, as shown in the following output:

Error: (6,9): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conver

This error message will also appear in the Visual Studio Error List, Visual Studio Code

PROBLEMS window, or JetBrains Rider Problems window.You cannot implicitly cast

a double variable into an int variable because it is potentially unsafe and could lose data, like the value after the decimal point. You must explicitly cast a double variable into an int variable using a pair of round brackets around the type you want to cast

the double type into. The pair of round brackets is the cast operator. Even then, you must beware that the part after the decimal point will be trimmed off without warning because you have chosen to perform an explicit cast and therefore understand the consequences.

Modify the assignment statement for the d variable to explicitly cast the variable c into an int , and add a comment to explain what will happen, as shown highlighted in the following code:

double c = 9.8;

int d = (int)c; // Compiler gives an error if you do not explicitly cast. WriteLine($"c is {c}, d is {d}"); // d loses the .8 part.

1. Run the code to view the results, as shown in the following output:

a is 10, b is 10

c is 9.8, d is 9

We must perform a similar operation when converting values between larger integers and smaller integers. Again, beware that you might lose information because any value too big will have its bits copied and then be interpreted in ways that you might not expect!

1. Enter statements to declare and assign a long (64-bit) integer variable to an int (32- bit) integer variable, both using a small value that will work and a too-large value that will not, as shown in the following code:

long e = 10; int f = (int)e;

WriteLine($"e is {e:N0}, f is {f:N0}"); e = long.MaxValue;

f = (int)e;

WriteLine($"e is {e:N0}, f is {f:N0}");

1. Run the code to view the results, as shown in the following output:

e is 10, f is 10

e is 9,223,372,036,854,775,807, f is -1

1. Modify the value of e to 5 billion, as shown in the following code:

e = 5_000_000_000;

1. Run the code to view the results, as shown in the following output:

e is 5,000,000,000, f is 705,032,704

Five billion cannot fit into a 32-bit integer, so it overflows (wraps around) to about 705 million. It is all to do with the binary representation of integer numbers. You will see more examples of integer overflow and how to handle it later in this chapter.

How negative numbers are represented in binary‌

You might have wondered why f had the value -1 in the previous code. Negative aka signed numbers use the first bit to represent negativity. If the bit is 0 (zero), then it is a positive number. If the bit is 1 (one), then it is a negative number.Let's write some code to illustrate this:

1. Enter statements to output the maximum value for an int in decimal and binary number formats, then output the values 8 to -8, decrementing by one, and finally output the minimum value for an int , as shown in the following code:

WriteLine("{0,12} {1,34}", "Decimal", "Binary");

WriteLine("{0,12} {0,34:B32}", int.MaxValue); for (int i = 8; i >= -8; i--)

{

WriteLine("{0,12} {0,34:B32}", i);

}

WriteLine("{0,12} {0,34:B32}", int.MinValue);

Note ,12 and ,34 mean right-align within those column widths. :B32 means format as binary padded with leading zeros to a width of 32.

1. Run the code to view the results, as shown in the following output:

Decimal

Binary

2147483647

01111111111111111111111111111111

8

00000000000000000000000000001000

7

00000000000000000000000000000111

6

00000000000000000000000000000110

5

00000000000000000000000000000101

4

00000000000000000000000000000100

3

00000000000000000000000000000011

2

00000000000000000000000000000010

1

00000000000000000000000000000001

0

00000000000000000000000000000000

-1

11111111111111111111111111111111

-2

11111111111111111111111111111110

-3

11111111111111111111111111111101

-4

11111111111111111111111111111100

-5

11111111111111111111111111111011

-6

11111111111111111111111111111010

-7

11111111111111111111111111111001

-8

11111111111111111111111111111000

-2147483648

10000000000000000000000000000000

1. Note that all the positive binary number representations start with 0 and all the negative binary number representations start with 1 . The decimal value -1 is represented by all ones in binary. That is why when you have an integer too large to fit in a 32-bit integer, it becomes -1 .

More Information: If you are interested in learning more about how signed numbers can be represented in computer systems, then you can read the following article: https://en.wikipedia.org/wiki/Signed_number_representations.

Converting with the System.Convert type‌

You can only cast between similar types, for example, between whole numbers like byte , int , and long , or between a class and its subclasses. You cannot cast a long to a string or a byte to a DateTime .An alternative to using the cast operator is to use

the System.Convert type. The System.Convert type can convert to and from all the C# number types, as well as Booleans, strings, and date and time values.Let's write some code to see this in action:

1. At the top of Program.cs , statically import the System.Convert class, as shown in the following code:

using static System.Convert; // To use the ToInt32 method.

Alternatively, add an entry to CastingConverting.csproj , as shown in the following markup:

1. At the bottom of Program.cs , type statements to declare and assign a value to

a double variable, convert it into an integer, and then write both values to the console, as shown in the following code:

double g = 9.8;

int h = ToInt32(g); // A method of System.Convert. WriteLine($"g is {g}, h is {h}");

1. Run the code and view the result, as shown in the following output:

g is 9.8, h is 10

An important difference between casting and converting is that converting rounds the double value 9.8 up to 10 instead of trimming the part after the decimal point. Another is that casting can allow overflows while converting will throw an exception.

Rounding numbers and the default rounding rules‌

You have now seen that the cast operator trims the decimal part of a real number and that the System.Convert methods round up or down. However, what is the rule for rounding?In British primary schools for children aged 5 to 11, pupils are taught to round up if the decimal part is .5 or higher and round down if the decimal part is less. Of course, these terms only make sense because at that age the pupils are only dealing with positive numbers. With negative numbers, these terms become confusing and those terms should be avoided. This is why the .NET API uses the enum values AwayFromZero , ToZero , ToEven , ToPositiveInfinity , and ToNegativeInfinity for improved clarity.Let's explore if C# follows the same primary school rule:

1. Type statements to declare and assign an array of double values, convert each of them into an integer, and then write the result to the console, as shown in the following code:

double[,] doubles = {

{ 9.49, 9.5, 9.51 },

{ 10.49, 10.5, 10.51 },

{ 11.49, 11.5, 11.51 },

{ 12.49, 12.5, 12.51 } ,

{ -12.49, -12.5, -12.51 },

{ -11.49, -11.5, -11.51 },

{ -10.49, -10.5, -10.51 },

{ -9.49, -9.5, -9.51 }

};

WriteLine($"| double | ToInt32 | double | ToInt32 | double | ToInt32 |"); for (int x = 0; x < 8; x++) { for (int y = 0; y < 3; y++) { Write($"| {doubles[x, y],6} | {ToInt32(doubles[x, y]),7} "); } WriteLine("|"); } WriteLine(); 1. Run the code and view the result, as shown in the following output: | double | ToInt32 | double | ToInt32 | double | ToInt32 | | 9.49 | 9 | 9.5 | 10 | 9.51 | 10 | | 10.49 | 10 | 10.5 | 10 | 10.51 | 11 | | 11.49 | 11 | 11.5 | 12 | 11.51 | 12 | | 12.49 | 12 | 12.5 | 12 | 12.51 | 13 | | -12.49 | -12 | -12.5 | -12 | -12.51 | -13 | | -11.49 | -11 | -11.5 | -12 | -11.51 | -12 | | -10.49 | -10 | -10.5 | -10 | -10.51 | -11 | | -9.49 | -9 | -9.5 | -10 | -9.51 | -10 | We have shown that the rule for rounding in C# is subtly different from the primary school rule: image It always rounds toward zero if the decimal part is less than the midpoint .5. image image It always rounds away from zero if the decimal part is more than the midpoint .5. It will round away from zero if the decimal part is the midpoint .5 and the non- decimal part is odd, but it will round toward zero if the non-decimal part is even. This rule is known as Banker's rounding, and it is preferred because it reduces bias by alternating when it rounds toward or away from zero. Sadly, other languages such as JavaScript use the primary school rule. Taking control of rounding rules‌ You can take control of the rounding rules by using the Round method of the Math class: 1. Type statements to round each of the double values using the "away from zero" rounding rule, also known as rounding "up," and then write the result to the console, as shown in the following code: foreach (double n in doubles) { WriteLine(format: "Math.Round({0}, 0, MidpointRounding.AwayFromZero) is {1}", arg0: n, arg1: Math.Round(value: n, digits: 0, mode: MidpointRounding.AwayFromZero)); } You can use a foreach statement to enumerate all the items in a multi-dimensional array. 1. Run the code and view the result, as shown in the following partial output: Math.Round(9.49, 0, MidpointRounding.AwayFromZero) is 9 Math.Round(9.5, 0, MidpointRounding.AwayFromZero) is 10 Math.Round(9.51, 0, MidpointRounding.AwayFromZero) is 10 Math.Round(10.49, 0, MidpointRounding.AwayFromZero) is 10 Math.Round(10.5, 0, MidpointRounding.AwayFromZero) is 11 Math.Round(10.51, 0, MidpointRounding.AwayFromZero) is 11 ... Good Practice: For every programming language that you use, check its rounding rules. They may not work the way you expect! You can read more about Math.Round at the following link: https://learn.microsoft.com/en-us/dotnet/api/system.math.round. Converting from any type to a string‌ The most common conversion is from any type into a string variable for outputting as human-readable text, so all types have a method named ToString that they inherit from the System.Object class.The ToString method converts the current value of any variable into a textual representation. Some types can't be sensibly represented as text, so they return their namespace and type name instead.Let's convert some types into a string : 1. Type statements to declare some variables, convert them to their string representation, and write them to the console, as shown in the following code: int number = 12; WriteLine(number.ToString()); bool boolean = true; WriteLine(boolean.ToString()); DateTime now = DateTime.Now; WriteLine(now.ToString()); object me = new(); WriteLine(me.ToString()); 1. Run the code and view the result, as shown in the following output: 12 True 08/28/2024 17:33:54 System.Object Passing any object to the WriteLine method implicitly converts it into a string , so it is not necessary to explicitly call ToString . We are doing so here just to emphasize what is happening. Explicitly calling ToString does avoid a boxing operation, so if you are developing games with Unity then that can help you avoid memory garbage collection issues. Converting from a binary object to a string‌ When you have a binary object like an image or video that you want to either store or transmit, you sometimes do not want to send the raw bits because you do not know how those bits could be misinterpreted, for example, by the network protocol transmitting them or another operating system that is reading the stored binary object.The safest thing to do is to convert the binary object into a string of safe characters. Programmers call this Base64 encoding.The Convert type has a pair of methods, ToBase64String and FromBase64String , that perform this conversion for you. Let's see them in action: 1. Type statements to create an array of bytes randomly populated with byte values, write each byte nicely formatted to the console, and then write the same bytes converted into Base64 to the console, as shown in the following code: // Allocate an array of 128 bytes. byte[] binaryObject = new byte[128]; // Populate the array with random bytes. Random.Shared.NextBytes(binaryObject); WriteLine("Binary Object as bytes:"); for (int index = 0; index < binaryObject.Length; index++) { Write($"{binaryObject[index]:X2} "); } WriteLine(); // Convert the array to Base64 string and output as text. string encoded = ToBase64String(binaryObject); WriteLine($"Binary Object as Base64: {encoded}"); By default, an int value would output assuming decimal notation, that is, Base10. You can use format codes such as :X2 to format the value using hexadecimal notation. 1. Run the code and view the result, as shown in the following output: Binary Object as bytes: EB 53 8B 11 9D 83 E6 4D 45 85 F4 68 F8 18 55 E5 B8 33 C9 B6 F4 00 10 7F CB 59 23 7B 26 18 16 30 Binary Object as Base64: 61OLEZ2D5k1FhfRo+BhV5bgzybb0ABB/y1kjeyYYFjAAI+aPqRCwqebsVPtNM+FoUEbEHV+ Parsing from strings to numbers or dates and times‌ The second most common conversion is from strings to numbers or date and time values.The opposite of ToString is Parse . Only a few types have a Parse method, including all the number types and DateTime .Let's see Parse in action: 1. At the top of Program.cs , import the namespace for working with cultures, as shown in the following code: using System.Globalization; // To use CultureInfo. 1. At the bottom of Program.cs , add statements to parse an integer and a date and time value from strings and then write the result to the console, as shown in the following code: // Set the current culture to make sure date parsing works. CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US"); int friends = int.Parse("27"); DateTime birthday = DateTime.Parse("4 June 1980"); WriteLine($"I have {friends} friends to invite to my party."); WriteLine($"My birthday is {birthday}."); WriteLine($"My birthday is {birthday:D}."); 1. Run the code and view the result, as shown in the following output: I have 27 friends to invite to my party. My birthday is 6/4/1980 12:00:00 AM. My birthday is Wednesday, June 4, 1980. By default, a date and time value outputs with the short date and time format. You can use format codes such as D to output only the date part using the long date format. Good Practice: Use the standard date and time format specifiers, as shown at the following link: https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard- date-and-time-format-strings#table-of-format-specifiers. Avoiding Parse exceptions by using the TryParse method‌ One problem with the Parse method is that it gives errors if the string cannot be converted: 1. Type a statement to attempt to parse a string containing letters into an integer variable, as shown in the following code: int count = int.Parse("abc"); 1. Run the code and view the result, as shown in the following output: Unhandled Exception: System.FormatException: Input string was not in a correct format. As well as the preceding exception message, you will see a stack trace. I have not included stack traces in this book because they take up too much space.To avoid errors, you can use the TryParse method instead. TryParse attempts to convert the input string and returns true if it can convert it and false if it cannot. Exceptions are a relatively expensive operation so they should be avoided when you can.The out keyword is required to allow the TryParse method to set the count variable when the conversion works.Let's see TryParse in action: 1. Replace the int count declaration with statements to use the TryParse method and ask the user to input a count for a number of eggs, as shown in the following code: Write("How many eggs are there? "); string? input = ReadLine(); if (int.TryParse(input, out int count)) { WriteLine($"There are {count} eggs."); } else { WriteLine("I could not parse the input."); } 1. Run the code, enter 12 , and view the result, as shown in the following output: How many eggs are there? 12 There are 12 eggs. 1. Run the code, enter twelve , and view the result, as shown in the following output: How many eggs are there? twelve I could not parse the input. You can also use methods of the System.Convert type to convert string values into other types; however, like the Parse method, it gives an error if it cannot convert. Understanding the Try method naming convention‌ .NET uses a standard signature for all methods that follow the Try naming convention. For any method named Something that returns a value of a specific type, its matching TrySomething method must return a bool to indicate success or failure and use an out parameter in place of the return value. For example: // A method that might throw an exception. int number = int.Parse("123"); // The Try equivalent of the method. bool success = int.TryParse("123", out int number); // Trying to create a Uri for a Web API. bool success = Uri.TryCreate("https://localhost:5000/api/customers", UriKind.Absolute, out Uri serviceUrl); Handling exceptions‌ You've seen several scenarios where errors have occurred when converting types. Some languages return error codes when something goes wrong. .NET uses exceptions that are richer and designed only for failure reporting. When this happens, we say a runtime exception has been thrown.Other systems might use return values that could have multiple uses. For example, if the return value is a positive number, it might represent the count of rows in a table, or if the return value is a negative number, it might represent some error code. When an exception is thrown, the thread is suspended and if the calling code has defined a try-catch statement, then it is given a chance to handle the exception. If the current method does not handle it, then its calling method is given a chance, and so on up the call stack.As you have seen, the default behavior of a console app is to output a message about the exception, including a stack trace, and then stop running the code. The application is terminated. This is better than allowing the code to continue executing in a potentially corrupt state. Your code should only catch and handle exceptions that it understands and can properly fix. Good Practice: Avoid writing code that will throw an exception whenever possible, perhaps by performing if statement checks. Sometimes you can't, and sometimes it is best to allow the exception to be caught by a higher-level component that is calling your code. You will learn how to do this in Chapter 4, Writing, Debugging, and Testing Functions. Wrapping error-prone code in a try block‌ When you know that a statement can cause an error, you should wrap that statement in a try block. For example, parsing from text to a number can cause an error. Any statements in the catch block will be executed only if an exception is thrown by a statement in the try block.We don't have to do anything inside the catch block. Let's see this in action: Use your preferred coding tool to add a new Console App / console project named HandlingExceptions to the Chapter03 solution. In Program.cs , delete any existing statements and then type statements to prompt the user to enter their age and then write their age to the console, as shown in the following code: WriteLine("Before parsing"); Write("What is your age? "); string? input = ReadLine(); try { int age = int.Parse(input); WriteLine($"You are {age} years old."); } catch { } WriteLine("After parsing"); You will see the following compiler message: Warning CS8604 Possible null reference argument for parameter 's' in 'int int.Parse(st . By default, in .NET 6 or later projects, Microsoft enables nullable reference types, so you will see many more compiler warnings like this. In production code, you should add code to check for null and handle that possibility appropriately, as shown in the following code: if (input is null) { WriteLine("You did not enter a value so the app has ended."); return; // Exit the app. } In this book, I will not give instructions to add these null checks every time because the code samples are not designed to be production-quality, and having null checks everywhere will clutter the code and use up valuable pages. You will probably see hundreds more examples of potentially null variables throughout the code samples in this book. Those warnings are safe to ignore for the book code examples. You only need to pay attention to similar warnings when you write your own production code. You will see more about null handling in Chapter 6, Implementing Interfaces and Inheriting Classes.In this case, it is impossible for input to be null because the user must press Enter for ReadLine to return, and if they have not typed any characters at that point then the ReadLine method will return an empty string . Let's tell the compiler that it does not need to show us this warning: 1. To disable the compiler warning, change input to input! , as shown highlighted in the following code: int age = int.Parse(input!); An exclamation mark ! after an expression is called the null-forgiving operator and it disables the compiler warning. The null-forgiving operator has no effect at runtime. If the expression could evaluate to null at runtime, perhaps because we assigned it in another way, then an exception would be thrown. This code includes two messages to indicate before parsing and after parsing to make the flow through the code clearer. These will be especially useful as the example code grows more complex. 1. Run the code, enter 49 , and view the result, as shown in the following output: Before parsing What is your age? 49 You are 49 years old. After parsing 1. Run the code, enter Kermit , and view the result, as shown in the following output: Before parsing What is your age? Kermit After parsing When the code was executed, the error exception was caught, the default message and stack trace were not output, and the console app continued running. This is better than the default behavior, but it might be useful to see the type of error that occurred. Good Practice: You should never use an empty catch statement like this in production code because it "swallows" exceptions and hides potential problems. You should at least log the exception if you cannot or do not want to handle it properly, or rethrow it so that higher-level code can decide instead. You will learn about logging in Chapter 4, Writing, Debugging, and Testing Functions. Catching all exceptions‌ To get information about any type of exception that might occur, you can declare a variable of type System.Exception to the catch block: 1. Add an exception variable declaration to the catch block and use it to write information about the exception to the console, as shown in the following code: catch (Exception ex) { WriteLine($"{ex.GetType()} says {ex.Message}"); } 1. Run the code, enter Kermit again, and view the result, as shown in the following output: Before parsing What is your age? Kermit System.FormatException says Input string was not in a correct format. After parsing Catching specific exceptions‌ Now that we know which specific type of exception occurred, we can improve our code by catching just that type of exception and customizing the message that we display to the user. You can think of this as a form of testing: 1. Leave the existing catch block, and above it, add a new catch block for the format exception type, as shown in the following highlighted code: catch (FormatException) { WriteLine("The age you entered is not a valid number format."); } catch (Exception ex) { WriteLine($"{ex.GetType()} says {ex.Message}"); } 1. Run the code, enter Kermit again, and view the result, as shown in the following output: Before parsing What is your age? Kermit The age you entered is not a valid number format. After parsing The reason we want to leave the more general catch below is that there might be other types of exceptions that can occur. 1. Run the code, enter 9876543210 , and view the result, as shown in the following output: Before parsing What is your age? 9876543210 System.OverflowException says Value was either too large or too small for an Int32. After parsing Let's add another catch block for this type of exception. 1. Leave the existing catch blocks, and add a new catch block for the overflow exception type, as shown in the following highlighted code: catch (OverflowException) { WriteLine("Your age is a valid number format but it is either too big or small."); } catch (FormatException) { WriteLine("The age you entered is not a valid number format."); } 1. Run the code, enter 9876543210 , and view the result, as shown in the following output: Before parsing What is your age? 9876543210 Your age is a valid number format but it is either too big or small. After parsing The order in which you catch exceptions is important. The correct order is related to the inheritance hierarchy of the exception types. You will learn about inheritance in Chapter 5, Building Your Own Types with Object-Oriented Programming. However, don't worry too much about this—the compiler will give you build errors if you get exceptions in the wrong order anyway. Good Practice: Avoid over-catching exceptions. They should often be allowed to propagate up the call stack to be handled at a level where more information is known about the circumstances that could change the logic of how they should be handled. You will learn about this in Chapter 4, Writing, Debugging, and Testing Functions. Catching with filters‌ You can also add filters to a catch statement using the when keyword, as shown in the following code: Write("Enter an amount: "); string amount = ReadLine()!; if (string.IsNullOrEmpty(amount)) return; try { decimal amountValue = decimal.Parse(amount); WriteLine($"Amount formatted as currency: {amountValue:C}"); } catch (FormatException) when (amount.Contains("$")) { WriteLine("Amounts cannot use the dollar sign!"); } catch (FormatException) { WriteLine("Amounts must only contain digits!"); } Checking for overflow‌ Earlier, we saw that when casting between number types, it was possible to lose information, for example, when casting from a long variable to an int variable. If the value stored in a type is too big, it will overflow. Throwing overflow exceptions with the checked statement‌ The checked statement tells .NET to throw an exception when an overflow happens instead of allowing it to happen silently, which is done by default for performance reasons.We will set the initial value of an int variable to its maximum value minus one. Then, we will increment it several times, outputting its value each time. Once it gets above its maximum value, it overflows to its minimum value and continues incrementing from there.Let's see this in action: 1. In Program.cs , type statements to declare and assign an integer to one less than its maximum possible value, and then increment it and write its value to the console three times, as shown in the following code: int x = int.MaxValue - 1; WriteLine($"Initial value: {x}"); x++; WriteLine($"After incrementing: {x}"); x++; WriteLine($"After incrementing: {x}"); x++; WriteLine($"After incrementing: {x}"); 1. Run the code and view the result that shows the value overflowing silently and wrapping around to large negative values, as shown in the following output: Initial value: 2147483646 After incrementing: 2147483647 After incrementing: -2147483648 After incrementing: -2147483647 1. Now, let's get the compiler to warn us about the overflow by wrapping the statements using a checked statement block, as shown highlighted in the following code: checked { int x = int.MaxValue - 1; WriteLine($"Initial value: {x}"); x++; WriteLine($"After incrementing: {x}"); x++; WriteLine($"After incrementing: {x}"); x++; WriteLine($"After incrementing: {x}"); } 1. Run the code and view the result that shows the overflow being checked and causing an exception to be thrown, as shown in the following output: Initial value: 2147483646 After incrementing: 2147483647 Unhandled Exception: System.OverflowException: Arithmetic operation resulted in an overflow. 1. Just like any other exception, we should wrap these statements in a try statement block and display a nicer error message for the user, as shown in the following code: try { // previous code goes here } catch (OverflowException) { WriteLine("The code overflowed but I caught the exception."); } 1. Run the code and view the result, as shown in the following output: Initial value: 2147483646 After incrementing: 2147483647 The code overflowed but I caught the exception. Disabling compiler overflow checks with the unchecked statement‌ The previous section was about the default overflow behavior at runtime and how to use the checked statement to change that behavior. This section is about compile- time overflow behavior and how to use the unchecked statement to change that behavior.A related keyword is unchecked . This keyword switches off overflow checks performed by the compiler within a block of code. Let's see how to do this: 1. Type the following statement at the end of the previous statements. The compiler will not compile this statement because it knows it would overflow: int y = int.MaxValue + 1; Hover your mouse pointer over the error, and note that a compile-time check is shown as an error message, as shown in Figure 3.4: image Figure 3.4: A compile-time check for integer overflow To disable compile-time checks, wrap the statement in an unchecked block, write the value of y to the console, decrement it, and repeat, as shown in the following code: unchecked { int y = int.MaxValue + 1; WriteLine($"Initial value: {y}"); y--; WriteLine($"After decrementing: {y}"); y--; WriteLine($"After decrementing: {y}"); } 1. Run the code and view the results, as shown in the following output: Initial value: -2147483648 After decrementing: 2147483647 After decrementing: 2147483646 Of course, it would be rare that you would want to explicitly switch off a check like this because it allows an overflow to occur. But perhaps you can think of a scenario where you might want that behavior. Practicing and exploring‌ Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring this chapter's topics with deeper research. Exercise 3.1 – Test your knowledge‌ Answer the following questions: What happens when you divide an int variable by 0 ? What happens when you divide a double variable by 0 ? What happens when you overflow an int variable, that is, set it to a value beyond its range? What is the difference between x = y++; and x = ++y; ? What is the difference between break , continue , and return when used inside a loop statement? What are the three parts of a for statement and which of them are required? What is the difference between the = and == operators? Does the following statement compile? for ( ; ; ) ; What does the underscore _ represent in a switch expression? What interface must an object "implement" to be enumerated over by using the foreach statement? Exercise 3.2 – Explore loops and overflow‌ What will happen if this code executes? int max = 500; for (byte i = 0; i < max; i++) { WriteLine(i); } Create a console app in Chapter03 named Ch03Ex02LoopsAndOverflow and enter the preceding code. Run the console app and view the output. What happens?What code could you add (don't change any of the preceding code) to warn us about the problem? Exercise 3.3 – Test your knowledge of operators‌ What are the values of x and y after the following statements execute? Create a console app in Chapter03 named Ch03Ex03Operators to test your assumptions: 1. Increment and addition operators: x = 3; y = 2 + ++x; 1. Binary shift operators: x = 3 << 2; y = 10 >> 1;

1. Bitwise operators:

x = 10 & 8;

y = 10 | 7;

Exercise 3.4 – Practice loops and operators‌

FizzBuzz is a group game for children to teach them about division. Players take turns to count incrementally, replacing any number divisible by 3 with the word fizz, any number divisible by 5 with the word buzz, and any number divisible by both with fizzbuzz.Create a console app in Chapter03 named Ch03Ex04FizzBuzz that outputs a simulated FizzBuzz game that counts up to 100. The output should look something like Figure 3.5:

image

Figure 3.5: A simulated FizzBuzz game output

Exercise 3.5 – Practice exception handling‌

Create a console app in Chapter03 named Ch03Ex05Exceptions that asks the user for two numbers in the range 0-255 and then divides the first number by the second:

Enter a number between 0 and 255: 100

Enter another number between 0 and 255: 8

100 divided by 8 is 12

Write exception handlers to catch any thrown errors, as shown in the following output:

Enter a number between 0 and 255: apples

Enter another number between 0 and 255: bananas FormatException: Input string was not in a correct format.

Exercise 3.6 – Explore C# 101 notebooks‌

Use the links to notebooks and videos on the following page to see interactive examples of C# using Polyglot Notebooks, as shown in Figure 3.6:https://github.com/dotnet/csharp- notebooks#c-101

image

Figure 3.6: An example C# 101 notebook titled What Are Loops?

Exercise 3.7 – Explore topics‌

Use the links on the following page to learn about the topics covered in this chapter in more detail:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter- 3---controlling-flow-converting-types-and-handling-exceptions

Summary‌

In this chapter, you learned how to:

image

Use operators to perform simple tasks.

image

image

Use branch and loop statements to implement logic. Work with single- and multi-dimensional arrays.

image

Convert between types.

image

Catch exceptions and handle integer overflow.

You are now ready to learn how to reuse blocks of code by defining functions, how to pass values into them and get values back, and how to track down bugs in your code and squash them using debugging and testing tools!

4 Writing, Debugging, and Testing Functions‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about writing functions to reuse code, debugging logic errors during development, logging exceptions during runtime, unit testing your code to remove bugs, and improving stability and reliability.This chapter covers the following topics:

image

Writing functions

image

Debugging during development

image

image

image

Hot reloading during development Logging during development and runtime Unit testing

image

Throwing and catching exceptions in functions

Writing functions‌

A fundamental principle of programming is Don't Repeat Yourself (DRY).While programming, if you find yourself writing the same statements over and over again, then turn those statements into a function. Functions are like tiny programs that complete one small task. For example, you might write a function to calculate sales tax and then reuse that function in many places in a financial application.Like programs, functions usually have inputs and outputs. They are sometimes described as black boxes, where you feed some raw materials in at one end and a manufactured item emerges at the other. Once created and thoroughly debugged and tested, you don't need to think about how they work.

Exploring top-level programs, functions, and namespaces‌

In Chapter 1, Hello, C#! Welcome, .NET!, we learned that since C# 10 and .NET 6, the default project template for console apps uses the top-level program feature introduced with C# 9.Once you start writing functions, it is important to understand how they work with the automatically generated Program class and its

$ method.Let's explore how the top- level program feature works when you define functions:

Use your preferred coding tool to create a new solution and project, as defined in the following list:

image

image

image

Project template: Console App / console Project file and folder: TopLevelFunctions Solution file and folder: Chapter04

image

image

Do not use top-level statements: Cleared. Enable native AOT publish: Cleared.

In Program.cs , delete the existing statements, define a local function at the bottom of the file, and call it, as shown in the following code:

using static System.Console;

WriteLine("* Top-level functions example"); WhatsMyNamespace(); // Call the function.

void WhatsMyNamespace() // Define a local function.

{

WriteLine("Namespace of Program class: {0}", arg0: typeof(Program).Namespace ?? "null");

}

Good Practice: Functions do not need to be at the bottom of the file, but it is good practice rather than mixing them up with other top-level statements. Types like classes must be declared at the bottom of the Program.cs file rather than in the middle of the file or you will see compiler error CS8803 , as shown at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler- messages/cs8803. It would be better to define types like classes in a separate file.

1. Run the console app and note that the namespace for the Program class is null , as shown in the following output:

* Top-level functions example Namespace of Program class: null

What is automatically generated for a local function?

The compiler automatically generates a Program class with a

$ function, then moves your statements and function inside the
$ method, which makes the function local, and renames the function, as shown highlighted in the following code:

using static System.Console; partial class Program

{

static void

$(String[] args)

{

WriteLine("* Top-level functions example");

<

$>g WhatsMyNamespace|0_0(); // Call the function.

void <

$>g WhatsMyNamespace|0_0() // Define a local function.

{

WriteLine("Namespace of Program class: {0}", arg0: typeof(Program).Namespace ?? "null");

}

}

}

For the compiler to know what statements need to go where, you must follow some rules:

image

Import statements ( using ) must go at the top of the Program.cs file.

image

Statements that will go in the

$ function can be mixed with functions in the middle of the Program.cs file. Any functions will become local functions in the

$ method.

The last point is important because local functions have limitations, like they cannot have XML comments to document them.

You are about to see some C# keywords like static and partial , which will be formally introduced in Chapter 5, Building Your Own Types with Object-Oriented Programming.

Defining a partial Program class with a static function‌

A better approach is to write any functions in a separate file and define them as static

members of the Program class:

Add a new class file named Program.Functions.cs . The name of this file does not actually matter but using this naming convention is sensible. You could name the file Gibberish.cs and it would have the same behavior.

In Program.Functions.cs , delete any existing statements and then add statements to define a partial Program class. Cut and paste the WhatsMyNamespace function to move it from Program.cs into Program.Functions.cs , and then add the static keyword to the function, as shown highlighted in the following code:

using static System.Console;

// Do not define a namespace so this class goes in the default empty namespace

// just like the auto-generated partial Program class. partial class Program

{

static void WhatsMyNamespace() // Define a static function.

{

WriteLine("Namespace of Program class: {0}", arg0: typeof(Program).Namespace ?? "null");

}

}

1. In Program.cs , confirm that its entire content is now just three statements, as shown in the following code:

using static System.Console;

WriteLine("* Top-level functions example"); WhatsMyNamespace(); // Call the function.

1. Run the console app and note that it has the same behavior as before.

What is automatically generated for a static function?

When you use a separate file to define a partial Program class with static functions, the compiler defines a Program class with a

$ function, and merges your function as a member of the Program class, as shown in the following highlighted code:

using static System.Console; partial class Program

{

static void

$(String[] args)

{

WriteLine("* Top-level functions example"); WhatsMyNamespace(); // Call the function.

}

static void WhatsMyNamespace() // Define a static function.

{

WriteLine("Namespace of Program class: {0}", arg0: typeof(Program).Namespace ?? "null");

}

}

Solution Explorer shows that your Program.Functions.cs class file merges its

partial Program with the auto-generated partial Program class, as shown in Figure 4.1:

image

Figure 4.1: Solution Explorer showing the merged partial Program class

Good Practice: Create any functions that you will call in Program.cs in a separate file and manually define them inside a partial Program class. This will merge them into the automatically generated Program class at the same level as the

$ method, instead of as local functions inside the
$ method.

It is important to note the lack of namespace declarations. Both the automatically generated

Program class and the explicitly defined Program class are in the default null namespace.

Warning! Do not define a namespace for your partial Program class. If you do, it will be in a different namespace and therefore will not merge with the auto-generated partial Program class.

Optionally, all the static methods in the Program class could be explicitly declared as private but this is the default anyway. Since all the functions will be called within the Program class itself the access modifier is not important.

Times table example‌

Let's say that you want to help your child learn their times tables, so you want to make it easy to generate a times table for a number, such as the 7 times table:

1 x 7 = 7

2 x 7 = 14

3 x 7 = 21

...

10 x 7 = 70

11 x 7 = 77

12 x 7 = 84

Most times tables have either 10, 12, or 20 rows, depending on how advanced the child is.You learned about the for statement earlier in this book, so you know that it can be used to generate repeated lines of output when there is a regular pattern, such as a 7 times table with 12 rows, as shown in the following code:

for (int row = 1; row <= 12; row++) { Console.WriteLine($"{row} x 7 = {row * 7}"); } However, instead of always outputting the 7 times table with 12 rows, we want to make this more flexible so it can output any size times table for any number. We can do this by creating a function.Let's explore functions by creating one to output any times table for numbers 0 to 255 of any size up to 255 rows (but it defaults to 12 rows): Use your preferred coding tool to create a new project, as defined in the following list: image image image Project template: Console App / console Project file and folder: WritingFunctions Solution file and folder: Chapter04 image In Visual Studio 2022, set the startup project for the solution to the current selection. In WritingFunctions.csproj , after the section, add a new

section to statically import System.Console for all C# files using the implicit usings

.NET SDK feature, as shown in the following markup:

Add a new class file to the project named Program.Functions.cs .

In Program.Functions.cs , replace any existing code with new statements to define a function named TimesTable in the partial Program class, as shown in the following code:

partial class Program

{

static void TimesTable(byte number, byte size = 12)

{

WriteLine($"This is the {number} times table with {size} rows:"); WriteLine();

for (int row = 1; row <= size; row++) { WriteLine($"{row} x {number} = {row * number}"); } WriteLine(); } } In the preceding code, note the following: image TimesTable must have a byte value passed to it as a parameter named number . image TimesTable can optionally have a byte value passed to it as a parameter named size . If a value is not passed, it defaults to 12 . image image TimesTable is a static method because it will be called by the static method

$ . TimesTable does not return a value to the caller, so it is declared with the void keyword before its name.

image

TimesTable uses a for statement to output the times table for the number passed to it with its number of rows equal to size .

1. In Program.cs , delete the existing statements and then call the function. Pass in a

byte value for the number parameter, for example, 7 , as shown in the following code:

TimesTable(7);

1. Run the code and then view the result, as shown in the following output:

This is the 7 times table with 12 rows:

1 x

7

=

7

2 x

7

=

14

3 x

7

=

21

4 x

7

=

28

5 x

7

=

35

6 x

7

=

42

7 x

7

=

49

8 x

7

=

56

9 x

7

=

63

10 x 7 = 70

11 x 7 = 77

12 x 7 = 84

1. Set the size parameter to 20 , as shown in the following code:

TimesTable(7, 20);

Run the console app and confirm that the times table now has twenty rows.

Good Practice: If a function has one or more parameters where just passing the values may not provide enough meaning, then you can optionally specify the name of the parameter as well as its value, as shown in the following code: TimesTable(number: 7, size: 10) .

Change the number passed into the TimesTable function to other byte values between 0

and 255 and confirm that the output times tables are correct.

Note that if you try to pass a non- byte number, for example, an int , double , or

string , an error is returned, as shown in the following output:

Error: (1,12): error CS1503: Argument 1: cannot convert from 'int' to 'byte'

A brief aside about arguments and parameters‌

In daily usage, most developers will use the terms argument and parameter interchangeably. Strictly speaking, the two terms have specific and subtly different meanings. But just like a person can be both a parent and a doctor, the two terms often apply to the same thing.A parameter is a variable in a function definition. For example, startDate is a parameter of the Hire function, as shown in the following code:

void Hire(DateTime startDate)

{

// Function implementation.

}

When a method is called, an argument is the data you pass into the method's parameters. For example, when is a variable passed as an argument to the Hire function, as shown in the following code:

DateTime when = new(year: 2024, month: 11, day: 5); Hire(when);

You might prefer to specify the parameter name when passing the argument, as shown in the following code:

DateTime when = new(year: 2024, month: 11, day: 5); Hire(startDate: when);

When talking about the call to the Hire function, startDate is the parameter, and when is the argument.

If you read the official Microsoft documentation, they use the phrases named and optional arguments and named and optional parameters interchangeably, as shown at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/programming- guide/classes-and-structs/named-and-optional-arguments.

It gets complicated because a single object can act as both a parameter and an argument, depending on context. For example, within the Hire function implementation, the startDate parameter could be passed as an argument to another function like SaveToDatabase , as shown in the following code:

void Hire(DateTime startDate)

{

...

SaveToDatabase(startDate, employeeRecord);

...

}

Naming things is one of the hardest parts of computing. A classic example is the parameter to the most important function in C#, Main . It defines a parameter named args , short for arguments, as shown in the following code:

static void Main(String[] args)

{

...

}

To summarize, parameters define inputs to a function, arguments are passed to a function when calling the function.

Good Practice: Try to use the correct term depending on the context, but do not get pedantic with other developers if they "misuse" a term. I must have used the terms parameter and argument thousands of times in this book. I'm sure some of those times I've been imprecise. Please do not @ me about it.

Writing a function that returns a value‌

The previous function performed actions (looping and writing to the console), but it did not return a value. Let's say that you need to calculate sales or value-added tax (VAT). In Europe, VAT rates can range from 8% in Switzerland to 27% in Hungary. In the United States, state sales taxes can range from 0% in Oregon to 8.25% in California.

Tax rates change all the time, and they vary based on many factors. The values used in this example do not need to be accurate.

Let's implement a function to calculate taxes in various regions around the world:

1. In Program.Functions.cs , in the Program class, write a function named CalculateTax , as shown in the following code:

static decimal CalculateTax(

decimal amount, string twoLetterRegionCode)

{

decimal rate = twoLetterRegionCode switch

{

"CH" => 0.08M, // Switzerland

"DK" or "NO" => 0.25M, // Denmark, Norway "GB" or "FR" => 0.2M, // UK, France

"HU" => 0.27M, // Hungary

"OR" or "AK" or "MT" => 0.0M, // Oregon, Alaska, Montana "ND" or "WI" or "ME" or "VA" => 0.05M,

"CA" => 0.0825M, // California

_ => 0.06M // Most other states.

};

return amount * rate;

}

In the preceding code, note the following:

image

CalculateTax has two inputs: a parameter named amount , which will be the amount of money spent, and a parameter named twoLetterRegionCode , which will be the region the amount is spent in.

image

CalculateTax will perform a calculation using a switch expression and then return the sales tax or VAT owed on the amount as a decimal value; so, before the name of the function, we have declared the data type of the return value to be decimal .

1. At the top of Program.Functions.cs , import the namespace to work with cultures, as shown in the following code:

using System.Globalization; // To use CultureInfo.

1. In Program.Functions.cs , in the Program class, write a function named ConfigureConsole , as shown in the following code:

static void ConfigureConsole(string culture = "en-US", bool useComputerCulture = false)

{

// To enable Unicode characters like Euro symbol in the console. OutputEncoding = System.Text.Encoding.UTF8;

if (!useComputerCulture)

{

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);

}

WriteLine($"CurrentCulture: {CultureInfo.CurrentCulture.DisplayName}");

}

This function enables UTF-8 encoding for the console output. This is necessary to output some special symbols like the Euro currency symbol. This function also controls the current culture used to format dates, times, and currency values.

1. In Program.cs , comment out any TimesTable method calls, and then call the ConfigureConsole method and the CalculateTax method, passing values for the amount such as 149 and a valid region code such as FR , as shown in the following code:

// TimesTable(number: 7, size: 10); ConfigureConsole();

decimal taxToPay = CalculateTax(amount: 149, twoLetterRegionCode: "FR"); WriteLine($"You must pay {taxToPay:C} in tax.");

// Alternatively, call the function in the interpolated string.

// WriteLine($"You must pay {CalculateTax(amount: 149,

// twoLetterRegionCode: "FR"):C} in tax.");

1. Run the code, view the result, and note that it uses US English culture, meaning US dollars for the currency, as shown in the following output:

CurrentCulture: English (United States) You must pay $29.80 in tax.

1. In Program.cs , change the ConfigureConsole method to use your local computer culture, as shown in the following code:

ConfigureConsole(useComputerCulture: true);

1. Run the code, view the result, and note that the currency should now show your local currency. For example, for me in the UK, I would see £29.80 , as shown in the following output:

CurrentCulture: English (United Kingdom) You must pay £29.80 in tax.

1. In Program.cs , change the ConfigureConsole method to use French culture, as shown in the following code:

ConfigureConsole(culture: "fr-FR");

1. Run the code, view the result, and note that the currency should now show Euros as used in France, as shown in the following output:

CurrentCulture: French (France) You must pay 29,80 € in tax.

Can you think of any problems with the CalculateTax function as written? What would happen if the user entered a code such as fr or UK ? How could you rewrite the function to improve it? Would using a switch statement instead of a switch expression be clearer?

Converting numbers from cardinal to ordinal‌

Numbers that are used to count are called cardinal numbers, for example, 1, 2, and 3, whereas numbers used to order are ordinal numbers, for example, 1st, 2nd, and 3rd. Let's create a function to convert cardinals to ordinals:

1. In Program.Functions.cs , write a function named CardinalToOrdinal that converts a cardinal uint value into an ordinal string value; for example, it converts 1 into "1st" , 2 into "2nd" , and so on, as shown in the following code:

static string CardinalToOrdinal(uint number)

{

uint lastTwoDigits = number % 100; switch (lastTwoDigits)

{

case 11: // Special cases for 11th to 13th. case 12:

case 13:

return $"{number:N0}th"; default:

uint lastDigit = number % 10; string suffix = lastDigit switch

{

1 => "st",

2 => "nd",

3 => "rd",

_ => "th"

};

return $"{number:N0}{suffix}";

}

}

From the preceding code, note the following:

image

CardinalToOrdinal has one input, a parameter of the uint type named number because we do not want to allow negative numbers, and one output: a return value of the string type.

image

A switch statement is used to handle the special cases of 11, 12, and 13.

image

A switch expression then handles all other cases: if the last digit is 1, then use st as the suffix; if the last digit is 2, then use nd as the suffix; if the last digit is 3, then use rd as the suffix; and if the last digit is anything else, then use th as the suffix.

1. In Program.Functions.cs , write a function named RunCardinalToOrdinal that uses a for statement to loop from 1 to 150, calling the CardinalToOrdinal function for each number and writing the returned string to the console, separated by a space character, as shown in the following code:

static void RunCardinalToOrdinal()

{

for (uint number = 1; number <= 150; number++) { Write($"{CardinalToOrdinal(number)} "); } WriteLine(); } 1. In Program.cs , comment out the CalculateTax statements, and call the RunCardinalToOrdinal method, as shown in the following code: RunCardinalToOrdinal(); 1. Run the console app and view the results, as shown in the following output: 1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th 11th 12th 13th 14th 15th 16th 17th 18th 19th 20th 21st In the RunCardinalToOrdinal function, change the maximum number to 1500 . Run the console app and view the results, as shown partially in the following output: 1,480th 1,481st 1,482nd 1,483rd 1,484th 1,485th 1,486th 1,487th 1,488th 1,489th 1,490th 1,491st Calculating factorials with recursion‌ The factorial of 5 is 120 because factorials are calculated by multiplying the starting number by one less than itself, and then by one less again, and so on until the number is reduced to 1. An example can be seen here: 5 x 4 x 3 x 2 x 1 = 120.The factorial function is defined for non-negative integers only, i.e., for 0, 1, 2, 3, and so on, and it is defined as: 0! = 1 n! = n × (n − 1)!, for n ∈ { 1, 2, 3, ... } We could leave it to the compiler to reject negative numbers by declaring the input parameter as uint as we did for the CardinalToOrdinal function, but this time let's see an alternative way to handle that: throwing an argument exception.Factorials are written like this: 5! , where the exclamation mark is read as “bang,” so 5! = 120, or five bang equals one hundred and twenty. Bang is a good term to use in the context of factorials because they increase in size very rapidly, just like an explosion.We will write a function named Factorial ; this will calculate the factorial for an int passed to it as a parameter. We will use a clever technique called recursion, which means a function that calls itself within its implementation, either directly or indirectly: 1. In Program.Functions.cs , write a function named Factorial , as shown in the following code: static int Factorial(int number) { if (number < 0) { throw new ArgumentOutOfRangeException(message: $"The factorial function is defined for non-negative integers only. Input: {number}", paramName: nameof(number)); } else if (number == 0) { return 1; } else { return number * Factorial(number - 1); } } As before, there are several noteworthy elements in the preceding code, including the following: image image If the input parameter number is negative, Factorial throws an exception. If the input parameter number is zero, Factorial returns 1 . image If the input parameter number is more than 0 (which it will be in all other cases), Factorial multiplies the number by the result of calling itself and passing one less than number . This makes the function recursive. More Information: Recursion is clever, but it can lead to problems, such as a stack overflow due to too many function calls because memory is used to store data on every function call, and it eventually uses too much. Iteration is a more practical, if less succinct, solution in languages such as C#. You can read more about this at the following link: https://en.wikipedia.org/wiki/Recursion_(computer_science)#Recursion_versus_iteration. 1. In Program.Functions.cs , write a function named RunFactorial that uses a for statement to output the factorials of numbers from 1 to 15, calls the Factorial function inside its loop, and then outputs the result, formatted using the code N0 , which means number format using thousand separators with zero decimal places, as shown in the following code: static void RunFactorial() { for (int i = 1; i <= 15; i++) { WriteLine($"{i}! = {Factorial(i):N0}"); } } Comment out the RunCardinalToOrdinal method call and call the RunFactorial method. Run the project and view the results, as shown in the following partial output: 1! = 1 2! = 2 3! = 6 4! = 24 ... 12! = 479,001,600 13! = 1,932,053,504 14! = 1,278,945,280 15! = 2,004,310,016 It is not immediately obvious in the previous output, but factorials of 13 and higher overflow the int type because they are so big. 12! is 479,001,600, which is about half a billion. The maximum positive value that can be stored in an int variable is about two billion. 13! is 6,227,020,800, which is about six billion, and when stored in a 32-bit integer, it overflows silently without showing any problems.What should you do to get notified when an overflow happens? Of course, we could solve the problem for 13! and 14! by using a long (64-bit integer) instead of an int (32-bit integer), but we will quickly hit the overflow limit again.The point of this section is to understand and show you that numbers can overflow, and not specifically how to calculate factorials higher than 12! . Let’s take a look: 1. Modify the Factorial function to check for overflows in the statement that calls itself, as shown highlighted in the following code: checked // for overflow { return number * Factorial(number - 1); } 1. Modify the RunFactorial function to change the starting number to -2 and to handle overflow and other exceptions when calling the Factorial function, as shown highlighted in the following code: static void RunFactorial() { for (int i = -2; i <= 15; i++) { try { WriteLine($"{i}! = {Factorial(i):N0}"); } catch (OverflowException) { WriteLine($"{i}! is too big for a 32-bit integer."); } catch (Exception ex) { WriteLine($"{i}! throws {ex.GetType()}: {ex.Message}"); } } } 1. Run the code and view the results, as shown in the following partial output: -2! throws System.ArgumentOutOfRangeException: The factorial function is defined for non-negativ -1! throws System.ArgumentOutOfRangeException: The factorial function is defined for non-negativ 0! = 1 1! = 1 2! = 2 ... 12! = 479,001,600 13! is too big for a 32-bit integer. 14! is too big for a 32-bit integer. 15! is too big for a 32-bit integer. Documenting functions with XML comments‌ By default, when calling a function such as CardinalToOrdinal , code editors will show a tooltip with basic information.Let's improve the tooltip by adding extra information: If you are using Visual Studio Code with the C# extension, you should navigate to View | Command Palette | Preferences: Open Settings (UI), and then search for formatOnType and make sure that it is enabled. C# XML documentation comments are a built-in feature of Visual Studio 2022 and JetBrains Rider, so you do not need to do anything to use them. On the line above the CardinalToOrdinal function, type three forward slashes /// , and note that they are expanded into an XML comment that recognizes that the function has a single parameter named number , as shown in the following code: ///

///

///

///

///

1. Enter suitable information for the XML documentation comment for the CardinalToOrdinal function. Add a summary, and describe the input parameter and the return value, as shown highlighted in the following code:

///

/// Pass a 32-bit unsigned integer and it will be converted into its ordinal equivalent.

///

/// Number as a cardinal value e.g. 1, 2, 3, and so on.

/// Number as an ordinal value e.g. 1st, 2nd, 3rd, and so on.

1. Now, when calling the function, you will see more details, as shown in Figure 4.2:

image

Figure 4.2: A tooltip showing the more detailed method signature

It is worth emphasizing that this feature is primarily designed to be used with a tool that converts the comments into documentation, like Sandcastle, which you can read more about at the following link: https://github.com/EWSoftware/SHFB. The tooltips that appear while entering code or hovering over the function name are a secondary feature.Local functions do not support XML comments because local functions cannot be used outside the member in which they are declared, so it makes no sense to generate documentation from them. Sadly, this also means no tooltip, which would still be useful, but neither Visual Studio 2022 nor Visual Studio Code recognize that.

Good Practice: Add XML documentation comments to all your functions except local functions.

Using lambdas in function implementations‌

F# is Microsoft's strongly typed functional-first programming language that, like C#, compiles to Intermediate Language (IL) to be executed by .NET. Functional languages evolved from lambda calculus, a computational system based only on functions. The code looks more like mathematical functions than steps in a recipe.Some of the important attributes of functional languages are defined in the following list:

image

Modularity: The same benefit of defining functions in C# applies to functional languages. Breaks up a large complex code base into smaller pieces.

image

Immutability: Variables in the C# sense do not exist. Any data value inside a function cannot change. Instead, a new data value can be created from an existing one. This reduces bugs.

image

Maintainability: Code is cleaner and clearer (for mathematically-inclined programmers!).

Since C# 6, Microsoft has worked to add features to the language to support a more functional approach, for example, adding tuples and pattern matching in C# 7, non-null reference types in C# 8, and improving pattern matching and adding records, that is, potentially immutable objects, in C# 9.In C# 6, Microsoft added support for expression- bodied function members. We will look at an example of this now. In C#, lambdas are the use of the => character to indicate a return value from a function.The Fibonacci sequence of numbers always starts with 0 and 1. Then the rest of the sequence is generated using the rule of adding together the previous two numbers, as shown in the following sequence of numbers:

0 1 1 2 3 5 8 13 21 34 55 ...

The next term in the sequence would be 34 + 55, which is 89.We will use the Fibonacci sequence to illustrate the difference between an imperative and declarative function implementation:

1. In Program.Functions.cs , write a function named FibImperative , which will be written in an imperative style, as shown in the following code:

static int FibImperative(uint term)

{

if (term == 0)

{

throw new ArgumentOutOfRangeException();

}

else if (term == 1)

{

return 0;

}

else if (term == 2)

{

return 1;

}

else

{

return FibImperative(term - 1) + FibImperative(term - 2);

}

}

1. In Program.Functions.cs , write a function named RunFibImperative that calls FibImperative inside a for statement that loops from 1 to 30, as shown in the following code:

static void RunFibImperative()

{

for (uint i = 1; i <= 30; i++) { WriteLine("The {0} term of the Fibonacci sequence is {1:N0}.", arg0: CardinalToOrdinal(i), arg1: FibImperative(term: i)); } } In Program.cs , comment out the other method calls, and call the RunFibImperative method. Run the console app and view the results, as shown in the following partial output: The 1st term of the Fibonacci sequence is 0. The 2nd term of the Fibonacci sequence is 1. The 3rd term of the Fibonacci sequence is 1. The 4th term of the Fibonacci sequence is 2. The 5th term of the Fibonacci sequence is 3. ... The 29th term of the Fibonacci sequence is 317,811. The 30th term of the Fibonacci sequence is 514,229. 1. In Program.Functions.cs , write a function named FibFunctional written in a declarative style, as shown in the following code: static int FibFunctional(uint term) => term switch

{

0 => throw new ArgumentOutOfRangeException(), 1 => 0,

2 => 1,

_ => FibFunctional(term - 1) + FibFunctional(term - 2)

};

1. In Program.Functions.cs , write a function to call it inside a for statement that loops from 1 to 30, as shown in the following code:

static void RunFibFunctional()

{

for (uint i = 1; i <= 30; i++) { WriteLine("The {0} term of the Fibonacci sequence is {1:N0}.", arg0: CardinalToOrdinal(i), arg1: FibFunctional(term: i)); } } In Program.cs , comment out the RunFibImperative method call, and call the RunFibFunctional method. Run the code and view the results (which will be the same as before). Now that you have seen some examples of functions, let's see how you can fix them when they have bugs. Debugging during development‌ In this section, you will learn how to debug problems at development time. You must use a code editor that has debugging tools, such as Visual Studio 2022 or Visual Studio Code. Creating code with a deliberate bug‌ Let's explore debugging by creating a console app with a deliberate bug, which we will then use the debugger tools in your code editor to track down and fix: Use your preferred coding tool to add a new Console App / console project named Debugging to the Chapter04 solution. Modify Debugging.csproj to statically import System.Console for all code files. In Program.cs , delete any existing statements and then, at the bottom of the file, add a function with a deliberate bug, as shown in the following code: double Add(double a, double b) { return a * b; // Deliberate bug! } 1. Above the Add function, write statements to declare and set some variables and then add them together using the buggy function, as shown in the following code: double a = 4.5; double b = 2.5; double answer = Add(a, b); WriteLine($"{a} + {b} = {answer}"); WriteLine("Press Enter to end the app."); ReadLine(); // Wait for user to press Enter. 1. Run the console application and view the result, as shown in the following output: 4.5 + 2.5 = 11.25 Press Enter to end the app. But wait, there's a bug! 4.5 added to 2.5 should be 7, not 11.25!We will use the debugging tools to hunt for and squish the bug. Setting a breakpoint and starting debugging‌ Breakpoints allow us to mark a line of code that we want to pause at to inspect the program state and find bugs. Using Visual Studio 2022 Let's set a breakpoint and then start debugging using Visual Studio 2022: Click in line 1, which is the statement that declares the variable named a . Navigate to Debug | Toggle Breakpoint or press F9. A red circle will appear in the margin bar on the left-hand side and the statement will be highlighted in red to indicate that a breakpoint has been set, as shown in Figure 4.3: image Figure 4.3: Toggling breakpoints using Visual Studio 2022 Breakpoints can be toggled off with the same action. You can also left-click in the margin to toggle a breakpoint on and off, or right-click a breakpoint to see more options, such as delete, disable, or edit conditions or actions for an existing breakpoint. 1. Navigate to Debug | Start Debugging or press F5. Visual Studio starts the console application and then pauses when it hits the breakpoint. This is known as break mode. Extra windows titled Locals (showing current values of local variables), Watch 1 (showing any watch expressions you have defined), Call Stack, Exception Settings, and Immediate Window may appear. The Debugging toolbar appears. The line that will be executed next is highlighted in yellow, and a yellow arrow points at the line from the margin bar, as shown in Figure 4.4: image Figure 4.4: Break mode in Visual Studio 2022 If you do not want to see how to use Visual Studio Code to start debugging, then you can skip the Using Visual Studio Code section and continue to the section titled Navigating with the debugging toolbar. Using Visual Studio Code Let's set a breakpoint and then start debugging using Visual Studio Code: Click in line 1, which is the statement that declares the variable named a . Navigate to Run | Toggle Breakpoint or press F9. A red circle will appear in the margin bar on the left-hand side to indicate that a breakpoint has been set. Breakpoints can be toggled off with the same action. You can also left-click in the margin to toggle a breakpoint on and off; right-click to see more options, such as remove, edit, or disable an existing breakpoint; or add a breakpoint, conditional breakpoint, or logpoint when a breakpoint does not yet exist.Logpoints, also known as tracepoints, indicate that you want to record some information without having to stop executing the code at that point. Navigate to View | Run, or in the left navigation bar, you can click the Run and Debug icon (the triangle "play" button and "bug") or press Ctrl + Shift + D (on Windows). At the top of the RUN AND DEBUG window, click the Run and Debug button, and then select the Debugging project, as shown in Figure 4.5: image Figure 4.5: Selecting the project to debug using Visual Studio Code If you are first prompted to choose a debugger, select C#, not .NET 5+ or .NET Core. Visual Studio Code starts the console app and then pauses when it hits the breakpoint. This is known as break mode. The line that will be executed next is highlighted in yellow, and a yellow block points at the line from the margin bar, as shown in Figure 4.6: image Figure 4.6: Break mode in Visual Studio Code Navigating with the debugging toolbar‌ Visual Studio 2022 has two debug-related buttons in its Standard toolbar to start or continue debugging and to hot reload changes to the running code, and a separate Debug toolbar for the rest of the tools. Visual Studio Code shows a floating toolbar with buttons to make it easy to access debugging features.Both are shown in Figure 4.7: image Figure 4.7: Debugging toolbars in Visual Studio 2022 and Visual Studio Code The following list describes the most common buttons in the toolbars: image Start/Continue/F5: This button is context-sensitive. It will either start a project running or continue running the project from the current position until it ends or hits a breakpoint. image Hot Reload: This button will reload compiled code changes without needing to restart the app. image Break All: This button will break into the next available line of code in a running app. image Stop Debugging/Stop/Shift + F5 (red square): This button will stop the debugging session. image Restart/Ctrl or Cmd + Shift + F5 (circular arrow): This button will stop and then immediately restart the program with the debugger attached again. image Show Next Statement: This button will move the current cursor to the next statement that will execute. image image Step Into/F11, Step Over/F10, and Step Out/Shift + F11 (blue arrows over dots): These buttons step through the code statements in various ways, as you will see in a moment. Show Threads in Source: This button allows you to examine and work with threads in the application that you're debugging. Debugging windows‌ While debugging, both Visual Studio 2022 and Visual Studio Code show extra windows that allow you to monitor useful information, such as variables, while you step through your code.The most useful windows are described in the following list: image image VARIABLES, including Locals, which shows the name, value, and type of any local variables automatically. Keep an eye on this window while you step through your code. WATCH, or Watch 1, which shows the value of variables and expressions that you manually enter. image CALL STACK, which shows the stack of function calls. image BREAKPOINTS, which shows all your breakpoints and allows finer control over them. When in break mode, there is also a useful window at the bottom of the edit area: image DEBUG CONSOLE or Immediate Window enables live interaction with your code. You can interrogate the program state, for example, by entering the name of a variable. For example, you can ask a question such as "What is 1+2?" by typing 1+2 and pressing Enter. Stepping through code‌ Let's explore some ways to step through the code using either Visual Studio 2022 or Visual Studio Code: The menu commands for debugging are on the Debug menu in Visual Studio 2022 or the Run menu in Visual Studio Code and JetBrains Rider. Navigate to Run or Debug | Step Into, click on the Step Into button in the toolbar, or press F11. The yellow highlight steps forward one line. Navigate to Run or Debug | Step Over, click on the Step Over button in the toolbar, or press F10. The yellow highlight steps forward one line. At the moment, you can see that there is no difference between using Step Into or Step Over because we are executing single statements. You should now be on the line that calls the Add method. The difference between Step Into and Step Over can be seen when you are about to execute a method call: image If you click on Step Into, the debugger steps into the method so that you can step through every line in that method. image If you click on Step Over, the whole method is executed in one go; it does not skip over the method without executing it. Click on Step Into to step inside the Add method. Hover your mouse pointer over the a or b parameters in the code editing window and note that a tooltip appears showing their current value. Select the expression a * b , right-click the expression, and select Add to Watch or Add Watch. The expression is added to the WATCH or Watch 1 window, showing that this operator is multiplying a by b to give the result 11.25 . In the WATCH or Watch 1 window, right-click the expression and choose Remove Expression or Delete Watch. Fix the bug by changing * to + in the Add function. Restart debugging by clicking the circular arrow Restart button or pressing Ctrl or Cmd + Shift + F5. Step over the function, take a minute to note how it now calculates correctly, and click the Continue button or press F5. With Visual Studio Code, note that when writing to the console during debugging, the output appears in the DEBUG CONSOLE window instead of the TERMINAL window, as shown in Figure 4.8: image Figure 4.8: Writing to the DEBUG CONSOLE during debugging Using the Visual Studio Code integrated terminal during debugging‌ By default, the console is set to use the internal DEBUG CONSOLE during debugging, which does not allow interactions like entering text from the ReadLine method.To improve the experience, we can change a setting to use the integrated terminal instead. First, let's modify the code to require interaction with the user: 1. At the top of Program.cs , add statements to prompt the user to enter a number and parse that as a double into the variable a , as shown highlighted in the following code: Write("Enter a number: "); string number = ReadLine()!; double a = double.Parse(number); Set a breakpoint on line 1 that writes the prompt, Enter a number . At the top of the RUN AND DEBUG window, click the Run and Debug button, and then select the Debugging project. Note that the Enter a number prompt is not written to either TERMINAL or DEBUG CONSOLE and neither window is waiting for the user to enter a number and press Enter. Stop debugging. At the top of the RUN AND DEBUG window, click the create a launch.json file link, and then, when prompted for the debugger, select C#, as shown in Figure 4.9: image Figure 4.9: Selecting a debugger for the launch.json file In the launch.json file editor, click the Add Configuration... button, and then select .NET: Launch .NET Core Console App, as shown in Figure 4.10: image Figure 4.10: Adding a launch configuration for a .NET Console App In launch.json , make the following additions and changes, as shown highlighted in the following configuration: image Comment out the preLaunchTask setting. image In the program path, add the Debugging project folder after the workspaceFolder variable. image In the program path, change to net8.0 .

image

image

In the program path, change to Debugging.dll . Change the console setting from internalConsole to integratedTerminal :

{

// Use IntelliSense to learn about possible attributes.

// Hover to view descriptions of existing attributes.

// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0",

"configurations": [

{

"name": ".NET Core Launch (console)", "type": "coreclr",

"request": "launch",

//"preLaunchTask": "build",

"program": "${workspaceFolder}/Debugging/bin/Debug/net8.0/Debugging.dll", "args": [],

"cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "integratedTerminal"

}

]

}

Remember that with Visual Studio Code, we open the Chapter04 folder to process the solution file, so the workspace folder is Chapter04 , not the Debugging project.

At the top of the RUN AND DEBUG window, note the dropdown list of launch configurations, and click the Start Debugging button (green triangle), as shown in Figure 4.11.

Navigate to View | Terminal and note the TERMINAL window is attached to the

Debugging.dll, as shown in Figure 4.11.

Step over the statement that writes Enter a number: to the console.

Step over the statement that calls ReadLine .

Type 5.5 and press Enter.

Continue stepping through the statements or press F5 or click Continue, and note the output written to the integrated terminal, as shown in Figure 4.11:

image

Figure 4.11: A launch configuration set to use the integrated terminal for user interaction

Customizing breakpoints‌

It is easy to make more complex breakpoints:

If you are still debugging, click the Stop button in the debugging toolbar, navigate to

Run or Debug | Stop Debugging, or press Shift + F5.

Navigate to Run | Remove All Breakpoints or Debug | Delete All Breakpoints.

Click on the WriteLine statement that outputs the answer.

Set a breakpoint by pressing F9 or navigating to Run or Debug | Toggle Breakpoint.

image

Right-click the breakpoint and choose the appropriate menu for your code editor: In Visual Studio Code, choose Edit Breakpoint....

image

In Visual Studio 2022, choose Conditions....

Type an expression, such as the answer variable must be greater than 9 , and then press Enter to accept it, and note the expression must evaluate to true for the breakpoint to activate, as shown in Figure 4.12:

image

Figure 4.12: Customizing a breakpoint with an expression using Visual Studio Code

Start debugging and note the breakpoint is not hit.

Stop debugging.

Edit the breakpoint or its conditions and change its expression to less than 9 .

Start debugging and note the breakpoint is hit.

Stop debugging.

Edit the breakpoint or its conditions (in Visual Studio 2022, click Add condition), select Hit Count, then enter a number such as 3 , meaning that you would have to hit the breakpoint three times before it activates, as shown in Figure 4.13:

image

Figure 4.13: Customizing a breakpoint with an expression and hot count using Visual Studio 2022

Hover your mouse over the breakpoint's red circle to see a summary, as shown in Figure 4.14:

image

Figure 4.14: A summary of a customized breakpoint in Visual Studio Code

You have now fixed a bug using some debugging tools and seen some advanced possibilities for setting breakpoints.

Hot reloading during development‌

Hot Reload is a feature that allows a developer to apply changes to code while the app is running and immediately see the effect. This is great for fixing bugs quickly. Hot Reload is also known as Edit and Continue. A list of the types of changes that you can make that support Hot Reload is found at the following link: https://aka.ms/dotnet/hot-reload.Just before the release of .NET 6, a high-level Microsoft employee caused controversy by attempting to make the feature Visual Studio-only. Luckily the open-source contingent within Microsoft successfully had the decision overturned. Hot Reload remains available using the command-line tool as well.Let's see it in action:

Use your preferred coding tool to add a new Console App / console project named

HotReloading to the Chapter04 solution.

Modify HotReloading.csproj to statically import System.Console for all code files.

In Program.cs , delete the existing statements and then write a message to the console every two seconds, as shown in the following code:

/* Visual Studio 2022: run the app, change the message, click Hot Reload.

* Visual Studio Code: run the app using dotnet watch, change the message. */ while (true)

{

WriteLine("Hello, Hot Reload!"); await Task.Delay(2000);

}

Hot reloading using Visual Studio 2022‌

If you are using Visual Studio, Hot Reload is built into the user interface:

In Visual Studio 2022, start the project and note that the message is output every two seconds.

Leave the project running.

In Program.cs , change Hello to Goodbye .

Navigate to Debug | Apply Code Changes or click the Hot Reload button in the toolbar, as shown in Figure 4.15, and note the change is applied without needing to restart the console app.

Drop down the Hot Reload button menu and select Hot Reload on File Save, as shown in

Figure 4.15:

image

Figure 4.15: Changing Hot Reload options

Change the message again, save the file, and note the console app updates automatically.

Hot reloading using Visual Studio Code and dotnet watch‌

If you are using Visual Studio Code, you must issue a special command when starting the console app to activate Hot Reload:

1. In Visual Studio Code, in TERMINAL, start the console app using dotnet watch , and note the output that shows that hot reload is active, as shown in the following output:

dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot

💡 Press "Ctrl + R" to restart. dotnet watch 🔧 Building...

Determining projects to restore...

All projects are up-to-date for restore.

HotReloading -> C:\cs12dotnet8\Chapter04\HotReloading\bin\Debug\net8.0\HotReloading.dll dotnet watch 🚀 Started

Hello, Hot Reload!

Hello, Hot Reload!

Hello, Hot Reload!

1. In Visual Studio Code, change Hello to Goodbye , and note that, after a couple of seconds, the change is applied without needing to restart the console app, as shown in the following output:

Hello, Hot Reload!

dotnet watch ⌚ File changed: .\Program.cs. Hello, Hot Reload!

Hello, Hot Reload!

dotnet watch 🔥 Hot reload of changes succeeded. Goodbye, Hot Reload!

Goodbye, Hot Reload!

1. Press Ctrl + C to stop it running, as shown in the following output:

Goodbye, Hot Reload!

dotnet watch 🛑 Shutdown requested. Press Ctrl+C again to force exit.

Now that you've seen tools for finding and removing bugs during development, let's see how you can track down less obvious problems that might happen during development and production.

Logging during development and runtime‌

Once you believe that all the bugs have been removed from your code, you would then compile a release version and deploy the application, so that people can use it. But no code is ever bug-free, and during runtime, unexpected errors can occur.End users are notoriously bad at remembering, admitting to, and then accurately describing what they were doing when an error occurred. You should not rely on them providing useful information to reproduce the problem so that you can understand what caused the problem and then fix it. Instead, you can instrument your code, which means logging events of interest and other data like timings.

Good Practice: Add code throughout your application to log what is happening, especially when exceptions occur, so that you can review the logs and use them to trace the issue and fix the problem. Although we will see logging again in Chapter 10, Working with Data Using Entity Framework Core, and in Chapter 14, Building Websites Using the Model-View-Controller Pattern, logging is a huge topic, so we can only cover the basics in this book.

Understanding logging options‌

.NET includes some built-in ways to instrument your code by adding logging capabilities. We will cover the basics in this book. But logging is an area where third parties have created a rich ecosystem of powerful solutions that extend what Microsoft provides. I cannot make specific recommendations because the best logging framework depends on your needs. But I include some common ones in the following list:

image

image

Apache log4net NLog

image

Serilog

I introduce structured logging using the third-party logging system Serilog in my companion book, Apps and Services with .NET 8, because the Serilog packages have the most downloads on https://www.nuget.org.

Good Practice: When interviewing for a developer position, the logging system used by an organization is a good thing to ask about to show that you understand the importance of logging and you know that the specifics of the implementation are likely to be different depending on the organization.

Instrumenting with Debug and Trace‌

You have seen the use of the Console type and its WriteLine method writing out to the console window. There is also a pair of types named Debug and Trace that have more flexibility in where they write out to:

image

image

The Debug class is used to add logging that gets written only during development. The Trace class is used to add logging that gets written during both development and runtime.

The Debug and Trace classes write to any trace listener. A trace listener is a type that can be configured to write output anywhere you like when the WriteLine method is called. There are several trace listeners provided by .NET, including one that outputs to the console, and you can even make your own by inheriting from the TraceListener type so you can write to anywhere you want.

Writing to the default trace listener

One trace listener, the DefaultTraceListener class, is configured automatically and writes to Visual Studio Code's DEBUG CONSOLE window or Visual Studio's Debug window. You can configure other trace listeners using code.Let's see trace listeners in action:

Use your preferred coding tool to add a new Console App / console project named

Instrumenting to the Chapter04 solution.

In Program.cs , delete the existing statements and then import the System.Diagnostics

namespace, as shown in the following code:

using System.Diagnostics; // To use Debug and Trace.

1. In Program.cs , write messages from the two classes, as shown in the following code:

Debug.WriteLine("Debug says, I am watching!"); Trace.WriteLine("Trace says, I am watching!");

If you are using Visual Studio 2022, navigate to View | Output and make sure Show output from: Debug is selected.

Start the Instrumenting project with debugging, and note that DEBUG CONSOLE in Visual Studio Code or the Output window in Visual Studio 2022 shows the two messages, mixed with other debugging information, such as loaded assembly DLLs, as shown in Figures 4.16 and 4.17:

image

Figure 4.16: Visual Studio Code DEBUG CONSOLE shows the two messages in blue

image

Figure 4.17: Visual Studio 2022 Output window shows Debug output including the two messages

Configuring trace listeners‌

Now, we will configure another trace listener that will write to a text file:

1. Before the Debug and Trace calls to WriteLine , add statements to create a new text file on the desktop and pass it into a new trace listener that knows how to write to a text file, and enable automatic flushing for its buffer during development, as shown in the following code:

string logPath = Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.DesktopDirectory), "log.txt");

Console.WriteLine($"Writing to: {logPath}"); TextWriterTraceListener logFile = new(File.CreateText(logPath)); Trace.Listeners.Add(logFile);

#if DEBUG

// Text writer is buffered, so this option calls

// Flush() on all listeners after writing. Trace.AutoFlush = true;

#endif

Good Practice: Any type that represents a file usually implements a buffer to improve performance. Instead of writing immediately to the file, data is written to an in-memory buffer, and only once the buffer is full will it be written in one chunk to the file.

This behavior can be confusing while debugging because we do not immediately see the results! Enabling AutoFlush means the Flush method is called automatically after every write. This reduces performance, so you should only set it on during debugging and not in production.

1. At the bottom of Program.cs , add statements to flush and close any buffered trace listeners for Debug and Trace , as shown in the following code:

// Close the text file (also flushes) and release resources. Debug.Close();

Trace.Close();

1. Run the release configuration of the console app:

image

In Visual Studio Code, enter the following command in the TERMINAL window for the

Instrumenting project and note that nothing will appear to have happened:

dotnet run --configuration Release

image

In Visual Studio 2022, in the standard toolbar, select Release in the Solution Configurations drop-down list, note that any statements in a #if DEBUG region are grayed out to indicate they are not compiled, as shown in Figure 4.18, and then navigate to Debug | Start Without Debugging.

image

Figure 4.18: Selecting the Release configuration in Visual Studio

On your desktop, open the file named log.txt and note that it contains the message

Trace says, I am watching! .

Run the debug configuration of the console app:

image

In Visual Studio Code, enter the following command in the TERMINAL window for the

Instrumenting project:

dotnet run --configuration Debug

image

In Visual Studio, in the standard toolbar, select Debug in the Solution Configurations

drop-down list and then navigate to Debug | Start Debugging.

1. On your desktop, open the file named log.txt and note that it contains both the message

Debug says, I am watching! and also the message Trace says, I am watching! .

Good Practice: When running with the Debug configuration, both Debug and Trace are active and will write to any trace listeners. When running with the Release configuration, only Trace will write to any trace listeners. You can therefore use Debug.WriteLine calls liberally throughout your code, knowing they will be stripped out automatically when you build the release version of your application and will therefore not affect performance.

Switching trace levels‌

The Trace.WriteLine calls are left in your code, even after release. So, it would be great to have fine control over when they are output. This is something we can do with a trace switch.The value of a trace switch can be set using a number or a word. For example, the number 3 can be replaced with the word Info , as shown in Table 4.1:

Number Word Description

Off This will output nothing.

Error This will output only errors.

Warning This will output errors and warnings.

Info This will output errors, warnings, and information.

Verbose This will output all levels. Table 4.1: Trace levels

Let's explore using trace switches. First, we will add some NuGet packages to our project to enable loading configuration settings from a JSON appsettings file.

Adding packages to a project in Visual Studio 2022

Visual Studio has a graphical user interface for adding packages:

In Solution Explorer, right-click the Instrumenting project and select Manage NuGet Packages.

Select the Browse tab.

Search for each of these NuGet packages and click the Install button, as shown in

Figure 4.19:

image

image

Microsoft.Extensions.Configuration.Binder Microsoft.Extensions.Configuration.Json

image

Figure 4.19: Installing NuGet packages using Visual Studio 2022

Good Practice: To use preview packages, for example, back in June 2023 for .NET 8 or during most of 2024 for .NET 9, you must select the Include prerelease checkbox as shown in Figure 4.19. There are also packages for loading configuration from XML files,

INI files, environment variables, and the command line. Use the most appropriate technique for setting configuration in your projects.

Adding packages to a project in Visual Studio Code

Visual Studio Code does not have a mechanism to add NuGet packages to a project, so we will use the command-line tool:

Navigate to the TERMINAL window for the Instrumenting project.

Enter the following command:

dotnet add package Microsoft.Extensions.Configuration.Binder

1. Enter the following command:

dotnet add package Microsoft.Extensions.Configuration.Json

dotnet add package adds a reference to a NuGet package to your project file. It will be downloaded during the build process. dotnet add reference adds a project-to-project reference to your project file. The referenced project will be compiled if needed during the build process.

Reviewing project packages for working with configuration

After adding the NuGet packages, we can see the references in the project file. Package references are case-insensitive, so you do not need to worry if they are not an exact case match. Let's review the package references:

1. Open Instrumenting.csproj and note the section with the added NuGet packages, as shown highlighted in the following markup:

Exe

net8.0

enable

enable

After the final release of .NET 7, Microsoft fixed a bug in Microsoft.Extensions.Configuration.Binder package version 7.0.3. This caused an exception to be thrown due to the way the previous edition read a setting. This is known as a bug- fix regression. Good unit tests should detect fixes that then cascade to cause other problems. It is also an example of unexpected issues with future package versions. If you have unexpected issues with a package, try an earlier version.

Add a file named appsettings.json to the Instrumenting project folder.

In appsettings.json , define a setting named PacktSwitch with a Value of Info , as shown in the following code:

{

"PacktSwitch": {

"Value": "Info"

}

}

Until Microsoft.Extensions.Configuration.Binder package version 7.0.3, you could set the Level property. For example: "Level": "Info" . After the bug fix, this now causes an exception to be thrown. Instead, we must set the Value property, or both. This is due to an internal class needing the Value to be set, as explained at the following link: https://github.com/dotnet/runtime/issues/82998.

1. In Visual Studio 2022 and JetBrains Rider, in Solution Explorer, right-click appsettings.json , select Properties, and then in the Properties window, change Copy to Output Directory to Copy always. This is necessary because unlike Visual Studio Code, which runs the console app in the project folder, Visual Studio runs the console app in Instrumenting\bin\Debug\net8.0 or Instrumenting\bin\Release\net8.0 . To confirm this is done correctly, review the element that was added to the project file, as shown in the following markup:

Always

The Copy to Output Directory property can be unreliable. In our code, we will read and output this file so we can see exactly what is being processed to catch any issues with changes not being copied correctly.

1. At the top of Program.cs , import the Microsoft.Extensions.Configuration namespace, as shown in the following code:

using Microsoft.Extensions.Configuration; // To use ConfigurationBuilder.

1. Before the statements that close Debug and Trace , add some statements to create a configuration builder that looks in the current folder for a file named appsettings.json , build the configuration, create a trace switch, set its level by binding to the configuration, and then output the four trace switch levels, as shown in the following code:

string settingsFile = "appsettings.json"; string settingsPath = Path.Combine(

Directory.GetCurrentDirectory(), settingsFile); Console.WriteLine("Processing: {0}", settingsPath); Console.WriteLine("--{0} contents--", settingsFile); Console.WriteLine(File.ReadAllText(settingsPath)); Console.WriteLine(" ");

ConfigurationBuilder builder = new(); builder.SetBasePath(Directory.GetCurrentDirectory());

// Add the settings file to the processed configuration and make it

// mandatory so an exception will be thrown if the file is not found. builder.AddJsonFile(settingsFile,

optional: false, reloadOnChange: true); IConfigurationRoot configuration = builder.Build(); TraceSwitch ts = new(

displayName: "PacktSwitch",

description: "This switch is set via a JSON config."); configuration.GetSection("PacktSwitch").Bind(ts); Console.WriteLine($"Trace switch value: {ts.Value}"); Console.WriteLine($"Trace switch level: {ts.Level}"); Trace.WriteLineIf(ts.TraceError, "Trace error"); Trace.WriteLineIf(ts.TraceWarning, "Trace warning"); Trace.WriteLineIf(ts.TraceInfo, "Trace information"); Trace.WriteLineIf(ts.TraceVerbose, "Trace verbose");

If the appsettings.json file is not found, then the following exception will be thrown:

System.IO.FileNotFoundException: The configuration file 'appsettings.json' was not found and i

.

1. After the statements that close Debug and Trace , add some statements to prompt the user to press Enter to exit the console app, as shown highlighted in the following code:

// Close the text file (also flushes) and release resources. Debug.Close();

Trace.Close();

Console.WriteLine("Press enter to exit."); Console.ReadLine();

Good Practice: Be careful not to close Debug or Trace before you are done using them. If you close them and then write to them, nothing will happen!

Set a breakpoint on the Bind statement.

Start debugging the Instrumenting console app project.

In the VARIABLES or Locals window, expand the ts variable expression, and note that its Level is Off and its TraceError , TraceWarning , and so on are all false , as shown in Figure 4.20:

image

Figure 4.20: Watching the trace switch variable properties in Visual Studio 2022

Step into the call to the Bind method by clicking the Step Into or Step Over buttons or pressing F11 or F10, and note the ts variable watch expression SwitchSetting , Value , and Level properties update to the Info level ( 3 ), and three of the four TraceX properties change to true , as shown in Figure 4.21:

image

Figure 4.21: Info trace level enables all but TraceVerbose

Step into or over the four calls to Trace.WriteLineIf and note that all levels up to

Info are written to the DEBUG CONSOLE or Output - Debug window, but not Verbose .

Stop debugging.

Modify appsettings.json to set a level value of 2 , which is the equivalent of Warning , as shown in the following JSON file:

{

"PacktSwitch": { "Value": "2"

}

}

Save the changes.

In Visual Studio Code, run the console application by entering the following command in the TERMINAL window for the Instrumenting project:

dotnet run --configuration Release

In Visual Studio 2022, in the standard toolbar, select Release in the Solution Configurations drop-down list and then run the console app by navigating to Debug | Start Without Debugging.

Open the file named log.txt and note that, this time, only trace error and warning levels are the output of the four potential trace levels, as shown in the following text file:

Trace says, I am watching! Trace error

Trace warning

If no --configuration argument is passed, the default trace switch level is Off ( 0 ), so none of the switch levels are output.

Logging information about your source code‌

When you write to a log, you will often want to include the name of the source code file, the name of the method, and the line number. In C# 10 and later, you can even get any

expressions passed as an argument to a function as a string value so you can log them.You can get all this information from the compiler by decorating function parameters with special attributes, as shown in Table 4.2:

Parameter example Description

[CallerMemberName] string member = "" Sets the string parameter named member to

the name of the method or property that is executing the method that defines this parameter.

[CallerFilePath] string filepath = "" Sets the string parameter named filepath to

the name of the source code file that contains the statement that is executing the method that defines this parameter.

[CallerLineNumber] int line = 0 Sets the int parameter named line to the

line number in the source code file of the statement that is executing the method that defines this parameter.

Sets the string parameter named expression

[CallerArgumentExpression( nameof(argumentExpression))] string expression = ""

to the expression that has been passed to the parameter named argumentExpression .

Table 4.2: Attributes to get information about the method caller

You must make these parameters optional by assigning default values to them.Let's see some code in action:

In the Instrumenting project, add a class file named Program.Functions.cs .

Delete any existing statements and then add statements to define a function named LogSourceDetails that uses the four special attributes to log information about the calling code, as shown in the following code:

using System.Diagnostics; // To use Trace.

using System.Runtime.CompilerServices; // To use [CallerX] attributes partial class Program

{

static void LogSourceDetails( bool condition,

[CallerMemberName] string member = "", [CallerFilePath] string filepath = "", [CallerLineNumber] int line = 0,

[CallerArgumentExpression(nameof(condition))] string expression = "")

{

Trace.WriteLine(string.Format(

"[{0}]\n {1} on line {2}. Expression: {3}", filepath, member, line, expression));

}

}

1. In Program.cs , at the bottom of the file, before the calls to close Debug and Trace , add statements to declare and set a variable that will be used in an expression that is passed to the function named LogSourceDetails , as shown highlighted in the following code:

int unitsInStock = 12; LogSourceDetails(unitsInStock > 10);

// Close the text file (also flushes) and release resources. Debug.Close();

Trace.Close();

We are just making up an expression in this scenario. In a real project, this might be an expression that is dynamically generated by the user making user interface selections to query a database or so on.

1. Run the console app without debugging, press Enter and close the console app, and then open the log.txt file and note the result, as shown in the following output:

[C:\cs12dotnet8\Chapter04\Instrumenting\Program.cs]

$ on line 44. Expression: unitsInStock > 10

Unit testing‌

Fixing bugs in code is expensive. The earlier that a bug is discovered in the development process, the less expensive it will be to fix.Unit testing is a good way to find bugs early in the development process because they test a small unit before they are integrated together or are seen by user acceptance testers. Some developers even follow the principle that programmers should create unit tests before they write code, and this is called Test- Driven Development (TDD).Microsoft has a proprietary unit testing framework known as MSTest. There is also a framework named NUnit. However, we will use the free and open- source third-party framework xUnit.net. All three do basically the same thing. xUnit was created by the same team that built NUnit, but they fixed the mistakes they felt they made previously. xUnit is more extensible and has better community support.

If you are curious about the pros and cons of the various testing systems, then there are hundreds of articles written by proponents of each. Just Google them: https://www.google.com/search?q=xunit+vs+nunit.

Understanding types of testing‌

Unit testing is just one of many types of testing, as described in Table 4.3: Type Description

Unit Tests the smallest unit of code, typically a method or function. Unit testing is performed on a unit of code isolated from its dependencies by mocking them if needed. Each unit should have multiple tests: some with typical inputs and expected outputs, some with extreme input values to test boundaries, and some with deliberately wrong inputs to test exception handling.

Integration Tests if the smaller units and larger components work together as a single piece of software. Sometimes involves integrating with external components for which you do not have source code.

System Tests the whole system environment in which your software will run.

Performance Tests the performance of your software; for example, your code must return a web page full of data to a visitor in under 20 milliseconds.

Load Tests how many requests your software can handle simultaneously while maintaining required performance, for example, 10,000 concurrent visitors to a website.

User Acceptance

Tests if users can happily complete their work using your software.

Table 4.3: Types of testing

Creating a class library that needs testing‌

First, we will create a function that needs testing. We will create it in a class library project separate from a console app project. A class library is a package of code that can be distributed and referenced by other .NET applications:

1. Use your preferred coding tool to add a new Class Library / classlib project named

CalculatorLib to the Chapter04 solution.

At this point, you will have created about a dozen new console app projects and added them to a solution. The only difference when adding a Class Library / classlib is to select a different project template. The rest of the steps are the same as adding a Console App / console project.

image

If you are using Visual Studio 2022:

Navigate to File | Add | New Project.

In the Add a new project dialog, search for and select Class Library [C#] and then click Next.

In the Configure your new project dialog, for the Project name, enter CalculatorLib , leave the location as C:\cs12dotnet8\Chapter04 , and then click Next.

In the Additional information dialog, select .NET 8.0 (Long Term Support), and then click Create.

image

If you are using Visual Studio Code:

In TERMINAL, switch to a terminal in the Chapter04 folder.

Use the dotnet CLI to create a new class library project named CalculatorLib , as shown in the following command: dotnet new classlib -o CalculatorLib

Use the dotnet CLI to add the new project folder to the solution, as shown in the following command: dotnet sln add CalculatorLib

Note the results, as shown in the following output:

Project CalculatorLib\CalculatorLib.csproj added to the solution.

For all code editors, in the CalculatorLib project, rename the file named Class1.cs to

Calculator.cs .

In Calculator.cs , modify the file to define a Calculator class (with a deliberate bug!), as shown in the following code:

namespace CalculatorLib; public class Calculator

{

public double Add(double a, double b)

{

return a * b;

}

}

Compile your class library project:

image

In Visual Studio 2022, navigate to Build | Build CalculatorLib.

image

In Visual Studio Code, in a TERMINAL window for the CalculatorLib folder, enter the command dotnet build . (You could also run this command in the Chapter04 folder but that would build the whole solution, which is unnecessary in this scenario.)

Use your preferred coding tool to add a new xUnit Test Project [C#] / xunit project named CalculatorLibUnitTests to the Chapter04 solution. For example, at the command prompt or terminal in the Chapter04 folder, enter the following commands:

dotnet new xunit -o CalculatorLibUnitTests dotnet sln add CalculatorLibUnitTests

1. In the CalculatorLibUnitTests project, add a project reference to the CalculatorLib

project:

image

If you are using Visual Studio 2022, in Solution Explorer, select the CalculatorLibUnitTests project, navigate to Project | Add Project Reference…, check the box to select the CalculatorLib project, and then click OK.

image

If you are using Visual Studio Code, use the dotnet add reference command, or in the file named CalculatorLibUnitTests.csproj , modify the configuration to add an item group with a project reference to the CalculatorLib project, as shown highlighted in the following markup:

The path for a project reference can use either forward / or back slashes \ because the paths are processed by the .NET SDK and changed if necessary for the current operating system.

1. Build the CalculatorLibUnitTests project.

Writing unit tests‌

A well-written unit test will have three parts:

image

image

Arrange: This part will declare and instantiate variables for input and output. Act: This part will execute the unit that you are testing. In our case, that means calling the method that we want to test.

image

Assert: This part will make one or more assertions about the output. An assertion is a belief that, if not true, indicates a failed test. For example, when adding 2 and 2, we would expect the result to be 4.

Now, we will write some unit tests for the Calculator class:

Rename the file UnitTest1.cs to CalculatorUnitTests.cs and then open it.

In Visual Studio Code, rename the class to CalculatorUnitTests . (Visual Studio prompts you to rename the class when you rename the file.)

In CalculatorUnitTests , import the CalculatorLib namespace, and then modify the CalculatorUnitTests class to have two test methods, one for adding 2 and 2, and another for adding 2 and 3, as shown in the following code:

using CalculatorLib; // To use Calculator. namespace CalculatorLibUnitTests;

public class CalculatorUnitTests

{

[Fact]

public void TestAdding2And2()

{

// Arrange: Set up the inputs and the unit under test. double a = 2;

double b = 2;

double expected = 4; Calculator calc = new();

// Act: Execute the function to test. double actual = calc.Add(a, b);

// Assert: Make assertions to compare expected to actual results. Assert.Equal(expected, actual);

}

[Fact]

public void TestAdding2And3()

{

double a = 2;

double b = 3;

double expected = 5; Calculator calc = new();

double actual = calc.Add(a, b); Assert.Equal(expected, actual);

}

}

Visual Studio 2022 still uses an older project item template that uses a nested namespace. The preceding code shows the modern project item template used by dotnet new and JetBrains Rider that uses a file-scoped namespace.

1. Build the CalculatorLibUnitTests project.

Running unit tests using Visual Studio 2022

Now we are ready to run the unit tests and see the results:

In Visual Studio, navigate to Test | Run All Tests.

In Test Explorer, note that the results indicate that two tests ran, one test passed, and one test failed, as shown in Figure 4.22:

image

Figure 4.22: The unit test results in Visual Studio 2022's Test Explorer

Running unit tests using Visual Studio Code

Now we are ready to run the unit tests and see the results:

If you have not recently built the test project, then build the CalculatorLibUnitTests project to make sure that the new testing feature in the C# Dev Kit extension recognizes the unit tests that you have written.

In Visual Studio Code, navigate to View | Testing, and note the TESTING window has a mini toolbar with buttons to Refresh Tests, Run Tests, Debug Tests, and so on.

In the TESTING window, expand the CalculatorLibUnitTests project to show the two tests.

Hover your mouse pointer over CalculatorUnitTests and then click the Run Tests button (black triangle icon) defined in that class.

Click the TEST RESULTS tab and note that the results indicate that two tests ran, one test passed, and one test failed, as shown in Figure 4.23:

image

Figure 4.23: The unit test results in Visual Studio Code's TERMINAL

Fixing the bug

Now you can fix the bug:

Fix the bug in the Add method.

Run the unit tests again to see that the bug has now been fixed and both tests have passed.

Now that we've written, debugged, logged, and unit-tested functions, let's finish this chapter by looking at how to throw and catch exceptions in functions.

Throwing and catching exceptions in functions‌

In Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions, you were introduced to exceptions and how to use a try-catch statement to handle them. But you should only catch and handle an exception if you have enough information to mitigate the issue. If you do not, then you should allow the exception to pass up through the call stack to a higher level.

Understanding usage errors and execution errors‌

Usage errors are when a programmer misuses a function, typically by passing invalid values as parameters. They could be avoided by that programmer changing their code to pass valid values. When some programmers first learn C# and .NET, they sometimes think exceptions can always be avoided because they assume all errors are usage errors. Usage errors should all be fixed before production runtime.Execution errors are when something happens at runtime that cannot be fixed by writing "better" code. Execution errors can be split into program errors and system errors. If you attempt to access a network resource but the network is down, you need to be able to handle that system error by logging an exception, and possibly backing off for a time and trying again. But some system errors, such as running out of memory, simply cannot be handled. If you attempt to open a file that does not exist, you might be able to catch that error and handle it programmatically by creating a new file.

Program errors can be programmatically fixed by writing smart code. System errors often cannot be fixed programmatically.

Commonly thrown exceptions in functions‌

Very rarely should you define new types of exceptions to indicate usage errors. .NET already defines many that you should use.When defining your own functions with parameters, your code should check the parameter values and throw exceptions if they have values that will prevent your function from properly functioning.For example, if a parameter to a function should not be null , throw ArgumentNullException . For other problems, throw ArgumentException , NotSupportedException , or InvalidOperationException . For any exception, include a message that describes the problem for whoever will have to read it (typically a developer audience for class libraries and functions, or end users if it is at the highest level of a GUI app), as shown in the following code:

static void Withdraw(string accountName, decimal amount)

{

if (string.IsNullOrWhiteSpace(accountName))

{

throw new ArgumentException(paramName: nameof(accountName));

}

if (amount <= 0) { throw new ArgumentOutOfRangeException(paramName: nameof(amount), message: $"{nameof(amount)} cannot be negative or zero."); } // process parameters } Good Practice: If a function cannot successfully perform its operation, you should consider it a function failure and report it by throwing an exception. Throwing exceptions using guard clauses‌ Instead of instantiating an exception using new , you can use static methods on the exception itself. When used in a function implementation to check argument values, they are known as guard clauses. Some were introduced with .NET 6, and more were added in .NET 8.Common guard clauses are shown in Table 4.4: Exception Guard clause methods ArgumentException ThrowIfNullOrEmpty , ThrowIfNullOrWhiteSpace ArgumentNullException ThrowIfNull ArgumentOutOfRangeException ThrowIfEqual , ThrowIfGreaterThan , ThrowIfGreaterThanOrEqual , ThrowIfLessThan , ThrowIfLessThanOrEqual , ThrowIfNegative , ThrowIfNegativeOrZero , ThrowIfNotEqual , ThrowIfZero Table 4.4: Common guard clauses Instead of writing an if statement and then throwing a new exception, we can simplify the previous example, as shown in the following code: static void Withdraw(string accountName, decimal amount) { ArgumentException.ThrowIfNullOrWhiteSpace(accountName, paramName: nameof(accountName)); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount, paramName: nameof(amount)); // process parameters } Understanding the call stack‌ The entry point for a .NET console application is the Main method (if you have explicitly defined this class) or

$ (if it was created for you by the top-level program feature) in the Program class.The Main method will call other methods, which call other methods, and so on, and these methods could be in the current project or referenced projects and NuGet packages, as shown in Figure 4.24:

image

Figure 4.24: A chain of method calls that create a call stack

Let's create a similar chain of methods to explore where we could catch and handle exceptions:

Use your preferred coding tool to add a new Class Library / classlib project named

CallStackExceptionHandlingLib to the Chapter04 solution.

Rename the Class1.cs file to Processor.cs .

In Processor.cs , modify its contents, as shown in the following code:

using static System.Console;

namespace CallStackExceptionHandlingLib; public class Processor

{

public static void Gamma() // public so it can be called from outside.

{

WriteLine("In Gamma");

Delta();

}

private static void Delta() // private so it can only be called internally.

{

WriteLine("In Delta"); File.OpenText("bad file path");

}

}

Use your preferred coding tool to add a new Console App / console project named

CallStackExceptionHandling to the Chapter04 solution.

In the CallStackExceptionHandling console app project, add a reference to the

CallStackExceptionHandlingLib class library project, as shown in the following markup:

Build the CallStackExceptionHandling console app project to make sure dependent projects are compiled and copied to the local bin folder.

In Program.cs , delete the existing statements, and then add statements to define two methods and chain calls to them and the methods in the class library, as shown in the following code:

using CallStackExceptionHandlingLib; // To use Processor. using static System.Console;

WriteLine("In Main");

Alpha(); void Alpha()

{

WriteLine("In Alpha");

Beta();

}

void Beta()

{

WriteLine("In Beta");

Processor.Gamma();

}

1. Run the console app without the debugger attached, and note the results, as shown in the following partial output:

In Main In Alpha In Beta In Gamma In Delta

Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\cs12dotnet8\Chapte File name: 'C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\bin\Debug\net8.0\bad file path'

at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, File at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess acc at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, File at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 buffe at System.IO.File.OpenText(String path)

at CallStackExceptionHandlingLib.Calculator.Delta() in C:\cs11dotnet8\Chapter04\CallStackExce at CallStackExceptionHandlingLib.Calculator.Gamma() in C:\cs12dotnet8\Chapter04\CallStackExce at Program.<

$>g Beta|0_1() in C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\Prog at Program.<
$>g Alpha|0_0() in C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\Pro at Program.
$(String[] args) in C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\Prog

Note that the call stack is upside-down. Starting from the bottom, you see:

image

The first call is to the

$ entry point function in the auto-generated Program

class. This is where arguments are passed in as a String array.

image

The second call is to the <

$>g Alpha|0_0 function. (The C# compiler renames it from Alpha when it adds it as a local function.)

image

image

The third call is to the Beta function. The fourth call is to the Gamma function.

image

The fifth call is to the Delta function. This function attempts to open a file by passing a bad file path. This causes an exception to be thrown. Any function with a try-catch statement could catch this exception. If it does not, the exception is

automatically passed up the call stack until it reaches the top, where .NET outputs the exception (and the details of this call stack).

Good Practice: Unless you need to step through your code to debug it, you should always run your code without the debugger attached. In this case, it is especially important not to attach the debugger because, if you do, it will catch the exception and show it in a GUI dialog box instead of outputting it as shown in the book.

Where to catch exceptions‌

Programmers can decide if they want to catch an exception near the failure point or centralized higher up the call stack. This allows your code to be simplified and standardized. You might know that calling an exception could throw one or more types of exception, but you do not need to handle any of them at the current point in the call stack.

Rethrowing exceptions‌

Sometimes you want to catch an exception, log it, and then rethrow it. For example, if you are writing a low-level class library that will be called from an application, your code may not have enough information to programmatically fix the error in a smart way, but the calling application might have more information and be able to. Your code should log the error in case the calling application does not, and then rethrow it up the call stack in case the calling application chooses to handle it better. There are three ways to rethrow an exception inside a catch block, as shown in the following list:

image

To throw the caught exception with its original call stack, call throw .

image

To throw the caught exception as if it was thrown at the current level in the call stack, call throw with the caught exception, for example, throw ex . This is usually poor practice because you have lost some potentially useful information for debugging but can be useful when you want to deliberately remove that information when it contains sensitive data.

image

To wrap the caught exception in another exception that can include more information in a message that might help the caller understand the problem, throw a new exception, and pass the caught exception as the innerException parameter.

If an error could occur when we call the Gamma function, then we could catch the exception and perform one of the three techniques of rethrowing an exception, as shown in the following code:

This code is just illustrative. You would never use all three techniques in the same

catch block!

try

{

Gamma();

}

catch (IOException ex)

{

LogException(ex);

// Throw the caught exception as if it happened here

// this will lose the original call stack. throw ex;

// Rethrow the caught exception and retain its original call stack. throw;

// Throw a new exception with the caught exception nested within it. throw new InvalidOperationException(

message: "Calculation had invalid values. See inner exception for why.", innerException: ex);

}

Let's see this in action with our call stack example:

1. In the CallStackExceptionHandling project, in Program.cs , in the Beta function, add a try-catch statement around the call to the Gamma function, as shown highlighted in the following code:

void Beta()

{

WriteLine("In Beta"); try

{

Processor.Gamma();

}

catch (Exception ex)

{

WriteLine($"Caught this: {ex.Message}"); throw ex;

}

}

Note your code editor will show a squiggle under the throw ex to warn you that you will lose call stack information, as described in the following code analyzer message,

Re-throwing caught exception changes stack information , with more details found at the following link: https://learn.microsoft.com/en-us/dotnet/fundamentals/code- analysis/quality-rules/ca2200.

1. Run the console app and note that the output excludes some details of the call stack, as shown in the following output:

Caught this: Could not find file 'C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\bin\Debug\ Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\cs12dotnet8\Chapte File name: 'C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\bin\Debug\net8.0\bad file path'

at Program.<

$>g Beta|0_1() in C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\Prog at Program.<
$>g Alpha|0_0() in C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\Pro at Program.
$(String[] args) in C:\cs12dotnet8\Chapter04\CallStackExceptionHandling\Prog

Remove the ex by replacing the statement throw ex; with throw; .

Run the console app and note that the output includes all the details of the call stack.

Implementing the tester-doer and try patterns‌

The tester-doer pattern can avoid some thrown exceptions (but not eliminate them completely). This pattern uses pairs of functions: one to perform a test and the other to perform an action that would fail if the test was not passed..NET implements this pattern itself. For example, before adding an item to a collection by calling the Add method, you can test to see if it is read-only, which would cause Add to fail and, therefore, throw an exception.For example, before withdrawing money from a bank account, you might test that the account is not overdrawn, as shown in the following code:

if (!bankAccount.IsOverdrawn())

{

bankAccount.Withdraw(amount);

}

The tester-doer pattern can add performance overhead, so you can also implement the try pattern, which, in effect, combines the test and do parts into a single function, as we saw with TryParse .Another problem with the tester-doer pattern occurs when you are using multiple threads. In this scenario, one thread calls the test function and it returns a value that indicates that it is okay to proceed. But then another thread executes, which changes the state. Then the original thread continues executing, assuming that everything is fine, but it is not fine. This is called a race condition. This topic is too advanced to cover how to handle it in this book.

Good Practice: Use the try pattern in preference to the tester-doer pattern.

If you implement your own try pattern function and it fails, remember to set the out parameter to the default value of its type and then return false , as shown in the following code:

static bool TryParse(string? input, out Person value)

{

if (someFailure)

{

value = default(Person); return false;

}

// Successfully parsed the string into a Person. value = new Person() { ... };

return true;

}

More Information: Now that you've been introduced to the basics of exceptions, you can learn more about the details by reading the official documentation at the following link: https://learn.microsoft.com/en-us/dotnet/standard/exceptions/.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring, with deeper research, the topics covered in this chapter.

Exercise 4.1 – Test your knowledge‌

Answer the following questions. If you get stuck, try googling the answers, if necessary, while remembering that if you get totally stuck, the answers are in the Appendix:

What does the C# keyword void mean?

What are some differences between imperative and functional programming styles?

In Visual Studio Code or Visual Studio, what is the difference between pressing F5, Ctrl or Cmd + F5, Shift + F5, and Ctrl or Cmd + Shift + F5?

Where does the Trace.WriteLine method write its output to?

What are the five trace levels?

What is the difference between the Debug and Trace classes?

When writing a unit test, what are the three "A"s?

When writing a unit test using xUnit, which attribute must you decorate the test methods with?

What dotnet command executes xUnit tests?

What statement should you use to rethrow a caught exception named ex without losing the stack trace?

Exercise 4.2 – Practice writing functions with debugging and unit testing‌

Prime factors are the combination of the smallest prime numbers that, when multiplied together, will produce the original number. Consider the following examples:

image

Prime factors of 4 are 2 x 2

image

Prime factor of 7 is 7

image

Prime factors of 30 are 5 x 3 x 2

image

Prime factors of 40 are 5 x 2 x 2 x 2

image

Prime factors of 50 are 5 x 5 x 2

Create three projects:

image

A class library named Ch04Ex02PrimeFactorsLib with a static class and static method named PrimeFactors , which, when passed an int variable as a parameter, returns a string showing its prime factors.

image

image

A unit test project named Ch04Ex02PrimeFactorsTests with a few suitable unit tests. A console application to use it, named Ch04Ex02PrimeFactorsApp .

To keep it simple, you can assume that the largest number entered will be 1,000.Use the debugging tools and write unit tests to ensure that your function works correctly with multiple inputs and returns the correct output.

Exercise 4.3 – Explore topics‌

Use the links on the following page to learn more about the topics covered in this chapter:

https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-4---writing- debugging-and-testing-functions

Summary‌

In this chapter, you learned:

image

How to write reusable functions with input parameters and return values, in both an imperative and functional style.

image

How to use the Visual Studio and Visual Studio Code debugging and diagnostic features like logging and unit tests to identify and fix any bugs in them.

image

How to throw and catch exceptions in functions and understand the call stack.

In the next chapter, you will learn how to build your own types using object-oriented programming techniques.

Building Your Own Types with Object-Oriented Programming‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about making your own types using object-oriented programming (OOP). You will learn about all the different categories of members that a type can have, including fields to store data and methods to perform actions. You will use OOP concepts such as aggregation and encapsulation. You will also learn about language features such as tuple syntax support, out variables, inferred tuple names, and default literals. Finally, you will learn about pattern matching and defining records to make the equality of variables and immutability easier to implement.This chapter will cover the following topics:

image

image

image

Talking about OOP Building class libraries Storing data in fields

image

Working with methods and tuples

image

image

Controlling access with properties and indexers Pattern matching with objects

image

Working with record types

Talking about OOP‌

An object in the real world is a thing, such as a car or a person, whereas an object in programming often represents something in the real world, such as a product or bank account, but it can also be something more abstract.In C#, we use the C# keywords class , record , and struct to define a type of object. You will learn about struct types in Chapter 6, Implementing Interfaces and Inheriting Classes. You can think of a type as being a blueprint or template for an object.The concepts of OOP are briefly described here:

image

Encapsulation is the combination of the data and actions that are related to an object. For example, a BankAccount type might have data, such as Balance and AccountName , as well as actions, such as Deposit and Withdraw . When encapsulating, you often want to control what can access those actions and the data, for example, restricting how the internal state of an object can be accessed or modified from the outside.

image

image

Composition is about what an object is made of. For example, a Car is composed of different parts, such as four Wheel objects, several Seat objects, and an Engine . Aggregation is about what can be combined with an object. For example, a Person is not part of a Car object, but they could sit in the driver's Seat and then become the car's Driver —two separate objects that are aggregated together to form a new component.

image

Inheritance is about reusing code by having a subclass derive from a base or superclass. All functionality in the base class is inherited by, and becomes available in, the derived class. For example, the base or super Exception class has some members that have the same implementation across all exceptions, and the sub or derived

SqlException class inherits those members and has extra members that are only relevant when a SQL database exception occurs, like a property for the database connection.

image

Abstraction is about capturing the core idea of an object and ignoring the details or specifics. C# has the abstract keyword that formalizes this concept but do not confuse the concept of abstraction with meaning the use of the abstract keyword because it is more than that. The concept of abstraction can also be achieved using interfaces. If a class is not explicitly abstract, then it can be described as being concrete. Bases or superclasses are often abstract; for example, the superclass Stream is abstract, and its subclasses, like FileStream and MemoryStream , are concrete. Only concrete classes can be used to create objects; abstract classes can only be used as the base for other classes because they miss some implementation. Abstraction is a tricky balance. If you make a class more abstract, more classes will be able to inherit from it, but at the same time, there will be less functionality to share. A real-world example of abstraction is the approach car manufacturers have taken to electric vehicles (EVs).

They create a common "platform" (basically just the battery and wheels) that is an abstraction of what all EVs need, and then add on top of that to build different vehicles like cars, trucks, vans, and so on. The platform on its own is not a complete product, like an abstract class.

image

Polymorphism is about allowing a derived class to override an inherited action to provide custom behavior.

There is a lot to cover in the next two chapters about OOP, and some parts of it are difficult to learn. At the end of Chapter 6, Implementing Interfaces and Inheriting Classes, I have written a summary of the categories of custom types and their capabilities with example code. This will help you review the most important facts and highlight the differences between choices, like an abstract class or an interface , and when to use them.

Building class libraries‌

Class library assemblies group types together into easily deployable units (DLL files). Apart from when you learned about unit testing, you have only created console apps to contain your code. To make the code that you write reusable across multiple projects, you should put it in class library assemblies, just like Microsoft does.

Creating a class library‌

The first task is to create a reusable .NET class library:

Use your preferred coding tool to create a new project, as defined in the following list:

image

image

image

Project template: Class Library / classlib Project file and folder: PacktLibraryNetStandard2 Solution file and folder: Chapter05

Open the PacktLibraryNetStandard2.csproj file, and note that, by default, class libraries created by the .NET 8 SDK target .NET 8 and, therefore, can only be referenced by other .NET 8-compatible assemblies, as highlighted in the following markup:

net8.0

enable

enable

1. Modify the framework to target .NET Standard 2.0, add an entry to explicitly use the C#

12 compiler, and statically import the System.Console class for all C# files, as highlighted in the following markup:

netstandard2.0

12

enable

enable

Although we can use the C# 12 compiler, some modern compiler features require a modern

.NET runtime. For example, we cannot use default implementations in an interface (introduced in C# 8) because it requires .NET Standard 2.1. We cannot use the required keyword (introduced in C# 11) because it requires an attribute introduced in .NET 7. But many useful modern compiler features, like raw literal strings, will be available to us.

Save and close the file.

Delete the file named Class1.cs .

Compile the project so that other projects can reference it later:

image

image

In Visual Studio 2022, navigate to Build | Build PacktLibraryNetStandard2. In Visual Studio Code, enter the following command: dotnet build .

Good Practice: To use all the latest C# language and .NET platform features, put types in a .NET 8 class library. To support legacy .NET platforms like .NET Core, .NET Framework, and Xamarin, put types that you might reuse in a .NET Standard 2.0 class library. By default, targeting .NET Standard 2.0 uses the C# 7 compiler, but this can be overridden so you get the benefits of the newer SDK and compiler even though you are limited to .NET Standard 2.0 APIs.

Understanding file-scoped namespaces‌

Traditionally, you define types like a class nested in a namespace, as shown in the following code:

namespace Packt.Shared

{

public class Person

{

}

}

If you define multiple types in the same code file, then they can be in different namespaces, since the types must be explicitly inside the curly braces for each namespace.If you use C# 10 or later, you can simplify your code by ending a namespace declaration with a semicolon and removing the curly braces, so the type definitions do not need to be indented, as shown in the following code:

// All types in this file will be defined in this file-scoped namespace. namespace Packt.Shared;

public class Person

{

}

This is known as a file-scoped namespace declaration. You can only have one file-scoped namespace per file. This feature is especially useful for book writers who have limited horizontal space.

Good Practice: Put each type that you create in its own code file, or at least put types in the same namespace in the same code file so that you can use file-scoped namespace declarations.

Defining a class in a namespace‌

The next task is to define a class that will represent a person:

In the PacktLibraryNetStandard2 project, add a new class file named Person.cs .

In Person.cs , delete any existing statements, set the namespace to Packt.Shared , and for the Person class, set the access modifier to public , as shown in the following code:

// All types in this file will be defined in this file-scoped namespace. namespace Packt.Shared;

public class Person

{

}

Good Practice: We're doing this because it is important to put your classes in a logically named namespace. A better namespace name would be domain-specific, for example, System.Numerics for types related to advanced numbers. In this case, the types we will create are Person , BankAccount , and WondersOfTheWorld , and they do not have a typical domain, so we will use the more generic Packt.Shared .

Understanding type access modifiers‌

Note that the C# keyword public is applied before class . This keyword is an access modifier, and it allows for any other code to access this class even outside this class library.If you do not explicitly apply the public keyword, then it will only be accessible within the assembly that defined it. This is because the implicit access modifier for a class is internal . We need this class to be accessible outside the assembly, so we must make sure it is public .If you have nested classes, meaning a class defined in another class, then the inner class could have the private access modifier, which would mean it is not accessible outside its parent class.Introduced with .NET 7, the file access modifier applied to a type means that type can only be used within its code file. This would only be useful if you define multiple classes in the same code file, which is rarely good practice but is used with source generators.

More Information: You can learn more about the file access modifier at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/file.

Good Practice: The two most common access modifiers for a class are public and internal (the default access modifier for a class if not specified). Always explicitly specify the access modifier for a class to make it clear what it is. Other access modifiers include private and file , but they are rarely used.

Understanding members‌

The Person type does not yet have any members encapsulated within it. We will create some over the following pages. Members can be fields, methods, or specialized versions of both. You'll find a description of them here:

image

Fields are used to store data. You can think of fields as variables that belong to a type. There are also three specialized categories of field, as shown in the following bullets:

image

Constant: The data never changes. The compiler literally copies the data into any code that reads it. For example, byte.MaxValue is always 255 .

image

Read-only: The data cannot change after the class is instantiated, but the data can be calculated or loaded from an external source at the time of instantiation. For example, DateTime.UnixEpoch is January 1, 1970.

image

Event: The data references one or more methods that you want to execute when something happens, such as clicking on a button or responding to a request from some other code. Events will be covered in Chapter 6, Implementing Interfaces and Inheriting Classes. For example, Console.CancelKeyPress happens when Ctrl+C or Ctrl+Break are pressed in a console app.

image

Methods are used to execute statements. You saw some examples when you learned about functions in Chapter 4, Writing, Debugging, and Testing Functions. There are also four specialized categories of methods:

image

Constructor: The statements execute when you use the new keyword to allocate memory to instantiate a class. For example, to instantiate Christmas Day 2023, you could write the following code: new DateTime(2023, 12, 25) .

image

Property: The statements execute when you get or set data. The data is commonly stored in a field but can be stored externally or calculated at runtime. Properties are the preferred way to encapsulate fields unless the memory address of the field needs to be exposed, for example, Console.ForegroundColor to set the current color of text in a console app.

image

Indexer: The statements execute when you get or set data using "array" syntax [] . For example, use name[o] to get the first character in the name variable, which is a string .

image

Operator: The statements execute when you apply an operator like + and / to operands of your type. For example, use a + b to add two variables together.

Importing a namespace to use a type‌

In this section, we will make an instance of the Person class.Before we can instantiate a class, we need to reference the assembly that contains it from another project. We will use the class in a console app:

Use your preferred coding tool to add a new Console App / console named PeopleApp to the Chapter05 solution. Make sure you add the new project to the existing Chapter05 solution because you are about to reference from the console app project to the existing class library project so both projects must be in the same solution.

If you use Visual Studio 2022:

Configure the startup project for the solution to the current selection.

In Solution Explorer, select the PeopleApp project, navigate to Project | Add Project Reference…, check the box to select the PacktLibraryNetStandard2 project, and then click OK.

In PeopleApp.csproj , add an entry to statically import the System.Console class, as shown in the following markup:

1. Navigate to Build | Build PeopleApp.

If you use Visual Studio Code:

Edit PeopleApp.csproj to add a project reference to PacktLibraryNetStandard2 , and add an entry to statically import the System.Console class, as highlighted in the following markup:

Exe

net8.0

enable

enable

1. In a terminal, compile the PeopleApp project and its dependency

PacktLibraryNetStandard2 project, as shown in the following command:

dotnet build

In the PeopleApp project, add a new class file named Program.Methods.cs .

In Program.Methods.cs , delete any existing statements, and define a partial Program class with a method to configure the console to enable special symbols, like the euro currency, and to control the current culture, as shown in the following code:

using System.Globalization; // To use CultureInfo. partial class Program

{

private static void ConfigureConsole( string culture = "en-US",

bool useComputerCulture = false, bool showCulture = true)

{

OutputEncoding = System.Text.Encoding.UTF8; if (!useComputerCulture)

{

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);

}

if (showCulture)

{

WriteLine($"Current culture: {CultureInfo.CurrentCulture.DisplayName}.");

}

}

}

By the end of this chapter, you will understand how the preceding method uses C# features like partial classes, optional parameters, and so on. If you would like to learn more about working with languages and cultures, as well as dates, times, and time zones, then there is a chapter about globalization and localization in my companion book, Apps and Services with .NET 8.

Instantiating a class‌

Now, we are ready to write statements to instantiate the Person class:

1. In the PeopleApp project, in the Program.cs file, delete the existing statements, then add statements to import the namespace for our Person class, and then call the ConfigureConsole method without any arguments so that it sets the current culture to US English, allowing all readers to see the same output, as shown in the following code:

using Packt.Shared; // To use Person.

ConfigureConsole(); // Sets current culture to US English.

// Alternatives:

// ConfigureConsole(useComputerCulture: true); // Use your culture.

// ConfigureConsole(culture: "fr-FR"); // Use French culture.

Although we could import the Packt.Shared namespace globally, it will be clearer to anyone reading this code where we import the types we use from if the import statement is at the top of the file, and the PeopleApp project will only have this one Program.cs file that needs the namespace imported.

1. In Program.cs , add statements to:

image

Create an instance of the Person type.

image

Output the instance using a textual description of itself.

The new keyword allocates memory for the object and initializes any internal data, as shown in the following code:

// Person bob = new Person(); // C# 1 or later.

// var bob = new Person(); // C# 3 or later. Person bob = new(); // C# 9 or later.

WriteLine(bob); // Implicit call to ToString().

// WriteLine(bob.ToString()); // Does the same thing.

1. Run the PeopleApp project and view the result, as shown in the following output:

Current culture: English (United States). Packt.Shared.Person

You might be wondering, "Why does the bob variable have a method named ToString ? The

Person class is empty!" Don't worry, we're about to find out!

Inheriting from System.Object‌

Although our Person class did not explicitly choose to inherit from a type, all types ultimately inherit directly or indirectly from a special type named System.Object . The implementation of the ToString method in the System.Object type outputs the full namespace and type name.Back in the original Person class, we could have explicitly told the compiler that Person inherits from the System.Object type, as shown in the following code:

public class Person : System.Object

When class B is inherited from class A, we say that A is the base or superclass, and B is the derived or subclass. In this case, System.Object is the base or superclass, and Person is the derived or subclass. You can also use the C# keyword object .Let's make our class explicitly inherit from object and then review what members all objects have:

1. Modify your Person class to explicitly inherit from object , as shown in the following code:

public class Person : object

1. Click inside the object keyword and press F12, or right-click on the object keyword and choose Go to Definition.

You will see the Microsoft-defined System.Object type and its members. This is something you don't need to understand the details of yet, but note that the class is in a .NET Standard 2.0 class library assembly and has a method named ToString , as shown in Figure 5.1:

image

Figure 5.1: System.Object class definition in .NET Standard 2.0

Good Practice: Assume other programmers know that if inheritance is not specified, the class will inherit from System.Object .

Avoiding a namespace conflict with a using alias‌

We need to learn a bit more about namespaces and their types. It is possible that there are two namespaces that contain the same type name, and importing both namespaces causes ambiguity. For example, JsonOptions exists in multiple Microsoft-defined namespaces. If you use the wrong one to configure JSON serialization, then it will be ignored and you'll be confused why!Let's review a made-up example:

// In the file, France.Paris.cs namespace France

{

public class Paris

{

}

}

// In the file, Texas.Paris.cs namespace France

{

public class Paris

{

}

}

// In the file, Program.cs using France;

using Texas;

Paris p = new();

If we build this project, then the compiler would complain with the following

error: Error CS0104: 'Paris' is an ambiguous reference between 'France.Paris' and 'Texas.Paris' We can define an alias for one of the namespaces to differentiate it, as shown in the following code:

using France; // To use Paris.

using Tx = Texas; // Tx becomes alias for the namespace, and it is not imported. Paris p1 = new(); // Creates an instance of France.Paris.

Tx.Paris p2 = new(); // Creates an instance of Texas.Paris.

Renaming a type with a using alias‌

Another situation where you might want to use an alias is if you would like to rename a type. For example, if you use the Environment class in the System namespace a lot, you could rename it with an alias to make it shorter, as shown in the following code:

using Env = System.Environment; WriteLine(Env.OSVersion); WriteLine(Env.MachineName); WriteLine(Env.CurrentDirectory);

Starting with C# 12, you can alias any type. This means you can rename existing types or give a type name to unnamed types like tuples, as you will see later in this chapter.

Storing data in fields‌

In this section, we will define a selection of fields in the class to store information about a person.

Defining fields‌

Let's say that we have decided that a person is composed of a name and a date and time of birth. We will encapsulate these two values inside a person, and the values will be visible outside it.

image

Inside the Person class, write statements to declare two public fields to store a person's name and the date of when they were born, as highlighted in the following code:

public class Person : object

{

#region Fields: Data or state for this person. public string? Name; // ? means it can be null. public DateTimeOffset Born;

#endregion

}

We have multiple choices for the data type of the Born field. .NET 6 introduced the DateOnly type. This would store only the date without a time value. DateTime stores the date and time of when the person was born, but it varies between local and UTC time. The best choice is DateTimeOffset , which stores the date, time, and hours offset from Universal Coordinated Time (UTC), which is related to the time zone. The choice depends on how much detail you need to store.

Types for fields‌

Since C# 8, the compiler has had the ability to warn you if a reference type like a string

could have a null value and, therefore, potentially throw a NullReferenceException . Since

.NET 6, the SDK enables those warnings by default. You can suffix the string type with a question mark, ? , to indicate that you accept this, and the warning disappears. You will learn more about nullability and how to handle it in Chapter 6, Implementing Interfaces and Inheriting Classes.You can use any type for a field, including arrays and collections such as lists and dictionaries. These would be used if you needed to store multiple values in one named field. In this example, a person only has one name and one date and time of birth.

Member access modifiers‌

Part of encapsulation is choosing how visible members are to other code.Note that, as we did with the class, we explicitly applied the public keyword to these fields. If we hadn't,

then they would be implicitly private to the class, which means they are accessible only inside the class.There are four member access modifier keywords, and two combinations of access modifier keywords that you can apply to a class member, like a field or method.

Member access modifiers apply to an individual member. They are similar to but separate from type access modifiers that apply to the whole type. The six possible combinations are shown in Table 5.1:

Member Access Modifier

Description

private The member is accessible inside the type only. This is the default.

internal The member is accessible inside the type and any type in the same assembly.

protected The member is accessible inside the type and any type that inherits from the type.

public The member is accessible everywhere.

internal protected The member is accessible inside the type, any type in the same assembly, and any type that inherits from the type. Equivalent to a fictional access modifier named internal_or_protected .

private protected The member is accessible inside the type and any type that inherits from the type and is in the same assembly. Equivalent to a fictional access modifier named internal_and_protected . This combination is only available with C# 7.2 or later.

Table 5.1: Six member access modifiers

Good Practice: Explicitly apply one of the access modifiers to all type members, even if you want to use the implicit access modifier for members, which is private .

Additionally, fields should usually be private or protected , and you should then create public properties to get or set the field values. This is because the property then controls access. You will do this later in the chapter.

Setting and outputting field values‌

Now we will use those fields in your code:

1. In Program.cs , after instantiating bob , add statements to set his name and date and time of birth, and then output those fields formatted nicely, as shown in the following code:

bob.Name = "Bob Smith"; bob.Born = new DateTimeOffset(

year: 1965, month: 12, day: 22,

hour: 16, minute: 28, second: 0,

offset: TimeSpan.FromHours(-5)); // US Eastern Standard Time.

WriteLine(format: "{0} was born on {1:D}.", // Long date. arg0: bob.Name, arg1: bob.Born);

The format code for arg1 is one of the standard date and time formats. D means a long date format and d would mean a short date format. You can learn more about standard date and time format codes at the following link: https://learn.microsoft.com/en- us/dotnet/standard/base-types/standard-date-and-time-format-strings.

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith was born on Wednesday, December 22, 1965.

If you change the call to ConfigureConsole to use your local computer culture or a specified culture like French in France ( "fr-FR" ), then your output will look different.

Setting field values using object initializer syntax‌

You can also initialize fields using a shorthand object initializer syntax with curly braces, which was introduced with C# 3. Let's see how:

1. Add statements underneath the existing code to create another new person named Alice. Note the different standard format code for the date and time of birth when writing her to the console, as shown in the following code:

Person alice = new()

{

Name = "Alice Jones",

Born = new(1998, 3, 7, 16, 28, 0,

// This is an optional offset from UTC time zone. TimeSpan.Zero)

};

WriteLine(format: "{0} was born on {1:d}.", // Short date. arg0: alice.Name, arg1: alice.Born);

We could have used string interpolation to format the output, but for long strings, it will wrap over multiple lines, which can be harder to read in a printed book. In the code examples in this book, remember that {0} is a placeholder for arg0 , and so on.

1. Run the PeopleApp project and view the result, as shown in the following output:

Alice Jones was born on 3/7/1998.

Good Practice: Use named parameters to pass arguments, so it is clearer what the values mean, especially for types like DateTimeOffset where there are a bunch of numbers one after the other.

Storing a value using an enum type‌

Sometimes, a value needs to be one of a limited set of options. For example, there are seven ancient wonders of the world, and a person may have one favorite. At other times, a value needs to be a combination of a limited set of options. For example, a person may have a bucket list of ancient world wonders they want to visit. We can store this data by defining an enum type.An enum type is a very efficient way of storing one or more choices because, internally, it uses integer values in combination with a lookup table of string descriptions. Let's see an example:

Add a new file to the PacktLibraryNetStandard2 project named

WondersOfTheAncientWorld.cs .

Modify the WondersOfTheAncientWorld.cs contents, as shown in the following code:

namespace Packt.Shared;

public enum WondersOfTheAncientWorld

{

GreatPyramidOfGiza, HangingGardensOfBabylon, StatueOfZeusAtOlympia, TempleOfArtemisAtEphesus, MausoleumAtHalicarnassus, ColossusOfRhodes, LighthouseOfAlexandria

}

1. In Person.cs , define a field to store a person's favorite ancient world wonder, as shown in the following code:

public WondersOfTheAncientWorld FavoriteAncientWonder;

1. In Program.cs , set Bob's favorite ancient wonder of the world and output it, as shown in the following code:

bob.FavoriteAncientWonder = WondersOfTheAncientWorld.StatueOfZeusAtOlympia; WriteLine(

format: "{0}'s favorite wonder is {1}. Its integer is {2}.", arg0: bob.Name,

arg1: bob.FavoriteAncientWonder,

arg2: (int)bob.FavoriteAncientWonder);

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith's favorite wonder is StatueOfZeusAtOlympia. Its integer is 2.

The enum value is internally stored as an int for efficiency. The int values are automatically assigned starting at 0 , so the third-world wonder in our enum has a value of 2 . You can assign int values that are not listed in the enum . They will output as the int value instead of a name, since a match will not be found.

Storing multiple values using an enum type‌

For the bucket list, we could create an array or collection of instances of the enum , and collections as fields will be shown later in this chapter, but there is a better approach for this scenario. We can combine multiple choices into a single value using enum flags. Let's see how:

1. Modify the enum by decorating it with the [Flags] attribute, and explicitly set a byte value for each wonder that represents different bit columns, as highlighted in the following code:

namespace Packt.Shared; [Flags]

public enum WondersOfTheAncientWorld : byte

{

None = 0b_0000_0000, // i.e. 0 GreatPyramidOfGiza = 0b_0000_0001, // i.e. 1 HangingGardensOfBabylon = 0b_0000_0010, // i.e. 2 StatueOfZeusAtOlympia = 0b_0000_0100, // i.e. 4 TempleOfArtemisAtEphesus = 0b_0000_1000, // i.e. 8 MausoleumAtHalicarnassus = 0b_0001_0000, // i.e. 16 ColossusOfRhodes = 0b_0010_0000, // i.e. 32 LighthouseOfAlexandria = 0b_0100_0000 // i.e. 64

}

We assign explicit values for each choice that will not overlap when looking at the bits stored in memory. We should also decorate the enum type with the System.Flags attribute so that when the value is returned, it can automatically match with multiple values as a comma- separated string instead of returning an int value.Normally, an enum type uses an int variable internally, but since we don't need values that big, we can reduce memory requirements by 75%, that is, 1 byte per value instead of 4 bytes, by telling it to use a byte variable. As another example, if you wanted to define an enum for days of the week, there will only ever be seven of them.If we want to indicate that our bucket list includes the Hanging Gardens of Babylon and the Mausoleum at Halicarnassus ancient world wonders, then we would want the 16 and 2 bits set to 1 . In other words, we would store the value 18 , as shown in Table 5.2:

64 32 16 8 4 2 1

0 0 1 0 0 1 0

Table 5.2: Storing 18 as bits in an enum

1. In Person.cs , leave the existing field to store a single favorite ancient world wonder and add the following statement to your list of fields to store multiple ancient world wonders, as shown in the following code:

public WondersOfTheAncientWorld BucketList;

1. In Program.cs , add statements to set the bucket list using the | operator (the bitwise logical OR ) to combine the enum values. We could also set the value using the number

18 cast into the enum type, as shown in the comment, but we shouldn't because that would make the code harder to understand, as shown in the following code:

bob.BucketList = WondersOfTheAncientWorld.HangingGardensOfBabylon

| WondersOfTheAncientWorld.MausoleumAtHalicarnassus;

// bob.BucketList = (WondersOfTheAncientWorld)18; WriteLine($"{bob.Name}'s bucket list is {bob.BucketList}.");

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith's bucket list is HangingGardensOfBabylon, MausoleumAtHalicarnassus.

Good Practice: Use the enum values to store combinations of discrete options. Derive an

enum type from byte if there are up to eight options, from ushort if there are up to

16 options, from uint if there are up to 32 options, and from ulong if there are up to

64 options.

Now that we have decorated the enum with the [Flags] attribute, combinations of values can be stored in a single variable or field. Now a programmer could store a combination of values in the FavoriteAncientWonder too when it should only store one value. To enforce this, we should convert the field into a property that allows us to take control over how other programmers can get and set the value. You will see how to do this later in this chapter.

Storing multiple values using collections‌

Let's now add a field to store a person's children. This is an example of aggregation because children are instances of a class that is related to the current person, but they are not part of the person itself. We will use a generic List collection type that can store an ordered collection of any type. You will learn more about collections in Chapter 8, Working with Common .NET Types. For now, just follow along:

image

In Person.cs , declare a new field to store multiple Person instances that represent the children of this person, as shown in the following code:

public List Children = new();

List is read aloud as "list of Person ," for example, "the type of the property named Children is a list of Person instances."We must ensure the collection is initialized to a new instance before we can add items to it; otherwise, the field will be null and throw runtime exceptions when we try to use any of its members, like Add .

Understanding generic collections‌

The angle brackets in the List type is a feature of C# called generics that was introduced in 2005 with C# 2. It's a fancy term for making a collection strongly typed, that is, the compiler knows specifically what type of object can be stored in the collection. Generics improve the performance and correctness of your code.Strongly typed has a different meaning than statically typed. The old System.Collection types are

statically typed to contain weakly typed System.Object items. The newer System.Collection.Generic types are statically typed to contain strongly typed instances.Ironically, the term generics means we can use a more specific static type!

1. In Program.cs , add statements to add three children for Bob , and then show how many children he has and what their names are, as shown in the following code:

// Works with all versions of C#. Person alfred = new Person(); alfred.Name = "Alfred"; bob.Children.Add(alfred);

// Works with C# 3 and later. bob.Children.Add(new Person { Name = "Bella" });

// Works with C# 9 and later. bob.Children.Add(new() { Name = "Zoe" });

WriteLine($"{bob.Name} has {bob.Children.Count} children:");

for (int childIndex = 0; childIndex < bob.Children.Count; childIndex++) { WriteLine($"> {bob.Children[childIndex].Name}");

}

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith has 3 children:

Alfred

Bella

Zoe

We could also use a foreach statement to enumerate over the collection. As an optional challenge, change the for statement to output the same information using foreach .

Making a field static‌

The fields that we have created so far have all been instance members, meaning that a different value of each field exists for each instance of the class that is created. The alice and bob variables have different Name values.Sometimes, you want to define a field that only has one value that is shared across all instances.These are called static members because fields are not the only members that can be static. Let's see what can be achieved using static fields using a bank account as an example. Each instance of BankAccount will have its own AccountName and Balance values, but all instances will share a single InterestRate value.Let's do it:

In the PacktLibraryNetStandard2 project, add a new class file named BankAccount.cs .

Modify the class to give it three fields – two instance fields and one static field – as shown in the following code:

namespace Packt.Shared; public class BankAccount

{

public string? AccountName; // Instance member. It could be null. public decimal Balance; // Instance member. Defaults to zero.

public static decimal InterestRate; // Shared member. Defaults to zero.

}

1. In Program.cs , add statements to set the shared interest rate, and then create two instances of the BankAccount type, as shown in the following code:

BankAccount.InterestRate = 0.012M; // Store a shared value in static field. BankAccount jonesAccount = new();

jonesAccount.AccountName = "Mrs. Jones"; jonesAccount.Balance = 2400;

WriteLine(format: "{0} earned {1:C} interest.", arg0: jonesAccount.AccountName,

arg1: jonesAccount.Balance * BankAccount.InterestRate); BankAccount gerrierAccount = new(); gerrierAccount.AccountName = "Ms. Gerrier"; gerrierAccount.Balance = 98;

WriteLine(format: "{0} earned {1:C} interest.", arg0: gerrierAccount.AccountName,

arg1: gerrierAccount.Balance * BankAccount.InterestRate);

1. Run the PeopleApp project and view the additional output:

Mrs. Jones earned $28.80 interest. Ms. Gerrier earned $1.18 interest.

Remember that C is a format code that tells .NET to use the current culture's currency format for the decimal numbers.

Fields are not the only members that can be static. Constructors, methods, properties, and other members can also be static.

Making a field constant‌

If the value of a field will never ever change, you can use the const keyword and assign a literal value at compile time. Any statement that changes the value will cause a compile- time error. Let's see a simple example:

1. In Person.cs , add a string constant for the species of a person, as shown in the following code:

// Constant fields: Values that are fixed at compilation. public const string Species = "Homo Sapiens";

1. To get the value of a constant field, you must write the name of the class, not the name of an instance of the class. In Program.cs , add a statement to write Bob's name and species to the console, as shown in the following code:

// Constant fields are accessible via the type. WriteLine($"{bob.Name} is a {Person.Species}.");

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith is a Homo Sapiens.

Examples of const fields in Microsoft types include System.Int32.MaxValue and

System.Math.PI because neither value will ever change, as you can see in Figure 5.2:

image

Figure 5.2: Examples of constants in System.Math class

Good Practice: Constants are not always the best choice for two important reasons: the value must be known at compile time, and it must be expressible as a literal string , Boolean , or number value. Every reference to the const field is replaced with the literal value at compile time, which will, therefore, not be reflected if the value changes in a future version and you do not recompile any assemblies that reference it to get the new value.

Making a field read-only‌

Often, a better choice for fields that should not change is to mark them as read-only:

1. In Person.cs , add a statement to declare an instance read-only field to store a person's home planet, as shown in the following code:

// Read-only fields: Values that can be set at runtime. public readonly string HomePlanet = "Earth";

1. In Program.cs , add a statement to write Bob's name and home planet to the console, as shown in the following code:

// Read-only fields are accessible via the variable. WriteLine($"{bob.Name} was born on {bob.HomePlanet}.");

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith was born on Earth.

Good Practice: Use read-only fields over constant fields for two important reasons: the value can be calculated or loaded at runtime and can be expressed using any executable statement. So, a read-only field can be set using a constructor or a field assignment. Every reference to the read-only field is a live reference, so any future changes will be correctly reflected by the calling code.

You can also declare static readonly fields whose values will be shared across all instances of the type.

Requiring fields to be set during instantiation‌

C# 11 introduced the required modifier. If you use it on a field or property, the compiler will ensure that you set the field or property to a value when you instantiate it. It

requires targeting .NET 7 or later, so we need to create a new class library first:

In the Chapter05 solution, add a new class library project named PacktLibraryModern that targets .NET 8. (The oldest supported version for the required modifier is .NET 7.)

In the PacktLibraryModern project, rename Class1.cs to Book.cs .

Modify the code file contents to give the class four fields, with two set as required , as shown in the following code:

namespace Packt.Shared; public class Book

{

// Needs .NET 7 or later as well as C# 11 or later. public required string? Isbn;

public required string? Title;

// Works with any version of .NET. public string? Author;

public int PageCount;

}

Note that all three string properties are nullable. Setting a property or field to be required does not mean that it cannot be null . It just means that it must be explicitly set to null .

1. In the PeopleApp console app project, add a reference to the PacktLibraryModern class library project:

image

If you use Visual Studio 2022, then in Solution Explorer, select the PeopleApp project, navigate to Project | Add Project Reference…, check the box to select the PacktLibraryModern project, and then click OK.

image

If you use Visual Studio Code, then edit PeopleApp.csproj to add a project reference to PacktLibraryModern , as highlighted in the following markup:

Build the PeopleApp project to compile its referenced dependencies and copy the class library .dll to the local bin folder.

In the PeopleApp project, in Program.cs , attempt to instantiate a Book without setting the Isbn and Title fields, as shown in the following code:

Book book = new();

1. Note that you will see a compiler error, as shown in the following output:

C:\cs12dotnet8\Chapter05\PeopleApp\Program.cs(137,13): error CS9035: Required member 'Book.Isbn' C:\cs12dotnet8\Chapter05\PeopleApp\Program.cs(137,13): error CS9035: Required member 'Book.Title

0 Warning(s)

2 Error(s)

1. In Program.cs , modify the statement to set the two required properties using object initialization syntax, as highlighted in the following code:

Book book = new()

{

Isbn = "978-1803237800",

Title = "C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals"

};

Note that the statement now compiles without errors.

In Program.cs , add a statement to output information about the book, as shown in the following code:

WriteLine("{0}: {1} written by {2} has {3:N0} pages.", book.Isbn, book.Title, book.Author, book.PageCount);

Before we run the project and view the output, let's talk about an alternative way that we could initialize fields (or properties) for a type.

Initializing fields with constructors‌

Fields often need to be initialized at runtime. You can do this in a constructor that will be called when you make an instance of a class using the new keyword. Constructors execute before any fields are set by the code that uses the type:

1. In Person.cs , add statements after the existing read-only HomePlanet field to define a second read-only field, and then set the Name and Instantiated fields in a constructor, as highlighted in the following code:

// Read-only fields: Values that can be set at runtime. public readonly string HomePlanet = "Earth";

public readonly DateTime Instantiated; #endregion

#region Constructors: Called when using new to instantiate a type. public Person()

{

// Constructors can set default values for fields

// including any read-only fields like Instantiated. Name = "Unknown";

Instantiated = DateTime.Now;

}

#endregion

1. In Program.cs , add statements to instantiate a new person and then output its initial field values, as shown in the following code:

Person blankPerson = new(); WriteLine(format:

"{0} of {1} was created at {2:hh:mm:ss} on a {2:dddd}.", arg0: blankPerson.Name,

arg1: blankPerson.HomePlanet, arg2: blankPerson.Instantiated);

1. Run the PeopleApp project and view the result from both the code about the book as well as the blank person, as shown in the following output:

978-1803237800: Title = "C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals writt Unknown of Earth was created at 11:58:12 on a Sunday

Defining multiple constructors‌

You can have multiple constructors in a type. This is especially useful to encourage developers to set initial values for fields:

1. In Person.cs , add statements to define a second constructor that allows a developer to set initial values for the person's name and home planet, as shown in the following code:

public Person(string initialName, string homePlanet)

{

Name = initialName; HomePlanet = homePlanet;

Instantiated = DateTime.Now;

}

1. In Program.cs , add statements to create another person using the constructor with two parameters, as shown in the following code:

Person gunny = new(initialName: "Gunny", homePlanet: "Mars"); WriteLine(format:

"{0} of {1} was created at {2:hh:mm:ss} on a {2:dddd}.", arg0: gunny.Name,

arg1: gunny.HomePlanet, arg2: gunny.Instantiated);

1. Run the PeopleApp project and view the result:

Gunny of Mars was created at 11:59:25 on a Sunday

Setting required fields with a constructor‌

Now let's return to the Book class example with its required fields:

1. In the PacktLibraryModern project, in Book.cs , add statements to define a pair of constructors, one that supports object initializer syntax and one to set the two required properties, as highlighted in the following code:

public class Book

{

// Constructor for use with object initializer syntax. public Book() { } // For use with object initializer syntax.

// Constructor with parameters to set required fields. public Book(string? isbn, string? title)

{

Isbn = isbn;

Title = title;

}

1. In Program.cs , comment out the statement that instantiates a book using object initializer syntax, add a statement to instantiate a book using the constructor, and then set the non- required properties for the book, as highlighted in the following code:

/*

// Instantiate a book using object initializer syntax. Book book = new()

{

Isbn = "978-1803237800",

Title = "C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals"

};

*/

Book book = new(isbn: "978-1803237800",

title: "C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals")

{

Author = "Mark J. Price", PageCount = 821

};

Note that you will see a compiler error as before, because the compiler cannot automatically tell that calling the constructor will have set the two required properties.

In the PacktLibraryModern project, in Book.cs , import the namespace to perform code analysis, and then decorate the constructor with the attribute to tell the compiler that it sets all the required properties and fields, as highlighted in the following code:

using System.Diagnostics.CodeAnalysis; // To use [SetsRequiredMembers]. namespace Packt.Shared;

public class Book

{

public Book() { } // For use with initialization syntax. [SetsRequiredMembers]

public Book(string isbn, string title)

In Program.cs , note the statement that calls the constructor now compiles without errors.

Optionally, run the PeopleApp project to confirm it behaves as expected, as shown in the following output:

978-1803237800: C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals written by Mar

More Information: You can learn more about required fields and how to set them using a constructor at the following link: https://learn.microsoft.com/en- us/dotnet/csharp/language-reference/keywords/required.

Constructors are a special category of method. Let's look at methods in more detail.

Working with methods and tuples‌

Methods are members of a type that execute a block of statements. They are functions that belong to a type.

Returning values from methods‌

Methods can return a single value or nothing:

image

A method that performs some actions but does not return a value indicates this with the

void type before the name of the method.

image

A method that performs some actions and returns a value indicates this with the type of the return value before the name of the method.

For example, in the next task, you will create two methods:

image

WriteToConsole : This will perform an action (writing some text to the console), but it will return nothing from the method, indicated by the void keyword.

image

GetOrigin : This will return a text value, indicated by the string keyword.

Let's write the code:

1. In Person.cs , add statements to define the two methods that I described earlier, as shown in the following code:

#region Methods: Actions the type can perform. public void WriteToConsole()

{

WriteLine($"{Name} was born on a {Born:dddd}.");

}

public string GetOrigin()

{

return $"{Name} was born on {HomePlanet}.";

}

#endregion

1. In Program.cs , add statements to call the two methods, as shown in the following code:

bob.WriteToConsole(); WriteLine(bob.GetOrigin());

1. Run the PeopleApp project and view the result, as shown in the following output:

Bob Smith was born on a Wednesday. Bob Smith was born on Earth.

Defining and passing parameters to methods‌

Methods can have parameters passed to them to change their behavior. Parameters are defined a bit like variable declarations but inside the parentheses of the method declaration, as you saw earlier in this chapter with constructors. Let's see more examples:

1. In Person.cs , add statements to define two methods, the first without parameters and the second with one parameter, as shown in the following code:

public string SayHello()

{

return $"{Name} says 'Hello!'";

}

public string SayHelloTo(string name)

{

return $"{Name} says 'Hello, {name}!'";

}

1. In Program.cs , add statements to call the two methods, and write the return value to the console, as shown in the following code:

WriteLine(bob.SayHello()); WriteLine(bob.SayHelloTo("Emily"));

1. Run the PeopleApp project and view the result:

Bob Smith says 'Hello!'

Bob Smith says 'Hello, Emily!'

When typing a statement that calls a method, IntelliSense shows a tooltip with the name, the type of any parameters, and the return type of the method.

Overloading methods‌

Instead of having two different method names, we could give both methods the same name. This is allowed because the methods each have a different signature.A method signature is a list of parameter types that can be passed when calling the method. Overloaded methods must differ in their list of parameters types. Two overloaded methods cannot have the same list of parameters types and differ only in their return types. Let's code an example:

In Person.cs , change the name of the SayHelloTo method to SayHello .

In Program.cs , change the method call to use the SayHello method, and note that the quick info for the method tells you that it has an additional overload, 1 of 2, as well as 2 of 2, in Visual Studio 2022, although other code editors may be different, as shown in Figure 5.3:

image

Figure 5.3: An IntelliSense tooltip for an overloaded method

Good Practice: Use overloaded methods to simplify your class by making it appear to have fewer methods.

Passing optional parameters‌

Another way to simplify methods is to make parameters optional. You make a parameter optional by assigning a default value inside the method parameter list. Optional parameters must always come last in the list of parameters.We will now create a method with three optional parameters:

1. In Person.cs , add statements to define the method, as shown in the following code:

public string OptionalParameters(string command = "Run!", double number = 0.0, bool active = true)

{

return string.Format(

format: "command is {0}, number is {1}, active is {2}", arg0: command,

arg1: number, arg2: active);

}

1. In Program.cs , add a statement to call the method and write its return value to the console, as shown in the following code:

WriteLine(bob.OptionalParameters());

Watch IntelliSense appear as you type the code. You will see a tooltip showing the three optional parameters with their default values.

Run the PeopleApp project and view the result, as shown in the following output:

command is Run!, number is 0, active is True

1. In Program.cs , add a statement to pass a string value for the command parameter and a

double value for the number parameter, as shown in the following code:

WriteLine(bob.OptionalParameters("Jump!", 98.5));

1. Run the PeopleApp project and see the result, as shown in the following output:

command is Jump!, number is 98.5, active is True

The default values for the command and number parameters have been replaced, but the default for active is still true .

Naming parameter values when calling methods‌

Optional parameters are often combined with naming parameters when you call the method, because naming a parameter allows the values to be passed in a different order than how they were declared:

1. In Program.cs , add a statement to pass a string value for the command parameter and a double value for the number parameter, but using named parameters, so that the order they are passed through can be swapped around, as shown in the following code:

WriteLine(bob.OptionalParameters(number: 52.7, command: "Hide!"));

1. Run the PeopleApp project and view the result, as shown in the following output:

command is Hide!, number is 52.7, active is True

You can even use named parameters to skip over optional parameters.

1. In Program.cs , add a statement to pass a string value for the command parameter using positional order, skip the number parameter, and use the named active parameter, as shown in the following code:

WriteLine(bob.OptionalParameters("Poke!", active: false));

1. Run the PeopleApp project and view the result, as shown in the following output:

command is Poke!, number is 0, active is False

Good Practice: Although you can mix named and positional parameter values, most developers prefer to read code that uses one or the other within the same method call.

Mixing optional and required parameters‌

At the moment, all the parameters in the OptionalParameters method are optional. What if one of them is required?

1. In Person.cs , add a fourth parameter without a default value to the OptionalParameters

method, as highlighted in the following code:

public string OptionalParameters(string command = "Run!", double number = 0.0, bool active = true, int count)

Build the project and note the compiler error:

Error CS1737 Optional parameters must appear after all required parameters .

In the OptionalParameters method, move the count parameter before the optional parameters, as shown in the following code:

public string OptionalParameters(int count, string command = "Run!",

double number = 0.0, bool active = true)

1. In Program.cs , modify all the calls to the OptionalParameters method to pass an int

value as the first argument, for example, as shown in the following code:

WriteLine(bob.OptionalParameters(3)); WriteLine(bob.OptionalParameters(3, "Jump!", 98.5)); WriteLine(bob.OptionalParameters(3, number: 52.7, command: "Hide!")); WriteLine(bob.OptionalParameters(3, "Poke!", active: false));

Remember that if you name the arguments, then you can change their positions, for example: bob.OptionalParameters(number: 52.7, command: "Hide!", count: 3) .

1. As you call the OptionalParameters method, note the tooltip that shows the one required, three optional parameters, and their default values in Visual Studio 2022, as shown in Figure 5.4:

image

Figure 5.4: IntelliSense showing the required and optional parameters as you type code

Controlling how parameters are passed‌

When a parameter is passed into a method, it can be passed in one of several ways:

image

By value (this is the default): Think of these as being in-only. Although the value can be changed, this only affects the parameter in the method.

image

As an out parameter: Think of these as being out-only. out parameters cannot have a default value assigned in their declaration and cannot be left uninitialized. They must be set inside the method; otherwise, the compiler will give an error.

image

By reference as a ref parameter: Think of these as being in-and-out. Like out parameters, ref parameters also cannot have default values, but since they can already be set outside the method, they do not need to be set inside the method.

image

As an in parameter: Think of these as being a reference parameter that is read-only. in parameters cannot have their value changed and the compiler will show an error if you try.

Let's see some examples of passing parameters in and out of a method:

1. In Person.cs , add statements to define a method with three parameters, one in

parameter, one ref parameter, and one out parameter, as shown in the following method:

public void PassingParameters(int w, in int x, ref int y, out int z)

{

// out parameters cannot have a default and they

// must be initialized inside the method. z = 100;

// Increment each parameter except the read-only x. w++;

// x++; // Gives a compiler error! y++;

z++;

WriteLine($"In the method: w={w}, x={x}, y={y}, z={z}");

}

1. In Program.cs , add statements to declare some int variables and pass them into the method, as shown in the following code:

int a = 10; int b = 20; int c = 30; int d = 40;

WriteLine($"Before: a={a}, b={b}, c={c}, d={d}"); bob.PassingParameters(a, b, ref c, out d); WriteLine($"After: a={a}, b={b}, c={c}, d={d}");

1. Run the PeopleApp project and view the result, as shown in the following output:

Before: a=10, b=20, c=30, d=40

In the method: w=11, x=20, y=31, z=101 After: a=10, b=20, c=31, d=101

image

When passing a variable as a parameter by default, its current value gets passed, not the variable itself. Therefore, w has a copy of the value of the a variable. The a variable retains its original value of 10 even after w is incremented to 11.

image

When passing a variable as an in parameter, a reference to the variable gets passed into the method. Therefore, x is a reference to b . If the b variable gets incremented by some other process while the method is executing, then the x parameter would show that.

image

When passing a variable as a ref parameter, a reference to the variable gets passed into the method. Therefore, y is a reference to c . The c variable gets incremented when the y parameter gets incremented.

image

When passing a variable as an out parameter, a reference to the variable gets passed into the method. Therefore, z is a reference to d . The value of the d variable gets replaced by whatever code executes inside the method.

We could simplify the code in the Main method by not assigning the value 40 to the d variable, since it will always be replaced anyway. In C# 7 and later, we can simplify code that uses the out parameter.

1. In Program.cs , add statements to declare some more variables, including an out

parameter named f declared inline, as shown in the following code:

int e = 50; int f = 60; int g = 70;

WriteLine($"Before: e={e}, f={f}, g={g}, h doesn't exist yet!");

// Simplified C# 7 or later syntax for the out parameter. bob.PassingParameters(e, f, ref g, out int h); WriteLine($"After: e={e}, f={f}, g={g}, h={h}");

1. Run the PeopleApp project and view the result, as shown in the following output:

Before: e=50, f=60, g=70, h doesn't exist yet! In the method: w=51, x=60, y=71, z=101

After: e=50, f=60, g=71, h=101

Understanding ref returns‌

In C# 7 or later, the ref keyword is not just for passing parameters into a method; it can also be applied to the return value. This allows an external variable to reference an internal variable and modify its value after the method call. This might be useful in advanced scenarios, for example, passing placeholders into big data structures, but it's beyond the scope of this book. If you are interested in learning more, then you can read the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/keywords/ref#reference-return-values.Now let's return to looking at more advanced scenarios of methods that return values.

Combining multiple returned values using tuples‌

Each method can only return a single value that has a single type. That type could be a simple type, such as string in the previous example; a complex type, such as Person ; or a collection type, such as List .Imagine that we want to define a method named GetTheData that needs to return both a string value and an int value. We could define a new class named TextAndNumber with a string field and an int field, and return an instance of that complex type, as shown in the following code:

public class TextAndNumber

{

public string Text; public int Number;

}

public class LifeTheUniverseAndEverything

{

public TextAndNumber GetTheData()

{

return new TextAndNumber

{

Text = "What's the meaning of life?", Number = 42

};

}

}

But defining a class just to combine two values together is unnecessary because, in modern versions of C#, we can use tuples. Tuples are an efficient way to combine two or more values into a single unit. I pronounce them as tuh-ples but I have heard other developers pronounce them as too-ples. To-may-toe, to-mah-toe, po-tay-toe, po-tah-toe, I guess.Tuples have been a part of some languages, such as F#, since their first version, but .NET only added support for them with .NET 4 in 2010, using the System.Tuple type.It was only with C# 7 in 2017 that C# added language syntax support for tuples using the parentheses characters () , and at the same time, .NET added a new System.ValueTuple type that is more efficient in some common scenarios than the old .NET 4 System.Tuple type. The C# tuple syntax uses the more efficient one.Let's explore tuples:

1. In Person.cs , add statements to define a method that returns a tuple combining a string

and int , as shown in the following code:

// Method that returns a tuple: (string, int). public (string, int) GetFruit()

{

return ("Apples", 5);

}

1. In Program.cs , add statements to call the GetFruit method, and then output the tuple's fields, which are automatically named Item1 and Item2 , as shown in the following code:

(string, int) fruit = bob.GetFruit(); WriteLine($"{fruit.Item1}, {fruit.Item2} there are.");

1. Run the PeopleApp project and view the result, as shown in the following output:

Apples, 5 there are.

Naming the fields of a tuple‌

To access the fields of a tuple, the default names are Item1 , Item2 , and so on.You can explicitly specify the field names:

1. In Person.cs , add statements to define a method that returns a tuple with named fields, as shown in the following code:

// Method that returns a tuple with named fields. public (string Name, int Number) GetNamedFruit()

{

return (Name: "Apples", Number: 5);

}

1. In Program.cs , add statements to call the method and output the tuple's named fields, as shown in the following code:

var fruitNamed = bob.GetNamedFruit();

WriteLine($"There are {fruitNamed.Number} {fruitNamed.Name}.");

We use var to shorten the following full syntax:

(string Name, int Number) fruitNamed = bob.GetNamedFruit();

1. Run the PeopleApp project and view the result, as shown in the following output:

There are 5 Apples.

If you construct a tuple from another object, you can use a feature introduced in C# 7.1 called tuple name inference.

1. In Program.cs , create two tuples, each made of a string and int value, as shown in the following code:

var thing1 = ("Neville", 4);

WriteLine($"{thing1.Item1} has {thing1.Item2} children."); var thing2 = (bob.Name, bob.Children.Count); WriteLine($"{thing2.Name} has {thing2.Count} children.");

In C# 7, both things would use the Item1 and Item2 naming schemes. In C# 7.1 and later,

thing2 can infer the names Name and Count .

Aliasing tuples‌

The ability to alias a tuple was introduced in C# 12 so that you can name the type and use that as the type name when declaring variables and parameters, for example, as shown in the following code:

using UnnamedParameters = (string, int); // Aliasing a tuple type.

// Aliasing a tuple type with parameter names. using Fruit = (string Name, int Number);

When aliasing tuples use the title case naming convention for its parameters, for example,

Name , Number , and BirthDate .Let's see an example:

1. In Program.cs , at the top of the file, define a named tuple type, as shown in the following code:

using Fruit = (string Name, int Number); // Aliasing a tuple type.

1. In Program.cs , copy and paste the statement that calls the GetNamedFruit method and change var to Fruit , as shown in the following code:

// Without an aliased tuple type.

//var fruitNamed = bob.GetNamedFruit();

// With an aliased tuple type.

Fruit fruitNamed = bob.GetNamedFruit();

1. Run the PeopleApp project and note the result is the same.

Deconstructing tuples‌

You can also deconstruct tuples into separate variables. The deconstructing declaration has the same syntax as named field tuples but without a named variable for the tuple, as shown in the following code:

// Store return value in a tuple variable with two named fields. (string name, int number) namedFields = bob.GetNamedFruit();

// You can then access the named fields. WriteLine($"{namedFields.name}, {namedFields.number}");

// Deconstruct the return value into two separate variables. (string name, int number) = bob.GetNamedFruit();

// You can then access the separate variables. WriteLine($"{name}, {number}");

Deconstruction has the effect of splitting the tuple into its parts and assigning those parts to new variables. Let's see it in action:

1. In Program.cs , add statements to deconstruct the tuple returned from the GetFruit

method, as shown in the following code:

(string fruitName, int fruitNumber) = bob.GetFruit(); WriteLine($"Deconstructed tuple: {fruitName}, {fruitNumber}");

1. Run the PeopleApp project and view the result, as shown in the following output:

Deconstructed tuple: Apples, 5

Deconstructing other types using tuples‌

Tuples are not the only type that can be deconstructed. Any type can have special methods, named Deconstruct , that break down an object into parts. You can have as many Deconstruct methods as you like as long as they have different signatures. Let's implement some for the Person class:

1. In Person.cs , add two Deconstruct methods with out parameters defined for the parts we want to deconstruct into, as shown in the following code:

// Deconstructors: Break down this object into parts. public void Deconstruct(out string? name,

out DateTimeOffset dob)

{

name = Name; dob = Born;

}

public void Deconstruct(out string? name, out DateTimeOffset dob,

out WondersOfTheAncientWorld fav)

{

name = Name; dob = Born;

fav = FavoriteAncientWonder;

}

1. In Program.cs , add statements to deconstruct bob , as shown in the following code:

var (name1, dob1) = bob; // Implicitly calls the Deconstruct method. WriteLine($"Deconstructed person: {name1}, {dob1}");

var (name2, dob2, fav2) = bob;

WriteLine($"Deconstructed person: {name2}, {dob2}, {fav2}");

You do not explicitly call the Deconstruct method. It is called implicitly when you assign an object to a tuple variable.

1. Run the PeopleApp project and view the result, as shown in the following output:

Deconstructed person: Bob Smith, 12/22/1965 4:28:00 PM -05:00

Deconstructed person: Bob Smith, 12/22/1965 4:28:00 PM -05:00, StatueOfZeusAtOlympia

Implementing functionality using local functions‌

A language feature introduced in C# 7 is the ability to define a local function.Local functions are the method equivalent of local variables. In other words, they are methods that are only accessible from within the containing method in which they have been defined. In other languages, they are sometimes called nested or inner functions.Local functions can be defined anywhere inside a method: the top, the bottom, or even somewhere in the middle!We will use a local function to implement a factorial calculation:

1. In Person.cs , add statements to define a Factorial function that uses a local function inside itself to calculate the result, as shown in the following code:

// Method with a local function.

public static int Factorial(int number)

{

if (number < 0) { throw new ArgumentException( $"{nameof(number)} cannot be less than zero."); } return localFactorial(number); int localFactorial(int localNumber) // Local function. { if (localNumber == 0) return 1; return localNumber * localFactorial(localNumber - 1); } } 1. In Program.cs , add statements to call the Factorial function, and write the return value to the console, with exception handling, as shown in the following code: // Change to -1 to make the exception handling code execute. int number = 5; try { WriteLine($"{number}! is {Person.Factorial(number)}"); } catch (Exception ex) { WriteLine($"{ex.GetType()} says: {ex.Message} number was {number}."); } 1. Run the PeopleApp project and view the result, as shown in the following output: 5! is 120 Change the number to -1 so that we can check the exception handling. Run the PeopleApp project and view the result, as shown in the following output: System.ArgumentException says: number cannot be less than zero. number was -1. Splitting classes using partial‌ When working on large projects with multiple team members, or when working with especially large and complex class implementations, it is useful to be able to split the definition of a class across multiple files. You do this using the partial keyword.Imagine that we want to add statements to the Person class that are automatically generated by a tool, like an object-relational mapper, that reads schema information from a database. If the class is defined as partial , then we can split the class into an autogenerated code file and a manually edited code file.Let's write some code that simulates this example: 1. In Person.cs , add the partial keyword, as highlighted in the following code: public partial class Person In the PacktLibraryNetStandard2 project/folder, add a new class file named PersonAutoGen.cs . Add statements to the new file, as shown in the following code: namespace Packt.Shared; // This file simulates an auto-generated class. public partial class Person { } 1. Build the PacktLibraryNetStandard2 project. If you see error: CS0260 Missing partial modifier on declaration of type 'Person'; another partial declaration , then make sure you have applied the partial keyword to both Person classes. The rest of the code we write for this chapter will be written in the PersonAutoGen.cs file.Now that you’ve seen lots of examples of fields and methods, we will look at some specialized types of methods that can be used to access fields to provide control and improve the developer’s experience. Controlling access with properties and indexers‌ Earlier, you created a method named GetOrigin that returned a string containing the name and origin of the person. Languages such as Java do this a lot. C# has a better way, and it is called properties.A property is simply a method (or a pair of methods) that acts and looks like a field when you want to get or set a value, but it acts like a method, thereby simplifying the syntax and enabling functionality, like validation and calculation, when you set and get a value. A fundamental difference between a field and a property is that a field provides a memory address to data. You could pass that memory address to an external component, like a Windows API C-style function call, and it could then modify the data. A property does not provide a memory address to its data, which provides more control. All you can do is ask the property to get or set the data. The property then executes statements and can decide how to respond, including refusing the request! Defining read-only properties‌ A readonly property only has a get implementation: image In PersonAutoGen.cs , in the Person class, add statements to define three properties: The first property will perform the same role as the GetOrigin method, using the property syntax that works with all versions of C#. image The second property will return a greeting message, using the lambda expression body => syntax from C# 6 and later.

image

The third property will calculate the person's age.

Here's the code:

#region Properties: Methods to get and/or set data or state.

// A readonly property defined using C# 1 to 5 syntax. public string Origin

{

get

{

return string.Format("{0} was born on {1}.", arg0: Name, arg1: HomePlanet);

}

}

// Two readonly properties defined using C# 6 or later

// lambda expression body syntax.

public string Greeting => $"{Name} says 'Hello!'"; public int Age => DateTime.Today.Year - Born.Year; #endregion

Good Practice: This isn't the best way to calculate someone's age, but we aren't learning how to calculate an age from a date and time of birth. If you need to do that properly, then read the discussion at the following link: https://stackoverflow.com/questions/9/how-do-i-calculate-someones-age-in-c.

1. In Program.cs , add the statements to get the properties, as shown in the following code:

Person sam = new()

{

Name = "Sam",

Born = new(1969, 6, 25, 0, 0, 0, TimeSpan.Zero)

};

WriteLine(sam.Origin);

WriteLine(sam.Greeting);

WriteLine(sam.Age);

1. Run the PeopleApp project and view the result, as shown in the following output:

Sam was born on Earth Sam says 'Hello!'

54

The output shows 54 because I ran the console app on July 5, 2023, when Sam was 54 years old.

Defining settable properties‌

To create a settable property, you must use the older syntax and provide a pair of methods— not just a get part, but also a set part:

1. In PersonAutoGen.cs , add statements to define a string property that has both a get

and set method (also known as a getter and setter), as shown in the following code:

// A read-write property defined using C# 3 auto-syntax. public string? FavoriteIceCream { get; set; }

Although you have not manually created a field to store the person's favorite ice cream, it is there, automatically created by the compiler for you.Sometimes, you need more control over what happens when a property is set. In this scenario, you must use a more detailed syntax and manually create a private field to store the value of the property.

1. In PersonAutoGen.cs , add statements to define a private string field, known as a

backing field, as shown in the following code:

// A private backing field to store the property value. private string? _favoritePrimaryColor;

Good Practice: Although there is no formal standard to name private fields, the most common is to use camel case with an underscore as a prefix.

1. In PersonAutoGen.cs , add statements to define a string property that has both get and

set and validation logic in the setter, as shown in the following code:

// A public property to read and write to the field. public string? FavoritePrimaryColor

{

get

{

return _favoritePrimaryColor;

}

set

{

switch (value?.ToLower())

{

case "red":

case "green":

case "blue":

_favoritePrimaryColor = value; break;

default:

throw new ArgumentException(

$"{value} is not a primary color. " + "Choose from: red, green, blue.");

}

}

}

Good Practice: Avoid adding too much code to your getters and setters. This could indicate a problem with your design. Consider adding private methods that you then call in the set and get methods to simplify your implementations.

1. In Program.cs , add statements to set Sam's favorite ice cream and color, and then write them out, as shown in the following code:

sam.FavoriteIceCream = "Chocolate Fudge";

WriteLine($"Sam's favorite ice-cream flavor is {sam.FavoriteIceCream}."); string color = "Red";

try

{

sam.FavoritePrimaryColor = color;

WriteLine($"Sam's favorite primary color is {sam.FavoritePrimaryColor}.");

}

catch (Exception ex)

{

WriteLine("Tried to set {0} to '{1}': {2}", nameof(sam.FavoritePrimaryColor), color, ex.Message);

}

The print book is limited to about 820 pages. If I added exception handling code to all code examples as we have done here, then I would probably have to remove at least one chapter from the book to make enough space. In the future, I will not explicitly tell you to add exception handling code, but get into the habit of adding it yourself when needed.

1. Run the PeopleApp project and view the result, as shown in the following output:

Sam's favorite ice-cream flavor is Chocolate Fudge. Sam's favorite primary color is Red.

Try to set the color to any value other than red, green, or blue, like black.

Run the PeopleApp project and view the result, as shown in the following output:

Tried to set FavoritePrimaryColor to 'Black': Black is not a primary color. Choose from: red, gr

Good Practice: Use properties instead of fields when you want to execute statements during a read or write to a field without using a method pair, like GetAge and SetAge .

Limiting flags enum values‌

Earlier in this chapter we defined a field to store a person's favorite ancient wonder. But we then made the enum able to store combinations of values. Now let's limit the favorite to one:

1. In Person.cs , comment out the FavoriteAncientWonder field and add a comment to note it has moved to the PersonAutoGen.cs code file, as shown in the following code:

// This has been moved to PersonAutoGen.cs as a property.

// public WondersOfTheAncientWorld FavoriteAncientWonder;

1. In PersonAutoGen.cs , add a private field and public property for FavoriteAncientWonder , as shown in the following code:

private WondersOfTheAncientWorld _favoriteAncientWonder; public WondersOfTheAncientWorld FavoriteAncientWonder

{

get { return _favoriteAncientWonder; } set

{

string wonderName = value.ToString(); if (wonderName.Contains(','))

{

throw new ArgumentException(

message: "Favorite ancient wonder can only have a single enum value.", paramName: nameof(FavoriteAncientWonder));

}

if (!Enum.IsDefined(typeof(WondersOfTheAncientWorld), value))

{

throw new ArgumentException(

$"{value} is not a member of the WondersOfTheAncientWorld enum.", paramName: nameof(FavoriteAncientWonder));

}

_favoriteAncientWonder = value;

}

}

We could simplify the validation by only checking if the value is defined in the original enum because IsDefined returns false for multiple values and undefined values. However, I want to show a different exception for multiple values, so I will use the fact that multiple values formatted as a string would include a comma in the list of names. This also means we must check for multiple values before we check if the value is defined. A comma-separated list is how multiple enum values are represented as a

string , but you cannot use commas to set multiple enum values. You should use | (the bitwise OR ).

1. In Program.cs , in the Storing a value using an enum type region, set Bob's favorite wonder to more than one enum value, as shown in the following code:

bob.FavoriteAncientWonder = WondersOfTheAncientWorld.StatueOfZeusAtOlympia |

WondersOfTheAncientWorld.GreatPyramidOfGiza;

1. Run the PeopleApp project and note the exception, as shown in the following output:

Unhandled exception. System.ArgumentException: Favorite ancient wonder can only have a single en at Packt.Shared.Person.set_FavoriteAncientWonder(WondersOfTheAncientWorld value) in C:\cs12do at Program.

$(String[] args) in C:\cs12dotnet8\Chapter05\PeopleApp\Program.cs:line 57

1. In Program.cs , set Bob's favorite wonder to an invalid enum value like 128, as shown in the following code:

bob.FavoriteAncientWonder = (WondersOfTheAncientWorld)128;

1. Run the PeopleApp project and note the exception, as shown in the following output:

Unhandled exception. System.ArgumentException: 128 is not a member of the WondersOfTheAncientWor

1. In Program.cs , set Bob's favorite wonder back to a valid single enum value.

Defining indexers‌

Indexers allow the calling code to use the array syntax to access a property. For example, the string type defines an indexer so that the calling code can access individual characters in the string , as shown in the following code:

string alphabet = "abcdefghijklmnopqrstuvwxyz";

char letterF = alphabet[5]; // 0 is a, 1 is b, and so on.

You can overload indexers so that different types can be used for their parameters. For example, as well as passing an int value, you could also pass a string value.We will define an indexer to simplify access to the children of a person:

1. In PersonAutoGen.cs , add statements to define an indexer to get and set a child using the index of the child, as shown in the following code:

#region Indexers: Properties that use array syntax to access them. public Person this[int index]

{

get

{

return Children[index]; // Pass on to the List indexer.

}

set

{

Children[index] = value;

}

}

#endregion

1. In PersonAutoGen.cs , add statements to define an indexer to get and set a child using the name of the child, as shown in the following code:

// A read-only string indexer. public Person this[string name]

{

get

{

return Children.Find(p => p.Name == name);

}

}

You will learn more about collections like List in Chapter 8, Working with Common

.NET Types, and how to write lambda expressions using => in Chapter 11, Querying and

Manipulating Data Using LINQ.

1. In Program.cs , add statements to add two children to Sam , and then access the first and second child using the longer Children field and the shorter indexer syntax, as shown in the following code:

sam.Children.Add(new() { Name = "Charlie",

Born = new(2010, 3, 18, 0, 0, 0, TimeSpan.Zero) }); sam.Children.Add(new() { Name = "Ella",

Born = new(2020, 12, 24, 0, 0, 0, TimeSpan.Zero) });

// Get using Children list.

WriteLine($"Sam's first child is {sam.Children[0].Name}."); WriteLine($"Sam's second child is {sam.Children[1].Name}.");

// Get using the int indexer.

WriteLine($"Sam's first child is {sam[0].Name}."); WriteLine($"Sam's second child is {sam[1].Name}.");

// Get using the string indexer.

WriteLine($"Sam's child named Ella is {sam["Ella"].Age} years old.");

1. Run the PeopleApp project and view the result, as shown in the following output:

Sam's first child is Charlie. Sam's second child is Ella.

Sam's first child is Charlie. Sam's second child is Ella.

Sam's child named Ella is 3 years old.

Pattern matching with objects‌

In Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions, you were introduced to basic pattern matching. In this section, we will explore pattern matching in more detail.

Pattern-matching flight passengers‌

In this example, we will define some classes that represent various types of passengers on a flight, and then we will use a switch expression with pattern matching to determine the cost of their flight:

In the PacktLibraryNetStandard2 project/folder, add a new file named FlightPatterns.cs .

If you use Visual Studio 2022, in FlightPatterns.cs , delete the existing statements, including the class named FlightPatterns , because we will define multiple classes, and none match the name of the code file.

In FlightPatterns.cs , add statements to define three types of passenger with different properties, as shown in the following code:

// All the classes in this file will be defined in the following namespace. namespace Packt.Shared;

public class Passenger

{

public string? Name { get; set; }

}

public class BusinessClassPassenger : Passenger

{

public override string ToString()

{

return $"Business Class: {Name}";

}

}

public class FirstClassPassenger : Passenger

{

public int AirMiles { get; set; }

public override string ToString()

{

return $"First Class with {AirMiles:N0} air miles: {Name}";

}

}

public class CoachClassPassenger : Passenger

{

public double CarryOnKG { get; set; } public override string ToString()

{

return $"Coach Class with {CarryOnKG:N2} KG carry on: {Name}";

}

}

You will learn about overriding the ToString method in Chapter 6, Implementing Interfaces and Inheriting Classes.

1. In Program.cs , add statements to define an object array containing five passengers of various types and property values, and then enumerate them, outputting the cost of their flight, as shown in the following code:

// An array containing a mix of passenger types. Passenger[] passengers = {

new FirstClassPassenger { AirMiles = 1_419, Name = "Suman" }, new FirstClassPassenger { AirMiles = 16_562, Name = "Lucy" }, new BusinessClassPassenger { Name = "Janice" },

new CoachClassPassenger { CarryOnKG = 25.7, Name = "Dave" }, new CoachClassPassenger { CarryOnKG = 0, Name = "Amit" },

};

foreach (Passenger passenger in passengers)

{

decimal flightCost = passenger switch

{

FirstClassPassenger p when p.AirMiles > 35_000 => 1_500M, FirstClassPassenger p when p.AirMiles > 15_000 => 1_750M, FirstClassPassenger _ => 2_000M,

BusinessClassPassenger _ => 1_000M, CoachClassPassenger p when p.CarryOnKG < 10.0 => 500M, CoachClassPassenger _ => 650M,

_ => 800M

};

WriteLine($"Flight costs {flightCost:C} for {passenger}");

}

While reviewing the preceding code, note the following:

image

Most code editors do not align the lambda symbols => as I have done above.

image

To pattern match on the properties of an object, you must name a local variable, like

p , which can then be used in an expression.

image

To pattern match on a type only, you can use _ to discard the local variable, for example, FirstClassPassenger _ means that you match on the type but you don't care what values any of its properties have, so a named variable like p is not needed. In a moment, you will see how we can improve the code even more.

image

The switch expression also uses _ to represent its default branch.

1. Run the PeopleApp project and view the result, as shown in the following output:

Flight costs $2,000.00 for First Class with 1,419 air miles: Suman Flight costs $1,750.00 for First Class with 16,562 air miles: Lucy Flight costs $1,000.00 for Business Class: Janice

Flight costs $650.00 for Coach Class with 25.70 KG carry on: Dave Flight costs $500.00 for Coach Class with 0.00 KG carry on: Amit

Enhancements to pattern matching in C# 9 or later‌

The previous examples worked with C# 8. Now we will look at some enhancements in C# 9 and later. First, you no longer need to use the underscore to discard the local variable when doing type matching:

1. In Program.cs , comment out the C# 8 syntax, and add C# 9 and later syntax to modify the branches for first-class passengers to use a nested switch expression and the new support for conditionals, like > , as highlighted in the following code:

decimal flightCost = passenger switch

{

/* C# 8 syntax

FirstClassPassenger p when p.AirMiles > 35_000 => 1_500M, FirstClassPassenger p when p.AirMiles > 15_000 => 1_750M, FirstClassPassenger _ => 2_000M, */

// C# 9 or later syntax FirstClassPassenger p => p.AirMiles switch

{

> 35_000 => 1_500M,

> 15_000 => 1_750M,

_ => 2_000M

},

BusinessClassPassenger => 1_000M, CoachClassPassenger p when p.CarryOnKG < 10.0 => 500M, CoachClassPassenger => 650M,

_ => 800M

};

1. Run the PeopleApp project to view the results, and note that they are the same as before.

You could also use the relational pattern in combination with the property pattern to avoid the nested switch expression, as shown in the following code:

FirstClassPassenger { AirMiles: > 35000 } => 1500, FirstClassPassenger { AirMiles: > 15000 } => 1750M, FirstClassPassenger => 2000M,

More Information: There are many more ways to use pattern matching in your projects. I recommend that you review the official documentation at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern- matching.

Working with record types‌

Before we dive into the new record language feature, let us see some other related new features of C# 9 and later.

Init-only properties‌

You have used object initialization syntax to instantiate objects and set initial properties throughout this chapter. Those properties can also be changed after instantiation.Sometimes, you want to treat properties like readonly fields so that they can be set during instantiation but not after. In other words, they are immutable. The init keyword enables this. It can be used in place of the set keyword in a property definition.Since this is a language feature not supported by .NET Standard 2.0, we cannot use it in the PacktLibraryNetStandard2 project. We must use it in the modern project:

In the PacktLibraryModern project, add a new file named Records.cs .

In Records.cs , define a person class with two immutable properties, as shown in the following code:

namespace Packt.Shared; public class ImmutablePerson

{

public string? FirstName { get; init; } public string? LastName { get; init; }

}

1. In Program.cs , add statements to instantiate a new immutable person, and then try to change one of its properties, as shown in the following code:

ImmutablePerson jeff = new()

{

FirstName = "Jeff", LastName = "Winger"

};

jeff.FirstName = "Geoff";

1. Compile the console app and note the compile error, as shown in the following output:

C:\cs12dotnet8\Chapter05\PeopleApp\Program.cs(404,1): error CS8852: Init-only property or indexe

1. Comment out the attempt to set the FirstName property after instantiation.

Even if you do not set FirstName in the object initializer, you still would not be able to set it post-initialization. If you need to force a property to be set, then apply the required keyword that you learned about earlier in this chapter.

Defining record types‌

Init-only properties provide some immutability to C#. You can take the concept further by using record types. These are defined by using the record keyword instead of (or as well as) the class keyword. That can make the whole object immutable, and it acts like a value when compared. We will discuss equality and comparisons of classes, records, and value types in more detail in Chapter 6, Implementing Interfaces and Inheriting Classes.Immutable records should not have any state (properties and fields) that change after instantiation. Instead, the idea is that you create new records from existing ones. The new record has the changed state. This is called non-destructive mutation. To do this, C# 9 introduced the

with keyword:

1. In Records.cs , add a record named ImmutableVehicle after the ImmutablePerson class, as shown in the following code:

public record ImmutableVehicle

{

public int Wheels { get; init; } public string? Color { get; init; } public string? Brand { get; init; }

}

1. In Program.cs , add statements to create a car and then a mutated copy of it, as shown in the following code:

ImmutableVehicle car = new()

{

Brand = "Mazda MX-5 RF",

Color = "Soul Red Crystal Metallic", Wheels = 4

};

ImmutableVehicle repaintedCar = car

with { Color = "Polymetal Grey Metallic" }; WriteLine($"Original car color was {car.Color}."); WriteLine($"New car color is {repaintedCar.Color}.");

1. Run the PeopleApp project to view the results, and note the change to the car color in the mutated copy, as shown in the following output:

Original car color was Soul Red Crystal Metallic. New car color is Polymetal Grey Metallic.

You could also release the memory for the car variable and the repaintedCar would still fully exist.

Equality of record types‌

One of the most important behaviors of record types is their equality. Two records with the same property values are considered equal. This may not sound surprising, but if you used a normal class instead of a record, then they would not be considered equal. Let's see:

In the PacktLibraryModern project, add a new file named Equality.cs .

In Equality.cs , define a class and a record type, as shown in the following code:

namespace Packt.Shared; public class AnimalClass

{

public string? Name { get; set; }

}

public record AnimalRecord

{

public string? Name { get; set; }

}

1. In Program.cs , add statements to create two instances of AnimalClass and two instances of AnimalRecord , and then compare them for equality, as shown in the following code:

AnimalClass ac1 = new() { Name = "Rex" }; AnimalClass ac2 = new() { Name = "Rex" }; WriteLine($"ac1 == ac2: {ac1 == ac2}"); AnimalRecord ar1 = new() { Name = "Rex" }; AnimalRecord ar2 = new() { Name = "Rex" }; WriteLine($"ar1 == ar2: {ar1 == ar2}");

1. Run the PeopleApp project to view the results, and note that two class instances are not equal even if they have the same property values, and two record instances are equal if they have the same property values, as shown in the following output:

ac1 == ac2: False ar1 == ar2: True

Class instances are only equal if they are literally the same object. This is true when their memory addresses are equal. You will learn more about the equality of types in Chapter 6, Implementing Interfaces and Inheriting Classes.

Positional data members in records‌

The syntax for defining a record can be greatly simplified using positional data members. Instead of using object initialization syntax with curly braces, sometimes you might prefer to provide a constructor with positional parameters, as you saw earlier in this chapter. You can also combine this with a deconstructor to split the object into individual parts, as shown in the following code:

public record ImmutableAnimal

{

public string Name { get; init; } public string Species { get; init; }

public ImmutableAnimal(string name, string species)

{

Name = name;

Species = species;

}

public void Deconstruct(out string name, out string species)

{

name = Name; species = Species;

}

}

The properties, constructor, and deconstructor can be generated for you:

1. In Records.cs , add statements to define another record using simplified syntax, known as positional records, as shown in the following code:

// Simpler syntax to define a record that auto-generates the

// properties, constructor, and deconstructor.

public record ImmutableAnimal(string Name, string Species);

1. In Program.cs , add statements to construct and deconstruct immutable animals, as shown in the following code:

ImmutableAnimal oscar = new("Oscar", "Labrador");

var (who, what) = oscar; // Calls the Deconstruct method. WriteLine($"{who} is a {what}.");

1. Run the PeopleApp project and view the results, as shown in the following output:

Oscar is a Labrador.

You will see records again when we look at C# 10 support to create struct records in

Chapter 6, Implementing Interfaces and Inheriting Classes.

More Information: There are many more ways to use records in your projects. I recommend that you review the official documentation at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records.

Defining a primary constructor for a class‌

Introduced with C# 12, you can define one constructor as part of the class definition. This is called the primary constructor. The syntax is the same as for positional data members in records, but the behavior is slightly different.Traditionally, we separate the class definition from any constructors, as shown in the following code:

public class Headset // Class definition.

{

// Constructor.

public Headset(string manufacturer, string productName)

{

// You can reference manufacturer and productName parameters in

// the constructor and the rest of the class.

}

}

With class primary constructors, you combine both into a more succinct syntax, as shown in the following code:

public class Headset(string manufacturer, string productName);

Let's see an example:

In the PacktLibraryModern project, add a class file named Headset.cs .

Modify the code file contents to give the class two parameters for manufacturer and product name respectively, as shown in the following code:

namespace Packt.Shared;

public class Headset(string manufacturer, string productName);

1. In Program.cs , add statements to instantiate a headset, as shown in the following code:

Headset vp = new("Apple", "Vision Pro"); WriteLine($"{vp.ProductName} is made by {vp.Manufacturer}.");

One of the differences between a record and a class type with a primary constructor is that its parameters don't become public properties automatically, so you will see CS1061 compiler errors. Neither ProductName nor productName are accessible outside the class.

1. In Headset.cs , modify the parameter declarations to make their first character lowercase, and then add statements to define two properties and set them using the parameters passed to the primary constructor, as highlighted in the following code:

namespace Packt.Shared;

public class Headset(string manufacturer, string productName)

{

public string Manufacturer { get; set; } = manufacturer; public string ProductName { get; set; } = productName;

}

1. Run the PeopleApp project and view the results, as shown in the following output:

Vision Pro is made by Apple.

1. In Program.cs , add a default parameterless constructor, as highlighted in the following code:

namespace Packt.Shared;

public class Headset(string manufacturer, string productName)

{

public string Manufacturer { get; set; } = manufacturer; public string ProductName { get; set; } = productName;

// Default parameterless constructor calls the primary constructor. public Headset() : this("Microsoft", "HoloLens") { }

}

1. In Program.cs , create an uninitialized instance of a headset and an instance for Meta Quest 3, as shown in the following code:

Headset holo = new();

WriteLine($"{holo.ProductName} is made by {holo.Manufacturer}."); Headset mq = new() { Manufacturer = "Meta", ProductName = "Quest 3" }; WriteLine($"{mq.ProductName} is made by {mq.Manufacturer}.");

1. Run the PeopleApp project and view the results, as shown in the following output:

Vision Pro is made by Apple. HoloLens is made by Microsoft. Quest 3 is made by Meta.

More Information: You can learn more about primary constructors for classes and structs at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/whats- new/tutorials/primary-constructors.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring this chapter's topics with deeper research.

Exercise 5.1 – Test your knowledge‌

Answer the following questions:

What are the seven access modifier keywords and combinations of keywords, and what do they do?

What is the difference between the static , const , and readonly keywords when applied to a type member?

What does a constructor do?

Why should you apply the [Flags] attribute to an enum type when you want to store combined values?

Why is the partial keyword useful?

What is a tuple?

What does the record keyword do?

What does overloading mean?

What is the difference between the following two statements? (Do not just say a ">" character!)

public List Children = new(); public List Children => new();

1. How do you make a method parameter optional?

Exercise 5.2 – Practice with access modifiers‌

Imagine that you are the compiler. What errors would you show when building the following projects? What would need to change to fix it?In a class library project, in Car.cs :

class Car

{

int Wheels { get; set; } public bool IsEV { get; set; } internal void Start()

{

Console.WriteLine("Starting...");

}

}

In a console app project that references the class library project, in Program.cs :

Car fiat = new() { Wheels = 4, IsEV = true }; fiat.Start();

Exercise 5.3 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-5--- building-your-own-types-with-object-oriented-programming

Summary‌

In this chapter, you learned about:

image

Making your own types using OOP.

image

Some of the different categories of members that a type can have, including fields to store data and methods to perform actions.

image

OOP concepts, such as aggregation and encapsulation

image

How to use modern C# features, like relational and property pattern matching enhancements, init -only properties, and record types.

In the next chapter, you will take these concepts further by defining operators, delegates, and events, implementing interfaces, and inheriting from existing classes.

Implementing Interfaces and Inheriting Classes‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about deriving new types from existing ones using object-oriented programming (OOP). You will learn how to use operators as an alternative method to implement simple functionality, and you will learn how to use generics to make your code safer and more performant. You will learn about delegates and events to exchange messages between types. You will see the differences between reference and value types. You will implement interfaces for common functionality. You will create a derived class to inherit from a base class to reuse functionality, override an inherited type member, and use polymorphism. Finally, you will learn how to create extension methods and cast between classes in an inheritance hierarchy.This chapter covers the following topics:

image

image

Setting up a class library and console application Static methods and overloading operators

image

image

Making types safely reusable with generics Raising and handling events

image

Implementing interfaces

image

image

Managing memory with reference and value types Working with null values

image

Inheriting from classes

image

image

image

Casting within inheritance hierarchies Inheriting and extending .NET types Summarizing custom type choices

Setting up a class library and console application‌

We will start by defining a solution with two projects, like the one created in Chapter 5, Building Your Own Types with Object-Oriented Programming. Even if you completed all the exercises in that chapter, follow the instructions below so that you start this chapter with fresh working projects:

Use your preferred coding tool to create a new project, as defined in the following list:

image

image

image

image

Project template: Class Library / classlib Project file and folder: PacktLibrary Solution file and folder: Chapter06 Framework: .NET 8.0 (Long-Term Support)

Add a new project, as defined in the following list:

image

Project template: Console App / console

image

Project file and folder: PeopleApp

image

image

image

image

Solution file and folder: Chapter06 Framework: .NET 8.0 (Long-Term Support) Do not use top-level statements: Cleared. Enable native AOT publish: Cleared.

In this chapter, both projects target .NET 8 and, therefore, use the C# 12 compiler by default.

In the PacktLibrary project, rename the file named Class1.cs to Person.cs .

In both projects, add to globally and statically import the System.Console

class, as shown in the following markup:

1. In Person.cs , delete any existing statements and define a Person class, as shown in the following code:

namespace Packt.Shared; public class Person

{

#region Properties

public string? Name { get; set; }

public DateTimeOffset Born { get; set; } public List Children = new(); #endregion

#region Methods

public void WriteToConsole()

{

WriteLine($"{Name} was born on a {Born:dddd}.");

}

public void WriteChildrenToConsole()

{

string term = Children.Count == 1 ? "child" : "children"; WriteLine($"{Name} has {Children.Count} {term}.");

}

#endregion

}

1. In the PeopleApp project, add a project reference to PacktLibrary , as shown in the following markup:

1. In Program.cs , delete the existing statements, write statements to create an instance of Person , and then write information about it to the console, as shown in the following code:

using Packt.Shared;

Person harry = new()

{

Name = "Harry",

Born = new(year: 2001, month: 3, day: 25,

hour: 0, minute: 0, second: 0, offset: TimeSpan.Zero)

};

harry.WriteToConsole();

If you use Visual Studio 2022, configure the startup project for the solution as the current selection.

Run the PeopleApp project and note the result, as shown in the following output:

Harry was born on a Sunday.

Static methods and overloading operators‌

This section is specifically about methods that apply to two instances of the same type. It is not about the more general case of methods that apply to zero, one, or more than two instances.I wanted to think of some methods that would apply to two Person instances that could also become operators, like + and * . What would adding two people together represent? What would multiplying two people represent? The obvious answers are getting married and having babies.

We will design our methods to enable us to model the story of Lamech and his two wives and their children, as described at the following link: https://www.kingjamesbibleonline.org/Genesis-4-19/.

We might want two instances of Person to be able to marry and procreate. We can implement this by writing methods and overriding operators. Instance methods are actions that an object does to itself; static methods are actions the type does.Which you choose depends on what makes the most sense for the action.

Good Practice: Having both static and instance methods to perform similar actions often makes sense. For example, string has both a Compare static method and a CompareTo instance method. This puts the choice of how to use the functionality in the hands of the programmers using your type, giving them more flexibility.

Implementing functionality using methods‌

Let's start by implementing some functionality by using both static and instance methods:

1. In Person.cs , add properties with private backing storage fields to indicate if that person is married and to whom, as shown in the following code:

// Allow multiple spouses to be stored for a person. public List Spouses = new();

// A read-only property to show if a person is married to anyone. public bool Married => Spouses.Count > 0;

1. In Person.cs , add one instance method and one static method that will allow two Person

objects to marry, as shown in the following code:

// Static method to marry two people.

public static void Marry(Person p1, Person p2)

{

ArgumentNullException.ThrowIfNull(p1); ArgumentNullException.ThrowIfNull(p2);

if (p1.Spouses.Contains(p2) || p2.Spouses.Contains(p1))

{

throw new ArgumentException(

string.Format("{0} is already married to {1}.", arg0: p1.Name, arg1: p2.Name));

}

p1.Spouses.Add(p2); p2.Spouses.Add(p1);

}

// Instance method to marry another person. public void Marry(Person partner)

{

Marry(this, partner); // "this" is the current person.

}

Note the following:

image

In the static method, the Person objects are passed as parameters named p1 and p2 , and guard clauses are used to check for null values. If either is already married to the other an exception is thrown; otherwise, they are each added to each other's list of spouses. You can model this differently if you want to allow two people to have multiple marriage ceremonies. In that case, you might choose to not throw an exception and instead do nothing. Their state of marriage would remain the same. Additional calls to Marry would not change if they are married or not. In this scenario, I want you to see that the code recognizes that they are already married by throwing an exception.

image

In the instance method, a call is made to the static method, passing the current person ( this ) and the partner they want to marry.

1. In Person.cs , add an instance method to the Person class that will output the spouses of a person if they are married, as shown in the following code:

public void OutputSpouses()

{

if (Married)

{

string term = Spouses.Count == 1 ? "person" : "people"; WriteLine($"{Name} is married to {Spouses.Count} {term}:"); foreach (Person spouse in Spouses)

{

WriteLine($" {spouse.Name}");

}

}

else

{

WriteLine($"{Name} is a singleton.");

}

}

1. In Person.cs , add one instance method and one static method to the Person class that will allow two Person objects to procreate if they are married to each other, as shown in the following code:

///

/// Static method to "multiply" aka procreate and have a child together.

///

/// Parent 1

/// Parent 2

/// A Person object that is the child of Parent 1 and Parent 2.

/// If p1 or p2 are null.

/// If p1 and p2 are not married. public static Person Procreate(Person p1, Person p2)

{

ArgumentNullException.ThrowIfNull(p1); ArgumentNullException.ThrowIfNull(p2);

if (!p1.Spouses.Contains(p2) && !p2.Spouses.Contains(p1))

{

throw new ArgumentException(string.Format(

"{0} must be married to {1} to procreate with them.", arg0: p1.Name, arg1: p2.Name));

}

Person baby = new()

{

Name = $"Baby of {p1.Name} and {p2.Name}", Born = DateTimeOffset.Now

};

p1.Children.Add(baby); p2.Children.Add(baby); return baby;

}

// Instance method to "multiply".

public Person ProcreateWith(Person partner)

{

return Procreate(this, partner);

}

Note the following:

image

In the static method named Procreate , the Person objects that will procreate are passed as parameters named p1 and p2 .

image

A new Person class named baby is created with a name composed of a combination of the two people who have procreated. This could be changed later by setting the returned baby variable's Name property. Although we could add a third parameter to the Procreate method for the baby name, we will define a binary operator later, and they

cannot have third parameters, so for consistency, we will just return the baby reference and let the calling code set the name of it.

image

The baby object is added to the Children collection of both parents and then returned. Classes are reference types, meaning a reference to the baby object stored in memory is added, not a clone of the baby object. You will learn the difference between reference types and value types later in Chapter 6, Implementing Interfaces and Inheriting Classes.

image

In the instance method named ProcreateWith , the Person object to procreate with is passed as a parameter named partner , and that, along with this , is passed to the static Procreate method to reuse the method implementation. this is a keyword that references the current instance of the class. It is a convention to use a different method name for related static and instance methods, for example, Compare(x, y) for the static method name and x.CompareTo(y) for the instance method name.

Good Practice: A method that creates a new object, or modifies an existing object, should return a reference to that object so that the caller can access the results.

1. In Program.cs , create three people and have them marry and then procreate with each other, noting that to add a double-quote character into a string , you must prefix it with a backslash character like this, \" , as shown in the following code:

// Implementing functionality using methods. Person lamech = new() { Name = "Lamech" }; Person adah = new() { Name = "Adah" }; Person zillah = new() { Name = "Zillah" };

// Call the instance method to marry Lamech and Adah. lamech.Marry(adah);

// Call the static method to marry Lamech and Zillah. Person.Marry(lamech, zillah);

lamech.OutputSpouses(); adah.OutputSpouses(); zillah.OutputSpouses();

// Call the instance method to make a baby. Person baby1 = lamech.ProcreateWith(adah); baby1.Name = "Jabal";

WriteLine($"{baby1.Name} was born on {baby1.Born}");

// Call the static method to make a baby.

Person baby2 = Person.Procreate(zillah, lamech); baby2.Name = "Tubalcain"; adah.WriteChildrenToConsole(); zillah.WriteChildrenToConsole(); lamech.WriteChildrenToConsole();

for (int i = 0; i < lamech.Children.Count; i++) { WriteLine(format: " {0}'s child #{1} is named \"{2}\".", arg0: lamech.Name, arg1: i, arg2: lamech.Children[i].Name); } I used a for instead of a foreach statement so that I could use the i variable with the indexer to access each child. 1. Run the PeopleApp project and view the result, as shown in the following output: Lamech is married to 2 people: Adah Zillah Adah is married to 1 person: Lamech Zillah is married to 1 person: Lamech Jabal was born on 05/07/2023 15:17:03 +01:00 Adah has 1 child. Zillah has 1 child. Lamech has 2 children: Lamech's child #0 is named "Jabal". Lamech's child #1 is named "Tubalcain". As you have just seen, for functionality that applies to two instances of an object type, it is easy to provide both static and instance methods to implement the same functionality. Neither static nor instance methods are best in all scenarios, and you cannot predict how your type might be used. It is best to provide both to allow a developer to use your types in the way that best fits the way they need.Now let's see how we can add a third way to provide the same functionality for two instances of a type. Implementing functionality using operators‌ The System.String class has a static method named Concat that concatenates two string values and returns the result, as shown in the following code: string s1 = "Hello "; string s2 = "World!"; string s3 = string.Concat(s1, s2); WriteLine(s3); // Hello World! Calling a method like Concat works, but it might be more natural for a programmer to use the + symbol operator to "add" two string values together, as shown in the following code: string s3 = s1 + s2; A well-known biblical phrase is Go forth and multiply, meaning to procreate. Let's write code so that the * (multiply) symbol will allow two Person objects to procreate. And we will use the + operator to marry two people.We do this by defining a static operator for the * symbol. The syntax is rather like a method, because in effect, an operator is a method, but it uses a symbol instead of a method name, which makes the syntax more concise: 1. In Person.cs , create a static operator for the + symbol, as shown in the following code: #region Operators // Define the + operator to "marry". public static bool operator +(Person p1, Person p2) { Marry(p1, p2); // Confirm they are both now married. return p1.Married && p2.Married; } #endregion The return type for an operator does not need to match the types passed as parameters to the operator, but the return type cannot be void . 1. In Person.cs , create a static operator for the * symbol, as shown in the following code: // Define the * operator to "multiply". public static Person operator *(Person p1, Person p2) { // Return a reference to the baby that results from multiplying. return Procreate(p1, p2); } Good Practice: Unlike methods, operators do not appear in IntelliSense lists for a type or a type instance when you enter a dot ( . ). For every operator that you define, make a method as well, because it may not be obvious to a programmer that the operator is available. The implementation of the operator can then call the method, reusing the code you have written. A second reason to provide a method is that operators are not supported by every language compiler; for example, although arithmetic operators like * are supported by Visual Basic and F#, there is no requirement that other languages support all operators supported by C#. You have to read the type definition or the documentation to discover if operators are implemented. 1. In Program.cs , comment out the statement that calls that static Marry method to marry Zillah and Lamech, and replace it with an if statement that uses the + operator to marry them, as shown in the following code: // Person.Marry(lamech, zillah); if (lamech + zillah) { WriteLine($"{lamech.Name} and {zillah.Name} successfully got married."); } 1. In Program.cs , after calling the Procreate method and before the statements that write the children to the console, use the * operator for Lamech to make two more babies with his wives Adah and Zillah, as highlighted in the following code: // Use the * operator to "multiply". Person baby3 = lamech * adah; baby3.Name = "Jubal"; Person baby4 = zillah * lamech; baby4.Name = "Naamah"; 1. Run the PeopleApp project and view the result, as shown in the following output: Lamech and Zillah successfully got married. Lamech is married to 2 people: Adah Zillah Adah is married to 1 person: Lamech Zillah is married to 1 person: Lamech Jabal was born on 05/07/2023 15:27:30 +01:00 Adah has 2 children. Zillah has 2 children. Lamech has 4 children: Lamech's child #0 is named "Jabal". Lamech's child #1 is named "Tubalcain". Lamech's child #2 is named "Jubal". Lamech's child #3 is named "Naamah". More Information: To learn more about operator overloading, you can read the documentation at the following link: https://learn.microsoft.com/en- us/dotnet/csharp/language-reference/operators/operator-overloading. Making types safely reusable with generics‌ In 2005, with C# 2 and .NET Framework 2, Microsoft introduced a feature named generics, which enables your types to be more safely reusable and more efficient. It does this by allowing a programmer to pass types as parameters, like how you can pass objects as parameters. This topic is only about types that need to provide flexibility for the types it works with. For example, collection types need to be able to store multiple instances of any type. That flexibility can be provided either by using the System.Object type or generics. For other scenarios that do not need type flexibility, the use of non-generic types is good practice. Working with non-generic types‌ First, let's look at an example of working with a non-generic type so that you can understand the problems that generics are designed to solve, such as weakly typed parameters and values, and performance problems caused by using System.Object . System.Collections.Hashtable can be used to store multiple values, each with a unique key that can later be used to quickly look up its value. Both the key and value can be any object because they are declared as System.Object . Although this provides flexibility, it is slow, and bugs are easier to introduce because no type checks are made when adding items.Let's write some code: 1. In Program.cs , create an instance of the non-generic collection, System.Collections.Hashtable , and then add four items to it, as shown in the following code: // Non-generic lookup collection. System.Collections.Hashtable lookupObject = new(); lookupObject.Add(key: 1, value: "Alpha"); lookupObject.Add(key: 2, value: "Beta"); lookupObject.Add(key: 3, value: "Gamma"); lookupObject.Add(key: harry, value: "Delta"); Note that three items have a unique integer key to look them up. The last item has a Person object as its key to look it up. This is valid in a non-generic collection. 1. Add statements to define a key with the value of 2 and use it to look up its value in the hash table, as shown in the following code: int key = 2; // Look up the value that has 2 as its key. WriteLine(format: "Key {0} has value: {1}", arg0: key, arg1: lookupObject[key]); 1. Add statements to use the harry object to look up its value, as shown in the following code: // Look up the value that has harry as its key. WriteLine(format: "Key {0} has value: {1}", arg0: harry, arg1: lookupObject[harry]); 1. Run the PeopleApp project and note that it works, as shown in the following output: Key 2 has value: Beta Key Packt.Shared.Person has value: Delta Although the code works, there is potential for mistakes because literally any type can be used for the key or value. If another developer used your lookup object and expected all the items to be a certain type, they might cast them to that type and get exceptions because some values might be a different type. A lookup object with lots of items would also give poor performance. Good Practice: Avoid types in the System.Collections namespace. Use types in the System.Collections.Generics and related namespaces instead. If you need to use a library that uses non-generic types, then of course you will have to use non-generic types. This is an example of what is commonly referred to as technical debt. Working with generic types‌ System.Collections.Generic.Dictionary can be used to store multiple values, each with a unique key that can later be used to quickly look up its value. Both the key and value can be any object, but you must tell the compiler what the types of the key and value will be when you first instantiate the collection. You do this by specifying types for the generic parameters in angle brackets <> , TKey , and TValue .

Good Practice: When a generic type has one definable type, it should be named T , for example, List , where T is the type stored in the list. When a generic type has multiple definable types, it should use T as a name prefix and have a sensible name, for example, Dictionary .

This provides flexibility, is faster, and bugs are easier to avoid because type checks are made when adding items at compile time. We will not need to explicitly specify the System.Collections.Generic namespace that contains Dictionary because it is implicitly and globally imported by default.Let's write some code to solve the problem by using generics:

1. In Program.cs , create an instance of the generic lookup collection

Dictionary and then add four items to it, as shown in the following code:

// Define a generic lookup collection. Dictionary lookupIntString = new(); lookupIntString.Add(key: 1, value: "Alpha"); lookupIntString.Add(key: 2, value: "Beta"); lookupIntString.Add(key: 3, value: "Gamma"); lookupIntString.Add(key: harry, value: "Delta");

1. Note the compile error when using harry as a key, as shown in the following output:

/Users/markjprice/Code/Chapter06/PeopleApp/Program.cs(98,32): error CS1503: Argument 1: cannot c

Replace harry with 4 .

Add statements to set the key to 3 , and use it to look up its value in the dictionary, as shown in the following code:

key = 3;

WriteLine(format: "Key {0} has value: {1}", arg0: key,

arg1: lookupIntString[key]);

1. Run the PeopleApp project and note that it works, as shown in the following output:

Key 3 has value: Gamma

You have now seen the difference between non-generic and generic types that need the flexibility to store any type. You know to always use generic collection types if possible. Unless you are unlucky enough to be forced to use a legacy non-generic library, you never need to write code that uses non-generic types that can store any type again.Just because it is good practice to use generic collection types in preference to non-generic collection

types does not mean the more general case is also true. Non-generic non-collection types and other types that do not need the flexibility to work with any type are used all the time.

Collection types just happen to be the most common type that benefits from generics.

Raising and handling events‌

Methods are often described as actions that an object can perform, either on itself or on related objects. For example, List can add an item to itself or clear itself, and File can create or delete a file in the filesystem.Events are often described as actions that happen to an object. For example, in a user interface, Button has a Click event, a click being something that happens to a button, and FileSystemWatcher listens to the filesystem for change notifications and raises events like Created and Deleted , which are triggered when a directory or file changes.Another way to think of events is that they provide a way of exchanging messages between two objects.Events are built on delegates, so let's start by having a look at what delegates are and how they work.

Calling methods using delegates‌

You have already seen the most common way to call or execute a method: using the . operator to access the method using its name. For example, Console.WriteLine tells the Console type to call its WriteLine method.The other way to call or execute a method is to use a delegate. If you have used languages that support function pointers, then think of a delegate as being a type-safe method pointer.In other words, a delegate contains the memory address of a method that must match the same signature as the delegate, enabling it to be called safely with the correct parameter types.

The code in this section is illustrative and not meant to be typed into a project. You will explore code like this in the next section, so for now just read the code and try to understand its meaning.

For example, imagine there is a method in the Person class that must have a string type passed as its only parameter, and it returns an int type, as shown in the following code:

public class Person

{

public int MethodIWantToCall(string input)

{

return input.Length; // It doesn't matter what the method does.

}

I can call this method on an instance of Person named p1 like this:

Person p1 = new();

int answer = p1.MethodIWantToCall("Frog");

Alternatively, I can define a delegate with a matching signature to call the method indirectly. Note that the names of the parameters do not have to match. Only the types of parameters and return values must match, as shown in the following code:

delegate int DelegateWithMatchingSignature(string s);

Good Practice: A delegate is a reference type like a class , so if you define one in Program.cs then it must be at the bottom of the file. It would be best to define it in its own class file, for example, Program.Delegates.cs . If you define a delegate in the middle of Program.cs , then you would see the following compiler error:

CS8803: Top-level statements must precede namespace and type declarations .

Now, I can create an instance of the delegate, point it at the method, and finally, call the delegate (which calls the method), as shown in the following code:

// Create a delegate instance that points to the method. DelegateWithMatchingSignature d = new(p1.MethodIWantToCall);

// Call the delegate, which then calls the method. int answer2 = d("Frog");

Examples of delegate use‌

You are probably thinking, "What's the point of that?" It provides flexibility. For example, we could use delegates to create a queue of methods that need to be called in order. Queuing actions that need to be performed is common in services to provide improved scalability.Another example is to allow multiple actions to execute in parallel. Delegates have built-in support for asynchronous operations that run on a different thread, which can provide improved responsiveness. The most important example is that delegates allow us to implement events to send messages between different objects that do not need to know about each other. Events are an example of loose coupling between components because they do not need to know about each other; they just need to know the event signature.

Status: It's complicated‌

Delegates and events are two of the most confusing features of C# and can take a few attempts to understand, so don't worry if you feel lost as we walk through how they work! Move on to other topics and come back again another day when your brain has had the opportunity to process the concepts while you sleep.

Defining and handling delegates‌

Microsoft has two predefined delegates for use as events. They both have two parameters:

image

object? sender : This parameter is a reference to the object raising the event or sending the message. The ? indicates that this reference could be null .

image

EventArgs e or TEventArgs e : This parameter contains additional relevant information about the event. For example, in a GUI app, you might define MouseMoveEventArgs , which has properties for the X and Y coordinates for the mouse pointer. A bank account might have a WithdrawEventArgs with a property for the Amount to withdraw.

Their signatures are simple, yet flexible, as shown in the following code:

// For methods that do not need additional argument values passed in. public delegate void EventHandler(object? sender, EventArgs e);

// For methods that need additional argument values passed in as

// defined by the generic type TEventArgs.

public delegate void EventHandler(object? sender, TEventArgs e);

Good Practice: When you want to define an event in your own type, you should use one of these two predefined delegates.

Let's explore delegates and events:

1. Add statements to the Person class and note the following points, as shown in the following code:

image

image

It defines an EventHandler delegate field named Shout . It defines an int field to store AngerLevel .

image

It defines a method named Poke .

image

Each time a person is poked, their AngerLevel increments. Once their AngerLevel reaches three, they raise the Shout event, but only if there is at least one event delegate pointing at a method defined somewhere else in the code; that is, it is not null :

#region Events

// Delegate field to define the event.

public EventHandler? Shout; // null initially.

// Data field related to the event. public int AngerLevel;

// Method to trigger the event in certain conditions. public void Poke()

{

AngerLevel++;

if (AngerLevel < 3) return; // If something is listening to the event... if (Shout is not null) { // ...then call the delegate to "raise" the event. Shout(this, EventArgs.Empty); } } #endregion Checking whether an object is not null before calling one of its methods is very common. C# 6 and later allows null checks to be simplified inline using a ? symbol before the . operator, as shown in the following code: Shout?.Invoke(this, EventArgs.Empty); In the PeopleApp project, add a new class file named Program.EventHandlers.cs . In Program.EventHandlers.cs , delete any existing statements, and then add a method with a matching signature that gets a reference to the Person object from the sender parameter and outputs some information about them, as shown in the following code: using Packt.Shared; // To use Person. // No namespace declaration so this extends the Program class // in the null namespace. partial class Program { // A method to handle the Shout event received by the harry object. private static void Harry_Shout(object? sender, EventArgs e) { // If no sender, then do nothing. if (sender is null) return; // If sender is not a Person, then do nothing. if (sender is not Person p) return; WriteLine($"{p.Name} is this angry: {p.AngerLevel}."); } } Good Practice: Microsoft's convention for method names that handle events is ObjectName_EventName . In this project, sender will always be a Person instance, so the null checks are not necessary, and the event handler could be much simpler with just the WriteLine statement. However, it is important to know that these types of null checks make your code more robust in cases of event misuse. 1. In Program.cs , add a statement to assign the method to the delegate field, and then add statements to call the Poke method four times, as shown in the following code: // Assign the method to the Shout delegate. harry.Shout = Harry_Shout; // Call the Poke method that eventually raises the Shout event. harry.Poke(); harry.Poke(); harry.Poke(); harry.Poke(); 1. Run the PeopleApp project and view the result, and note that Harry says nothing the first two times he is poked, and only gets angry enough to shout once he's been poked at least three times, as shown in the following output: Harry is this angry: 3. Harry is this angry: 4. Defining and handling events‌ You've now seen how delegates implement the most important functionality of events: the ability to define a signature for a method that can be implemented by a completely different piece of code, calling that method and any others that are hooked up to the delegate field.But what about events? There is less to them than you might think.When assigning a method to a delegate field, you should not use the simple assignment operator as we did in the preceding example.Delegates are multicast, meaning that you can assign multiple delegates to a single delegate field. Instead of the = assignment, we could have used the += operator so that we could add more methods to the same delegate field. When the delegate is called, all the assigned methods are called, although you have no control over the order in which they are called. Do not use events to implement a queuing system to buy concert tickets; otherwise, the wrath of millions of Swifties will fall upon you.If the Shout delegate field already referenced one or more methods, by assigning another method, that method would replace all the others. With delegates that are used for events, we usually want to make sure that a programmer only ever uses either the += operator or the -= operator to assign and remove methods: 1. To enforce this, in Person.cs , add the event keyword to the delegate field declaration, as highlighted in the following code: public event EventHandler? Shout; 1. Build the PeopleApp project and note the compiler error message, as shown in the following output: Program.cs(41,13): error CS0079: The event 'Person.Shout' can only appear on the left hand side This is (almost) all that the event keyword does! If you will never have more than one method assigned to a delegate field, then technically you do not need "events," but it is still good practice to indicate your meaning and that you expect a delegate field to be used as an event. 1. In Program.cs , modify the comment and the method assignment to use += instead of just = , as highlighted in the following code: // Assign the method to the Shout event delegate. harry.Shout += Harry_Shout; Run the PeopleApp project and note that it has the same behavior as before. In Program.EventHandlers.cs , create a second event handler for Harry's Shout event, as shown in the following code: // Another method to handle the event received by the harry object. private static void Harry_Shout_2(object? sender, EventArgs e) { WriteLine("Stop it!"); } 1. In Program.cs , after the statement that assigns the Harry_Shout method to the Shout event, add a statement to attach the new event handler to the Shout event too, as shown highlighted in the following code: // Assign the method(s) to the Shout event delegate. harry.Shout += Harry_Shout; harry.Shout += Harry_Shout_2; 1. Run the PeopleApp project, view the result, and note that both event handlers execute whenever an event is raised, which only happens once the anger level is three or more, as shown in the following output: Harry is this angry: 3. Stop it! Harry is this angry: 4. Stop it! That's it for events. Now let's look at interfaces. Implementing interfaces‌ Interfaces are a way to implement standard functionality and connect different types to make new things. Think of them like the studs on top of LEGO™ bricks, which allow them to "stick" together, or electrical standards for plugs and sockets.If a type implements an interface, then it makes a promise to the rest of .NET that it supports specific functionality. Therefore, they are sometimes described as contracts. Common interfaces‌ Table 6.1 shows some common interfaces that your types might implement: Interface Method(s) Description IComparable CompareTo(other) This defines a comparison method that a type implements to order or sort its instances. IComparer Compare(first, second) This defines a comparison method that a secondary type implements to order or sort instances of a primary type. IDisposable Dispose() This defines a disposal method to release unmanaged resources more efficiently than waiting for a finalizer. See the Releasing unmanaged resources section later in this chapter for more details. IFormattable ToString(format, culture) This defines a culture-aware method to format the value of an object into a string representation. IFormatter Serialize(stream, object) Deserialize(stream) This defines methods to convert an object to and from a stream of bytes for storage or transfer. IFormatProvider GetFormat(type) This defines a method to format inputs based on a language and region. Table 6.1: Some common interfaces that your types might implement Comparing objects when sorting‌ One of the most common interfaces that you will want to implement in your types that represent data is IComparable . If a type implements one of the IComparable interfaces, then arrays and collections containing instances of that type can be sorted.This is an example of an abstraction for the concept of sorting. To sort any type, the minimum functionality would be the ability to compare two items and decide which goes before the other. If a type implements that minimum functionality, then a sorting algorithm can use it to sort instances of that type in any way the sorting algorithm wants to.The IComparable interface has one method named CompareTo . This has two variations, one that works with a nullable object type and one that works with a nullable generic type T , as shown in the following code: namespace System { public interface IComparable { int CompareTo(object? obj); } public interface IComparable

{

int CompareTo(T? other);

}

}

The in keyword specifies that the type parameter T is contravariant, which means that you can use a less derived type than that specified. For example, if Employee derives from Person , then both can be compared with each other.

For example, the string type implements IComparable by returning -1 if the string should be sorted before the string being compared to, 1 if it should be sorted after, and 0 if they are equal. The int type implements IComparable by returning -1 if the int is less than the int being compared to, 1 if it is greater, and 0 if they are equal. CompareTo return values can be summarized as shown in Table 6.2:

this before other this is equal to other this after other

-1 0 1

Table 6.2: Summary of the CompareTo return values

Before we implement the IComparable interface and its CompareTo method for the Person class, let's see what happens when we try to sort an array of Person instances without implementing this interface, including some that are null or have a null value for their Name property:

In the PeopleApp project, add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , delete any existing statements, and then define a method for the partial Program class that will output all the names of a collection of people passed as a parameter, with a title beforehand, as shown in the following code:

using Packt.Shared; partial class Program

{

private static void OutputPeopleNames( IEnumerable people, string title)

{

WriteLine(title);

foreach (Person? p in people)

{

WriteLine(" {0}",

p is null ? " Person" : p.Name ?? " Name");

/* if p is null then output: Person else output: p.Name

unless p.Name is null then output: Name */

}

}

}

1. In Program.cs , add statements that create an array of Person instances, call the OutputPeopleNames method to write the items to the console, and then attempt to sort the array and write the items to the console again, as shown in the following code:

Person?[] people =

{

null,

new() { Name = "Simon" },

new() { Name = "Jenny" },

new() { Name = "Adam" }, new() { Name = null }, new() { Name = "Richard" }

};

OutputPeopleNames(people, "Initial list of people:"); Array.Sort(people);

OutputPeopleNames(people,

"After sorting using Person's IComparable implementation:");

1. Run the PeopleApp project and an exception will be thrown. As the message explains, to fix the problem, our type must implement IComparable , as shown in the following output:

Unhandled Exception: System.InvalidOperationException: Failed to compare two elements in the arr

1. In Person.cs , after inheriting from object , add a comma and enter IComparable , as highlighted in the following code:

public class Person : IComparable

Your code editor will draw a red squiggle under the new code to warn you that you have not yet implemented the method you promised to. Your code editor can write the skeleton implementation for you.

Click on the light bulb and then click Implement interface.

Scroll down to the bottom of the Person class to find the method that was written for you, as shown in the following code:

public int CompareTo(Person? other)

{

throw new NotImplementedException();

}

Delete the statement that throws the NotImplementedException error.

Add statements to handle variations of input values, including null , and call the CompareTo method of the Name field, which uses the string type's implementation of CompareTo , and return the result, as shown in the following code:

int position;

if (other is not null)

{

if ((Name is not null) && (other.Name is not null))

{

// If both Name values are not null, then

// use the string implementation of CompareTo. position = Name.CompareTo(other.Name);

}

else if ((Name is not null) && (other.Name is null))

{

position = -1; // this Person precedes other Person.

}

else if ((Name is null) && (other.Name is not null))

{

position = 1; // this Person follows other Person.

}

else

{

position = 0; // this and other are at same position.

}

}

else if (other is null)

{

position = -1; // this Person precedes other Person.

}

else

{

position = 0; // this and other are at same position.

}

return position;

We have chosen to compare two Person instances by comparing their Name fields. Person instances will, therefore, be sorted alphabetically by their name. null values will be sorted to the bottom of the collection. Storing the calculated position before returning it is useful when debugging. I've also used more round brackets than the compiler needs to make the code easier for me to read. If you prefer fewer brackets, then feel free to remove them.

1. Run the PeopleApp project, and note that this time it works as it should, sorted alphabetically by name, as shown in the following output:

Initial list of people:

Simon

Person Jenny

Adam

Name Richard

After sorting using Person's IComparable implementation: Adam

Jenny Richard Simon

Name

Person

Good Practice: If you want to sort an array or collection of instances of your type, then implement the IComparable interface.

Comparing objects using a separate class‌

Sometimes, you won't have access to the source code for a type, and it might not implement the IComparable interface. Luckily, there is another way to sort instances of a type. You can create a separate type that implements a slightly different interface, named IComparer :

1. In the PacktLibrary project, add a new class file named PersonComparer.cs , containing a class implementing the IComparer interface that will compare two people, that is, two Person instances. Implement it by comparing the length of their Name fields, or if the names are the same length, then compare the names alphabetically, as shown in the following code:

namespace Packt.Shared;

public class PersonComparer : IComparer

{

public int Compare(Person? x, Person? y)

{

int position;

if ((x is not null) && (y is not null))

{

if ((x.Name is not null) && (y.Name is not null))

{

// If both Name values are not null...

// ...then compare the Name lengths...

int result = x.Name.Length.CompareTo(y.Name.Length);

// ...and if they are equal... if (result == 0)

{

// ...then compare by the Names... return x.Name.CompareTo(y.Name);

}

else

{

// ...otherwise compare by the lengths. position = result;

}

}

else if ((x.Name is not null) && (y.Name is null))

{

position = -1; // x Person precedes y Person.

}

else if ((x.Name is null) && (y.Name is not null))

{

position = 1; // x Person follows y Person.

}

else // x.Name and y.Name are both null.

{

position = 0; // x and y are at same position.

}

}

else if ((x is not null) && (y is null))

{

position = -1; // x Person precedes y Person.

}

else if ((x is null) && (y is not null))

{

position = 1; // x Person follows y Person.

}

else // x and y are both null.

{

position = 0; // x and y are at same position.

}

return position;

}

}

1. In Program.cs , add statements to sort the array using an alternative implementation, as shown in the following code:

Array.Sort(people, new PersonComparer()); OutputPeopleNames(people,

"After sorting using PersonComparer's IComparer implementation:");

1. Run the PeopleApp project, and view the result of sorting the people by the length of their names and then alphabetically, as shown in the following output:

After sorting using PersonComparer's IComparer implementation: Adam

Jenny Simon Richard

Name

Person

This time, when we sort the people array, we explicitly ask the sorting algorithm to use the PersonComparer type instead so that the people are sorted with the shortest names first, like Adam , and the longest names last, like Richard , and when the lengths of two or more names are equal they are sorted alphabetically, like Jenny and Simon .

Implicit and explicit interface implementations‌

Interfaces can be implemented implicitly and explicitly. Implicit implementations are simpler and more common. Explicit implementations are only necessary if a type must have multiple methods with the same name and signature. Personally, the only time I can remember ever having to explicitly implement an interface is when writing the code example for this book.For example, both IGamePlayer and IKeyHolder might have a method called Lose with the same parameters because both a game and a key can be lost. The members of an interface are always and automatically public because they have to be accessible for another type to implement them!In a type that must implement both interfaces, only one implementation of Lose can be the implicit method. If both interfaces can share the same implementation, there is no problem, but if not, then the other Lose method will have to be implemented differently and called explicitly, as shown in the following code:

public interface IGamePlayer

{

void Lose();

}

public interface IKeyHolder

{

void Lose();

}

public class Person : IGamePlayer, IKeyHolder

{

public void Lose() // Implicit implementation.

{

// Implement losing a key.

}

public void IGamePlayer.Lose() // Explicit implementation.

{

// Implement losing a game.

}

}

Person p = new();

p.Lose(); // Calls implicit implementation of losing a key. ((IGamePlayer)p).Lose(); // Calls explicit implementation of losing a game.

// Alternative way to do the same. IGamePlayer player = p as IGamePlayer;

player.Lose(); // Calls explicit implementation of losing a game.

Defining interfaces with default implementations‌

A language feature introduced in C# 8 is default implementations for an interface. This allows an interface to contain implementation. This breaks the clean separation between interfaces that define a contract and classes and other types that implement them. It is considered by some .NET developers to be a perversion of the language.Let's see it in action:

1. In the PacktLibrary project, add a new file named IPlayable.cs , and modify the statements to define a public IPlayable interface with two methods to Play and Pause , as shown in the following code:

namespace Packt.Shared; public interface IPlayable

{

void Play(); void Pause();

}

1. In the PacktLibrary project, add a new class file named DvdPlayer.cs , and modify the statements in the file to implement the IPlayable interface, as shown in the following code:

namespace Packt.Shared;

public class DvdPlayer : IPlayable

{

public void Pause()

{

WriteLine("DVD player is pausing.");

}

public void Play()

{

WriteLine("DVD player is playing.");

}

}

This is useful, but what if we decide to add a third method named Stop ? Before C# 8, this would be impossible once at least one type is implemented in the original interface. One of the main traits of an interface is that it is a fixed contract.C# 8 allows you to add new members to an interface after release if those new members have a default implementation. C# purists do not like the idea, but for practical reasons, such as avoiding breaking changes or having to define a whole new interface, it is useful, and other languages such as Java and Swift enable similar techniques.

Support for default interface implementations requires some fundamental changes to the underlying platform, so they are only supported with C# if the target framework is .NET

5 or later, .NET Core 3 or later, or .NET Standard 2.1. They are, therefore, not supported by .NET Framework.

Let's add a default implementation to the interface:

1. Modify the IPlayable interface to add a Stop method with a default implementation, as highlighted in the following code:

namespace Packt.Shared; public interface IPlayable

{

void Play(); void Pause();

void Stop() // Default interface implementation.

{

WriteLine("Default implementation of Stop.");

}

}

1. Build the PeopleApp project, and note that the projects compile successfully despite the DvdPlayer class not implementing Stop . In the future, we could override the default implementation of Stop by implementing it in the DvdPlayer class.

Although controversial, default implementations in interfaces might be useful in scenarios where the most common implementation is known at the time of defining the interface.

Therefore, it is best if the interface defines that implementation once, and then most types that implement that interface can inherit it without needing to implement their own.

However, if the interface definer does not know how the member should or even could be implemented, then it is a waste of effort to add a default implementation because it will

always be replaced.Think about the IComparable interface that you saw earlier in this chapter. It defines a CompareTo method. What might a default implementation of that method be? Personally, I think it's obvious that there is no default implementation that would make any practical sense. The least-worst implementation that I can think of would be to compare the string values returned from calling ToString on the two objects. However, every type really should implement its own CompareTo method. You are likely to find the same with 99.9% of the interfaces you use.Now let's look at how types are stored in a computer's memory.

Managing memory with reference and value types‌

I mentioned reference types a couple of times. Let's look at them in more detail.

Understanding stack and heap memory‌

There are two categories of memory: stack memory and heap memory. With modern operating systems, the stack and heap can be anywhere in physical or virtual memory.Stack memory is faster to work with but limited in size. It is fast because it is managed directly by the CPU and it uses a last-in, first-out mechanism, so it is more likely to have data in its L1 or L2 cache. Heap memory is slower but much more plentiful.On Windows, for ARM64, x86, and x64 machines, the default stack size is 1 MB. It is 8 MB on a typical modern Linux-based operating system. For example, in a macOS or Linux terminal, I can enter the command

ulimit -a to discover that the stack size is limited to 8,192 KB and that other memory is "unlimited." This limited amount of stack memory is why it is so easy to fill it up and get a "stack overflow."

Defining reference and value types‌

There are three C# keywords that you can use to define object types: class , record , and struct . All can have the same members, such as fields and methods. One difference between them is how memory is allocated:

image

When you define a type using record or class , you define a reference type. This means that the memory for the object itself is allocated on the heap, and only the memory address of the object (and a little overhead) is stored on the stack. Reference types always use a little stack memory.

image

When you define a type using record struct or struct , you define a value type. This means that the memory for the object itself is allocated to the stack.

If a struct uses field types that are not of the struct type, then those fields will be stored on the heap, meaning the data for that object is stored in both the stack and the heap.These are the most common struct types:

image

Number System types: byte , sbyte , short , ushort , int , uint , long , ulong , float ,

double , and decimal

image

Other System types: char , DateTime , DateOnly , TimeOnly , and bool

image

System.Drawing types: Color , Point , PointF , Size , SizeF , Rectangle , and RectangleF

Almost all the other types are class types, including string aka System.String and object

aka System.Object .

Apart from the difference in terms of where in memory the data for a type is stored, the other major differences are that you cannot inherit from a struct , and struct objects are compared for equality using values instead of memory addresses.

How reference and value types are stored in memory‌

Imagine that you have a console app that calls some method that uses some reference and value type variables, as shown in the following code:

void SomeMethod()

{

int number1 = 49; long number2 = 12;

System.Drawing.Point location = new(x: 4, y: 5); Person kevin = new() { Name = "Kevin",

Born = new(1988, 9, 23, 0, 0, 0, TimeSpace.Zero) }; Person sally;

}

Let's review what memory is allocated on the stack and heap when this method is executed, as shown in Figure 6.1 and as described in the following list:

image

The number1 variable is a value type (also known as struct ), so it is allocated on the stack, and it uses 4 bytes of memory since it is a 32-bit integer. Its value, 49 , is stored directly in the variable.

image

The number2 variable is also a value type, so it is also allocated on the stack, and it uses 8 bytes since it is a 64-bit integer.

image

The location variable is also a value type, so it is allocated on the stack, and it uses 8 bytes since it is made up of two 32-bit integers, x and y .

image

The kevin variable is a reference type (also known as class ), so 8 bytes for a 64-bit memory address (assuming a 64-bit operating system) are allocated on the stack, and enough bytes are allocated on the heap to store an instance of a Person .

image

The sally variable is a reference type, so 8 bytes for a 64-bit memory address are allocated on the stack. It is currently unassigned ( null ), meaning no memory has yet been allocated for it on the heap. If we were to later assign kevin to sally , then the memory address of the Person on the heap would be copied into sally , as shown in the following code:

sally = kevin; // Both variables point at the same Person on heap.

image

Figure 6.1: How value and reference types are allocated in the stack and heap

All the allocated memory for a reference type is stored on the heap except for its memory address on the stack. If a value type such as DateTimeOffset is used for a field of a reference type like Person , then the DateTimeOffset value is stored on the heap, as shown in Figure 6.1.If a value type has a field that is a reference type, then that part of the value type is stored on the heap. Point is a value type that consists of two fields, both of which are themselves value types, so the entire object can be allocated on the stack. If the Point value type had a field that was a reference type, like string , then the string bytes would be stored on the heap.When the method completes, all the stack memory is automatically released from the top of the stack. However, heap memory could still be allocated after a method returns. It is the .NET runtime garbage collector's responsibility to release this memory at a future date. Heap memory is not immediately released to improve performance. We will learn about the garbage collector later in this section.The console app might then call another method that needs some more stack memory to be allocated to it, and so on. Stack memory is literally a stack: memory is allocated at the top of the stack and removed from there when it is no longer needed.C# developers do not have control over the allocation or release of memory. Memory is automatically allocated when methods are called, and that memory is automatically released when the method returns. This is known as verifiably safe code.C# developers can allocate and access raw memory using unsafe code.

The stackalloc keyword is used to allocate a block of memory on the stack. Memory allocated is released automatically when the method that allocated it returns. This is an advanced feature not covered in this book. You can read about unsafe code and stackalloc at the following links: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe- code and https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/operators/stackalloc.

Understanding boxing‌

Boxing is nothing to do with being punched in the face, although for Unity game developers struggling to manage limited memory it can sometimes feel like it.Boxing in C# is when a value type is moved to heap memory and wrapped inside a System.Object instance. Unboxing is when that value is moved back onto the stack. Unboxing happens explicitly. Boxing happens implicitly, so it can happen without the developer realizing. Boxing can take up to 20 times longer than without boxing.For example, an int value can be boxed and then unboxed, as shown in the following code:

int n = 3;

object o = n; // Boxing happens implicitly.

n = (int)o; // Unboxing only happens explicitly.

A common scenario is passing value types to formatted strings, as shown in the following code:

string name = "Hilda";

DateTime hired = new(2024, 2, 21); int days = 5;

// hired and days are value types that will be boxed. Console.WriteLine("{0} hired on {1} for {2} days.", name, hired, days);

The name variable is not boxed because string is a reference type and is therefore already on the heap.Boxing and unboxing operations have a negative impact on performance. Although it can be useful for a .NET developer to be aware of and to avoid boxing, for most .NET project types and for many scenarios, boxing is not worth worrying too much about because the overhead is dwarfed by other factors like making a network call or updating the user interface.But for games developed for the Unity platform, its garbage collector does not release boxed values as quickly or automatically and therefore it is more critical to avoid boxing as much as possible. For this reason, JetBrains Rider with its Unity Support plugin will complain about boxing operations whenever they occur in your code. Unfortunately, it does not differentiate between Unity and other project types.

More Information: You can learn more about boxing at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and- unboxing.

Equality of types‌

It is common to compare two variables using the == and != operators. The behavior of these two operators is different for reference types and value types.When you check the equality of two value type variables, .NET literally compares the values of those two variables on the stack and returns true if they are equal.

1. In Program.cs , add statements to declare two integers with equal values and then compare them, as shown in the following code:

int a = 3; int b = 3;

WriteLine($"a: {a}, b: {b}"); WriteLine($"a == b: {a == b}");

1. Run the PeopleApp project and view the result, as shown in the following output:

a: 3, b: 3

a == b: True

When you check the equality of two reference type variables, .NET compares the memory addresses of those two variables and returns true if they are equal.

1. In Program.cs , add statements to declare two Person instances with equal names, and then compare the variables and their names, as shown in the following code:

Person p1 = new() { Name = "Kevin" }; Person p2 = new() { Name = "Kevin" }; WriteLine($"p1: {p1}, p2: {p2}");

WriteLine($"p1.Name: {p1.Name}, p2.Name: {p2.Name}"); WriteLine($"p1 == p2: {p1 == p2}");

1. Run the PeopleApp project and view the result, as shown in the following output:

p1: Packt.Shared.Person, p2: Packt.Shared.Person p1.Name: Kevin, p2.Name: Kevin

p1 == p2: False

This is because they are not the same object. If both variables literally pointed to the same object on the heap, then they would be equal.

1. Add statements to declare a third Person object and assign p1 to it, as shown in the following code:

Person p3 = p1;

WriteLine($"p3: {p3}");

WriteLine($"p3.Name: {p3.Name}"); WriteLine($"p1 == p3: {p1 == p3}");

1. Run the PeopleApp project and view the result, as shown in the following output:

p3: Packt.Shared.Person p3.Name: Kevin

p1 == p3: True

The one exception to this behavior of reference types is the string type. It is a reference type, but the equality operators have been overridden to make them behave as if they were value types.

1. Add statements to compare the Name properties of two Person instances, as shown in the following code:

// string is the only class reference type implemented to

// act like a value type for equality. WriteLine($"p1.Name: {p1.Name}, p2.Name: {p2.Name}"); WriteLine($"p1.Name == p2.Name: {p1.Name == p2.Name}");

1. Run the PeopleApp project and view the result, as shown in the following output:

p1.Name: Kevin, p2.Name: Kevin p1.Name == p2.Name: True

You can do the same as string with your classes to override the equality operator == to return true , even if the two variables are not referencing the same object (the same memory address on the heap) but, instead, their fields have the same values. However, that is beyond the scope of this book.

Good Practice: Alternatively, use a record class because one of its benefits is that it implements this equality behavior for you.

Defining struct types‌

Let's explore defining your own value types:

In the PacktLibrary project, add a file named DisplacementVector.cs .

image

Modify the file, as shown in the following code, and note the following: The type is declared using struct instead of class .

image

It has two int properties, named X and Y , that will auto-generate two private fields with the same data type, which will be allocated on the stack.

image

It has a constructor to set initial values for X and Y .

image

It has an operator to add two instances together that returns a new instance of the type, with X added to X , and Y added to Y :

namespace Packt.Shared;

public struct DisplacementVector

{

public int X { get; set; } public int Y { get; set; }

public DisplacementVector(int initialX, int initialY)

{

X = initialX;

Y = initialY;

}

public static DisplacementVector operator +( DisplacementVector vector1, DisplacementVector vector2)

{

return new(

vector1.X + vector2.X, vector1.Y + vector2.Y);

}

}

1. In Program.cs , add statements to create two new instances of DisplacementVector , add them together, and output the result, as shown in the following code:

DisplacementVector dv1 = new(3, 5); DisplacementVector dv2 = new(-2, 7); DisplacementVector dv3 = dv1 + dv2;

WriteLine($"({dv1.X}, {dv1.Y}) + ({dv2.X}, {dv2.Y}) = ({dv3.X}, {dv3.Y})");

1. Run the PeopleApp project and view the result, as shown in the following output:

(3, 5) + (-2, 7) = (1, 12)

Value types always have a default constructor even if an explicit one is not defined because the values on the stack must be initialized, even if they are initialized to default values. For the two integer fields in DisplacementVector , they will be initialized to 0 .

1. In Program.cs , add statements to create a new instance of DisplacementVector , and output the object's properties, as shown in the following code:

DisplacementVector dv4 = new(); WriteLine($"({dv4.X}, {dv4.Y})");

1. Run the PeopleApp project and view the result, as shown in the following output:

(0, 0)

1. In Program.cs , add statements to create a new instance of DisplacementVector , and compare it to dv1 , as shown in the following code:

DisplacementVector dv5 = new(3, 5); WriteLine($"dv1.Equals(dv5): {dv1.Equals(dv5)})"); WriteLine($"dv1 == dv5: {dv1 == dv5})");

1. Note that you cannot compare struct variables using == , but you can call the Equals method, which has a default implementation that compares all fields within the struct for equality. We could now make our struct overload the == operator ourselves, but an easier way is to use a feature introduced with C# 10: record struct types.

Good Practice: If the total memory used by all the fields in your type is 16 bytes or less, your type only uses value types for its fields, and you will never want to derive from your type, then Microsoft recommends that you use struct . If your type uses more than 16 bytes of stack memory, it uses reference types for its fields, or you might want to inherit from it, then use class .

Defining record struct types‌

C# 10 introduced the ability to use the record keyword with struct types as well as class

types. Let's see an example:

1. In the DisplacementVector type, add the record keyword, as highlighted in the following code:

public record struct DisplacementVector(int X, int Y);

In Program.cs , note that == now does not have a compiler error.

Run the PeopleApp project and view the result, as shown in the following output:

dv1.Equals(dv5): True) dv1 == dv5: True)

A record struct has all the same benefits over a record class that a struct has over a class . One difference between record struct and record class declared using primary constructor syntax is that record struct is not immutable, unless you also apply the readonly keyword to the record struct declaration. A struct does not implement the == and

!= operators, but they are automatically implemented with a record struct .

Good Practice: With this change, Microsoft recommends explicitly specifying class if you want to define a record class , even though the class keyword is optional, as shown in the following code: public record class ImmutableAnimal(string Name); .

Releasing unmanaged resources‌

In the previous chapter, we saw that constructors can be used to initialize fields and that a type may have multiple constructors. Imagine that a constructor allocates an unmanaged resource, that is, anything that is not controlled by .NET, such as a file or mutex under the control of the operating system. The unmanaged resource must be manually released because .NET cannot do it for us using its automatic garbage collection feature.Garbage collection is an advanced topic, so for this topic, I will show some code examples, but you do not need to write the code yourself.Each type can have a single finalizer that will be called by the .NET runtime when the resources need to be released. A finalizer has the same name as a constructor, that is, the name of the type, but it is prefixed with a tilde, ~ , as shown in the following code:

public class ObjectWithUnmanagedResources

{

public ObjectWithUnmanagedResources() // Constructor.

{

// Allocate any unmanaged resources.

}

~ObjectWithUnmanagedResources() // Finalizer aka destructor.

{

// Deallocate any unmanaged resources.

}

}

Do not confuse a finalizer (also known as a destructor) with a Deconstruct method. A destructor releases resources; in other words, it destroys an object in memory. A Deconstruct method returns an object split up into its constituent parts and uses the C# deconstruction syntax, for example, when working with tuples. See Chapter 5, Building Your Own Types with Object-Oriented Programming, for details of Deconstruct methods.The preceding code example is the minimum you should do when working with unmanaged resources. However, the problem with only providing a finalizer is that the .NET garbage collector requires two garbage collections to completely release the allocated resources for this type.Though optional, it is recommended to also provide a method to allow a developer who uses your type to explicitly release resources. This would allow the garbage collector to release managed parts of an unmanaged resource, such as a file, immediately and deterministically. This would mean it releases the managed memory part of the object in a single garbage collection instead of two rounds of garbage collection.There is a standard mechanism to do this by implementing the IDisposable interface, as shown in the following example:

public class ObjectWithUnmanagedResources : IDisposable

{

public ObjectWithUnmanagedResources()

{

// Allocate unmanaged resource.

}

~ObjectWithUnmanagedResources() // Finalizer.

{

Dispose(false);

}

bool disposed = false; // Indicates if resources have been released. public void Dispose()

{

Dispose(true);

// Tell garbage collector it does not need to call the finalizer. GC.SuppressFinalize(this);

}

protected virtual void Dispose(bool disposing)

{

if (disposed) return;

// Deallocate the *unmanaged* resource.

// ...

if (disposing)

{

// Deallocate any other *managed* resources.

// ...

}

disposed = true;

}

}

There are two Dispose methods, one public and one protected :

image

The public void Dispose method will be called by a developer using your type. When called, both unmanaged and managed resources need to be deallocated.

image

The protected virtual void Dispose method with a bool parameter is used internally to implement the deallocation of resources. It needs to check the disposing parameter and disposed field because if the finalizer thread has already run and called the

~ObjectWithUnmanagedResources method, then only managed resources need to be deallocated by the garbage collector.

The call to GC.SuppressFinalize(this) is what notifies the garbage collector that it no longer needs to run the finalizer, removing the need for a second garbage collection.

Ensuring that Dispose is called‌

When someone uses a type that implements IDisposable , they can ensure that the public

Dispose method is called with the using statement, as shown in the following code:

using (ObjectWithUnmanagedResources thing = new())

{

// Code that uses thing.

}

The compiler converts your code into something like the following, which guarantees that even if an exception occurs, the Dispose method will still be called:

ObjectWithUnmanagedResources thing = new(); try

{

// Code that uses thing.

}

finally

{

if (thing != null) thing.Dispose();

}

When someone uses a type that implements IAsyncDisposable , they can ensure that the public

Dispose method is called with the await using statement, as shown in the following code:

await using (ObjectWithUnmanagedResources thing = new())

{

// Code that uses async thing.

}

You will see practical examples of releasing unmanaged resources with IDisposable , using statements, and try ... finally blocks in Chapter 9, Working with Files, Streams, and Serialization.

Working with null values‌

You have seen how reference types are different from value types in how they are stored in memory, as well as how to store primitive values like numbers in struct variables. But what

if a variable does not yet have a value? How can we indicate that? C# has the concept of a

null value, which can be used to indicate that a variable has not been set.

Making a value type nullable‌

By default, value types like int and DateTime must always have a value, hence their name. Sometimes, for example, when reading values stored in a database that allows empty, missing, or null values, it is convenient to allow a value type to be null . We call this a nullable value type.You can enable this by adding a question mark as a suffix to the type when declaring a variable.Let's see an example. We will create a new project because some of the null handling options are set at the project level:

Use your preferred coding tool to add a new Console App / console project named

NullHandling to the Chapter06 solution.

In NullHandling.csproj , add an to globally and statically import the

System.Console class.

In Program.cs , delete the existing statements, and then add statements to declare and assign values, including null , to int variables, one suffixed with ? and one not, as shown in the following code:

int thisCannotBeNull = 4;

thisCannotBeNull = null; // CS0037 compiler error! WriteLine(thisCannotBeNull);

int? thisCouldBeNull = null; WriteLine(thisCouldBeNull); WriteLine(thisCouldBeNull.GetValueOrDefault()); thisCouldBeNull = 7; WriteLine(thisCouldBeNull); WriteLine(thisCouldBeNull.GetValueOrDefault());

1. Build the project and note the compile error, as shown in the following output:

Cannot convert null to 'int' because it is a non-nullable value type

1. Comment out the statement that gives the compile error, as shown in the following code:

//thisCannotBeNull = null; // CS0037 compiler error!

1. Run the project and view the result, as shown in the following output:

4

0

7

7

The second line is blank because it outputs the null value.

1. Add statements to use alternative syntax, as shown in the following code:

// The actual type of int? is Nullable. Nullable thisCouldAlsoBeNull = null; thisCouldAlsoBeNull = 9; WriteLine(thisCouldAlsoBeNull);

Click on Nullable and press F12, or right-click and choose Go To Definition.

Note that the generic value type, Nullable , must have a type T , which is a struct , aka a value type, and it has useful members like HasValue , Value , and GetValueOrDefault , as shown in Figure 6.2:

image

Figure 6.2: Revealing Nullable members

Good Practice: When you append a ? after a struct type, you change it to a different type. For example, DateTime? becomes Nullable .

Understanding null-related initialisms‌

Before we see some code, let's review some commonly used initialisms in Table 6.3: Initialism Meaning Description

NRT Nullable Reference Type

A compiler feature introduced with C# 8 and enabled by default in new projects with C# 10, which performs static analysis of your code at design time and shows warnings of potential misuse of null values for reference types.

NRE NullReferenceException An exception thrown at runtime when dereferencing a null

value, aka accessing a variable or member on an object that is null .

ANE ArgumentNullException An exception thrown at runtime by a method, property, or

indexer invocation when an argument or value is null , and when the business logic determines that it is not valid.

Table 6.3: Commonly used initialisms

Understanding nullable reference types‌

The use of the null value is so common, in so many languages, that many experienced programmers never question the need for its existence. However, there are many scenarios where we could write better, simpler code if a variable is not allowed to have a null value.The most significant change to the C# 8 language compiler was the introduction of checks and warnings for nullable and non-nullable reference types. "But wait!", you are probably thinking, "Reference types are already nullable!"And you would be right, but in C#

8 and later, reference types can be configured to warn you about null values by setting a file- or project-level option, enabling this useful new feature. Since this is a big change for C#, Microsoft decided to make the feature an opt-in.It will take several years for this new C# language compiler feature to make an impact, since thousands of existing library packages and apps will expect the old behavior. Even Microsoft did not have time to fully implement this new feature in all the main .NET packages until .NET 6. Important libraries

like Microsoft.Extensions for logging, dependency injections, and configuration were not annotated until .NET 7.During the transition, you can choose between several approaches for your own projects:

image

Default: For projects created using .NET 5 or earlier, no changes are needed. Non- nullable reference types are not checked. For projects created using .NET 6 or later, nullability checks are enabled by default, but this can be disabled by either deleting the entry in the project file or setting it to disable .

image

Opt-in project and opt-out files: Enable the feature at the project level, and for any files that need to remain compatible with old behavior, opt out. This was the approach Microsoft used internally while it updated its own packages to use this new feature.

image

Opt-in files: Only enable the NRT feature for individual files.

Warning! This NRT feature does not prevent null values – it just warns you about them, and the warnings can be disabled, so you still need to be careful!

string firstName; // Allows null but gives warning when potentially null. string? lastName; // Allows null and does not give warning if null.

Controlling the nullability warning check feature‌

To enable the nullability warning check feature at the project level, have the

element set to enable in your project file, as highlighted in the following markup:

...

enable

To disable the nullability warning check feature at the project level, have the

element set to disable in your project file, as highlighted in the following markup:

...

disable

You could also remove the element completely because the default, if not explicitly set, is disabled.To disable the feature at the file level, add the following to the top of a code file:

#nullable disable

To enable the feature at the file level, add the following to the top of a code file:

#nullable enable

Disabling null and other compiler warnings‌

You could decide to enable the nullability feature at the project or file level but then disable some of the 50+ warnings related to it. Some common nullability warnings are shown in Table 6.4:

Code Description

CS8600 Converting a null literal or a possible null value to a non-nullable type.

CS8601 A possible null reference assignment.

CS8602 A dereference of a possibly null reference.

CS8603 A possible null reference return.

CS8604 A possible null reference argument for a parameter.

CS8618 A non-nullable field '' must contain a non-null value when exiting a constructor. Consider declaring the field as nullable.

CS8625 Cannot convert a null literal to a non-nullable reference type.

CS8655 The switch expression does not handle some null inputs (it is not exhaustive). Table 6.4: Common nullability warnings

You can disable compiler warnings for a whole project. To do so, add a NoWarn element with a semicolon-separated list of compiler warning codes, as shown in the following markup:

CS8600;CS8602

To disable compiler warnings at the statement level, you can disable and then restore a specified compiler warning to temporarily suppress it for a block of statements, as shown in the following code:

#pragma warning disable CS8602 WriteLine(firstName.Length); WriteLine(lastName.Length); #pragma warning restore CS8602

These techniques can be used for any compiler warnings, not just those related to nullability.

Declaring non-nullable variables and parameters‌

If you enable NRTs and you want a reference type to be assigned the null value, then you will have to use the same syntax to make a value type nullable, that is, adding a ? symbol after the type declaration.So, how do NRTs work? Let's look at an example. When storing information about an address, you might want to force a value for the street, city, and region, but the building can be left blank, that is, null :

In the NullHandling project, add a class file named Address.cs .

in Address.cs , delete any existing statements and then add statements to declare an

Address class with four fields, as shown in the following code:

namespace Packt.Shared; public class Address

{

public string? Building; public string Street; public string City; public string Region;

}

After a few seconds, note the warnings about non-nullable fields, like Street not being initialized, as shown in Figure 6.3:

image

Figure 6.3: Warning messages about non-nullable fields in the Error List window

Assign the empty string value to the Street field, and define constructors to set the other fields that are non-nullable, as highlighted in the following code:

public string Street = string.Empty; public string City;

public string Region; public Address()

{

City = string.Empty;

Region = string.Empty;

}

// Call the default parameterless constructor

// to ensure that Region is also set. public Address(string city) : this()

{

City = city;

}

1. In Program.cs , import the namespace to use Address , as shown in the following code:

using Packt.Shared; // To use Address.

1. In Program.cs , add statements to instantiate an Address and set its properties, as shown in the following code:

Address address = new(city: "London")

{

Building = null, Street = null, Region = "UK"

};

1. Note the Warning CS8625 on setting the Street but not the Building , as shown in the following output:

CS8625 Cannot convert null literal to non-nullable reference type.

1. Append an exclamation mark after null when setting Street , as highlighted in the following code:

Street = null!, // null-forgiving operator.

Note that the warning disappears.

Add statements that will dereference the Building and Street properties, as shown in the following code:

WriteLine(address.Building.Length); WriteLine(address.Street.Length);

1. Note the Warning CS8602 on setting the Building but not the Street , as shown in the following output:

CS8602 Dereference of a possibly null reference.

At runtime it is still possible for an exception to be thrown when working with Street , but the compiler should continue to warn you of potential exceptions when working with Building so that you can change your code to avoid them.

1. Use the null-conditional operator to return null instead of accessing the Length , as shown in the following code:

WriteLine(address.Building?.Length);

1. Run the console app, and note that the statement that accesses the Length of the Building outputs a null value (blank line), but a runtime exception occurs when we access the Length of the Street , as shown in the following output:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of a

1. Wrap the statement that accesses the Street length in a null check, as shown in the following code:

if (address.Street is not null)

{

WriteLine(address.Street.Length);

}

It is worth reminding yourself that an NRT is only about asking the compiler to provide warnings about potential null values that might cause problems. It does not actually change the behavior of your code. It performs a static analysis of your code at compile time.This explains why the new language feature is named nullable reference types. Starting with C# 8.0, unadorned reference types can become non-nullable, and the same syntax is used to make a reference type nullable, as it is used for value types.

Suffixing a reference type with ? does not change the type. This is different from suffixing a value type with ? that changes its type to Nullable . Reference types can already have null values. All you do with nullable reference types (NRTs) is tell the compiler that you expect it to be null , so the compiler does not need to warn you.

However, this does not remove the need to perform null checks throughout your code.

Now let's look at language features to work with null values that change the behavior of your code and work well as a complement to NRTs.

Checking for null‌

Checking whether a nullable reference type or nullable value type variable currently contains null is important because if you do not, a NullReferenceException can be thrown, which results in an error. You should check for a null value before using a nullable variable, as shown in the following code:

// Check that the variable is not null before using it. if (thisCouldBeNull != null)

{

// Access a member of thisCouldBeNull. int length = thisCouldBeNull.Length;

...

}

C# 7 introduced is combined with the ! ( not ) operator as an alternative to != , as shown in the following code:

if (!(thisCouldBeNull is null))

{

C# 9 introduced is not as an even clearer alternative, as shown in the following code:

if (thisCouldBeNull is not null)

{

Good Practice: Although you traditionally would use the expression

(thisCouldBeNull != null) , this is no longer considered good practice because the developer could have overloaded the != operator to change how it works. Using pattern matching with is null and is not null are the only guaranteed ways to check for null . For many developers it is still instinctual to use != , so I apologize in advance if you catch me still using it!

If you try to use a member of a variable that might be null , use the null-conditional operator, ?. , as shown in the following code:

string authorName = null; int? authorNameLength;

// The following throws a NullReferenceException. authorNameLength = authorName.Length;

// Instead of throwing an exception, null is assigned. authorNameLength = authorName?.Length;

Sometimes, you want to either assign a variable to a result or use an alternative value, such as 3 , if the variable is null . You do this using the null-coalescing operator, ?? , as shown in the following code:

// Result will be 25 if authorName?.Length is null. authorNameLength = authorName?.Length ?? 25;

Checking for null in method parameters‌

Even if you enable NRTs, when defining methods with parameters, it is good practice to check for null values.In earlier versions of C#, you would have to write if statements to check for null parameter values and then throw an ArgumentNullException for any parameter that is null , as shown in the following code:

public void Hire(Person manager, Person employee)

{

if (manager is null)

{

throw new ArgumentNullException(paramName: nameof(manager));

}

if (employee is null)

{

throw new ArgumentNullException(paramName: nameof(employee));

}

...

}

C# 10 introduced a convenience method to throw an exception if an argument is null , as shown in the following code:

public void Hire(Person manager, Person employee)

{

ArgumentNullException.ThrowIfNull(manager); ArgumentNullException.ThrowIfNull(employee);

...

}

C# 11 previews proposed and introduced a new !! operator that does this for you, as shown in the following code:

public void Hire(Person manager!!, Person employee!!)

{

...

}

The if statement and throwing of the exception would be done for you. The code is injected and executed before any statements that you write.This proposal was controversial within the C# developer community. Some would prefer the use of attributes to decorate parameters instead of a pair of characters. The .NET product team said they reduced the .NET libraries by more than 10,000 lines of code by using this feature. That sounds like a good reason to use it to me! And no one must use it if they choose not to. Unfortunately, the team eventually decided to remove the feature, so now we all have to write the null checks manually. If you're interested in this story, then you can read more about it at the following link:https://devblogs.microsoft.com/dotnet/csharp-11-preview-updates/#remove- parameter-null-checking-from-c-11I include this story in this book because I think it's an interesting example of Microsoft being transparent, by developing .NET in the open and listening to and responding to feedback from the community.

Good Practice: Always remember that nullable is a warning check, not an enforcement. You can read more about the compiler warnings relating to null at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler- messages/nullable-warnings.

That's more than enough talk about "nothing"! Let's look at the meat of this chapter, inheritance.

Inheriting from classes‌

The Person type we created earlier derived (inherited) from System.Object . Now, we will create a subclass that inherits from Person :

In the PacktLibrary project, add a new class file named Employee.cs .

Modify its contents to define a class named Employee that derives from Person , as shown in the following code:

namespace Packt.Shared;

public class Employee : Person

{

}

1. In the PeopleApp project, in Program.cs , add statements to create an instance of the

Employee class, as shown in the following code:

Employee john = new()

{

Name = "John Jones",

Born = new(year: 1990, month: 7, day: 28,

hour: 0, minute: 0, second: 0, offset: TimeSpan.Zero))

};

john.WriteToConsole();

1. Run the PeopleApp project and view the result, as shown in the following output:

John Jones was born on a Saturday.

Note that the Employee class has inherited all the members of Person .

Extending classes to add functionality‌

Now, we will add some employee-specific members to extend the class:

1. In Employee.cs , add statements to define two properties, for an employee code and the date they were hired (we do not need to know a start time, so we can use the DateOnly type), as shown in the following code:

public string? EmployeeCode { get; set; } public DateOnly HireDate { get; set; }

1. In Program.cs , add statements to set John's employee code and hire date, as shown in the following code:

john.EmployeeCode = "JJ001";

john.HireDate = new(year: 2014, month: 11, day: 23); WriteLine($"{john.Name} was hired on {john.HireDate:yyyy-MM-dd}.");

1. Run the PeopleApp project and view the result, as shown in the following output:

John Jones was hired on 2014-11-23.

Hiding members‌

So far, the WriteToConsole method is inherited from Person , and it only outputs the employee's name and date and time of birth. We might want to change what this method does for an employee:

1. In Employee.cs , add statements to redefine the WriteToConsole method, as highlighted in the following code:

namespace Packt.Shared;

public class Employee : Person

{

public string? EmployeeCode { get; set; } public DateOnly HireDate { get; set; } public void WriteToConsole()

{

WriteLine(format:

"{0} was born on {1:dd/MM/yy} and hired on {2:dd/MM/yy}.", arg0: Name, arg1: Born, arg2: HireDate);

}

}

1. Run the PeopleApp project, view the result, and note that the first line of output is before the employees were hired; hence, it has a default date, as shown in the following output:

John Jones was born on 28/07/90 and hired on 01/01/01. John Jones was hired on 2014-11-23.

Your coding tool warns you that your method now hides the method from Person by drawing a squiggle under the method name, the PROBLEMS/Error List window includes more details, and the compiler will output a warning when you build and run the console application, as shown in Figure 6.4:

image

Figure 6.4: Hidden method warning

As the warning describes, you should hide this message by applying the new keyword to the method, indicating that you are deliberately replacing the old method, as highlighted in the following code:

public new void WriteToConsole()

Make this fix now.

Understanding this and base keywords‌

There are two special C# keywords that can be used to refer to the current object instance or the base class that it inherits from:

image

this : Represents the current object instance. For example, in the Person class instance members (but not in static members), you could use the expression this.Born to access the Born field of the current object instance. You rarely need to use it, since the expression Born would also work. It is only when there is a local variable also named Born that you would need to use this.Born , to explicitly say you are referring to the field, not the local variable.

image

base : Represents the base class that the current object inherits from. For example, anywhere in the Person class, you could use the expression base.ToString() to call the base class implementation of that method.

You will (hopefully) remember from Chapter 5, Building Your Own Types with Object- Oriented Programming, that to access static members you must use the type name.

Overriding members‌

Rather than hiding a method, it is usually better to override it. You can only override it if the base class chooses to allow overriding, by applying the virtual keyword to any methods that should allow overriding.Let's see an example:

1. In Program.cs , add a statement to write the value of the john variable to the console using its string representation, as shown in the following code:

WriteLine(john.ToString());

1. Run the PeopleApp project and note that the ToString method is inherited from System.Object , so the implementation returns the namespace and type name, as shown in the following output:

Packt.Shared.Employee

1. In Person.cs (not in the Employee class!), override this behavior by adding a ToString method to output the name of the person as well as the type name, as shown in the following code:

#region Overridden methods

public override string ToString()

{

return $"{Name} is a {base.ToString()}.";

}

#endregion

The base keyword allows a subclass to access members of its superclass, that is, the

base class that it inherits or derives from.

1. Run the PeopleApp project and view the result. Now, when the ToString method is called, it outputs the person's name, as well as returning the base class's implementation of ToString , as shown in the following output:

John Jones is a Packt.Shared.Employee.

Good Practice: Many real-world APIs, for example, Microsoft's Entity Framework Core, Castle's DynamicProxy, and Optimizely CMS's content models, require the properties that you define in your classes to be marked as virtual so that they can be overridden.

Carefully decide which of your method and property members should be marked as virtual .

Inheriting from abstract classes‌

Earlier in this chapter, you learned about interfaces that can define a set of members that a type must have to meet a basic level of functionality. These are very useful, but their main limitation is that until C# 8 they could not provide any implementation of their own.This is a particular problem if you still need to create class libraries that will work with .NET Framework and other platforms that do not support .NET Standard 2.1.In those earlier platforms, you could use an abstract class as a sort of halfway house between a pure interface and a fully implemented class.When a class is marked as abstract , this means that it cannot be instantiated because you have indicated that the class is not complete. It needs more implementation before it can be instantiated. For example, the System.IO.Stream class is abstract because it implements common functionality that all streams would need but is not complete, and therefore, it is useless without more implementation that is specific to the type of stream, so you cannot instantiate it using new Stream() .Let's compare the two types of interface and the two types of class, as shown in the following code:

public interface INoImplementation // C# 1 and later.

{

void Alpha(); // Must be implemented by derived type.

}

public interface ISomeImplementation // C# 8 and later.

{

void Alpha(); // Must be implemented by derived type. void Beta()

{

// Default implementation; can be overridden.

}

}

public abstract class PartiallyImplemented // C# 1 and later.

{

public abstract void Gamma(); // Must be implemented by derived type. public virtual void Delta() // Can be overridden.

{

// Implementation.

}

}

public class FullyImplemented : PartiallyImplemented, ISomeImplementation

{

public void Alpha()

{

// Implementation.

}

public override void Gamma()

{

// Implementation.

}

}

// You can only instantiate the fully implemented class. FullyImplemented a = new();

// All the other types give compile errors. PartiallyImplemented b = new(); // Compile error! ISomeImplementation c = new(); // Compile error! INoImplementation d = new(); // Compile error!

Choosing between an interface and an abstract class‌

You have now seen examples of implementing the concept of abstraction using either an interface or an abstract class. Which should you pick? Now that an interface can have default implementations for its members, is the abstract keyword for a class obsolete?Well, let’s think about a real example. Stream is an abstract class. Would or could the .NET team use an interface for that today? Every member of an interface must be public (or at least match the interface's access level which could be internal if it should only be used in the class library that it's defined in). An abstract class has more flexibility in its members' access modifiers.Another advantage of an abstract class over an interface is that serialization often does not work for an interface. So, no, we still need to be able to define abstract classes.

Preventing inheritance and overriding‌

You can prevent another developer from inheriting from your class by applying the sealed keyword to its definition. For example, no one can inherit from Scrooge McDuck, as shown in the following code:

public sealed class ScroogeMcDuck

{

}

An example of sealed in .NET is the string class. Microsoft has implemented some extreme optimizations inside the string class that could be negatively affected by your inheritance, so Microsoft prevents that.You can prevent someone from further overriding a virtual method in your class by applying the sealed keyword to the method. For example, no one can change the way Lady Gaga sings, as shown in the following code:

namespace Packt.Shared; public class Singer

{

// Virtual allows this method to be overridden. public virtual void Sing()

{

WriteLine("Singing...");

}

}

public class LadyGaga : Singer

{

// The sealed keyword prevents overriding the method in subclasses. public sealed override void Sing()

{

WriteLine("Singing with style...");

}

}

You can only seal an overridden method.

Understanding polymorphism‌

You have now seen two ways to change the behavior of an inherited method. We can hide it using the new keyword (known as non-polymorphic inheritance), or we can override it (known as polymorphic inheritance).Both ways can access members of the base or superclass by using the base keyword, so what is the difference?It all depends on the type of variable holding a reference to the object. For example, a variable of the Person type can hold a reference to a Person class, or any type that derives from Person .Let's see how this could affect your code:

1. In Employee.cs , add statements to override the ToString method so that it writes the employee's name and code to the console, as shown in the following code:

public override string ToString()

{

return $"{Name}'s code is {EmployeeCode}.";

}

1. In Program.cs , write statements to create a new employee named Alice stored in a variable of type Employee , also store Alice in a second variable of type Person , and then call both variables' WriteToConsole and ToString methods, as shown in the following code:

Employee aliceInEmployee = new()

{ Name = "Alice", EmployeeCode = "AA123" }; Person aliceInPerson = aliceInEmployee; aliceInEmployee.WriteToConsole(); aliceInPerson.WriteToConsole(); WriteLine(aliceInEmployee.ToString()); WriteLine(aliceInPerson.ToString());

1. Run the PeopleApp project and view the result, as shown in the following output:

Alice was born on 01/01/01 and hired on 01/01/01 Alice was born on a Monday

Alice's code is AA123 Alice's code is AA123

When a method is hidden with new , the compiler is not smart enough to know that the object is an Employee , so it calls the WriteToConsole method in Person .When a method is overridden with virtual and override , the compiler is smart enough to know that although the variable is declared as a Person class, the object itself is an Employee class, and therefore, the Employee implementation of ToString is called.The member modifiers and the effect they have are summarized in the following table:

Variable type Member modifier Method executed In class

Person WriteToConsole Person

Employee new WriteToConsole Employee

Person

virtual

ToString

Employee

Employee

override

ToString

Employee

In my opinion, polymorphism is academic to most programmers. If you get the concept, that's cool; but, if not, I suggest that you don't worry about it. Some people like to make others feel inferior by saying understanding polymorphism is important for all C# programmers, but in my opinion, it's not. There are thousands of other topics that your time and effort will be better spent on.You can have a successful career with C# and never need to be able to explain polymorphism, just as a racing car driver doesn't need to explain the engineering behind fuel injection.

Good Practice: You should use virtual and override rather than new to change the implementation of an inherited method whenever possible.

Casting within inheritance hierarchies‌

Casting between types is subtly different from converting between types. Casting is between similar types, like between a 16-bit integer and a 32-bit integer, or between a superclass and one of its subclasses. Converting is between dissimilar types, such as between text and a number.For example, if you need to work with multiple types of stream , then instead of declaring specific types of stream like MemoryStream or FileStream , you could declare an array of Stream , the supertype of MemoryStream and FileStream .

Implicit casting‌

In the previous example, you saw how an instance of a derived type can be stored in a variable of its base type (or its base's base type, and so on). When we do this, it is called implicit casting.

Explicit casting‌

The opposite of implicit casting is explicit casting, and you must use parentheses around the type you want to cast into as a prefix to do it:

1. In Program.cs , add a statement to assign the aliceInPerson variable to a new Employee

variable, as shown in the following code:

Employee explicitAlice = aliceInPerson;

Your coding tool displays a red squiggle and a compile error, as shown in Figure 6.5:

image

Figure 6.5: A missing explicit cast compile error

Change the statement to prefix the assigned variable name with a cast to the Employee

type, as shown highlighted in the following code:

Employee explicitAlice = (Employee)aliceInPerson;

Avoiding casting exceptions‌

The compiler is now happy; however, because aliceInPerson might be a different derived type, like Student instead of Employee , we need to be careful. In a real application with more complex code, the current value of this variable could have been set to a Student instance, and then this statement would throw an InvalidCastException error at runtime.

Using is to check a type‌

We can handle this by writing a try statement, but there is a better way. We can check the type of an object using the is keyword:

1. Wrap the explicit cast statement in an if statement, as highlighted in the following code:

if (aliceInPerson is Employee)

{

WriteLine($"{nameof(aliceInPerson)} is an Employee."); Employee explicitAlice = (Employee)aliceInPerson;

// Safely do something with explicitAlice.

}

1. Run the PeopleApp project and view the result, as shown in the following output:

aliceInPerson is an Employee.

You could simplify the code further using a declaration pattern, and this will avoid the need to perform an explicit cast, as shown in the following code:

if (aliceInPerson is Employee explicitAlice)

{

WriteLine($"{nameof(aliceInPerson)} is an Employee.");

// Safely do something with explicitAlice.

}

What if you want to execute a block of statements when Alice is not an employee?In the past, you would have had to use the ! (not) operator, as shown in the following code:

if (!(aliceInPerson is Employee))

With C# 9 and later, you can use the not keyword, as shown in the following code:

if (aliceInPerson is not Employee)

Using as to cast a type‌

Alternatively, you can use the as keyword to cast a type. Instead of throwing an exception, the as keyword returns null if the type cannot be cast:

1. In Program.cs , add statements to cast Alice using the as keyword, and then check whether the return value is not null, as shown in the following code:

Employee? aliceAsEmployee = aliceInPerson as Employee; if (aliceAsEmployee is not null)

{

WriteLine($"{nameof(aliceInPerson)} as an Employee.");

// Safely do something with aliceAsEmployee.

}

Since accessing a member of a null variable will throw a NullReferenceException error, you should always check for null before using the result.

1. Run the PeopleApp project and view the result, as shown in the following output:

aliceInPerson as an Employee.

Good Practice: Use the is and as keywords to avoid throwing exceptions when casting between derived types. If you don't do this, you must write try - catch statements for InvalidCastException .

Inheriting and extending .NET types‌

.NET has pre-built class libraries containing hundreds of thousands of types. Rather than creating your own completely new types, you can often get a head start by deriving from one of Microsoft's types to inherit some or all its behavior, and then overriding or extending it.

Inheriting exceptions‌

As an example of inheritance, we will derive a new type of exception:

In the PacktLibrary project, add a new class file named PersonException.cs .

Modify the contents of the file to define a class named PersonException with three constructors, as shown in the following code:

namespace Packt.Shared;

public class PersonException : Exception

{

public PersonException() : base() { }

public PersonException(string message) : base(message) { } public PersonException(string message, Exception innerException)

: base(message, innerException) { }

}

Unlike ordinary methods, constructors are not inherited, so we must explicitly declare and explicitly call the base constructor implementations in System.Exception (or whichever exception class you derived from) to make them available to programmers who might want to use those constructors with our custom exception.

1. In Person.cs , add statements to define a method that throws an exception if a date/time parameter is earlier than a person's date and time of birth, as shown in the following code:

public void TimeTravel(DateTime when)

{

if (when <= Born) { throw new PersonException("If you travel back in time to a date earlier than your own birth, } else { WriteLine($"Welcome to {when:yyyy}!"); } } 1. In Program.cs , add statements to test what happens when employee John Jones tries to time-travel too far back, as shown in the following code: try { john.TimeTravel(when: new(1999, 12, 31)); john.TimeTravel(when: new(1950, 12, 25)); } catch (PersonException ex) { WriteLine(ex.Message); } 1. Run the PeopleApp project and view the result, as shown in the following output: Welcome to 1999! If you travel back in time to a date earlier than your own birth, then the universe will explode Good Practice: When defining your own exceptions, give them the same three constructors that explicitly call the built-in ones in System.Exception . Other exceptions that you might inherit from may have more. Extending types when you can't inherit‌ Earlier, we saw how the sealed modifier can be used to prevent inheritance.Microsoft has applied the sealed keyword to the System.String class so that no one can inherit and potentially break the behavior of strings.Can we still add new methods to strings? Yes, if we use a language feature named extension methods, which was introduced with C# 3.0. To properly understand extension methods, we need to review static methods first. Using static methods to reuse functionality‌ Since the first version of C#, we've been able to create static methods to reuse functionality, such as the ability to validate that a string contains an email address. The implementation will use a regular expression that you will learn more about in Chapter 8, Working with Common .NET Types.Let's write some code: In the PacktLibrary project, add a new class file named StringExtensions.cs . Modify StringExtensions.cs , as shown in the following code, and note the following: image The class imports a namespace to handle regular expressions. image The IsValidEmail method is static , and it uses the Regex type to check for matches against a simple email pattern that looks for valid characters before and after the @ symbol: using System.Text.RegularExpressions; // To use Regex. namespace Packt.Shared; public class StringExtensions { public static bool IsValidEmail(string input) { // Use a simple regular expression to check // that the input string is a valid email. return Regex.IsMatch(input, @"[a-zA-Z0-9\.-_]+@[a-zA-Z0-9\.-_]+"); } } 1. In Program.cs , add statements to validate two examples of email addresses, as shown in the following code: string email1 = "pamela@test.com"; string email2 = "ian&test.com"; WriteLine("{0} is a valid e-mail address: {1}", arg0: email1, arg1: StringExtensions.IsValidEmail(email1)); WriteLine("{0} is a valid e-mail address: {1}", arg0: email2, arg1: StringExtensions.IsValidEmail(email2)); 1. Run the PeopleApp project and view the result, as shown in the following output: pamela@test.com is a valid e-mail address: True ian&test.com is a valid e-mail address: False This works, but extension methods can reduce the amount of code we must type and simplify the usage of this function. Using extension methods to reuse functionality‌ It is easy to turn static methods into extension methods: 1. In StringExtensions.cs , add the static modifier before the class, and then add the this modifier before the string type, as highlighted in the following code: public static class StringExtensions { public static bool IsValidEmail(this string input) { Good Practice: These two changes tell the compiler that it should treat the method as one that extends the string type. 1. In Program.cs , add statements to use the extension method for string values that need to be checked for valid email addresses, as shown in the following code: WriteLine("{0} is a valid e-mail address: {1}", arg0: email1, arg1: email1.IsValidEmail()); WriteLine("{0} is a valid e-mail address: {1}", arg0: email2, arg1: email2.IsValidEmail()); Note the subtle simplification in the syntax to call the IsValidEmail method. The older, longer syntax still works too. The IsValidEmail extension method now appears to be a method just like all the actual instance methods of the string type, such as IsNormalized , except with a small down arrow on the method icon to indicate an extension method, as shown in Figure 6.6: image Figure 6.6: Extension methods appear in IntelliSense alongside instance methods Run the PeopleApp project and view the result, which will be the same as before. Good Practice: Extension methods cannot replace or override existing instance methods. You cannot, for example, redefine the Insert method. The extension method will appear as an overload in IntelliSense, but an instance method will be called in preference to an extension method with the same name and signature. Although extension methods might not seem to give a big benefit, in Chapter 11, Querying and Manipulating Data Using LINQ, you will see some extremely powerful uses of extension methods. Summarizing custom type choices‌ Now that we have covered OOP and the C# features that enable you to define your own types, let's summarize what you've learned. Categories of custom type and their capabilities‌ class Yes Single Reference Heap sealed class Yes None Reference Heap abstract class No Single Reference Heap record or record class Yes Single Value Heap struct or record struct Yes None Value Stack interface No Multiple Reference Heap Categories of custom type and their capabilities are summarized in the following table: Type Instantiation Inheritance Equality Memory It is best to think about these differences by starting with the "normal" case and then spotting the differences in other cases. For example, a "normal" class can be instantiated with new , it supports single inheritance, it uses memory reference equality, and its state is stored in heap memory.Now let's highlight what is different about the more specialized types of classes: image A sealed class does not support inheritance. image An abstract class does not allow instantiation with new . image A record class uses value equality instead of reference equality. We can do the same for other types compared to a "normal" class: image A struct or record struct does not support inheritance, it uses value equality instead of reference equality, and its state is stored in stack memory. image An interface does not allow instantiation with new and supports multiple inheritance. Mutability and records‌ A common misconception is that record types are immutable, meaning its instance property and field values cannot be changed after initialization. However, the mutability of a record type actually depends on how the record is defined. Let's explore mutability: In the PacktLibrary project, add a new class file named Mutability.cs . Modify Mutability.cs , as shown in the following code, and note the following: namespace Packt.Shared; // A mutable record class. public record class C1 { public string? Name { get; set; } } // An immutable record class. public record class C2(string? Name); // A mutable record struct. public record struct S1 { public string? Name { get; set; } } // Another mutable record struct. public record struct S2(string? Name); // An immutable record struct. public readonly record struct S3(string? Name); 1. In the PeopleApp project, in Program.cs , create an instance of each type, setting the initial Name value to Bob , and then modify the Name property to Bill , you will see the two types that are immutable after initialization because they will give the compiler error CS8852 , as shown in the following code: C1 c1 = new() { Name = "Bob" }; c1.Name = "Bill"; C2 c2 = new(Name: "Bob"); c2.Name = "Bill"; // CS8852: Init-only property. S1 s1 = new() { Name = "Bob" }; s1.Name = "Bill"; S2 s2 = new(Name: "Bob"); s2.Name = "Bill"; S3 s3 = new(Name: "Bob"); s3.Name = "Bill"; // CS8852: Init-only property. Note that record C1 is mutable and C2 is immutable. Note that S1 and S2 are mutable and S3 is immutable. Comment out the two statements that cause compiler errors. Microsoft made some interesting design choices with records. Make sure you remember the subtle differences in behavior when combining record, class, struct and using different types of declaration of each. Comparing inheritance and implementation‌ For me, the terms inherit and implement are different, and in the early days of C# and .NET you could strictly apply them to classes and interfaces respectfully. For example, the FileStream class inherits from the Stream class, and the Int32 struct implements the IComparable interface.Inherit implies some functionality that a subclass gets "for free" by inheriting from its base aka super class. Implement implies some functionality that is NOT inherited but instead MUST be provided by the subclass. This is why I chose to title this chapter Implementing Interfaces and Inheriting Classes.Before C# 8, interfaces were always purely contracts. There was no functionality in an interface that you could inherit. In those days, you could strictly use the term implement for interfaces that represent a list of members that your type must implement, and use the term inherit for classes with functionality that your type can inherit and potentially override.With C# 8, interfaces can now include default implementations, making them more like abstract classes, and the term inherit for an interface that has default implementations does make sense. But I feel uncomfortable with this capability, as do many other .NET developers because it messes up what used to be a clean language design. Default interfaces also require changes to the underlying .NET runtime, so they cannot be used with legacy platforms like .NET Standard 2.0 class libraries and .NET Framework.Classes can also have abstract members, for example, methods or properties without any implementation, just like an interface could have. When a subclass inherits from this class, it MUST provide an implementation of those abstract members, and the base class must be decorated with the abstract keyword to prevent it from being instantiated using new because it is missing some functionality. Reviewing illustrative code‌ Let's review some example code that illustrates some of the important differences between types. Note the following: image image To simplify the code, I have left out access modifiers like private and public . Instead of normal brace formatting, to save vertical space I have put all the method implementation on one statement, for example: void M1() { /* implementation */ } image Using "I" as a prefix for interfaces is a convention, not a requirement. It is useful to highlight interfaces using this prefix, since only interfaces support multiple inheritance. Here's the code: // These are both "classic" interfaces in that they are pure contracts. // They have no functionality, just the signatures of members that // must be implemented. interface IAlpha { // A method that must be implemented in any type that implements // this interface. void M1(); } interface IBeta { void M2(); // Another method. } // A type (a struct in this case) implementing an interface. // ": IAlpha" means Gamma promises to implement all members of IAlpha. struct Gamma : IAlpha { void M1() { /* implementation */ } } // A type (a class in this case) implementing two interfaces. class Delta : IAlpha, IBeta { void M1() { /* implementation */ } void M2() { /* implementation */ } } // A sub class inheriting from a base aka super class. // ": Delta" means inherit all members from Delta. class Episilon : Delta { // This can be empty because this inherits M1 and M2 from Delta. // You could also add new members here. } // A class with one inheritable method and one abstract method // that must be implemented in sub classes. A class with at least // one abstract member must be decorated with the abstract keyword // to prevent instantiation. abstract class Zeta { // An implemented method would be inherited. void M3() { /* implementation */ } // A method that must be implemented in any type that inherits // this abstract class. abstract void M4(); } // A class inheriting the M3 method from Zeta but it must provide // an implementation for M4. class Eta : Zeta { void M4() { /* implementation */ } } // In C# 8 and later, interfaces can have default implementations // as well as members that must be implemented. // Requires: .NET Standard 2.1, .NET Core 3.0 or later. interface ITheta { void M3() { /* implementation */ } void M4(); } // A class inheriting the default implementation from an interface // and must provide an implementation for M4. class Iota : ITheta { void M4() { /* implementation */ } } Practicing and exploring‌ Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring this chapter's topics with more in-depth research. Exercise 6.1 – Test your knowledge‌ Answer the following questions: What is a delegate? What is an event? How are a base class and a derived class related, and how can the derived class access the base class? What is the difference between is and as operators? Which keyword is used to prevent a class from being derived from or a method from being further overridden? Which keyword is used to prevent a class from being instantiated with the new keyword? Which keyword is used to allow a member to be overridden? What's the difference between a destructor and a deconstruct method? What are the signatures of the constructors that all exceptions should have? What is an extension method, and how do you define one? Exercise 6.2 – Practice creating an inheritance hierarchy‌ Explore inheritance hierarchies by following these steps: Add a new console app named Ch06Ex02Inheritance to your Chapter06 solution. Create a class named Shape with properties named Height , Width , and Area . Add three classes that derive from it - Rectangle , Square , and Circle —with any additional members you feel are appropriate and that override and implement the Area property correctly. In Program.cs , add statements to create one instance of each shape, as shown in the following code: Rectangle r = new(height: 3, width: 4.5); WriteLine($"Rectangle H: {r.Height}, W: {r.Width}, Area: {r.Area}"); Square s = new(5); WriteLine($"Square H: {s.Height}, W: {s.Width}, Area: {s.Area}"); Circle c = new(radius: 2.5); WriteLine($"Circle H: {c.Height}, W: {c.Width}, Area: {c.Area}"); 1. Run the console app and ensure that the result looks like the following output: Rectangle H: 3, W: 4.5, Area: 13.5 Square H: 5, W: 5, Area: 25 Circle H: 5, W: 5, Area: 19.6349540849362 Exercise 6.3 – Writing better code‌ Read the following online-only section to learn how to use analyzers to write better code: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch06-writing-better-code.md. Exercise 6.4 – Explore topics‌ Use the links on the following page to learn more about the topics covered in this chapter: https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-6--- implementing-interfaces-and-inheriting-classes Summary‌ In this chapter, you learned about: image image Operators Generic types image image Delegates and events Implementing interfaces image image Memory usage differences between reference and value types Working with null values image Deriving and casting types using inheritance image Base and derived classes, how to override a type member, and using polymorphism In the next chapter, you will learn how .NET is packaged and deployed, and in subsequent chapters, the types that it provides you with to implement common functionality, such as file handling and database access. 7 Packaging and Distributing .NET Types‌‌‌ Join our book community on Discord https://packt.link/EarlyAccess image This chapter is about how C# keywords are related to .NET types, and about the relationship between namespaces and assemblies. You'll become familiar with how to package and publish your .NET apps and libraries for cross-platform use. We also cover how to decompile .NET assemblies for learning purposes and why you cannot prevent others from decompiling your code.In an online-only section, Exercise 7.3 – Porting from .NET Framework to modern .NET, you can learn how to use legacy .NET Framework libraries in .NET libraries and the possibility of porting legacy .NET Framework code bases to modern .NET. In another online- only section, Exercise 7.4 – Creating source generators, you will learn how to create source generators that can dynamically add source code to your projects, a very powerful feature.This chapter covers the following topics: image image The road to .NET 8 Understanding .NET components image image image Publishing your applications for deployment Native ahead-of-time compilation Decompiling .NET assemblies image image Packaging your libraries for NuGet distribution Working with preview features The road to .NET 8‌ This part of the book is about the functionality in the Base Class Library (BCL) APIs provided by .NET and how to reuse functionality across all the different .NET platforms using .NET Standard.From .NET Core 2.0 onward, the support for a minimum of .NET Standard 2.0 is important because it provides many of the APIs that were missing from the first version of .NET Core. The 15 years' worth of libraries and applications that .NET Framework developers had available to them that are relevant for modern development have now been migrated to .NET and can run cross-platform on macOS and Linux variants, as well as on Windows..NET Standard 2.1 added about 3,000 new APIs. Some of those APIs need runtime changes that would break backward compatibility, so .NET Framework 4.8 only implements .NET Standard 2.0; .NET Core 3.0, Xamarin, Mono, and Unity implement .NET Standard 2.1..NET 5 removed the need for .NET Standard because all project types can now target a single version of .NET. The same applies to .NET 6 and later. Each version from .NET 5 onward is backward compatible with previous versions. This means a class library that targets .NET 5 can be used by any .NET 5 or later projects of any type. Now that .NET versions have been released with full support for mobile and desktop apps built using .NET MAUI, the need for .NET Standard has been further reduced.Since you might still need to create class libraries for legacy .NET Framework projects or legacy Xamarin mobile apps, there is still a need to create .NET Standard 2.0 class libraries. And currently, you must also use a .NET Standard 2.0 class library to create a source generator.To summarize the progress that .NET has made since the first version of .NET Core in 2016, I have compared the major .NET Core and modern .NET versions with the equivalent .NET Framework versions in the following list: image .NET Core 1.x: Much smaller API compared to .NET Framework 4.6.1, which was the current version in March 2016. image .NET Core 2.x: Reached API parity with .NET Framework 4.7.1 for modern APIs because they both implement .NET Standard 2.0. image .NET Core 3.x: Larger API compared to .NET Framework for modern APIs because .NET Framework 4.8 does not implement .NET Standard 2.1. image .NET 5: Even larger API compared to .NET Framework 4.8 for modern APIs, with much- improved performance. image .NET 6: Continued improvements to performance and expanded APIs with optional support for mobile apps in .NET MAUI that was added in May 2022. image .NET 7: Final unification with the support for mobile apps with .NET MAUI available as an optional workload. This book does not cover .NET MAUI development. Packt has multiple books that specialize in .NET MAUI, and you can find them by searching their website. image .NET 8: Continues to improve the platform and should be used for all new development. .NET Core 1.0, June 2016‌ Implemented an API suitable for building modern cross-platform apps, including web and cloud applications and services for Linux using ASP.NET Core. .NET Core 1.1, November 2016‌ Fixed bugs, increased the number of Linux distributions supported, supported .NET Standard 1.6, and improved performance, especially with ASP.NET Core for web apps and services. .NET Core 2.0, August 2017‌ Implemented .NET Standard 2.0, the ability to reference .NET Framework libraries, and added more performance improvements. .NET Core 2.1, May 2018‌ Focused on an extendable tooling system, added new types like Span , new APIs for cryptography and compression, a Windows Compatibility Pack with an additional 20,000 APIs to help port old Windows applications, Entity Framework Core value conversions, LINQ GroupBy conversions, data seeding, query types, and even more performance improvements, including the topics listed in Table 7.1:

Feature Chapter Topic

Spans 8 Working with spans, indexes, and ranges Brotli compression 9 Compressing with the Brotli algorithm EF Core lazy loading 10 Enabling lazy loading

EF Core data seeding 10 Understanding data seeding Table 7.1: Features of .NET Core 2.1

.NET Core 2.2, December 2018‌

Focused on diagnostic improvements for the runtime, optional tiered compilation, and added new features to ASP.NET Core and Entity Framework Core like spatial data support using types from the NetTopologySuite (NTS) library, query tags, and collections of owned entities.

.NET Core 3.0, September 2019‌

Added support for building Windows desktop applications using Windows Forms (2001), Windows Presentation Foundation (WPF; 2006), and Entity Framework 6.3. Also introduced side-by-side and app-local deployments; a fast JSON reader; serial port access and other pinout access for Internet of Things (IoT) solutions; and tiered compilation by default, including the topics listed in Table 7.2:

Feature Chapter Topic

Embedding .NET in-app 7 Publishing your applications for deployment Index and Range 8 Working with spans, indexes, and ranges System.Text.Json 9 High-performance JSON processing

Table 7.2: Features of .NET Core 3.0

.NET Core 3.1, December 2019‌

Added bug fixes and refinements so that it could be a Long Term Support (LTS) release.

.NET 5, November 2020‌

Unified the various .NET platforms except mobile, refined the platform, and improved performance, including the topics listed in Table 7.3:

Feature Chapter Topic

Half type 8 Working with numbers

Regular expression performance improvements 8 Regular expression performance

improvements

System.Text.Json improvements 9 High-performance JSON processing

EF Core generated SQL 10 Getting the generated SQL

EF Core Filtered Include 10 Filtering included entities

EF Core Scaffold-DbContext now singularizes using Humanizer

Table 7.3: Features of .NET 5

.NET 6, November 2021‌

10 Scaffolding models using an existing database

Added more features to EF Core for data management, new types for working with dates and times, and improved performance yet again, including the topics listed in Table 7.4:

Feature Chapter Topic

Check .NET SDK status 7 Checking your .NET SDKs for updates

Link trim mode as default 7 Reducing the size of apps using app trimming

EnsureCapacity for List 8 Improving performance by ensuring the capacity of a collection

Low-level file API using

RandomAccess

Reading and writing with random access handles

EF Core configuration 10 Configuring preconvention models

conventions

New LINQ methods

11

Building LINQ expressions with the Enumerable class

TryGetNonEnumeratedCount

Table 7.4: Features of .NET 6

11

Aggregating sequences

.NET 7, November 2022‌

Finally unified with the mobile platform, added more features like string syntax coloring and IntelliSense, support for creating and extracting Tar archives, and improving the performance of inserts and updates with EF Core, including the topics listed in Table 7.5:

Feature Chapter Topic

[StringSyntax] attribute 8 Activating regular expression syntax coloring

[GeneratedRegex] attribute 8 Improving regular expression performance with source

generators

Tar archive support 9 Exercise 9.3 – Working with Tar archives

ExecuteUpdate and

ExecuteDelete

More efficient updates and deletes

Order and OrderDescending 11 Sorting by the item itself Table 7.5: Features of .NET 7

.NET 8, November 2023‌

Improved native AOT (ahead-of-time) compilation support, and added some advanced features for library authors related to source generation, including the topics listed in Table 7.6:

Feature Chapter Topic

Terminal logger for build output and changed default publish configuration

Managing projects using the dotnet CLI

Simplified output paths 7 Controlling where build artifacts are created

Improved native AOT support 7 Native ahead-of-time compilation

New Random methods 8 Generating random numbers for games and similar apps

New array, collection, and span initialization syntax

Initializing collections using collection expressions

Frozen collections 8 Read-only, immutable, and frozen collections

New data validation attributes 10 Using EF Core annotation attributes to define the model

Table 7.6: Features of .NET 8

Checking your .NET SDKs for updates‌

Introduced with .NET 6, Microsoft added a command to check the versions of .NET SDKs and runtimes that you have installed and warn you if any need updating. For example, enter the following command:

dotnet sdk check

You will see results, including the status of available updates, as shown in the following partial output:

.NET SDKs:

Version Status

5.0.214

.NET 5.0 is out of support.

6.0.101

Patch 6.0.119 is available.

6.0.314

Up to date.

7.0.304

Patch 7.0.305 is available.

8.0.100

Up to date.

Good Practice: To maintain support from Microsoft, you must keep your .NET SDKs and

.NET runtimes up to date with the latest patches.

Understanding .NET components‌

.NET is made up of several pieces, which are shown in the following list:

image

Language compilers: These turn your source code written with languages such as C#, F#, and Visual Basic into intermediate language (IL) code stored in assemblies. With C# 6 and later, Microsoft switched to an open-source rewritten compiler known as Roslyn, which is also used by Visual Basic.

image

Common Language Runtime (CoreCLR): This runtime loads assemblies, compiles the IL code stored in them into native code instructions for your computer's CPU, and executes the code within an environment that manages resources such as threads and memory.

image

Base Class Libraries (BCL or CoreFX): These are prebuilt assemblies of types packaged and distributed using NuGet for performing common tasks when building applications. You can use them to quickly build anything you want, rather like combining LEGO™ pieces.

Assemblies, NuGet packages, and namespaces‌

An assembly is where a type is stored in the filesystem. Assemblies are a mechanism for deploying code. For example, the System.Data.dll assembly contains types for managing data. To use types in other assemblies, they must be referenced. Assemblies can be static (pre- created) or dynamic (generated at runtime). Dynamic assemblies are an advanced feature that we will not cover in this book. Assemblies can be compiled into a single file as a DLL (class library) or an EXE (console app).Assemblies are distributed as NuGet packages, which are files that are downloadable from public online feeds and can contain multiple assemblies and other resources. You will also hear about project SDKs, workloads, and platforms, which are combinations of NuGet packages.Microsoft's NuGet feed is found here: https://www.nuget.org/.

What is a namespace?‌

A namespace is the address of a type. Namespaces are a mechanism to uniquely identify a type by requiring a full address rather than just a short name. In the real world, Bob of 34 Sycamore Street is different from Bob of 12 Willow Drive.In .NET, the IActionFilter interface of the System.Web.Mvc namespace is different from the IActionFilter interface of the System.Web.Http.Filters namespace.

Dependent assemblies‌

If an assembly is compiled as a class library and provides types for other assemblies to use, then it has the file extension .dll (dynamic link library), and it cannot be executed standalone.Likewise, if an assembly is compiled as an application, then it has the file extension .exe (executable) and can be executed standalone. Before .NET Core 3, console apps were compiled to .dll files and had to be executed by the dotnet run command or a host

executable.Any assembly can reference one or more class library assemblies as dependencies, but you cannot have circular references. So, assembly B cannot reference assembly A if assembly A already references assembly B. The compiler will warn you if you attempt to add a dependency reference that would cause a circular reference. Circular references are often a warning sign of poor code design. If you are sure that you need a circular reference, then use an interface to solve it.

Microsoft .NET project SDKs‌

By default, console applications have a dependency reference on the Microsoft .NET project SDK. This platform contains thousands of types in NuGet packages that almost all applications would need, such as the System.Int32 and System.String types.When using .NET, you reference the dependency assemblies, NuGet packages, and platforms that your application needs in a project file.Let's explore the relationship between assemblies and namespaces:

Use your preferred code editor to create a new project, as defined in the following list:

image

Project template: Console App / console

image

Project file and folder: AssembliesAndNamespaces

image

Solution file and folder: Chapter07

Open AssembliesAndNamespaces.csproj and note that it is a typical project file for a

.NET application, as shown in the following markup:

Exe

net8.0

enable

enable

1. After the section, add a new section to statically import System.Console for all C# files using the implicit usings .NET SDK feature, as shown in the following markup:

Namespaces and types in assemblies‌

Many common .NET types are in the System.Runtime.dll assembly. There is not always a one-to- one mapping between assemblies and namespaces. A single assembly can contain many namespaces and a namespace can be defined in many assemblies. You can see the relationship between some assemblies and the namespaces that they supply types for, as shown in Table 7.7:

Assembly Example namespaces Example types

System.Runtime.dll System , System.Collections ,

System.Collections.Generic

Int32 , String ,

IEnumerable

System.Console.dll System Console

System.Threading.dll System.Threading Interlocked , Monitor ,

Mutex

System.Xml.XDocument.dll System.Xml.Linq XDocument , XElement ,

XNode

Table 7.7: Examples of assemblies and their namespaces

NuGet packages‌

.NET is split into a set of packages, distributed using a Microsoft-supported package management technology named NuGet. Each of these packages represents a single assembly of the same name. For example, the System.Collections package contains the System.Collections.dll assembly.The following are the benefits of packages:

image

image

Packages can be easily distributed on public feeds. Packages can be reused.

image

Packages can ship on their own schedule.

image

Packages can be tested independently of other packages.

image

Packages can support different OSes and CPUs by including multiple versions of the same assembly built for different OSes and CPUs.

image

Packages can have dependencies specific to only one library.

image

Apps are smaller because unreferenced packages aren't part of the distribution. Table

7.8 lists some of the more important packages and their important types:

Package Important types

System.Runtime Object , String , Int32 , Array System.Collections List , Dictionary System.Net.Http HttpClient , HttpResponseMessage System.IO.FileSystem File , Directory System.Reflection Assembly , TypeInfo , MethodInfo

Table 7.8: Some important packages and their important types

Understanding frameworks‌

There is a two-way relationship between frameworks and packages. Packages define the APIs, while frameworks group packages. A framework without any packages would not define any APIs..NET packages each support a set of frameworks. For example, the System.IO.FileSystem package version 4.3.0 supports the following frameworks:

image

.NET Standard, version 1.3 or later.

image

.NET Framework, version 4.6 or later.

image

Six Mono and Xamarin platforms (for example, Xamarin.iOS).

More Information: You can read the details at the following link: https://www.nuget.org/packages/System.IO.FileSystem/ #supportedframeworks-body-tab.

Importing a namespace to use a type‌

Let's explore how namespaces are related to assemblies and types:

1. In the AssembliesAndNamespaces project, in Program.cs , delete the existing statements and then enter the following code:

XDocument doc = new();

Recent versions of code editors will often automatically add a namespace import statement to fix the problem I want you to see. Please delete the using statement that your code editor writes for you.

1. Build the project and note the compiler error message, as shown in the following output:

CS0246 The type or namespace name 'XDocument' could not be found (are you missing a using direct

The XDocument type is not recognized because we have not told the compiler what the namespace of the type is. Although this project already has a reference to the assembly that contains the type, we also need to either prefix the type name with its namespace, for example, System.Xml.Linq.XDocument , or import the namespace.

Click inside the XDocument class name. Your code editor displays a light bulb, showing that it recognizes the type and can automatically fix the problem for you.

Click the light bulb, and select using System.Xml.Linq; from the menu.

This will import the namespace by adding a using statement to the top of the file. Once a namespace is imported at the top of a code file, then all the types within the namespace are available for use in that code file by just typing their name, without the type name needing to be fully qualified by prefixing it with its namespace.I like to add a comment after importing a namespace to remind me why I need to import that namespace, as shown in the following code:

using System.Xml.Linq; // To use XDocument.

If you do not comment your namespaces, you or other developers will not know why they are imported and might delete them, breaking the code. Or they might never delete imported namespaces "just in case" they are needed, potentially cluttering the code unnecessarily. This is why most modern code editors have features to remove unused namespaces. This technique also subconsciously trains you, while you are learning, to remember which namespace you need to import to use a particular type or extension method.

Relating C# keywords to .NET types‌

One of the common questions I get from new C# programmers is, "What is the difference between string with a lowercase s and String with an uppercase S?"The short answer is easy: none. The long answer is that all C# keywords that represent types like string or int are aliases for a .NET type in a class library assembly.When you use the string keyword, the compiler recognizes it as a System.String type. When you use the int type, the compiler recognizes it as a System.Int32 type.Let's see this in action with some code:

1. In Program.cs , declare two variables to hold string values, one using lowercase string

and one using uppercase String , as shown in the following code:

string s1 = "Hello"; String s2 = "World"; WriteLine($"{s1} {s2}");

Run the AssembliesAndNamespaces project and note that string and String both work and literally mean the same thing.

In AssembliesAndNamespaces.csproj , add an entry to prevent the System namespace from being globally imported, as shown in the following markup:

1. In Program.cs , and in the Error List or PROBLEMS window, note the compiler error message, as shown in the following output:

CS0246 The type or namespace name 'String' could not be found (are you missing a using directive

1. At the top of Program.cs , import the System namespace with a using statement that will fix the error, as shown in the following code:

using System; // To use String.

Good Practice: When you have a choice, use the C# keyword instead of the actual type because the keywords do not need a namespace to be imported.

Mapping C# aliases to .NET types‌

Table 7.9 shows the 18 C# type keywords along with their actual .NET types:

Keyword .NET type Keyword .NET type string System.String char System.Char sbyte System.SByte byte System.Byte short System.Int16 ushort System.UInt16 int System.Int32 uint System.UInt32 long System.Int64 ulong System.UInt64 nint System.IntPtr nuint System.UIntPtr float System.Single double System.Double decimal System.Decimal bool System.Boolean

object System.Object dynamic System.Dynamic.DynamicObject

Table 7.9: C# type keywords and their actual .NET types

Other .NET programming language compilers can do the same thing. For example, the Visual Basic .NET language has a type named Integer , which is its alias for System.Int32 .

Understanding native-sized integers‌

C# 9 introduced the nint and nuint keyword aliases for native-sized integers, meaning that the storage size for the integer value is platform-specific. They store a 32-bit integer in a 32-bit process and sizeof() returns 4 bytes; they store a 64-bit integer in a 64-bit process and sizeof() returns 8 bytes. The aliases represent pointers to the integer value in memory, which is why their .NET names are IntPtr and UIntPtr . The actual storage type will be either System.Int32 or System.Int64 , depending on the process.In a 64-bit process, the following code:

WriteLine($"Environment.Is64BitProcess = {Environment.Is64BitProcess}"); WriteLine($"int.MaxValue = {int.MaxValue:N0}"); WriteLine($"nint.MaxValue = {nint.MaxValue:N0}");

produces this output:

Environment.Is64BitProcess = True int.MaxValue = 2,147,483,647

nint.MaxValue = 9,223,372,036,854,775,807

Revealing the location of a type‌

Code editors provide built-in documentation for .NET types. Let's start by making sure that you have the expected experience and then explore:

image

If you are using Visual Studio 2022, then make sure you have disabled Source Link: Navigate to Tools | Options.

image

image

In the search box, enter navigation in source . Select Text Editor | C# | Advanced.

image

Clear the Enable navigation to Source Link and Embedded sources checkbox, and then click OK.

Right-click inside XDocument and choose Go to Definition.

Navigate to the top of the code file, expand the collapsed region, and note the assembly filename is System.Xml.XDocument.dll , but the class is in the System.Xml.Linq namespace, as shown in the following code and in Figure 7.1:

#region Assembly System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a

// C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.0\ref\net8.0\System.Runtime.dll #endregion

image

Figure 7.1: Assembly and namespace that contains the XDocument type

Close the XDocument [from metadata] tab.

Right-click inside string or String and choose Go to Definition.

Navigate to the top of the code file, expand the collapsed region, and note the assembly filename is System.Runtime.dll but the class is in the System namespace.

Your code editor is technically lying to you. If you remember when we wrote code in Chapter 2, Speaking C#, when we revealed the extent of the C# vocabulary, we discovered that the System.Runtime.dll assembly contains zero types.What the System.Runtime.dll assembly does contain are type-forwarders. These are special types that appear to exist in an assembly but are implemented elsewhere. In this case, they are implemented deep inside the .NET runtime using highly optimized code.You might want to use a type-forwarder if you refactor a type to move it from its original assembly to a different one. Without defining a type-forwarder, any projects that reference the original assembly will not find the type in it and a runtime exception will be thrown. You can read more about this contrived example at the following link: https://learn.microsoft.com/en-us/dotnet/standard/assembly/type-forwarding.

Sharing code with legacy platforms using .NET Standard‌

Before .NET Standard, there were Portable Class Libraries (PCLs). With PCLs, you could create a library of code and explicitly specify which platforms you want the library to support, such as Xamarin, Silverlight, and Windows 8. Your library could then use the intersection of APIs that are supported by the specified platforms.Microsoft realized that this was unsustainable, so they created .NET Standard—a single API that all future .NET platforms would support. There are older versions of .NET Standard, but .NET Standard 2.0 was an attempt to unify all important recent .NET platforms. .NET Standard 2.1 was released in late 2019 but only .NET Core 3.0 and that year's version of Xamarin support its new features. For the rest of this book, I will use the term .NET Standard to mean .NET Standard 2.0..NET Standard is like HTML5 in that they are both standards that a platform should support. Just as Google's Chrome browser and Microsoft's Edge browser implement the HTML5 standard, .NET Core, .NET Framework, and Xamarin all implement .NET Standard. If you want to create a library of types that will work across variants of legacy .NET, you can do so most easily with .NET Standard.

Good Practice: Since many of the API additions in .NET Standard 2.1 required runtime changes, and .NET Framework is Microsoft's legacy platform that needs to remain as unchanging as possible, .NET Framework 4.8 remained on .NET Standard 2.0 rather than implementing .NET Standard 2.1. If you need to support .NET Framework customers, then you should create class libraries on .NET Standard 2.0, even though it is not the latest and does not support all the recent language and BCL new features.

Your choice of which .NET Standard version to target comes down to a balance between maximizing platform support and available functionality. A lower version supports more platforms but has a smaller set of APIs. A higher version supports fewer platforms but has a larger set of APIs. Generally, you should choose the lowest version that supports all the APIs that you need.

Understanding defaults for class libraries with different SDKs‌

When using the dotnet SDK tool to create a class library, it might be useful to know which target framework will be used by default, as shown in Table 7.10:

SDK Default target framework for new class libraries

.NET Core 3.1 netstandard2.0

.NET 6 net6.0

.NET 7 net7.0

.NET 8 net8.0

Table 7.10: .NET SDKs and their default target framework for new class libraries

Of course, just because a class library targets a specific version of .NET by default, it does not mean you cannot change it after creating a class library project using the default template.You can manually set the target framework to a value that supports the projects that need to reference that library, as shown in Table 7.11:

Class library target framework

Can be used by projects that target

netstandard2.0 .NET Framework 4.6.1 or later, .NET Core 2 or later, .NET 5 or later, Mono

5.4 or later, Xamarin.Android 8 or later, and Xamarin.iOS 10.14 or later.

netstandard2.1 .NET Core 3 or later, .NET 5 or later, Mono 6.4 or later, Xamarin.Android

10 or later, and Xamarin.iOS 12.16 or later.

net6.0 .NET 6 or later.

net7.0 .NET 7 or later.

net8.0 .NET 8 or later.

Table 7.11: Class library target frameworks and the projects that can use them

Good Practice: Always check the target framework of a class library and then manually change it to something more appropriate if necessary. Make a conscious decision about what it should be rather than accepting the default.

Creating a .NET Standard class library‌

We will create a class library using .NET Standard 2.0 so that it can be used across all important .NET legacy platforms and cross-platform on Windows, macOS, and Linux operating systems, while also having access to a wide set of .NET APIs:

1. Use your preferred code editor to add a new Class Library / classlib project named

SharedLibrary that targets .NET Standard 2.0 to the Chapter07 solution:

image

If you use Visual Studio 2022, when prompted for the Target Framework, select .NET Standard 2.0, and then configure the startup project for the solution to the current selection.

image

If you use Visual Studio Code, include a switch to target .NET Standard 2.0, as shown in the following command:

dotnet new classlib -f netstandard2.0

Good Practice: If you need to create types that use new features in .NET 8, as well as types that only use .NET Standard 2.0 features, then you can create two separate class libraries: one targeting .NET Standard 2.0 and one targeting .NET 8.

An alternative to manually creating two class libraries is to create one that supports multi-targeting. If you would like me to add a section about multi-targeting to the next edition, please let me know. You can read about multi-targeting here: https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/cross-platform- targeting#multi-targeting.

Controlling the .NET SDK‌

By default, executing dotnet commands uses the highest version installed .NET SDK. There may be times when you want to control which SDK is used.For example, once .NET 9 is available in preview starting in February 2024, or the final version in November 2024, you might install it. But you would probably want your experience to match the book steps that use the .NET 8 SDK. But once you install a .NET 9 SDK, then it will be used by default. You can control the .NET SDK used by default by using a global.json file, which contains the version to use. The dotnet command searches the current folder and then each ancestor folder in turn for a global.json file to see if it should use a different .NET SDK version.You do not need to complete the following steps, but if you want to try and do not already have .NET 6 SDK installed, then you can install it from the following link:https://dotnet.microsoft.com/download/dotnet/6.0

Create a subdirectory/folder in the Chapter07 folder named ControlSDK .

On Windows, start Command Prompt or Windows Terminal. On macOS, start Terminal. If you are using Visual Studio Code, then you can use the integrated terminal.

In the ControlSDK folder, at the command prompt or terminal, enter a command to list the installed .NET SDKs, as shown in the following command:

dotnet --list-sdks

1. Note the results and the version number of the latest .NET 6 SDK installed, as shown highlighted in the following output:

5.0.214 [C:\Program Files\dotnet\sdk]

6.0.314 [C:\Program Files\dotnet\sdk]

7.0.304 [C:\Program Files\dotnet\sdk]

8.0.100 [C:\Program Files\dotnet\sdk]

1. Create a global.json file that forces the use of the latest .NET Core 6.0 SDK that you have installed (which might be later than mine), as shown in the following command:

dotnet new globaljson --sdk-version 6.0.314

1. Note the result, as shown in the following output:

The template "global.json file" was created successfully.

1. Use your preferred code editor to open the global.json file and review its contents, as shown in the following markup:

{

"sdk": {

"version": "6.0.314"

}

}

For example, to open it with Visual Studio Code, enter the command: code global.json .

1. In the ControlSDK folder, at the command prompt or terminal, enter a command to create a class library project, as shown in the following command:

dotnet new classlib

1. If you do not have the .NET 6 SDK installed, then you will see an error, as shown in the following output:

Could not execute because the application was not found or a compatible .NET SDK is not installe

1. If you do have the .NET 6 SDK installed, then a class library project will be created that targets .NET 6 by default, as shown highlighted in the following markup:

net6.0

enable

enable

Mixing SDKs and framework targets‌

Many organizations decide to target a long-term support version of .NET to get up to three years of support from Microsoft. Doing this does not mean you lose the benefits of improvements to the C# language during the lifetime of the .NET runtime that you need to target.You can easily continue to target the .NET 8 runtime while installing and using future C# compilers, as shown in Figure 7.2 and illustrated in the following list:

November 2023: Install .NET SDK 8.0.100 and use it to build projects that target .NET 8 and use the C# 12 compiler by default. Every month, update to .NET 8 SDK patches on the development computer and update to .NET 8 runtime patches on any deployment computers.

February 2024: Optionally, install .NET SDK 9 Preview 1 to explore the new C# language and .NET library features. Note that you won't be able to use new library features while targeting .NET 8. Previews are released monthly between February and October each year. Read the monthly announcement blog posts to find out about the new features in that preview.

November 2024: Install .NET SDK 9.0.100 and use it to build projects that continue to target .NET 8 and use the C# 13 compiler for its new features. You will be using a fully supported SDK and fully supported runtime. You can also use new features in EF Core 9 because it will continue to target .NET 8.

February 2025: Optionally, install .NET 10 previews to explore new C# language and .NET library features. Start planning if any new library and ASP.NET Core features in .NET 9 and .NET 10 can be applied to your .NET 8 projects when you are ready to migrate.

November 2025: Install .NET 10.0.100 SDK and use it to build projects that target .NET

8 and use the C# 14 compiler. Migrate your .NET 8 projects to .NET 10 since it is an LTS release. You have until November 2026 to complete the migration when .NET 8 reaches end- of-life.

image

Figure 7.2: Targeting .NET 8 for long-term support while using the latest C# compilers

When deciding to install a .NET SDK, remember that the latest is used by default to build any .NET projects. Once you've installed a .NET 9 SDK preview, it will be used by default for all projects, unless you force the use of an older, fully supported SDK version like

8.0.100 or a later patch.

Publishing your code for deployment‌

If you write a novel and you want other people to read it, you must publish it.Most developers write code for other developers to use in their own projects, or for users to run as an app. To do so, you must publish your code as packaged class libraries or executable applications.There are three ways to publish and deploy a .NET application. They are:

image

image

image

Framework-dependent deployment (FDD) Framework-dependent executable (FDE) Self-contained

If you choose to deploy your application and its package dependencies, but not .NET itself, then you rely on .NET already being on the target computer. This works well for web applications deployed to a server because .NET and lots of other web applications are likely already on the server.FDD means you deploy a DLL that must be executed by the dotnet command-line tool. FDE means you deploy an EXE that can be run directly from the command line. Both require the appropriate version of the .NET runtime to be installed on the system.Sometimes, you want to be able to give someone a USB stick containing your application built for their operating system and know that it can execute on their computer. You would want to perform a self-contained deployment. While the size of the deployment files will be larger, you'll know that it will work.

Creating a console app to publish‌

Let's explore how to publish a console app:

Use your preferred code editor to add a new Console App / console project named

DotNetEverywhere to the Chapter07 solution. Make sure you target .NET 8.

Modify the project file to statically import the System.Console class in all C# files.

In Program.cs , delete the existing statements, and then add a statement to output a message saying the console app can run everywhere and some information about the operating system, as shown in the following code:

WriteLine("I can run everywhere!");

WriteLine($"OS Version is {Environment.OSVersion}."); if (OperatingSystem.IsMacOS())

{

WriteLine("I am macOS.");

}

else if (OperatingSystem.IsWindowsVersionAtLeast(major: 10, build: 22000))

{

WriteLine("I am Windows 11.");

}

else if (OperatingSystem.IsWindowsVersionAtLeast(major: 10))

{

WriteLine("I am Windows 10.");

}

else

{

WriteLine("I am some other mysterious OS.");

}

WriteLine("Press any key to stop me.");

ReadKey(intercept: true); // Do not output the key that was pressed.

1. Run the DotNetEverywhere project and note the results when run on Windows 11, as shown in the following output:

I can run everywhere!

OS Version is Microsoft Windows NT 10.0.22000.0. I am Windows 11.

Press any key to stop me.

1. In DotNetEverywhere.csproj , add the runtime identifiers (RIDs) to target three operating systems inside the element, as shown highlighted in the following markup:

Exe

net8.0

enable

enable

win10-x64;osx-x64;osx.11.0-arm64;linux-x64;linux-arm64

image

The win10-x64 RID value means Windows 10 or Windows Server 2016 64-bit. You could also use the win10-arm64 RID value to deploy to a Microsoft Surface Pro X.

image

The osx-x64 RID value means macOS Sierra 10.12 or later. You can also specify version- specific RID values like osx.10.15-x64 (Catalina), osx.11.0-x64 (Big Sur on Intel), or osx.11.0-arm64 (Big Sur on Apple Silicon).

image

The linux-x64 RID value means most desktop distributions of Linux, like Ubuntu, CentOS,

Debian, or Fedora. Use linux-arm for Raspbian or Raspberry Pi OS 32-bit. Use

linux-arm64 for a Raspberry Pi running Ubuntu 64-bit.

There are two elements that you can use to specify runtime identifiers. Use

if you only need to specify one. Use if you need to specify multiple, as we did in the preceding example. If you use the wrong one,

then the compiler will give an error and it can be difficult to understand why with only one character difference!

Understanding dotnet commands‌

When you install the .NET SDK, it includes a command-line interface (CLI) named dotnet .The

.NET CLI has commands that work on the current folder to create a new project using templates:

On Windows, start Command Prompt or Windows Terminal. On macOS, start Terminal. If you prefer to use Visual Studio 2022 or Visual Studio Code, then you can use the integrated terminal.

Enter the dotnet new list (or dotnet new -l or dotnet new --list with older SDKs) command to list your currently installed templates, the most common of which are shown in Table 7.12:

Template Name Short Name Language

.NET MAUI App maui C#

.NET MAUI Blazor App maui-blazor C#

ASP.NET Core Empty web C#, F#

ASP.NET Core gRPC Service grpc C#

ASP.NET Core Web API webapi C#, F# ASP.NET Core Web API (native AOT) api C# ASP.NET Core Web App (Model-View-Controller) mvc C#, F# Blazor Web App blazor C#

Class Library classlib C#, F#, VB

Console App console C#, F#, VB

EditorConfig File editorconfig

global.json File globaljson

Solution File sln

xUnit Test Project xunit

Table 7.12: Project template full and short names

.NET MAUI projects are not supported for Linux. The team has said they have left that work to the open source community. If you need to create a truly cross-platform graphical app, then take a look at Avalonia at the following link: https://avaloniaui.net/.

Getting information about .NET and its environment‌

It is useful to see what .NET SDKs and runtimes are currently installed, alongside information about the operating system, as shown in the following command:

dotnet --info

Note the results, as shown in the following partial output:

.NET SDK (reflecting any global.json): Version: 8.0.100

Commit: 3fe444af72 Runtime Environment:

OS Name: Windows

OS Version: 10.0.22621 OS Platform: Windows RID: win10-x64

Base Path: C:\Program Files\dotnet\sdk\8.0.100\

.NET workloads installed:

There are no installed workloads to display. Host (useful for support):

Version: 8.0.0 Commit: bc78804f5d

.NET SDKs installed:

5.0.214 [C:\Program Files\dotnet\sdk]

6.0.317 [C:\Program Files\dotnet\sdk]

7.0.401 [C:\Program Files\dotnet\sdk]

8.0.100 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:

Microsoft.AspNetCore.App 5.0.17 [...\dotnet\shared\Microsoft.AspNetCore.All]

...

Managing projects using the dotnet CLI‌

The .NET CLI has the following commands that work on the project in the current folder, to manage the project:

image

dotnet help : This shows the command-line help.

image

dotnet new : This creates a new .NET project or file.

image

dotnet tool : This installs or manages tools that extend the .NET experience.

image

dotnet workload : This manages optional workloads like .NET MAUI.

image

dotnet restore : This downloads dependencies for the project.

image

dotnet build : This builds, aka compiles, a .NET project. A new switch introduced with

.NET 8 is --tl (meaning terminal logger), which provides a modern output. For example, it provides real-time information about what the build is doing. You can learn more at the following link: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet- build#options.

image

dotnet build-server : This interacts with servers started by a build.

image

dotnet msbuild : This runs MS Build Engine commands.

image

image

image

dotnet clean : This removes the temporary outputs from a build. dotnet test : This builds and then runs unit tests for the project. dotnet run : This builds and then runs the project.

image

dotnet pack : This creates a NuGet package for the project.

image

dotnet publish : This builds and then publishes the project, either with dependencies or as a self-contained application. In .NET 7 and earlier, this published the Debug configuration by default. In .NET 8 and later, it now publishes the Release configuration by default.

image

dotnet add : This adds a reference to a package or class library to the project.

image

dotnet remove : This removes a reference to a package or class library from the project.

image

dotnet list : This lists the package or class library references for the project.

Publishing a self-contained app‌

Now that you have seen some example dotnet tool commands, we can publish our cross-platform console app:

At the command prompt or terminal, make sure that you are in the DotNetEverywhere

folder.

Enter a command to build and publish the self-contained release version of the console application for Windows 10, as shown in the following command:

dotnet publish -c Release -r win10-x64 --self-contained

1. Note the build engine restores any needed packages, compiles the project source code into an assembly DLL, and creates a publish folder, as shown in the following output:

MSBuild version 17.8.0+14c24b2d3 for .NET Determining projects to restore...

All projects are up-to-date for restore.

DotNetEverywhere -> C:\cs12dotnet8\Chapter07\DotNetEverywhere\bin\Release\net8.0\win10-x64\Dot DotNetEverywhere -> C:\cs12dotnet8\Chapter07\DotNetEverywhere\bin\Release\net8.0\win10-x64\pub

1. Enter the following commands to build and publish the release versions for macOS and Linux variants:

dotnet publish -c Release -r osx-x64 --self-contained

dotnet publish -c Release -r osx.11.0-arm64 --self-contained dotnet publish -c Release -r linux-x64 --self-contained dotnet publish -c Release -r linux-arm64 --self-contained

Good Practice: You could automate these commands by using a scripting language like PowerShell and execute the script file on any operating system using the cross-platform PowerShell Core. I have done this for you at the following link: https://github.com/markjprice/cs12dotnet8/tree/main/scripts/publish-scripts.

Open Windows File Explorer or a macOS Finder window, navigate to DotNetEverywhere\bin\Release\net8.0 , and note the output folders for the five operating systems.

In the win10-x64 folder, open the publish folder, and note all the supporting assemblies, like Microsoft.CSharp.dll .

Select the DotNetEverywhere executable file, and note it is 154 KB, as shown in Figure 7.3:

image

Figure 7.3: The DotNetEverywhere executable file for Windows 10 64-bit

If you are on Windows, then double-click to execute the program and note the result, as shown in the following output:

I can run everywhere!

OS Version is Microsoft Windows NT 10.0.22621.0. I am Windows 11.

Press any key to stop me.

Press any key to close the console app and its window.

Note that the total size of the publish folder and all its files is 68.3 MB.

In the osx.11.0-arm64 folder, select the publish folder, note all the supporting assemblies, and then select the DotNetEverywhere executable file. Note that the executable is 125 KB, and the publish folder is about 73.9 MB. There is no .exe file extension for published applications on macOS, so the filename will not have an extension.

If you copy any of those publish folders to the appropriate operating system, the console app will run; this is because it is a self-contained deployable .NET application. For

example, here it is on macOS with Intel:

I can run everywhere!

OS Version is Unix 13.5.2 I am macOS.

Press any key to stop me.

This example used a console app, but you could just as easily create an ASP.NET Core website or web service, or a Windows Forms or WPF app. Of course, you can only deploy Windows desktop apps to Windows computers, not Linux or macOS.

Publishing a single-file app‌

If you can assume that .NET is already installed on the computer on which you want to run your app, then you can use the extra flags when you publish your app for release to say that it does not need to be self-contained and that you want to publish it as a single file (if possible), as shown in the following command (which must be entered on a single line):

dotnet publish -r win10-x64 -c Release --no-self-contained

/p:PublishSingleFile=true

This will generate two files: DotNetEverywhere.exe and DotNetEverywhere.pdb . The .exe file is the executable. The .pdb file is a program debug database file that stores debugging information.If you prefer the .pdb file to be embedded in the .exe file, for example, to ensure it is deployed with its assembly, then add a element to the

element in your .csproj file and set it to embedded , as shown highlighted in the following markup:

Exe

net8.0

enable

enable

win10-x64;osx-x64;osx.11.0-arm64;linux-x64;linux-arm64

embedded

If you cannot assume that .NET is already installed on a computer, then although Linux also only generates the two files, expect the following additional files for Windows: coreclr.dll , clrjit.dll , clrcompression.dll , and mscordaccore.dll .Let's see an example for Windows:

1. At the command prompt or terminal, in the DotNetEverywhere folder, enter the command to build the self-contained release version of the console app for Windows 10, as shown in the following command:

dotnet publish -c Release -r win10-x64 --self-contained /p:PublishSingleFile=true

1. Navigate to the DotNetEverywhere\bin\Release\net8.0\win10-x64\publish folder and select the DotNetEverywhere executable file. Note that the executable is now 62.6 MB, and there is also a .pdb file that is 11 KB. The sizes of these files on your system will vary.

Reducing the size of apps using app trimming‌

One of the problems with deploying a .NET app as a self-contained app is that the .NET libraries take up a lot of space. One of the biggest needs is to reduce the size of Blazor WebAssembly components because all the .NET libraries need to be downloaded to the browser.Luckily, you can reduce this size by not packaging unused assemblies with your

deployments. Introduced with .NET Core 3, the app trimming system can identify the assemblies needed by your code and remove those that are not needed. This was known as copyused trim mode.With .NET 5, the trimming went further by removing individual types, and even members, like methods from within an assembly if they are not used. For example, with a Hello World console app, the System.Console.dll assembly is trimmed from 61.5 KB to 31.5 KB. This was known as link trim mode, but it was not enabled by default.With .NET 6, Microsoft added annotations to their libraries to indicate how they can be safely trimmed so the trimming of types and members was made the default.With .NET 7, Microsoft renamed link to full and copyused to partial .The catch is how well the trimming identifies unused assemblies, types, and members. If your code is dynamic, perhaps using reflection, then it might not work correctly, so Microsoft also allows manual control.There are two ways to enable type-level and member-level, aka full , trimming. Since this level of trimming is the default with .NET 6 or later, all we need to do is enable trimming without setting a trim level or mode.The first way is to add an element in the project file, as shown in the following markup:

true

The second way is to add a flag when publishing, as shown highlighted in the following command:

dotnet publish ... -p:PublishTrimmed=True

There are two ways to enable assembly-level, aka partial , trimming.The first way is to add two elements in the project file, as shown in the following markup:

true

partial

The second way is to add two flags when publishing, as shown highlighted in the following command:

dotnet publish ... -p:PublishTrimmed=True -p:TrimMode=partial

Controlling where build artifacts are created‌

Traditionally, each project has its own bin and obj subfolders where temporary files are created during the build process. When you publish, the files are created in the bin folder.You might prefer to put all these temporary files and folders somewhere else.

Introduced with .NET 8 is the ability to control where build artifacts are created. Let's see how:

1. At the command prompt or terminal for the Chapter07 folder, enter the following command:

dotnet new buildprops --use-artifacts

1. Note the success message, as shown in the following output:

The template "MSBuild Directory.Build.props file" was created successfully.

We could have created this file in the cs12dotnet8 folder, and it would then affect all projects in all chapters.

1. In the Chapter07 folder, open the Directory.Build.props file, as shown in the following markup:

enable

enable

Good Practice: Preview features are not supported in production code. Preview features are likely to have breaking changes before the final release. Enable preview features at your own risk. Switch to a GA release future SDK like .NET 9 to use new compiler features while still targeting older but longer supported versions of .NET like .NET 8.

Requiring preview features‌

The [RequiresPreviewFeatures] attribute is used to indicate assemblies, types, or members that use and therefore require warnings about preview features. A code analyzer then scans for this assembly and generates warnings if needed. If your code does not use any preview features, you will not see any warnings. If you use any preview features, then your code should warn consumers of your code that you use preview features.

Enabling preview features‌

In the Project file, add an element to enable preview features and an element to enable preview language features, as shown highlighted in the following markup:

Exe

net8.0

enable

enable

true

preview

Method interceptors‌

An interceptor is a method that substitutes for a call to an interceptable method with a call to itself. They are an advanced feature most commonly used in source generators. If readers are interested, then I might add a section about them to the ninth edition.

More Information: You can learn more about interceptors at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#interceptors.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring with deeper research into the topics of this chapter.

Exercise 7.1 – Test your knowledge‌

Answer the following questions:

What is the difference between a namespace and an assembly?

How do you reference another project in a .csproj file?

What is the benefit of a tool like ILSpy?

Which .NET type does the C# float alias represent?

When porting an application from .NET Framework to .NET 6, what tool should you run before porting, and what tool could you run to perform much of the porting work?

What is the difference between framework-dependent and self-contained deployments of

.NET applications?

What is a RID?

What is the difference between the dotnet pack and dotnet publish commands?

What types of applications written for the .NET Framework can be ported to modern .NET?

Can you use packages written for .NET Framework with modern .NET?

Exercise 7.2 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-7--- packaging-and-distributing-net-types

Exercise 7.3 – Porting from .NET Framework to modern .NET‌

If you are interested in porting legacy projects from .NET Framework to modern .NET, then I have written an online-only section at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch07-porting.md

Exercise 7.4 – Creating source generators‌

If you are interested in creating source generators, then I have written an online-only section at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch07- source-generators.mdYou can find examples of source generators at the following link:https://github.com/amis92/csharp-source-generators

Exercise 7.5 – Explore PowerShell‌

PowerShell is Microsoft's scripting language for automating tasks on every operating system. Microsoft recommends Visual Studio Code with the PowerShell extension for writing PowerShell scripts.Since PowerShell is its own extensive language, there is not enough space in this book to cover it. You can learn about some key concepts from a Microsoft training module at the following link: https://learn.microsoft.com/en-us/training/modules/introduction-to- powershell/You can read the official documentation at the following link: https://learn.microsoft.com/en-us/powershell/.

Exercise 7.6 – Improving performance in .NET‌

Microsoft has made significant improvements to performance in the past few years. You should review the blog posts written by Stephen Toub to learn what the team changed and why. His posts are famously long, detailed, and brilliant!You can find the posts about the improvements at the following links:https://devblogs.microsoft.com/dotnet/performance- improvements-in-net-core/ - 25 pageshttps://devblogs.microsoft.com/dotnet/performance- improvements-in-net-core-2-1/ - 20 pageshttps://devblogs.microsoft.com/dotnet/performance- improvements-in-net-core-3-0/ - 41 pageshttps://devblogs.microsoft.com/dotnet/performance- improvements-in-net-5/ - 43 pageshttps://devblogs.microsoft.com/dotnet/performance- improvements-in-net-6/ - 100

pageshttps://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/ - 156 pageshttps://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/ - 218 pagesGet

ready for .NET 9's article being almost 300 pages. 😉

Summary‌

In this chapter, we:

image

image

Reviewed the journey to .NET 8 for BCL functionality. Explored the relationship between assemblies and namespaces.

image

image

image

Saw options for publishing an app for distribution to multiple operating systems. Learned how to publish to native AOT for faster startup and smaller memory footprint. Learned how to decompile .NET assemblies for educational purposes.

image

image

Packaged and distributed a class library. Learned how to activate preview features.

In the next chapter, you will learn about some common BCL types that are included with modern .NET.

8 Working with Common .NET Types‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about some common types that are included with .NET. These include types for manipulating numbers, text, and collections; improving working with spans, indexes, and ranges; and in an optional online-only section, working with network resources.This chapter covers the following topics:

image

image

Working with numbers Working with text

image

image

image

Pattern matching with regular expressions Storing multiple objects in collections Working with spans, indexes, and ranges

Working with numbers‌

One of the most common types of data is numbers. The most common types in .NET for working with numbers are shown in Table 8.1:

Namespace Example type(s) Description

System SByte , Int16 , Int32 ,

Int64 , Int128

System Byte , UInt16 , UInt32 ,

UInt64 , UInt128

Integers; that is, zero, and positive and negative whole numbers.

Cardinals; that is, zero and positive whole numbers.

System Half , Single , Double Reals; that is, floating-point numbers.

System Decimal Accurate reals; that is, for use in science, engineering, or financial scenarios.

System.Numerics BigInteger , Complex ,

Quaternion

Table 8.1: Common .NET number types

Arbitrarily large integers, complex numbers, and quaternion numbers.

.NET has had the 32-bit float and 64-bit double types since .NET Framework 1.0 was released in 2002. The IEEE 754 specification also defines a 16-bit floating-point standard. Machine learning and other algorithms would benefit from this smaller, lower-precision number type, so Microsoft introduced the System.Half type with .NET 5 and later. Currently, the C# language does not define a half alias, so you must use the .NET type System.Half . This might change in the future. System.Int128 and System.UInt128 were introduced with .NET 7, and they too do not yet have a C# alias keyword.

Working with big integers‌

The largest whole number that can be stored in .NET types that have a C# alias is about eighteen and a half quintillion, stored in an unsigned 64-bit integer using ulong . But what if you need to store numbers larger than that?Let's explore numerics:

Use your preferred code editor to create a new project, as defined in the following list:

image

image

image

Project template: Console App / console Project file and folder: WorkingWithNumbers Solution file and folder: Chapter08

In the project file, add an element to statically and globally import the System.Console

class.

In Program.cs , delete the existing statements and then add a statement to import

System.Numerics , as shown in the following code:

using System.Numerics; // To use BigInteger.

1. Add statements to output the maximum value of the ulong type, and a number with 30 digits using BigInteger , as shown in the following code:

const int width = 40;

WriteLine("ulong.MaxValue vs a 30-digit BigInteger"); WriteLine(new string('-', width));

ulong big = ulong.MaxValue; WriteLine($"{big,width:N0}"); BigInteger bigger =

BigInteger.Parse("123456789012345678901234567890");

WriteLine($"{bigger,width:N0}");

The width constant with the value 40 in the format code means “right-align 40 characters,” so both numbers are lined up to the right-hand edge. The N0 means “use thousand separators and zero decimal places.”

1. Run the code and view the result, as shown in the following output:

ulong.MaxValue vs a 30-digit BigInteger

image

18,446,744,073,709,551,615

123,456,789,012,345,678,901,234,567,890

Working with complex numbers‌

A complex number can be expressed as a + bi, where a and b are real numbers and i is an imaginary unit, where i2 = −1. If the real part a is zero, it is a pure imaginary number. If the imaginary part b is zero, it is a real number.Complex numbers have practical applications in many STEM (science, technology, engineering, and mathematics) fields of study. They are added by separately adding the real and imaginary parts of the summands; consider this:

(a + bi) + (c + di) = (a + c) + (b + d)i

Let's explore complex numbers:

1. In Program.cs , add statements to add two complex numbers, as shown in the following code:

Complex c1 = new(real: 4, imaginary: 2); Complex c2 = new(real: 3, imaginary: 7); Complex c3 = c1 + c2;

// Output using the default ToString implementation. WriteLine($"{c1} added to {c2} is {c3}");

// Output using a custom format.

WriteLine("{0} + {1}i added to {2} + {3}i is {4} + {5}i", c1.Real, c1.Imaginary,

c2.Real, c2.Imaginary, c3.Real, c3.Imaginary);

1. Run the code and view the result, as shown in the following output:

<4; 2> added to <3; 7> is <7; 9>

4 + 2i added to 3 + 7i is 7 + 9i

.NET 6 and earlier used a different default format for complex numbers:

(4, 2) added to (3, 7) is (7, 9) . In .NET 7 and later, the default format was changed to use angle brackets and semi-colons because some cultures use round brackets to indicate negative numbers and use commas for decimal numbers. At the time of writing, the official documentation has not been updated to use the new format, as shown at the following link: https://learn.microsoft.com/en- us/dotnet/api/system.numerics.complex.tostring.

Generating random numbers for games and similar apps‌

In scenarios that don't need truly random numbers like games, you can create an instance of the Random class, as shown in the following code example:

Random r = new();

Random has a constructor with a parameter for specifying a seed value used to initialize its pseudo-random number generator, as shown in the following code:

Random r = new(Seed: 46378);

As you learned in Chapter 2, Speaking C#, parameter names should use camel case. The developer who defined the constructor for the Random class broke this convention. The parameter name should be seed , not Seed .

Good Practice: Shared seed values act as a secret key, so if you use the same random number generation algorithm with the same seed value in two applications, then they can generate the same "random" sequences of numbers. Sometimes this is necessary, for example, when synchronizing a GPS receiver with a satellite, or when a game needs to randomly generate the same level. But usually, you want to keep your seed secret.

To avoid allocating more memory than necessary, .NET 6 introduced a shared static instance of Random that you can access instead of creating your own.The Random class has commonly used methods for generating random numbers, as described in the following list:

image

Next : This method returns a random int (whole number) and it takes two parameters, minValue and maxValue , but maxValue is not the maximum value that the method returns! It is an exclusive upper bound, meaning maxValue is one more than the maximum value returned. Use the NextInt64 method to return a long integer.

image

NextDouble : This method returns a number that is greater than or equal to 0.0 and less than and never equal to 1.0 . Use the NextSingle method to return a float .

image

NextBytes : This method populates an array of any size with random byte ( 0 to 255 ) values. It is common to format byte values as hexadecimal, for example, 00 to FF .

Let's see some examples of generating pseudo-random numbers:

1. In Program.cs , add statements to access the shared Random instance, and then call its methods to generate random numbers, as shown in the following code:

Random r = Random.Shared;

// minValue is an inclusive lower bound i.e. 1 is a possible value.

// maxValue is an exclusive upper bound i.e. 7 is not a possible value. int dieRoll = r.Next(minValue: 1, maxValue: 7); // Returns 1 to 6.

WriteLine($"Random die roll: {dieRoll}");

double randomReal = r.NextDouble(); // Returns 0.0 to less than 1.0. WriteLine($"Random double: {randomReal}");

byte[] arrayOfBytes = new byte[256];

r.NextBytes(arrayOfBytes); // Fills array with 256 random bytes. Write("Random bytes: ");

for (int i = 0; i < arrayOfBytes.Length; i++) { Write($"{arrayOfBytes[i]:X2} "); } WriteLine(); 1. Run the code and view the result, as shown in the following output: Random die roll: 1 Random double: 0.06735275453092382 Random bytes: D9 38 CD F3 5B 40 2D F4 5B D0 48 DF F7 B6 67 C1 95 A1 2C 58 42 CF 70 6C C3 BE 82 D In scenarios that do need truly random numbers like cryptography, there are specialized types for that, like RandomNumberGenerator . I plan to cover this and other cryptographic types in the companion book, Tools and Skills for .NET 8 Pros in a chapter titled Protecting Data and Apps Using Cryptography, expected to publish in the first half of 2024. .NET 8 introduced two new Random methods, as described in the following list: image GetItems : This method is passed an array or read-only span of any type T of choices and the number of items you want to generate, and then it returns that number of items randomly selected from the choices.

image

Shuffle : This method is passed an array or span of any type T and the order of items is randomized.

Let's see an example of each:

1. In Program.cs , add statements to access the shared Random instance, and then call its methods to generate random numbers, as shown in the following code:

string[] beatles = r.GetItems(

choices: new[] { "John", "Paul", "George", "Ringo" }, length: 10);

Write("Random ten beatles:"); foreach (string beatle in beatles)

{

Write($" {beatle}");

}

WriteLine(); r.Shuffle(beatles); Write("Shuffled beatles:");

foreach (string beatle in beatles)

{

Write($" {beatle}");

}

WriteLine();

1. Run the code and view the result, as shown in the following output:

Random ten beatles: Paul Paul John John John John Paul John George Ringo Shuffled beatles: George John Paul Paul John John John Ringo Paul John

Generating GUIDs‌

A GUID (globally unique identifier) is a 128-bit text string that represents a unique value for identification. As a developer, you will need to generate GUIDs when a unique reference is needed to identify information. Traditionally, database and computer systems may have used an incrementing integer value, but a GUID is more likely to avoid conflicts in multi- tasking systems.The System.Guid type is a value type ( struct ) that represents a GUID value. It has Parse and TryParse methods to take an existing GUID value represented as a string and convert it into the Guid type. It has a NewGuid method to generate a new value.Let's see how we can generate GUID values and output them:

1. In Program.cs , add statements to access the shared Random instance, and then call its methods to generate random numbers, as shown in the following code:

WriteLine($"Empty GUID: {Guid.Empty}."); Guid g = Guid.NewGuid(); WriteLine($"Random GUID: {g}.");

byte[] guidAsBytes = g.ToByteArray(); Write("GUID as byte array: ");

for (int i = 0; i < guidAsBytes.Length; i++) { Write($"{guidAsBytes[i]:X2} "); } WriteLine(); 1. Run the code and view the result, as shown in the following output: Empty GUID: 00000000-0000-0000-0000-000000000000. Random GUID: c7a11eea-45a5-4619-964a-a9cce1e4220c. GUID as byte array: EA 1E A1 C7 A5 45 19 46 96 4A A9 CC E1 E4 22 0C Working with text‌ One of the other most common types of data for variables is text. The most common types in .NET for working with text are shown in Table 8.2: Namespace Type Description System Char Storage for a single text character System String Storage for multiple text characters System.Text StringBuilder Efficiently manipulates strings System.Text.RegularExpressions Regex Efficiently pattern-matches strings Table 8.2: Common .NET types for working with text Getting the length of a string‌ Let's explore some common tasks when working with text; for example, sometimes you need to find out the length of a piece of text stored in a string variable: Use your preferred code editor to add a new Console App / console project named WorkingWithText to the Chapter08 solution. In the WorkingWithText project, in Program.cs , delete the existing statements and then add statements to define a variable to store the name of the city London, and then write its name and length to the console, as shown in the following code: string city = "London"; WriteLine($"{city} is {city.Length} characters long."); 1. Run the code and view the result, as shown in the following output: London is 6 characters long. Getting the characters of a string‌ The string class uses an array of char internally to store the text. It also has an indexer, which means that we can use the array syntax to read its characters. Array indexes start at zero, so the third character will be at index 2.Let's see this in action: 1. Add a statement to write the characters at the first and fourth positions in the string variable, as shown in the following code: WriteLine($"First char is {city[0]} and fourth is {city[3]}."); 1. Run the code and view the result, as shown in the following output: First char is L and fourth is d. Splitting a string‌ Sometimes, you need to split some text wherever there is a character, such as a comma: 1. Add statements to define a single string variable containing comma-separated city names, then use the Split method and specify that you want to treat commas as the separator, and then enumerate the returned array of string values, as shown in the following code: string cities = "Paris,Tehran,Chennai,Sydney,New York,Medellín"; string[] citiesArray = cities.Split(','); WriteLine($"There are {citiesArray.Length} items in the array:"); foreach (string item in citiesArray) { WriteLine($" {item}"); } 1. Run the code and view the result, as shown in the following output: There are 6 items in the array: Paris Tehran Chennai Sydney New York Medellín Later in this chapter, you will learn how to handle more complex string-splitting scenarios using a regular expression. Getting part of a string‌ Sometimes, you need to get part of some text. The IndexOf method has nine overloads that return the index position of a specified char or string within a string . The Substring method has two overloads, as shown in the following list: image Substring(startIndex, length) : Returns part of a string starting at startIndex and containing the next length characters image Substring(startIndex) : Returns part of a string starting at startIndex and containing all characters up to the end of the string Let's explore a simple example: 1. Add statements to store a person's full name in a string variable with a space character between the first and last names, find the position of the space, and then extract the first name and last name as two parts so that they can be recombined in a different order, as shown in the following code: string fullName = "Alan Shore"; int indexOfTheSpace = fullName.IndexOf(' '); string firstName = fullName.Substring( startIndex: 0, length: indexOfTheSpace); string lastName = fullName.Substring( startIndex: indexOfTheSpace + 1); WriteLine($"Original: {fullName}"); WriteLine($"Swapped: {lastName}, {firstName}"); 1. Run the code and view the result, as shown in the following output: Original: Alan Shore Swapped: Shore, Alan If the format of the initial full name was different, for example, "LastName, FirstName" , then the code would need to be different. As an optional exercise, try writing some statements that would change the input "Shore, Alan" into "Alan Shore" . Checking a string for content‌ Sometimes, you need to check whether a piece of text starts or ends with some characters or contains some characters. You can achieve this with methods named StartsWith , EndsWith , and Contains : 1. Add statements to store a string value and then check if it starts with or contains a couple of different string values, as shown in the following code: string company = "Microsoft"; WriteLine($"Text: {company}"); WriteLine("Starts with M: {0}, contains an N: {1}", arg0: company.StartsWith("M"), arg1: company.Contains("N")); 1. Run the code and view the result, as shown in the following output: Text: Microsoft Starts with M: True, contains an N: False Comparing string values‌ Two common tasks with string values are sorting (aka collating) and comparing. For example, when a user enters their username or password, you need to compare what they entered with what is stored.The string class implements the IComparable interface meaning that you can easily compare two string values using the CompareTo instance method and it will return -1 , 0 , or 1 depending on if the value is "less than," "equal to," or "greater than" the other. You saw an example of this when you implemented the IComparable interface for the Person class in Chapter 6, Implementing Interfaces and Inheriting Classes.But the lower or upper casing of characters can affect ordering, and the ordering rules for text are culture dependent. For example, double-L is treated as a single character in traditional Spanish, as shown in Table 8.3: Culture Description Example string values Spanish In 1994, the Royal Spanish Academy issued a Modern: llegar comes before lugar . new alphabetization rule to treat LL and CH Traditional: llegar comes after lugar . as Latin alphabetic characters instead of separate individual characters. Swedish In 2006, the Swedish Academy issued a new rule. Before 2006, V and W were the same character. Since 2006, they are treated as separate characters. German Phonebook ordering is different than dictionary ordering, for example, umlauts are sorted as combinations of letters. German The character ß is sorted as SS . This is a common issue with addresses since the word for street is Straße . Swedish words mostly only use V. Loanwords (words taken from other languages) that contain W can now keep those Ws instead of replacing the Ws with Vs. Müller and Mueller in phonebook ordering are the same name. Straße and Strasse have the same meaning. Table 8.3: Examples of ordering rules in European languages For consistency and performance, you sometimes want to make comparisons in a culture- invariant way. It is therefore better to use the static method Compare .Let's see some examples: 1. At the top of Program.cs , import the namespace for working with cultures and enable special characters like the Euro currency symbol, as shown in the following code: using System.Globalization; // To use CultureInfo. OutputEncoding = System.Text.Encoding.UTF8; // Enable Euro symbol. 1. In Program.cs , define some text variables and compare them in different cultures, as shown in the following code: CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US"); string text1 = "Mark"; string text2 = "MARK"; WriteLine($"text1: {text1}, text2: {text2}"); WriteLine("Compare: {0}.", string.Compare(text1, text2)); WriteLine("Compare (ignoreCase): {0}.", string.Compare(text1, text2, ignoreCase: true)); WriteLine("Compare (InvariantCultureIgnoreCase): {0}.", string.Compare(text1, text2, StringComparison.InvariantCultureIgnoreCase)); 1. Run the code, view the result, and note that a lowercase "a" is "less than" ( -1 ) an upper case "A," so the comparison returns -1. But we can either set an option to ignore case, or even better, do a culture- and case-invariant comparison to treat the two string values as equal ( 0 ), as shown in the following output: text1: Mark, text2: MARK Compare: -1. Compare (ignoreCase): 0. Compare (InvariantCultureIgnoreCase): 0. More Information: You can learn more about string comparisons at the following link: https://learn.microsoft.com/en-us/globalization/locale/sorting-and-string-comparison. Joining, formatting, and other string members‌ There are many other string members, as shown in Table 8.4: Member Description Trim , TrimStart , TrimEnd These methods trim whitespace characters such as space, tab, and carriage return from the start and/or end. ToUpper , ToLower These convert all the characters into uppercase or lowercase. Insert , Remove These methods insert or remove some text. Replace This replaces some text with other text. string.Empty This can be used instead of allocating memory each time you use a literal string value using an empty pair of double quotes ( "" ). string.Concat This concatenates two string variables. The + operator does the equivalent when used between string operands. string.Join This concatenates one or more string variables with a character in between each one. string.IsNullOrEmpty This checks whether a string variable is null or empty. string.IsNullOrWhiteSpace This checks whether a string variable is null or whitespace; that is, a mix of any number of horizontal and vertical spacing characters, for example, tab, space, carriage return, line feed, and so on. string.Format An alternative method to string interpolation for outputting formatted string values, which uses positioned instead of named parameters. Table 8.4: Joining, formatting, and other string members Some of the preceding methods are static methods. This means that the method can only be called from the type, not from a variable instance. In the preceding table, I indicated the static methods by prefixing them with string. , as in string.Format . Let's explore some of these methods: 1. Add statements to take an array of string values and combine them back together into a single string variable with separators using the Join method, as shown in the following code: string recombined = string.Join(" => ", citiesArray); WriteLine(recombined);

1. Run the code and view the result, as shown in the following output:

Paris => Tehran => Chennai => Sydney => New York => Medellín

1. Add statements to use positioned parameters and interpolated string formatting syntax to output the same three variables twice, as shown in the following code:

string fruit = "Apples"; decimal price = 0.39M; DateTime when = DateTime.Today;

WriteLine($"Interpolated: {fruit} cost {price:C} on {when:dddd}."); WriteLine(string.Format("string.Format: {0} cost {1:C} on {2:dddd}.",

arg0: fruit, arg1: price, arg2: when));

Some code editors like JetBrains Rider will warn you about boxing operations. These are slow but not a problem in this scenario. To avoid boxing, call ToString on price and when .

1. Run the code and view the result, as shown in the following output:

Interpolated: Apples cost $0.39 on Friday. string.Format: Apples cost $0.39 on Friday.

Note that we could have simplified the second statement because Console.WriteLine supports the same format codes as string.Format , as shown in the following code:

WriteLine("WriteLine: {0} cost {1:C} on {2:dddd}.", arg0: fruit, arg1: price, arg2: when);

Building strings efficiently‌

You can concatenate two strings to make a new string using the String.Concat method or simply by using the + operator. But both choices are bad practice when combining more than a few values because .NET must create a completely new string in memory.This might not be noticeable if you are only adding two string values, but if you concatenate inside a loop with many iterations, it can have a significant negative impact on performance and memory use. You can concatenate string variables more efficiently using the StringBuilder type.I have written an online-only section for the companion book, Apps and Services with .NET 8, about performance benchmarking using string concatenations as the main example. You can optionally complete the section and its practical coding tasks at the following link: https://github.com/markjprice/apps-services-net8/blob/main/docs/ch01-benchmarking.md.

More Information: You can see examples of using StringBuilder at the following link: https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder#examples.

Pattern matching with regular expressions‌

Regular expressions are useful for validating input from the user. They are very powerful and can get very complicated. Almost all programming languages have support for regular expressions and use a common set of special characters to define them.Let's try out some example regular expressions:

Use your preferred code editor to add a new Console App / console project named

WorkingWithRegularExpressions to the Chapter08 solution.

In Program.cs , delete the existing statements and then import the following namespace:

using System.Text.RegularExpressions; // To use Regex.

Checking for digits entered as text‌

We will start by implementing the common example of validating number input:

1. In Program.cs , add statements to prompt the user to enter their age and then check that it is valid using a regular expression that looks for a digit character, as shown in the following code:

Write("Enter your age: ");

string input = ReadLine()!; // Null-forgiving operator. Regex ageChecker = new(@"\d"); WriteLine(ageChecker.IsMatch(input) ? "Thank you!" :

$"This is not a valid age: {input}");

Note the following about the code:

image

The @ character switches off the ability to use escape characters in the string . Escape characters are prefixed with a backslash. For example, \t means a tab and \n means a new line. When writing regular expressions, we need to disable this feature. To paraphrase the television show The West Wing, "Let backslash be backslash."

image

Once escape characters are disabled with @ , then they can be interpreted by a regular expression. For example, \d means digit. You will learn about more regular expression symbols that are prefixed with a backslash later in this topic.

1. Run the code, enter a whole number such as 34 for the age, and view the result, as shown in the following output:

Enter your age: 34 Thank you!

1. Run the code again, enter carrots , and view the result, as shown in the following output:

Enter your age: carrots

This is not a valid age: carrots

1. Run the code again, enter bob30smith , and view the result, as shown in the following output:

Enter your age: bob30smith Thank you!

The regular expression we used is \d , which means one digit. However, it does not specify what can be entered before and after that one digit. This regular expression could be described in English as "Enter any characters you want as long as you enter at least one digit character."In regular expressions, you indicate the start of some input with the caret

^ symbol and the end of some input with the dollar $ symbol. Let's use these symbols to indicate that we expect nothing else between the start and end of the input except for a digit.

1. Add a ^ and a $ to change the regular expression to ^\d$ , as shown highlighted in the following code:

Regex ageChecker = new(@"^\d$");

Run the code again and note that it rejects any input except a single digit.

Add a + after the \d expression to modify the meaning to one or more digits, as shown highlighted in the following code:

Regex ageChecker = new(@"^\d+$");

1. Run the code again and note the regular expression only allows zero or positive whole numbers of any length.

Regular expression performance improvements‌

The .NET types for working with regular expressions are used throughout the .NET platform and many of the apps built with it. As such, they have a significant impact on performance. But until now, they have not received much optimization attention from Microsoft.With .NET 5 and later, the types in the System.Text.RegularExpressions namespace have rewritten implementations to squeeze out maximum performance. Common regular expression benchmarks using methods like IsMatch are now five times faster. And the best thing is, you do not have to change your code to get the benefits!With .NET 7 and later, the IsMatch method of the Regex class now has an overload for a ReadOnlySpan as its input, which gives even better performance.

Understanding the syntax of a regular expression‌

Some common symbols that you can use in regular expressions are shown in Table 8.5:

Symbol

Meaning

Symbol

Meaning

^

Start of input

$

End of input

\d

A single digit

\D

A single non -digit

\s

Whitespace

\S

Non -whitespace

\w

Word characters

\W

Non -word characters

[A-Za-z0-9] Range(s) of characters \^ ^ (caret) character

[aeiou] Set of characters [^aeiou] Not in a set of characters

. Any single character \. . (dot) character Table 8.5: Common regular expression symbols

In addition, some common regular expression quantifiers that affect the previous symbols in a regular expression are shown in Table 8.6:

Symbol Meaning Symbol Meaning

+ One or more ? One or none

{3} Exactly three {3,5} Three to five

{3,} At least three {,3} Up to three

Table 8.6: Common regular expression quantifiers

Examples of regular expressions‌

Some examples of regular expressions with a description of their meaning are shown in Table 8.7:

Expression Meaning

\d A single digit somewhere in the input

a The character “a” somewhere in the input

Bob The word “Bob” somewhere in the input

^Bob The word “Bob” at the start of the input

Bob$ The word “Bob” at the end of the input

^\d{2}$ Exactly two digits

^[0-9]{2}$ Exactly two digits

^[A-Z]{4,}$ At least four uppercase English letters in the ASCII character set only

^[A-Za-z]{4,}$ At least four upper or lowercase English letters in the ASCII character set only

^[A-Z]{2}\d{3}$ Two uppercase English letters in the ASCII character set and three digits only

^[A-Za-z\u00c0-\u017e]+$

At least one uppercase or lowercase English letter in the ASCII character set or European letters in the Unicode character set, as shown in the following list:

ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝ

Þßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿıŒœŠšŸŽž

^d.g$ The letter d , then any character, and then the letter g , so it would match both dig and dog or any single character between the d and g

^d\.g$ The letter d , then a dot . , and then the letter g , so it would match d.g only

Table 8.7: Examples of regular expressions with descriptions of their meaning

Good Practice: Use regular expressions to validate input from the user. The same regular expressions can be reused in other languages such as JavaScript and Python.

Splitting a complex comma-separated string‌

Earlier in this chapter, you learned how to split a simple comma-separated string variable. But what about the following example of film titles?

"Monsters, Inc.","I, Tonya","Lock, Stock and Two Smoking Barrels"

The string value uses double quotes around each film title. We can use these to identify whether we need to split on a comma (or not). The Split method is not powerful enough, so we can use a regular expression instead.

Good Practice: You can read a fuller explanation in the Stack Overflow article that inspired this task at the following link: https://stackoverflow.com/questions/18144431/regex-to-split-a-csv.

To include double quotes inside a string value, we prefix them with a backslash, or we could use the C# 11 raw string literal feature in C# 11 or later:

1. Add statements to store a complex comma-separated string variable, and then split it in a dumb way using the Split method, as shown in the following code:

// C# 1 to 10: Use escaped double-quote characters \"

// string films = "\"Monsters, Inc.\",\"I, Tonya\",\"Lock, Stock and Two Smoking Barrels\"";

// C# 11 or later: Use """ to start and end a raw string literal string films = """

"Monsters, Inc.","I, Tonya","Lock, Stock and Two Smoking Barrels" """;

WriteLine($"Films to split: {films}"); string[] filmsDumb = films.Split(',');

WriteLine("Splitting with string.Split method:"); foreach (string film in filmsDumb)

{

WriteLine($" {film}");

}

1. Add statements to define a regular expression to split and write the film titles in a smart way, as shown in the following code:

Regex csv = new( "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)");

MatchCollection filmsSmart = csv.Matches(films); WriteLine("Splitting with regular expression:"); foreach (Match film in filmsSmart)

{

WriteLine($" {film.Groups[2].Value}");

}

In a later section, you will see how you can get a source generator to auto-generate XML comments for a regular expression to explain how it works. This is really useful for regular expressions that you might have copied from a website.

1. Run the code and view the result, as shown in the following output:

Splitting with string.Split method: "Monsters

Inc." "I

Tonya"

"Lock

Stock and Two Smoking Barrels" Splitting with regular expression:

Monsters, Inc.

I, Tonya

Lock, Stock and Two Smoking Barrels

Activating regular expression syntax coloring‌

If you use Visual Studio 2022 as your code editor, then you probably noticed that when passing a string value to the Regex constructor, you see color syntax highlighting, as shown in Figure 8.1:

image

Figure 8.1: Regular expression color syntax highlighting when using the Regex constructor

This would be a good time to remind print book readers who will only see the preceding figure in grayscale that they can see all figures in full color as a PDF at the following link: https://static.packt-cdn.com/downloads/???_ColorImages.pdf

Why does this string get syntax coloring for regular expressions when most string values do not? Let's find out:

1. Right-click on the new constructor, select Go To Implementation, and note the string parameter named pattern is decorated with an attribute named StringSyntax that has the string constant Regex value passed to it, as shown highlighted in the following code:

public Regex([StringSyntax(StringSyntaxAttribute.Regex)] string pattern) : this(pattern, culture: null)

{

}

1. Right-click on the StringSyntax attribute, select Go To Implementation, and note there are 12 recognized string syntax formats that you can choose from as well as Regex , as shown in the following partial code:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, public sealed class StringSyntaxAttribute : Attribute

{

public const string CompositeFormat = "CompositeFormat"; public const string DateOnlyFormat = "DateOnlyFormat"; public const string DateTimeFormat = "DateTimeFormat"; public const string EnumFormat = "EnumFormat";

public const string GuidFormat = "GuidFormat"; public const string Json = "Json";

public const string NumericFormat = "NumericFormat"; public const string Regex = "Regex";

public const string TimeOnlyFormat = "TimeOnlyFormat"; public const string TimeSpanFormat = "TimeSpanFormat"; public const string Uri = "Uri";

public const string Xml = "Xml";

}

1. In the WorkingWithRegularExpressions project, add a new class file named Program.Strings.cs , delete any existing statements, and then define some string constants in a partial Program class, as shown in the following code:

partial class Program

{

private const string DigitsOnlyText = @"^\d+$"; private const string CommaSeparatorText =

"(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";

}

Note that the two string constants do not have any color syntax highlighting yet.

1. In Program.cs , replace the literal string with the string constant for the digits-only regular expression, as shown highlighted in the following code:

Regex ageChecker = new(DigitsOnlyText);

1. In Program.cs , replace the literal string with the string constant for the comma- separator regular expression, as shown highlighted in the following code:

Regex csv = new(CommaSeparatorText);

Run the WorkingWithRegularExpressions project and confirm that the regular expression behavior is as before.

In Program.Strings.cs , import the namespace for the [StringSyntax] attribute and then decorate both string constants with it, as shown highlighted in the following code:

using System.Diagnostics.CodeAnalysis; // To use [StringSyntax]. partial class Program

{

[StringSyntax(StringSyntaxAttribute.Regex)] private const string DigitsOnlyText = @"^\d+$"; [StringSyntax(StringSyntaxAttribute.Regex)] private const string CommaSeparatorText =

"(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";

}

1. In Program.Strings.cs , add another string constant for formatting a date, as shown in the following code:

[StringSyntax(StringSyntaxAttribute.DateTimeFormat)] private const string FullDateTime = "";

Click inside the empty string, type the letter d , and note the IntelliSense, as shown in Figure 8.2:

image

Figure 8.2: IntelliSense activated due to the StringSyntax attribute

Finish entering the date format and as you type, note the IntelliSense:

dddd, d MMMM yyyy .

Inside, at the end of the DigitsOnlyText string literal, enter a \ , and note the IntelliSense to help you write a valid regular expression, as shown in Figure 8.3:

image

Figure 8.3: IntelliSense for writing a regular expression

Delete the \ that you entered to trigger IntelliSense.

The [StringSyntax] attribute is a new feature introduced in .NET 7. It depends on your code editor whether it is recognized. The .NET BCL has more than 350 parameters, properties, and fields that are now decorated with this attribute.

Improving regular expression performance with source generators‌

When you pass a string literal or string constant to the constructor of Regex , the class parses the string and transforms it into an internal tree structure that represents the expression in an optimized way that can be executed efficiently by a regular expression interpreter.You can also compile regular expressions by specifying a RegexOptions , as in the following code:

Regex ageChecker = new(DigitsOnlyText, RegexOptions.Compiled);

Unfortunately, compiling has the negative effect of slowing down the initial creation of the regular expression. After creating the tree structure that would then be executed by the interpreter, the compiler then must convert the tree into IL code, and then that IL code needs to be JIT compiled into native code. If you’re only running the regular expression a few times, it is not worth compiling it, which is why it is not the default behavior. If you’re running the regular expression more than a few times, for example, because it will be used to validate the URL for every incoming HTTP request to a website, then it is worth compiling. But even then, you should only use compilation if you must use .NET 6 or earlier..NET 7 introduced a source generator for regular expressions that recognizes if you decorate a partial method that returns Regex with the [GeneratedRegex] attribute. It generates an implementation of that method that implements the logic for the regular expression.Let's see it in action:

1. In the WorkingWithRegularExpressions project, add a new class file named Program.Regexs.cs and modify its content to define some partial methods, as shown in the following code:

using System.Text.RegularExpressions; // To use [GeneratedRegex]. partial class Program

{

[GeneratedRegex(DigitsOnlyText, RegexOptions.IgnoreCase)] private static partial Regex DigitsOnly(); [GeneratedRegex(CommaSeparatorText, RegexOptions.IgnoreCase)] private static partial Regex CommaSeparator();

}

1. In Program.cs , replace the new constructor with a call to the partial method that returns the digits-only regular expression, as shown highlighted in the following code:

Regex ageChecker = DigitsOnly();

1. In Program.cs , replace the new constructor with a call to the partial method that returns the comma-separator regular expression, as shown highlighted in the following code:

Regex csv = CommaSeparator();

Hover your mouse pointer over the partial methods and note the tooltip describes the behavior of the regular expression, as shown in Figure 8.4:

image

Figure 8.4: Tooltip for a partial method shows a description of the regular expression

Right-click the DigitsOnly partial method, select Go To Definition, and note that you can review the implementation of the auto-generated partial methods, as shown in Figure 8.5:

image

Figure 8.5: The auto-generated source code for the regular expression

Run the project and confirm that the functionality is the same as before.

You can learn more about the improvements to regular expressions with .NET 7 at the following link: https://devblogs.microsoft.com/dotnet/regular-expression-improvements- in-dotnet-7.

Storing multiple objects in collections‌

Another of the most common types of data is collections. If you need to store multiple values in a variable, then you can use a collection.A collection is a data structure in memory that can manage multiple items in different ways, although all collections have some shared functionality.The most common types in .NET for working with collections are shown in Table 8.8:

Namespace Example type(s) Description

System .Collections IEnumerable ,

IEnumerable System .Collections .Generic List ,

Dictionary ,

Queue ,

Stack

System .Collections .Concurrent BlockingCollection ,

ConcurrentDictionary ,

ConcurrentQueue System .Collections .Immutable ImmutableArray ,

ImmutableDictionary ,

ImmutableList ,

ImmutableQueue

Table 8.8: Common .NET collection types

Common features of all collections‌

Interfaces and base classes used by collections.

Introduced in C# 2.0 with .NET Framework 2.0. These collections allow you to specify the type you want to store using a generic type parameter (which is safer, faster, and more efficient).

These collections are safe to use in multithreaded scenarios.

Designed for scenarios where the contents of the original collection will never change, although they can create modified collections as a new instance.

All collections implement the ICollection interface; this means that they must have a Count property to tell you how many objects are in them, and three other members, as shown in the following code:

namespace System.Collections;

public interface ICollection : IEnumerable

{

int Count { get; }

bool IsSynchronized { get; } object SyncRoot { get; }

void CopyTo(Array array, int index);

}

For example, if we had a collection named passengers , we could do this:

int howMany = passengers.Count;

As you have probably surmised, CopyTo copies the collection to an array. IsSynchronized and

SyncRoot are used in multithreading scenarios, so I do not cover them in this book.All

collections implement the IEnumerable interface, which means that they can be iterated using the foreach statement. They must have a GetEnumerator method that returns an object that implements IEnumerator ; this means that the returned object must have MoveNext and Reset methods for navigating through the collection and a Current property containing the current item in the collection, as shown in the following code:

namespace System.Collections; public interface IEnumerable

{

IEnumerator GetEnumerator();

}

public interface IEnumerator

{

object Current { get; } bool MoveNext();

void Reset();

}

For example, to perform an action on each object in the passengers collection, we could write the following code:

foreach (Passenger p in passengers)

{

// Perform an action on each passenger.

}

As well as the object -based collection interface, there is also a generic collection interface, where the generic type defines the type stored in the collection. It has additional members like IsReadOnly , Add , Clear , Contains , and Remove , as shown in the following code:

namespace System.Collections.Generic;

public interface ICollection : IEnumerable, IEnumerable

{

int Count { get; }

bool IsReadOnly { get; } void Add(T item);

void Clear();

bool Contains(T item);

void CopyTo(T[] array, int index); bool Remove(T item);

}

Working with lists‌

Lists, that is, a type that implements IList , are ordered collections, with an int

index to show the position of an item within the list, as shown in the following code:

namespace System.Collections.Generic; [DefaultMember("Item")] // aka "this" indexer.

public interface IList : ICollection, IEnumerable, IEnumerable

{

T this[int index] { get; set; } int IndexOf(T item);

void Insert(int index, T item); void RemoveAt(int index);

}

The [DefaultMember] attribute allows you to specify which member is accessed by default when no member name is specified. To make IndexOf the default member, you would use [DefaultMember("IndexOf")] . To specify the indexer, you use [DefaultMember("Item")] .

IList derives from ICollection , so it has a Count property, and an Add method to put an item at the end of the collection, as well as an Insert method to put an item in the

list at a specified position, and RemoveAt to remove an item at a specified position.Lists are a good choice when you want to manually control the order of items in a collection. Each item in a list has a unique index (or position) that is automatically assigned. Items can be any type defined by T and items can be duplicated. Indexes are int types and start from 0 , so the first item in a list is at index 0 , as shown in Table 8.9:

Index Item

London

Paris

London

Sydney

Table 8.9: Cities in a list with indexes

If a new item (for example, Santiago) is inserted between London and Sydney, then the index of Sydney is automatically incremented. Therefore, you must be aware that an item’s index can change after inserting or removing items, as shown in Table 8.10:

Index Item

London

Paris

London

Santiago

Sydney

Table 8.10: Cities list after an item is inserted

Good Practice: Some developers can get into a poor habit of using List and other collections when an array would be better. Use arrays instead of collections if the data will not change size after instantiation. You should also use lists initially while you are adding and removing items, but then convert them into an array once you are done with manipulating the items.

Let's explore lists:

Use your preferred code editor to add a new Console App / console project named

WorkingWithCollections to the Chapter08 solution.

Add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , define a partial Program class with a generic method to output a collection of T values with a title, as shown in the following code:

partial class Program

{

private static void OutputCollection( string title, IEnumerable collection)

{

WriteLine($"{title}:"); foreach (T item in collection)

{

WriteLine($" {item}");

}

}

}

1. In Program.cs , delete the existing statements and then add some statements to illustrate some of the common ways of defining and working with lists, as shown in the following code:

// Simple syntax for creating a list and adding three items. List cities = new();

cities.Add("London"); cities.Add("Paris"); cities.Add("Milan");

/* Alternative syntax that is converted by the compiler into the three Add method calls above.

List cities = new()

{ "London", "Paris", "Milan" }; */

/* Alternative syntax that passes an array of string values to AddRange method.

List cities = new();

cities.AddRange(new[] { "London", "Paris", "Milan" }); */ OutputCollection("Initial list", cities);

WriteLine($"The first city is {cities[0]}."); WriteLine($"The last city is {cities[cities.Count - 1]}."); cities.Insert(0, "Sydney");

OutputCollection("After inserting Sydney at index 0", cities); cities.RemoveAt(1);

cities.Remove("Milan");

OutputCollection("After removing two cities", cities);

1. Run the code and view the result, as shown in the following output:

Initial list: London Paris

Milan

The first city is London. The last city is Milan.

After inserting Sydney at index 0: Sydney

London Paris Milan

After removing two cities: Sydney

Paris

Working with dictionaries‌

Dictionaries are a good choice when each value (or object) has a unique sub-value (or a made-up value) that can be used as a key to quickly find a value in the collection later. The key must be unique. For example, if you are storing a list of people, you could choose to use a government-issued identity number as the key. Dictionaries are called hashmaps in other languages like Python and Java.Think of the key as being like an index entry in a real-world dictionary. It allows you to quickly find the definition of a word because the

words (in other words, keys) are kept sorted; if we know we're looking for the definition of manatee, we will jump to the middle of the dictionary to start looking, because the letter m is in the middle of the alphabet.Dictionaries in programming are similarly smart when looking something up. They must implement the interface IDictionary , as shown in the following code:

namespace System.Collections.Generic; [DefaultMember("Item")] // aka "this" indexer. public interface IDictionary

: ICollection>, IEnumerable>, IEnumerable

{

TValue this[TKey key] { get; set; } ICollection Keys { get; } ICollection Values { get; } void Add(TKey key, TValue value); bool ContainsKey(TKey key);

bool Remove(TKey key);

bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);

}

Items in a dictionary are instances of the struct , aka the value type,

KeyValuePair , where TKey is the type of the key and TValue is the type of the value, as shown in the following code:

namespace System.Collections.Generic;

public readonly struct KeyValuePair

{

public KeyValuePair(TKey key, TValue value); public TKey Key { get; }

public TValue Value { get; } [EditorBrowsable(EditorBrowsableState.Never)]

public void Deconstruct(out TKey key, out TValue value); public override string ToString();

}

An example Dictionary uses a string as the key and a Person instance as the value. Dictionary uses string values for both, as shown in Table 8.11:

Key Value

BSA Bob Smith

MW Max Williams BSB Bob Smith

AM Amir Mohammed

Table 8.11: Dictionary with keys and values Let's explore dictionaries:

1. At the top of Program.cs , define an alias for the Dictionary class where

TKey and TValue are both string , as shown in the following code:

// Define an alias for a dictionary with string key and string value.

using StringDictionary = System.Collections.Generic.Dictionary;

1. In Program.cs , add some statements to illustrate some of the common ways of working with dictionaries, for example, looking up word definitions, as shown in the following code:

// Declare a dictionary without the alias.

// Dictionary keywords = new();

// Use the alias to declare the dictionary. StringDictionary keywords = new();

// Add using named parameters.

keywords.Add(key: "int", value: "32-bit integer data type");

// Add using positional parameters. keywords.Add("long", "64-bit integer data type");

keywords.Add("float", "Single precision floating point number");

/* Alternative syntax; compiler converts this to calls to Add method. Dictionary keywords = new()

{

{ "int", "32-bit integer data type" },

{ "long", "64-bit integer data type" },

{ "float", "Single precision floating point number" },

}; */

/* Alternative syntax; compiler converts this to calls to Add method. Dictionary keywords = new()

{

["int"] = "32-bit integer data type", ["long"] = "64-bit integer data type",

["float"] = "Single precision floating point number",

}; */

OutputCollection("Dictionary keys", keywords.Keys); OutputCollection("Dictionary values", keywords.Values); WriteLine("Keywords and their definitions:");

foreach (KeyValuePair item in keywords)

{

WriteLine($" {item.Key}: {item.Value}");

}

// Look up a value using a key. string key = "long";

WriteLine($"The definition of {key} is {keywords[key]}.");

The trailing commas after the third item is added to the dictionary are optional and the compiler will not complain about them. This is convenient so that you can change the order of the three items without having to delete and add commas in the right places.

1. Run the code and view the result, as shown in the following output:

Dictionary keys: int

long float

Dictionary values:

32-bit integer data type 64-bit integer data type

Single precision floating point number Keywords and their definitions:

int: 32-bit integer data type long: 64-bit integer data type

float: Single precision floating point number The definition of long is 64-bit integer data type

In Chapter 11, Querying and Manipulating Data Using LINQ, you will learn how to create dictionaries and lookups from existing data sources like tables in a database using LINQ methods like ToDictionary and ToLookup . This is much more common than manually adding items to a dictionary as shown in this section.

Sets, stacks, and queues‌

Sets are a good choice when you want to perform set operations between two collections. For example, you may have two collections of city names, and you want to know which names appear in both sets (known as the intersect between the sets). Items in a set must be unique.

Common set methods are shown in Table 8.12:

Method Description

Add If the item does not already exist in the set, then it is added. Returns

true if the item was added, and false if it was already in the set.

ExceptWith Removes the items in the set passed as the parameter from the set.

IntersectWith Removes the items not in the set passed as the parameter and in the set.

IsProperSubsetOf ,

IsProperSupersetOf

, IsSubsetOf ,

IsSupersetOf

A subset is a set whose items are all in the other set. A proper subset is a set whose items are all in the other set but there is at least one item in the other set that is not in the set. A superset is a set that contains all the items in the other set. A proper superset is a set that contains all the items in the other set and at least one more not in the other set.

Overlaps The set and the other set share at least one common item.

SetEquals The set and the other set contain exactly the same items.

SymmetricExceptWith Removes the items not in the set passed as the parameter from the set and adds any that are missing.

UnionWith Adds any items in the set passed as the parameter to the set that are not already in the set.

Table 8.12: Set methods

Let's explore example code for sets:

1. In Program.cs , add some statements to add items to a set, as shown in the following code:

HashSet names = new(); foreach (string name in

new[] { "Adam", "Barry", "Charlie", "Barry" })

{

bool added = names.Add(name); WriteLine($"{name} was added: {added}.");

}

WriteLine($"names set: {string.Join(',', names)}.");

1. Run the code and view the result, as shown in the following output:

Adam was added: True.

Barry was added: True.

Charlie was added: True.

Barry was added: False.

names set: Adam,Barry,Charlie.

You will see more set operations in Chapter 11, Querying and Manipulating Data Using LINQ.Stacks are a good choice when you want to implement last-in, first-out (LIFO) behavior. With a stack, you can only directly access or remove the one item at the top of the stack, although you can enumerate to read through the whole stack of items. You cannot, for example, directly access the second item in a stack.For example, word processors use a stack to remember the sequence of actions you have recently performed, and then when you press Ctrl + Z, it will undo the last action in the stack, and then the next-to-last action, and so on.Queues are a good choice when you want to implement the first-in, first-out (FIFO) behavior. With a queue, you can only directly access or remove the item at the front of the queue, although you can enumerate to read through the whole queue of items. You cannot, for example, directly access the second item in a queue.For example, background processes use a queue to process work items in the order that they arrive, just like people standing in line at the post office..NET 6 introduced the PriorityQueue , where each item in the queue has a priority value assigned, as well as its position in the queue.Let's explore example code for queues:

1. In Program.cs , add some statements to illustrate some of the common ways of working with queues, for example, handling customers in a queue for coffee, as shown in the following code:

Queue coffee = new(); coffee.Enqueue("Damir"); // Front of the queue. coffee.Enqueue("Andrea"); coffee.Enqueue("Ronald"); coffee.Enqueue("Amin"); coffee.Enqueue("Irina"); // Back of the queue.

OutputCollection("Initial queue from front to back", coffee);

// Server handles next person in queue. string served = coffee.Dequeue(); WriteLine($"Served: {served}.");

// Server handles next person in queue. served = coffee.Dequeue(); WriteLine($"Served: {served}.");

OutputCollection("Current queue from front to back", coffee); WriteLine($"{coffee.Peek()} is next in line."); OutputCollection("Current queue from front to back", coffee);

1. Run the code and view the result, as shown in the following output:

Initial queue from front to back: Damir

Andrea Ronald Amin Irina

Served: Damir.

Served: Andrea.

Current queue from front to back: Ronald

Amin Irina

Ronald is next in line.

Current queue from front to back: Ronald

Amin Irina

1. In Program.Helpers.cs , in the partial Program class, add a static method named

OutputPQ , as shown in the following code:

private static void OutputPQ(string title, IEnumerable<(TElement Element, TPriority Priority)> collection)

{

WriteLine($"{title}:");

foreach ((TElement, TPriority) item in collection)

{

WriteLine($" {item.Item1}: {item.Item2}");

}

}

Note that the OutputPQ method is generic. You can specify the two types used in the tuples that are passed in as collection .

1. In Program.cs , add some statements to illustrate some of the common ways of working with priority queues, as shown in the following code:

PriorityQueue vaccine = new();

// Add some people.

// 1 = High priority people in their 70s or poor health.

// 2 = Medium priority e.g. middle-aged.

// 3 = Low priority e.g. teens and twenties. vaccine.Enqueue("Pamela", 1);

vaccine.Enqueue("Rebecca", 3);

vaccine.Enqueue("Juliet", 2);

vaccine.Enqueue("Ian", 1);

OutputPQ("Current queue for vaccination", vaccine.UnorderedItems); WriteLine($"{vaccine.Dequeue()} has been vaccinated."); WriteLine($"{vaccine.Dequeue()} has been vaccinated."); OutputPQ("Current queue for vaccination", vaccine.UnorderedItems); WriteLine($"{vaccine.Dequeue()} has been vaccinated."); WriteLine("Adding Mark to queue with priority 2."); vaccine.Enqueue("Mark", 2);

WriteLine($"{vaccine.Peek()} will be next to be vaccinated."); OutputPQ("Current queue for vaccination", vaccine.UnorderedItems);

1. Run the code and view the result, as shown in the following output:

Current queue for vaccination: Pamela: 1

Rebecca: 3

Juliet: 2

Ian: 1

Pamela has been vaccinated. Ian has been vaccinated.

Current queue for vaccination: Juliet: 2

Rebecca: 3

Juliet has been vaccinated.

Adding Mark to queue with priority 2 Mark will be next to be vaccinated. Current queue for vaccination:

Mark: 2

Rebecca: 3

Collection add and remove methods‌

Each collection has a different set of methods to "add" and "remove" items, as shown in

Table 8.13:

Collection "Add" "Remove" Description

methods methods

List Add ,

Insert

Remove

,

Lists are ordered so items have an integer index position. Add

will add a new item at the end of the list. Insert will add a

RemoveAt new item at the index position specified.

Dictionary Add Remove Dictionaries are not ordered so items do not have integer index

positions. You can check if a key has been used by calling the

ContainsKey method.

Stack Push Pop Stacks always add a new item at the top of the stack using the

Push method. The first item is at the bottom. Items are always removed from the top of the stack using the Pop method. Call the Peek method to see this value without removing it. Stacks are LIFO.

Queue Enqueue Dequeue Queues always add a new item at the end of the queue using the

Enqueue method. The first item is at the front of the queue. Items are always removed from the front of the queue using the Dequeue method. Call the Peek method to see this value without removing it. Queues are FIFO.

Table 8.13: Collection "add" and "remove" methods

Sorting collections‌

A List class can be sorted by manually calling its Sort method (but remember that the indexes of each item will change). Manually sorting a list of string values or other built- in types will work without extra effort on your part, but if you create a collection of your own type, then that type must implement an interface named IComparable . You learned how to do this in Chapter 6, Implementing Interfaces and Inheriting Classes.A Stack or Queue collection cannot be sorted because you wouldn't usually want that functionality; for example, you would probably never sort a queue of guests checking into a hotel. But sometimes, you might want to sort a dictionary or a set.Sometimes it would be useful to have an automatically sorted collection, that is, one that maintains the items in a sorted order as you add and remove them.There are multiple auto-sorting collections to choose from. The differences between these sorted collections are often subtle but can have an impact on the memory requirements and performance of your application, so it is worth putting effort into picking the most appropriate option for your requirements.Some common auto-sorting collections are shown in Table 8.14:

Collection Description

SortedDictionary This represents a collection of key/value pairs that are

sorted by key. Internally it maintains a binary tree for items.

SortedList This represents a collection of key-value pairs that are

sorted by key. The name is misleading because this is not a list. Compared to SortedDictionary , retrieval performance is similar, it uses less memory, and insert and remove operations are slower for unsorted data. If it is populated from sorted data, then it is faster. Internally, it maintains a sorted array with a binary search to find elements.

SortedSet This represents a collection of unique objects that are maintained in a sorted order.

Table 8.14: Common auto-sorting collections

Specialized collections‌

There are a few other collections for special situations.The System.Collections.BitArray collection manages a compact array of bit values, which are represented as Booleans, where true indicates that the bit is on (value is 1) and false indicates that the bit is off (value is 0).The System.Collections.Generics.LinkedList collection represents a doubly linked list where every item has a reference to its previous and next items. They provide better performance compared to List for scenarios where you will frequently insert and remove items from the middle of the list. In a LinkedList , the items do not have to be rearranged in memory.

Read-only, immutable, and frozen collections‌

When we looked at the generic collection interface, we saw that it has a property named IsReadOnly . This is useful when we want to pass a collection to a method but not allow it to make changes.For example, we might define a method, as shown in the following code:

void ReadCollection(ICollection collection)

{

// We can check if the collection is read-only. if (collection.IsReadOnly)

{

// Read the collection.

}

else

{

WriteLine("You have given me a collection that I could change!");

}

}

Generic collections like List and Dictionary have an AsReadOnly method to create a ReadOnlyCollection that references the original collection. Although the ReadOnlyCollection has to have an Add and a Remove method because it implements ICollection , it throws a NotImplementedException to prevent changes. If the original collection has items added or removed, the ReadOnlyCollection will see those changes. You can think of a ReadOnlyCollection as a protected view of a collection.Let's see how we can make sure a collection is read-only:

In the WorkingWithCollections project, in Program.Helpers.cs , add a method that should only be given a read-only dictionary with string for the type of key and value, but the naughty method tries to call Add , as shown in the following code:

private static void UseDictionary( IDictionary dictionary)

{

WriteLine($"Count before is {dictionary.Count}."); try

{

WriteLine("Adding new item with GUID values.");

// Add method with return type of void. dictionary.Add(

key: Guid.NewGuid().ToString(), value: Guid.NewGuid().ToString());

}

catch (NotSupportedException)

{

WriteLine("This dictionary does not support the Add method.");

}

WriteLine($"Count after is {dictionary.Count}.");

}

Note the type of parameter is IDictionary . Using an interface provides more flexibility because we can pass either a Dictionary , a ReadOnlyDictionary , or anything else that implements that interface.

1. In Program.cs , add statements to pass the keywords dictionary to this naughty method, as shown in the following code:

UseDictionary(keywords);

1. Run the code, view the result, and note that the naughty method was able to add a new key-value pair, so the count has incremented, as shown in the following output:

Count before is 3.

Adding new item with GUID values. Count after is 4.

1. In Program.cs , comment out the UseDictionary statement and then add a statement to pass the dictionary converted into a read-only collection, as shown in the following code:

//UseDictionary(keywords); UseDictionary(keywords.AsReadOnly());

1. Run the code, view the result, and note that this time the method was not able to add an item, so the count is the same, as shown in the following output:

Count before is 3.

Adding new item with GUID values.

This dictionary does not support the Add method. Count after is 3.

1. At the top of Program.cs , import the System.Collections.Immutable namespace, as shown in the following code:

using System.Collections.Immutable; // To use ImmutableDictionary.

1. In Program.cs , comment out the AsReadOnly statement and then add a statement to pass the keywords converted into an immutable dictionary, as shown highlighted in the following code:

//UseDictionary(keywords.AsReadOnly()); UseDictionary(keywords.ToImmutableDictionary());

1. Run the code, view the result, and note that this time the method was also not able to add a default value, so the count is the same – it is the same behavior as using a read-only collection, so what's the point of immutable collections?

If you import the System.Collections.Immutable namespace, then any collection that implements IEnumerable is given six extension methods to convert it into an immutable collection like a list, dictionary, set, and so on.Although the immutable collection will have a method named Add , it does not add an item to the original immutable collection!

Instead, it returns a new immutable collection with the new item in it. The original immutable collection still only has the original items in it.Let's see an example:

1. In Program.cs , add statements to convert the keywords dictionary into an immutable dictionary and then add a new keyword definition to it by randomly generating GUID values, as shown in the following code:

ImmutableDictionary immutableKeywords = keywords.ToImmutableDictionary();

// Call the Add method with a return value. ImmutableDictionary newDictionary =

immutableKeywords.Add(

key: Guid.NewGuid().ToString(), value: Guid.NewGuid().ToString());

OutputCollection("Immutable keywords dictionary", immutableKeywords); OutputCollection("New keywords dictionary", newDictionary);

1. Run the code, view the result, and note that the immutable keywords dictionary does not get modified when you call the Add method on it; instead, it returns a new dictionary with all the existing keywords plus the newly added keyword, as shown in the following output:

Immutable keywords dictionary:

[float, Single precision floating point number] [long, 64-bit integer data type]

[int, 32-bit integer data type] New keywords dictionary:

[d0e099ff-995f-4463-ae7f-7b59ed3c8d1d, 3f8e4c38-c7a3-4b20-acb3-01b2e3c86e8c] [float, Single precision floating point number]

[long, 64-bit integer data type] [int, 32-bit integer data type]

Newly added items will not always appear at the top of the dictionary as shown in my output above. Internally, the order is defined by the hash of the key. This is why dictionaries are sometimes called hash tables.

Good Practice: To improve performance, many applications store a shared copy of commonly accessed objects in a central cache. To safely allow multiple threads to work with those objects knowing they won't change, you should make them immutable or use a concurrent collection type, which you can read about at the following link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.

The generic collections have some potential performance issues related to how they are designed. First, being generic, the types of items or types used for keys and values for a dictionary have a big effect on performance depending on what they are. Since they could be any type, the .NET team cannot optimize the algorithm. string and int types are the most used in real life. If the .NET team could rely on those always being the types used, then they could greatly improve performance.Second, collections are dynamic, meaning that new items can be added, and existing items can be removed at any time. Even more optimizations could be made if the .NET team knew that no more changes would be made to the collection..NET 8 introduces a new concept: frozen collections. Hmmm, we already have immutable collections, so what is different about frozen collections? Are they tasty and delicious like ice cream? The idea is that 95% of the time, a collection is populated and then never changed. So, if we could optimize them at the time of creation, then those optimizations could be made, adding some time and effort upfront, but then after that, performance for reading the collection could be greatly improved.In .NET 8, there are only two frozen collections: FrozenDictionary and FrozenSet . More may come in future versions of .NET, but these are the two most common scenarios that would benefit from the frozen concept.Let's go:

1. At the top of Program.cs , import the System.Collections.Frozen namespace, as shown in the following code:

using System.Collections.Frozen; // To use FrozenDictionary.

1. At the bottom of Program.cs , add statements to convert the keywords dictionary into a frozen dictionary, output its items, and then look up the definition of long , as shown in the following code:

// Creating a frozen collection has an overhead to perform the

// sometimes complex optimizations. FrozenDictionary frozenKeywords =

keywords.ToFrozenDictionary();

OutputCollection("Frozen keywords dictionary", frozenKeywords);

// Lookups are faster in a frozen dictionary. WriteLine($"Define long: {frozenKeywords["long"]}");

1. Run the code and view the result, as shown in the following output:

Frozen keywords dictionary:

[int, 32-bit integer data type] [long, 64-bit integer data type]

[float, Single precision floating point number] Define long: 64-bit integer data type

What the Add method does depends on the type, as summarized in the following list:

image

List : Adds a new item to the end of the existing list.

image

Dictionary : Adds a new item to the existing dictionary in a position determined by its internal structure.

image

ReadOnlyCollection : Throws a not-supported exception.

image

ImmutableList : Returns a new list with the new item in it. Does not affect the original list.

image

ImmutableDictionary : Returns a new dictionary with the new item in it. Does not affect the original dictionary.

image

FrozenDictionary : Does not exist.

More Information: The documentation for frozen collections is found at the following link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen.

Initializing collections using collection expressions‌

Introduced with C# 12 is a new consistent syntax for initializing arrays, collections, and span variables.With C# 11 and earlier, you would have to declare and initialize an array, collection, or span of int values using the following code:

int[] numbersArray11 = { 1, 3, 5 };

List numbersList11 = new() { 1, 3, 5 };

Span numbersSpan11 = stackalloc int[] { 1, 3, 5 };

Starting with C# 12, you can now consistently use square brackets, and the compiler will do the right thing, as shown in the following code:

int[] numbersArray12 = [ 1, 3, 5 ];

List numbersList12 = [ 1, 3, 5 ];

Span numbersSpan12 = [ 1, 3, 5 ];

More Information: You can learn more about collection expressions at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/proposals/csharp-12.0/collection-expressions.

Good practice with collections‌

Since .NET 1.1, types like StringBuilder have had a method named EnsureCapacity that can presize its internal storage array to the expected final size of the string . This improves performance because it does not have to repeatedly increment the size of the array as more characters are appended.Since .NET Core 2.1, types like Dictionary and HashSet have also had EnsureCapacity .In .NET 6 and later, collections like List , Queue , and Stack now have an EnsureCapacity method too, as shown in the following code:

List names = new(); names.EnsureCapacity(10_000);

// Load ten thousand names into the list.

Let's say you need to create a method to process a collection. For maximum flexibility, you could declare the input parameter to be IEnumerable and make the method generic, as shown in the following code:

void ProcessCollection(IEnumerable collection)

{

// Process the items in the collection,

// perhaps using a foreach statement.

}

I could pass an array, a list, a queue, or a stack, containing any type, like int , string , Person , or anything else that implements IEnumerable , into this method and it will process the items. However, the flexibility to pass any collection to this method comes at a performance cost.One of the performance problems with IEnumerable is also one of its benefits: deferred execution, also known as lazy loading. Types that implement this interface do not have to implement deferred execution, but many do.But the worst performance problem with IEnumerable is that the iteration must allocate an object on the heap. To avoid this memory allocation, you should define your method using a concrete type, as shown highlighted in the following code:

void ProcessCollection(List collection)

{

// Process the items in the collection,

// perhaps using a foreach statement.

}

This will use the List.Enumerator GetEnumerator() method, which returns a struct , instead of the IEnumerator GetEnumerator() method, which returns a reference type. Your code will be two to three times faster and require less memory. As with all recommendations related to performance, you should confirm the benefit by running performance tests on your actual code in a product environment.

Working with spans, indexes, and ranges‌

One of Microsoft's goals with .NET Core 2.1 was to improve performance and resource usage. A key .NET feature that enables this is the Span type.

Using memory efficiently using spans‌

When manipulating arrays, you will often create new copies or subsets of existing ones so that you can process just the subset. This is not efficient because duplicate objects must be created in memory.If you need to work with a subset of an array, use a span because it is like a window into the original array. This is more efficient in terms of memory usage and improves performance. Spans only work with arrays, not collections, because the memory must be contiguous.Before we look at spans in more detail, we need to understand some related objects: indexes and ranges.

Identifying positions with the Index type‌

C# 8 introduced two features for identifying an item's index position within an array and a range of items using two indexes.You learned in the previous section that objects in a list can be accessed by passing an integer into their indexer, as shown in the following code:

int index = 3;

Person p = people[index]; // Fourth person in array. char letter = name[index]; // Fourth letter in name.

The Index value type is a more formal way of identifying a position, and supports counting from the end, as shown in the following code:

// Two ways to define the same index, 3 in from the start. Index i1 = new(value: 3); // Counts from the start

Index i2 = 3; // Using implicit int conversion operator.

// Two ways to define the same index, 5 in from the end. Index i3 = new(value: 5, fromEnd: true);

Index i4 = ^5; // Using the caret ^ operator.

Identifying ranges with the Range type‌

The Range value type uses Index values to indicate the start and end of its range, using its constructor, C# syntax, or its static methods, as shown in the following code:

Range r1 = new(start: new Index(3), end: new Index(7));

Range r2 = new(start: 3, end: 7); // Using implicit int conversion. Range r3 = 3..7; // Using C# 8.0 or later syntax.

Range r4 = Range.StartAt(3); // From index 3 to last index. Range r5 = 3..; // From index 3 to last index.

Range r6 = Range.EndAt(3); // From index 0 to index 3. Range r7 = ..3; // From index 0 to index 3.

Extension methods have been added to string values (which internally use an array of char ), int arrays, and spans to make ranges easier to work with. These extension methods accept a range as a parameter and return a Span . This makes them very memory-efficient.

Using indexes, ranges, and spans‌

Let's explore using indexes and ranges to return spans:

Use your preferred code editor to add a new Console App / console project named

WorkingWithRanges to the Chapter08 solution.

In Program.cs , delete the existing statements and then add statements to compare using the string type's Substring method with ranges to extract parts of someone's name, as shown in the following code:

string name = "Samantha Jones";

// Getting the lengths of the first and last names. int lengthOfFirst = name.IndexOf(' ');

int lengthOfLast = name.Length - lengthOfFirst - 1;

// Using Substring.

string firstName = name.Substring( startIndex: 0,

length: lengthOfFirst);

string lastName = name.Substring( startIndex: name.Length - lengthOfLast, length: lengthOfLast);

WriteLine($"First: {firstName}, Last: {lastName}");

// Using spans.

ReadOnlySpan nameAsSpan = name.AsSpan(); ReadOnlySpan firstNameSpan = nameAsSpan[0..lengthOfFirst]; ReadOnlySpan lastNameSpan = nameAsSpan[^lengthOfLast..]; WriteLine($"First: {firstNameSpan}, Last: {lastNameSpan}");

1. Run the code and view the result, as shown in the following output:

First: Samantha, Last: Jones First: Samantha, Last: Jones

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring with deeper research into the topics in this chapter.

Exercise 8.1 – Test your knowledge‌

Use the web to answer the following questions:

What is the maximum number of characters that can be stored in a string variable?

When and why should you use a SecureString type?

When is it appropriate to use a StringBuilder class?

When should you use a LinkedList class?

When should you use a SortedDictionary class rather than a SortedList class?

In a regular expression, what does $ mean?

In a regular expression, how can you represent digits?

Why should you not use the official standard for email addresses to create a regular expression to validate a user's email address?

What characters are output when the following code runs?

string city = "Aberdeen";

ReadOnlySpan citySpan = city.AsSpan()[^5..^0]; WriteLine(citySpan.ToString());

1. How could you check that a web service is available before calling it?

Exercise 8.2 – Practice regular expressions‌

In the Chapter08 solution, create a console app named Ch08Ex02RegularExpressions that prompts the user to enter a regular expression and then prompts the user to enter some input, and compare the two for a match until the user presses Esc, as shown in the following output:

The default regular expression checks for at least one digit.

Enter a regular expression (or press ENTER to use the default): ^[a-z]+$ Enter some input: apples

apples matches ^[a-z]+$? True

Press ESC to end or any key to try again.

Enter a regular expression (or press ENTER to use the default): ^[a-z]+$ Enter some input: abc123xyz

abc123xyz matches ^[a-z]+$? False

Press ESC to end or any key to try again.

Exercise 8.3 – Practice writing extension methods‌

In the Chapter08 solution, create a class library named Ch08Ex03NumbersAsWordsLib and projects to test it. It should define extension methods that extend number types such as BigInteger and int with a method named ToWords that returns a string describing the number.For example, 18,000,000 would be eighteen million, and 18,456,002,032,011,000,007 would be eighteen quintillion, four hundred and fifty-six quadrillion, two trillion, thirty- two billion, eleven million, and seven.You can read more about names for large numbers at the following link: https://en.wikipedia.org/wiki/Names_of_large_numbers.

Exercise 8.4 – Working with network resources‌

If you are interested in some low-level types for working with network resources, then you can read an online-only section found at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch08-network-resources.md

Exercise 8.5 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:

https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-8---working- with-common-net-types

Summary‌

In this chapter, you explored:

image

Choices for types to store and manipulate numbers.

image

image

Handling text, including using regular expressions for validating input. Collections to use for storing multiple items.

image

Working with indexes, ranges, and spans.

In the next chapter, we will manage files and streams, encode and decode text, and perform serialization.

Working with Files, Streams, and Serialization‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about reading and writing to files and streams, text encoding, and serialization. Applications that do not interact with the filesystem are extraordinarily rare. As a .NET developer, almost every application that you build will need to manage the filesystem and create, open, read, and write to and from files. Most of those files will contain text, so it is important to understand how text is encoded. And finally, after working with objects in memory, you will need to store them somewhere permanently for later reuse. You do that using a technique called serialization.In this chapter, we will cover the following topics:

image

Managing the filesystem

image

image

image

Reading and writing with streams Encoding and decoding text Serializing object graphs

image

Working with environment variables

Managing the filesystem‌

Your applications will often need to perform input and output operations with files and directories in different environments. The System and System.IO namespaces contain classes for this purpose.

Handling cross-platform environments and filesystems‌

Let's explore how to handle cross-platform environments and the differences between Windows, Linux, and macOS. Paths are different for Windows, macOS, and Linux, so we will start by exploring how .NET handles this:

Use your preferred code editor to create a new project, as defined in the following list:

image

image

image

Project template: Console App / console Project file and folder: WorkingWithFileSystems Solution file and folder: Chapter09

In the project file, add a package reference for Spectre.Console , and then add elements to import the following classes statically and globally: System.Console , System.IO.Directory , System.IO.Path , and System.Environment , as shown in the following markup:

Build the WorkingWithFileSystems project to restore packages.

Add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , add a partial Program class with a SectionTitle method, as shown in the following code:

// null namespace to merge with auto-generated Program. partial class Program

{

private static void SectionTitle(string title)

{

WriteLine();

ConsoleColor previousColor = ForegroundColor;

// Use a color that stands out on your system. ForegroundColor = ConsoleColor.DarkYellow; WriteLine($"*** {title} ***");

ForegroundColor = previousColor;

}

}

image

1. In Program.cs , add statements to use a Spectre.Console table to do the following: Output the path and directory separation characters.

image

Output the path of the current directory.

image

Output some special paths for system files, temporary files, and documents:

using Spectre.Console; // To use Table.

#region Handling cross-platform environments and filesystems SectionTitle("Handling cross-platform environments and filesystems");

// Create a Spectre Console table. Table table = new();

// Add two columns with markup for colors. table.AddColumn("[blue]MEMBER[/]"); table.AddColumn("[blue]VALUE[/]");

// Add rows.

table.AddRow("Path.PathSeparator", PathSeparator.ToString()); table.AddRow("Path.DirectorySeparatorChar",

DirectorySeparatorChar.ToString()); table.AddRow("Directory.GetCurrentDirectory()",

GetCurrentDirectory()); table.AddRow("Environment.CurrentDirectory", CurrentDirectory); table.AddRow("Environment.SystemDirectory", SystemDirectory); table.AddRow("Path.GetTempPath()", GetTempPath()); table.AddRow("");

table.AddRow("GetFolderPath(SpecialFolder", "");

table.AddRow(" .System)", GetFolderPath(SpecialFolder.System)); table.AddRow(" .ApplicationData)",

GetFolderPath(SpecialFolder.ApplicationData)); table.AddRow(" .MyDocuments)",

GetFolderPath(SpecialFolder.MyDocuments)); table.AddRow(" .Personal)",

GetFolderPath(SpecialFolder.Personal));

// Render the table to the console AnsiConsole.Write(table); #endregion

The Environment type has many other useful members that we did not use in this code, including the OSVersion and ProcessorCount properties.

1. Run the code and view the result, as shown using Visual Studio 2022 on Windows in Figure 9.1:

image

Figure 9.1: Showing filesystem information with Visual Studio 2022 on Windows

More Information: You can learn more about using Spectre Console tables at the following link: https://spectreconsole.net/widgets/table.

When running the console app using dotnet run on a Mac, the path and directory separator characters are different and the CurrentDirectory will be the project folder, not a folder inside bin , as shown in Figure 9.2:

image

Figure 9.2: Showing filesystem information with the CLI on macOS

Good Practice: Windows uses a backslash ( \ ) for the directory separator character. macOS and Linux use a forward slash ( / ) for the directory separator character. Do not assume what character is used in your code when combining paths; use Path.DirectorySeparatorChar .

In future sections of this chapter, we will create directories and files in the Personal special folder, so make a note of where that is for your operating system. For example, if you're using Linux, it should be $USER/Documents .

Managing drives‌

To manage drives, use the DriveInfo type, which has a static method that returns information about all the drives connected to your computer. Each drive has a drive type.Let's explore drives:

1. In Program.cs , write statements to get all the drives and output their name, type, size, available free space, and format, but only if the drive is ready, as shown in the following code:

SectionTitle("Managing drives"); Table drives = new(); drives.AddColumn("[blue]NAME[/]"); drives.AddColumn("[blue]TYPE[/]"); drives.AddColumn("[blue]FORMAT[/]"); drives.AddColumn(new TableColumn(

"[blue]SIZE (BYTES)[/]").RightAligned()); drives.AddColumn(new TableColumn(

"[blue]FREE SPACE[/]").RightAligned());

foreach (DriveInfo drive in DriveInfo.GetDrives())

{

if (drive.IsReady)

{

drives.AddRow(drive.Name, drive.DriveType.ToString(), drive.DriveFormat, drive.TotalSize.ToString("N0"), drive.AvailableFreeSpace.ToString("N0"));

}

else

{

drives.AddRow(drive.Name, drive.DriveType.ToString(), string.Empty, string.Empty, string.Empty);

}

}

AnsiConsole.Write(drives);

Good Practice: Check that a drive is ready before reading properties such as TotalSize , or you will see an exception thrown with removable drives.

On Linux, by default, your console app will only have permission to read the Name and DriveType properties when run as a normal user. An UnauthorizedAccessException is thrown for DriveFormat , TotalSize , and AvailableFreeSpace . Run the console app as superuser to avoid this issue, as shown in the following command:

sudo dotnet run . Using sudo is fine in a development environment, but in a production environment, it's recommended to edit your permissions to avoid running with elevated permissions. On Linux, the name and drive format columns might also need to be wider, for example, 55 and 12 characters wide respectively.

1. Run the code and view the result, as shown in Figure 9.3:

image

Figure 9.3: Showing drive information on Windows and macOS

Managing directories‌

To manage directories, use the Directory , Path , and Environment static classes. These types include many members to work with the filesystem.When constructing custom paths, you must be careful to write your code so that it makes no assumptions about the platform, for example, what to use for the directory separator character:

1. In Program.cs , write statements to do the following:

image

Define a custom path under the user's home directory by creating an array of strings for the directory names, and then properly combine them with the Path type's Combine method.

image

Check for the existence of the custom directory path using the Exists method of the

Directory class.

image

Create and then delete the directory, including files and subdirectories within it, using the CreateDirectory and Delete methods of the Directory class:

SectionTitle("Managing directories"); string newFolder = Combine(

GetFolderPath(SpecialFolder.Personal), "NewFolder"); WriteLine($"Working with: {newFolder}");

// We must explicitly say which Exists method to use

// because we statically imported both Path and Directory. WriteLine($"Does it exist? {Path.Exists(newFolder)}"); WriteLine("Creating it...");

CreateDirectory(newFolder);

// Let's use the Directory.Exists method this time. WriteLine($"Does it exist? {Directory.Exists(newFolder)}"); Write("Confirm the directory exists, and then press any key."); ReadKey(intercept: true);

WriteLine("Deleting it..."); Delete(newFolder, recursive: true);

WriteLine($"Does it exist? {Path.Exists(newFolder)}");

In .NET 6 and earlier, only the Directory class had an Exists method. In .NET 7 or later, the Path class also has an Exists method. Both can be used to check for the existence of a path.

1. Run the code, view the result, and use your favorite file management tool to confirm that the directory has been created before pressing Enter to delete it, as shown in the following output:

Working with: C:\Users\markj\OneDrive\Documents\NewFolder Does it exist? False

Creating it...

Does it exist? True

Confirm the directory exists, and then press any key. Deleting it...

Does it exist? False

Managing files‌

When working with files, you can statically import the file type, just as we did for the directory type. However, for the next example, we will not do so because it has some of the same methods as the directory type, and they would conflict. The file type has a short enough name not to matter in this case. The steps are as follows:

In Program.cs , write statements to do the following:

Check for the existence of a file.

Create a text file.

Write a line of text to the file.

Close the file to release system resources and file locks (this would normally be done inside a try - finally statement block to ensure that the file is closed even if an exception occurs when writing to it).

Copy the file to a backup.

Delete the original file.

Read the backup file's contents and then close it:

SectionTitle("Managing files");

// Define a directory path to output files starting

// in the user's folder. string dir = Combine(

GetFolderPath(SpecialFolder.Personal), "OutputFiles"); CreateDirectory(dir);

// Define file paths.

string textFile = Combine(dir, "Dummy.txt"); string backupFile = Combine(dir, "Dummy.bak"); WriteLine($"Working with: {textFile}");

WriteLine($"Does it exist? {File.Exists(textFile)}");

// Create a new text file and write a line to it. StreamWriter textWriter = File.CreateText(textFile); textWriter.WriteLine("Hello, C#!");

textWriter.Close(); // Close file and release resources. WriteLine($"Does it exist? {File.Exists(textFile)}");

// Copy the file, and overwrite if it already exists. File.Copy(sourceFileName: textFile,

destFileName: backupFile, overwrite: true); WriteLine(

$"Does {backupFile} exist? {File.Exists(backupFile)}"); Write("Confirm the files exist, and then press any key."); ReadKey(intercept: true);

// Delete the file.

File.Delete(textFile);

WriteLine($"Does it exist? {File.Exists(textFile)}");

// Read from the text file backup. WriteLine($"Reading contents of {backupFile}:"); StreamReader textReader = File.OpenText(backupFile); WriteLine(textReader.ReadToEnd()); textReader.Close();

1. Run the code and view the result, as shown in the following output:

Working with: C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.txt Does it exist? False

Does it exist? True

Does C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.bak exist? True Confirm the files exist, and then press any key.

Does it exist? False

Reading contents of C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.bak: Hello, C#!

Managing paths‌

Sometimes, you need to work with parts of a path; for example, you might want to extract just the folder name, the filename, or the extension. Sometimes, you need to generate temporary folders and filenames. You can do this with static methods of the Path class:

1. In Program.cs , add the following statements:

SectionTitle("Managing paths");

WriteLine($"Folder Name: {GetDirectoryName(textFile)}"); WriteLine($"File Name: {GetFileName(textFile)}"); WriteLine("File Name without Extension: {0}",

GetFileNameWithoutExtension(textFile)); WriteLine($"File Extension: {GetExtension(textFile)}"); WriteLine($"Random File Name: {GetRandomFileName()}"); WriteLine($"Temporary File Name: {GetTempFileName()}");

1. Run the code and view the result, as shown in the following output:

Folder Name: C:\Users\markj\OneDrive\Documents\OutputFiles File Name: Dummy.txt

File Name without Extension: Dummy File Extension: .txt

Random File Name: u45w1zki.co3 Temporary File Name:

C:\Users\markj\AppData\Local\Temp\tmphdmipz.tmp

GetTempFileName creates a zero-byte file and returns its name, ready for you to use.

GetRandomFileName just returns a filename; it doesn't create the file.

Getting file information‌

To get more information about a file or directory, for example, its size or when it was last accessed, you can create an instance of the FileInfo or DirectoryInfo class. FileInfo and DirectoryInfo both inherit from FileSystemInfo , so they both have members such as LastAccessTime and Delete , as well as extra members specific to themselves, as shown in Table 9.1:

Class Members

FileSystemInfo

Fields: FullPath , OriginalPath

Properties: Attributes , CreationTime , CreationTimeUtc , Exists , Extension , FullName , LastAccessTime , LastAccessTimeUtc , LastWriteTime , LastWriteTimeUtc , Name

Methods: Delete , GetObjectData , Refresh

DirectoryInfo

Properties: Parent , Root

Methods: Create , CreateSubdirectory , EnumerateDirectories , EnumerateFiles , EnumerateFileSystemInfos , GetAccessControl , GetDirectories , GetFiles ,

GetFileSystemInfos , MoveTo , SetAccessControl

FileInfo

Properties: Directory , DirectoryName ,

IsReadOnly , Length

Methods: AppendText , CopyTo , Create , CreateText , Decrypt , Encrypt , GetAccessControl , MoveTo , Open , OpenRead , OpenText , OpenWrite , Replace , SetAccessControl

Table 9.1: Classes to get information about files and directories

Let's write some code that uses a FileInfo instance to efficiently perform multiple actions on a file:

1. In Program.cs , add statements to create an instance of FileInfo for the backup file, and write information about it to the console, as shown in the following code:

SectionTitle("Getting file information"); FileInfo info = new(backupFile); WriteLine($"{backupFile}:");

WriteLine($" Contains {info.Length} bytes."); WriteLine($" Last accessed: {info.LastAccessTime}"); WriteLine($" Has readonly set to {info.IsReadOnly}.");

1. Run the code and view the result, as shown in the following output:

C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.bak: Contains 12 bytes.

Last accessed: 13/07/2023 12:11:12 Has readonly set to False.

The number of bytes might be different on your operating system because operating systems can use different line endings.

Controlling how you work with files‌

When working with files, you often need to control how they are opened. The File.Open method has overloads to specify additional options using enum values.The enum types are as follows:

image

FileMode : This controls what you want to do with the file, like CreateNew ,

OpenOrCreate , or Truncate .

image

FileAccess : This controls what level of access you need, like ReadWrite .

image

FileShare : This controls locks on the file to allow other processes the specified level of access, like Read .

You might want to open a file and read from it, and allow other processes to read it too, as shown in the following code:

FileStream file = File.Open(pathToFile, FileMode.Open, FileAccess.Read, FileShare.Read);

There is also an enum for attributes of a file as follows:

image

FileAttributes : This is to check a FileSystemInfo -derived type’s Attributes property for values like Archive and Encrypted .

You could check a file or directory's attributes, as shown in the following code:

FileInfo info = new(backupFile);

WriteLine("Is the backup file compressed? {0}", info.Attributes.HasFlag(FileAttributes.Compressed));

Now that you've learned some common ways to work with the directories and files in the filesystem, you next need to learn how to read and write the data stored in a file, that is, how to work with streams.

Reading and writing with streams‌

In Chapter 10, Working with Data Using Entity Framework Core, you will use a file named Northwind.db , but you will not work with the file directly. Instead, you will interact with the SQLite database engine, which in turn will read and write to the file. In scenarios where there is no other system that "owns" the file and does the reading and writing for you, you will use a file stream to work directly with the file.A stream is a sequence of bytes that can be read from and written to. Although files can be processed rather like arrays, with random access provided by knowing the position of a byte within the file, it is more efficient to process a file as a stream in which the bytes can be accessed in sequential order. When a human does the processing, they tend to need random access so that they can jump around the data making changes and return to the data they worked on earlier. When an automated system does the processing, they tend to be able to work sequentially and only need to "touch" the data once.Streams can also be used to process terminal input and output and networking resources, such as sockets and ports, that do not provide random access and cannot seek (that is, move) to a position. You can write code to process some arbitrary bytes without knowing or caring where they come from. Your code simply reads or writes to a stream, and another piece of code handles where the bytes are stored.

Understanding abstract and concrete streams‌

There is an abstract class named Stream that represents any type of stream. Remember that an abstract class cannot be instantiated using new ; it can only be inherited. This is because it is only partially implemented.There are many concrete classes that inherit from this base class, including FileStream , MemoryStream , BufferedStream , GZipStream , and SslStream . They all work the same way. All streams implement IDisposable , so they have a Dispose method to release unmanaged resources.Some of the common members of the Stream class are described in Table 9.2:

Member Description

CanRead , CanWrite Length , Position

Close ,

Dispose

These properties determine if you can read from and write to the stream.

These properties determine the total number of bytes and the current position within the stream. These properties may throw a NotSupportedException for some types of streams, for example, if CanSeek returns false .

This method closes the stream and releases its resources. You can call either method since the implementation of Dispose literally calls Close !

Flush If the stream has a buffer, then this method writes the bytes in the buffer to the stream, and the buffer is cleared.

CanSeek This property determines if the Seek method can be used.

Seek This method moves the current position to the one specified in its parameter.

Read ,

ReadAsync

These methods read a specified number of bytes from the stream into a byte array and advance the position.

ReadByte This method reads the next byte from the stream and advances the position.

Write ,

WriteAsync

These methods write the contents of a byte array into the stream.

WriteByte This method writes a byte to the stream. Table 9.2: Common members of the Stream class

Understanding storage streams‌

Some storage streams that represent a location where the bytes will be stored are described in Table 9.3:

Namespace Class Description

System.IO FileStream Bytes stored in the filesystem.

System.IO MemoryStream Bytes stored in memory in the current process.

System.Net.Sockets NetworkStream Bytes stored at a network location. Table 9.3: Storage stream classes

FileStream has been rewritten in .NET 6 to have much higher performance and reliability on Windows. You can read more about this at the following link: https://devblogs.microsoft.com/dotnet/file-io-improvements-in-dotnet-6/.

Understanding function streams‌

Function streams cannot exist on their own but can only be "plugged into" other streams to add functionality. Some are described in Table 9.4:

Namespace Class Description

System.Security.Cryptography CryptoStream This encrypts and decrypts the stream.

System.IO.Compression GZipStream ,

DeflateStream

These compress and decompress the stream.

System.Net.Security AuthenticatedStream This sends credentials across the

stream.

Table 9.4: Function stream classes

Understanding stream helpers‌

Although there will be occasions where you need to work with streams at a low level, most often, you can plug helper classes into the chain to make things easier. All the helper types for streams implement IDisposable , so they have a Dispose method to release unmanaged resources.Some helper classes to handle common scenarios are described in Table 9.5:

Namespace Class Description

System.IO StreamReader This reads from the underlying stream as plain text.

System.IO StreamWriter This writes to the underlying stream as plain text.

System.IO BinaryReader This reads from streams as .NET types. For example, the ReadDecimal

method reads the next 16 bytes from the underlying stream as a

decimal value, and the ReadInt32 method reads the next 4 bytes as an int value.

System.IO BinaryWriter This writes to streams as .NET types. For example, the Write method with a decimal parameter writes 16 bytes to the underlying stream, and the Write method with an int parameter writes 4 bytes.

System.Xml XmlReader This reads from the underlying stream using the XML format. System.Xml XmlWriter This writes to the underlying stream using the XML format. Table 9.5: Stream helper classes

Building a stream pipeline‌

It is very common to combine a helper like StreamWriter and multiple function streams like CryptoStream and GZipStream with a storage stream like FileStream into a pipeline, as shown in Figure 9.4:

image

Figure 9.4: Writing plain text, then encrypting and compressing it into a file stream

Your code would just call a simple helper method like WriteLine to send a string value like "Hello" through the pipeline until it arrives at its final destination having been encrypted and compressed so it gets written to the file as "G7x" (or whatever it would be).

Writing to text streams‌

When you open a file to read or write to it, you use resources outside of .NET. These are called unmanaged resources and must be disposed of when you are done working with them. To deterministically control when they are disposed of, we can call the Dispose method. When the Stream class was first designed, all cleanup code was expected to go in the Close method. But later, the concept of IDisposable was added to .NET and Stream had to implement a Dispose method. Later, the using statement was added to .NET, which can automatically call Dispose . So today, you can call either Close or Dispose , and they actually do the same thing.Let's type some code to write text to a stream:

Use your preferred code editor to add a new Console App / console project named

WorkingWithStreams to the Chapter09 solution:

image

In Visual Studio, set the startup project for the solution to the current selection.

In the project file, add an element to import the System.Console , System.Environment , and System.IO.Path classes statically and globally.

Add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , add a partial Program class with a SectionTitle and an

OutputFileInfo method, as shown in the following code:

// null namespace to merge with auto-generated Program. partial class Program

{

private static void SectionTitle(string title)

{

ConsoleColor previousColor = ForegroundColor; ForegroundColor = ConsoleColor.DarkYellow; WriteLine($"*** {title} ***"); ForegroundColor = previousColor;

}

private static void OutputFileInfo(string path)

{

WriteLine("**** File Info ****"); WriteLine($"File: {GetFileName(path)}"); WriteLine($"Path: {GetDirectoryName(path)}");

WriteLine($"Size: {new FileInfo(path).Length:N0} bytes."); WriteLine("/ ");

WriteLine(File.ReadAllText(path)); WriteLine(" /");

}

}

Add a new class file named Viper.cs .

In Viper.cs , define a static class named Viper with a static array of string values named Callsigns , as shown in the following code:

namespace Packt.Shared; public static class Viper

{

// Define an array of Viper pilot call signs. public static string[] Callsigns = new[]

{

"Husker", "Starbuck", "Apollo", "Boomer", "Bulldog", "Athena", "Helo", "Racetrack"

};

}

1. In Program.cs , delete the existing statements, and then import the namespace to work with the Viper class, as shown in the following code:

using Packt.Shared; // To use Viper.

1. In Program.cs , add statements to enumerate the Viper call signs, writing each one on its own line in a single text file, as shown in the following code:

SectionTitle("Writing to text streams");

// Define a file to write to.

string textFile = Combine(CurrentDirectory, "streams.txt");

// Create a text file and return a helper writer. StreamWriter text = File.CreateText(textFile);

// Enumerate the strings, writing each one to the stream

// on a separate line.

foreach (string item in Viper.Callsigns)

{

text.WriteLine(item);

}

text.Close(); // Release unmanaged file resources. OutputFileInfo(textFile);

Calling Close on the stream writer helper will call Close on the underlying stream. This in turn calls Dispose to release unmanaged file resources.

1. Run the code and view the result, as shown in the following output:

**** File Info **** File: streams.txt

Path: C:\cs12dotnet8\Chapter09\WorkingWithStreams\bin\Debug\net8.0 Size: 68 bytes.

/------------------

Husker Starbuck Apollo Boomer Bulldog Athena Helo Racetrack

------------------/

1. Open the file that was created, and confirm that it contains the list of call signs, as well as a blank line because we are effectively calling WriteLine twice: once when we write the last call sign to the file, and again when we read the whole file and write it out to the console.

Remember that if you run the project at the command prompt using dotnet run , then the path will be the project folder. It will not include bin\Debug\net8.0 .

Writing to XML streams‌

There are two ways to write an XML element, as follows:

image

WriteStartElement and WriteEndElement : Use this pair when an element might have child elements.

image

WriteElementString : Use this when an element does not have children.

Now, let's try storing the Viper pilot call signs array of string values in an XML file:

1. At the top of Program.cs , import the System.Xml namespace, as shown in the following code:

using System.Xml; // To use XmlWriter and so on.

1. At the bottom of Program.cs , add statements that enumerate the call signs, writing each one as an element in a single XML file, as shown in the following code:

SectionTitle("Writing to XML streams");

// Define a file path to write to.

string xmlFile = Combine(CurrentDirectory, "streams.xml");

// Declare variables for the filestream and XML writer. FileStream? xmlFileStream = null;

XmlWriter? xml = null; try

{

xmlFileStream = File.Create(xmlFile);

// Wrap the file stream in an XML writer helper and tell it

// to automatically indent nested elements. xml = XmlWriter.Create(xmlFileStream,

new XmlWriterSettings { Indent = true });

// Write the XML declaration. xml.WriteStartDocument();

// Write a root element. xml.WriteStartElement("callsigns");

// Enumerate the strings, writing each one to the stream. foreach (string item in Viper.Callsigns)

{

xml.WriteElementString("callsign", item);

}

// Write the close root element. xml.WriteEndElement();

}

catch (Exception ex)

{

// If the path doesn't exist the exception will be caught. WriteLine($"{ex.GetType()} says {ex.Message}");

}

finally

{

if (xml is not null)

{

xml.Close();

WriteLine("The XML writer's unmanaged resources have been disposed.");

}

if (xmlFileStream is not null)

{

xmlFileStream.Close();

WriteLine("The file stream's unmanaged resources have been disposed.");

}

}

OutputFileInfo(xmlFile);

1. Optionally, right-click in the Close method of xmlFileStream , select Go To Implementation, and note the implementations of the Dispose , Close , and Dispose(bool) methods, as shown in the following code:

public void Dispose() => Close(); public virtual void Close()

{

// When initially designed, Stream required that all cleanup logic

// went into Close(), but this was thought up before IDisposable

// was added and never revisited. All subclasses

// should put their cleanup now in Dispose(bool). Dispose(true);

GC.SuppressFinalize(this);

}

protected virtual void Dispose(bool disposing)

{

// Note: Never change this to call other virtual methods on Stream

// like Write, since the state on subclasses has already been

// torn down. This is the last code to run on cleanup for a stream.

}

The Close and Dispose(bool) methods are virtual in the Stream class because they are designed to be overridden in a derived class, like FileStream , to do the work of releasing unmanaged resources.

1. Run the code and view the result, as shown in the following output:

**** File Info ****

The XML writer's unmanaged resources have been disposed. The file stream's unmanaged resources have been disposed. File: streams.xml

Path: C:\cs12dotnet8\Chapter09\WorkingWithStreams\bin\Debug\net8.0 Size: 320 bytes.

/------------------

Husker

Starbuck

Apollo

Boomer

Bulldog

Athena

Helo

Racetrack

-------------------/

Good Practice: Before calling the Dispose method, check that the object is not null .

Simplifying disposal by using the using statement‌

You can simplify the code that needs to check for a null object and then call its Dispose method by using the using statement. Generally, I would recommend using using rather than manually calling Dispose because it’s less code to write, unless you need a greater level of control.Confusingly, there are two uses for the using keyword: importing a namespace and generating a finally statement that calls Dispose on an object implementing

IDisposable .The compiler changes a using statement block into a try - finally statement without a catch statement. You can use nested try statements; so, if you do want to catch any exceptions, you can, as shown in the following code example:

using (FileStream file2 = File.OpenWrite( Path.Combine(path, "file2.txt")))

{

using (StreamWriter writer2 = new StreamWriter(file2))

{

try

{

writer2.WriteLine("Welcome, .NET!");

}

catch(Exception ex)

{

WriteLine($"{ex.GetType()} says {ex.Message}");

}

} // Automatically calls Dispose if the object is not null.

} // Automatically calls Dispose if the object is not null.

You can even simplify the code further by not explicitly specifying the braces and indentation for the using statements, as shown in the following code:

using FileStream file2 = File.OpenWrite( Path.Combine(path, "file2.txt"));

using StreamWriter writer2 = new(file2); try

{

writer2.WriteLine("Welcome, .NET!");

}

catch(Exception ex)

{

WriteLine($"{ex.GetType()} says {ex.Message}");

}

Compressing streams‌

XML is relatively verbose, so it takes up more space in bytes than plain text. Let's see how we can squeeze the XML using a common compression algorithm known as GZIP.In .NET Core 2.1, Microsoft introduced an implementation of the Brotli compression algorithm. In performance, Brotli is like the algorithm used in DEFLATE and GZIP, but the output is about 20% denser.Let's compare the two compression algorithms:

Add a new class file named Program.Compress.cs .

In Program.Compress.cs , write statements to use instances of GZipStream or BrotliStream to create a compressed file containing the same XML elements as before, and then decompress it while reading it and outputting to the console, as shown in the following code:

using Packt.Shared; // To use Viper.

using System.IO.Compression; // To use BrotliStream, GZipStream. using System.Xml; // To use XmlWriter, XmlReader.

partial class Program

{

private static void Compress(string algorithm = "gzip")

{

// Define a file path using the algorithm as file extension. string filePath = Combine(

CurrentDirectory, $"streams.{algorithm}"); FileStream file = File.Create(filePath); Stream compressor;

if (algorithm == "gzip")

{

compressor = new GZipStream(file, CompressionMode.Compress);

}

else

{

compressor = new BrotliStream(file, CompressionMode.Compress);

}

using (compressor)

{

using (XmlWriter xml = XmlWriter.Create(compressor))

{

xml.WriteStartDocument(); xml.WriteStartElement("callsigns"); foreach (string item in Viper.Callsigns)

{

xml.WriteElementString("callsign", item);

}

}

} // Also closes the underlying stream. OutputFileInfo(filePath);

// Read the compressed file. WriteLine("Reading the compressed XML file:"); file = File.Open(filePath, FileMode.Open); Stream decompressor;

if (algorithm == "gzip")

{

decompressor = new GZipStream(

file, CompressionMode.Decompress);

}

else

{

decompressor = new BrotliStream( file, CompressionMode.Decompress);

}

using (decompressor)

using (XmlReader reader = XmlReader.Create(decompressor)) while (reader.Read())

{

// Check if we are on an element node named callsign. if ((reader.NodeType == XmlNodeType.Element)

&& (reader.Name == "callsign"))

{

reader.Read(); // Move to the text inside element. WriteLine($"{reader.Value}"); // Read its value.

}

// Alternative syntax with property pattern matching:

// if (reader is { NodeType: XmlNodeType.Element,

// Name: "callsign" })

}

}

}

1. In Program.cs , add calls to Compress with parameters to use gzip and brotli

algorithms, as shown in the following code:

SectionTitle("Compressing streams"); Compress(algorithm: "gzip"); Compress(algorithm: "brotli");

1. Run the code, and compare the sizes of the XML file and the compressed XML file using

gzip and brotli algorithms, as shown in the following output:

**** File Info **** File: streams.gzip

Path: C:\cs12dotnet8\Chapter09\WorkingWithStreams\bin\Debug\net8.0 Size: 151 bytes.

/------------------

?

z?{??}En?BYjQqf~???????Bj^r~Jf^??RiI??????MrbNNqfz^1?i?QZ??Zd?@H?$%?&gc?t,?????*????H?????t?&?d?

------------------/

Reading the compressed XML file: Husker

Starbuck Apollo Boomer Bulldog

Athena Helo Racetrack

**** File Info **** File: streams.brotli

Path: C:\cs12dotnet8\Chapter09\WorkingWithStreams\bin\Debug\net8.0 Size: 117 bytes.

/-------------------

??d?&?_????\@?Gm????/?h>?6????? ??^? ???wE?'?t?+??F??]

?T?\?~??A?J?Q?q6 ?‐??

???

--------------------/

Reading the compressed XML file: Husker

Starbuck Apollo Boomer Bulldog Athena Helo Racetrack

To summarize:

image

Uncompressed: 320 bytes

image

image

GZIP-compressed: 151 bytes Brotli compressed: 117 bytes

As well as choosing a compression mode, you can also choose a compression level. You can learn more about this at the following link: https://learn.microsoft.com/en- us/dotnet/api/system.io.compression.compressionlevel.

Reading and writing with random access handles‌

For the first 20 years of .NET's life, the only API to work directly with files was the stream classes. These work great for automated tasks that only need to process data sequentially. But when a human interacts with the data, they often want to jump around and return multiple times to the same location. With .NET 6 and later, there is a new API for working with files without needing a file stream and in a random access way. Let's see a simple example:

Use your preferred code editor to add a new Console App / console project named

WorkingWithRandomAccess to the Chapter09 solution:

In the project file, add an element to import the System.Console class statically and globally.

In Program.cs , delete the existing statements, and then get a handle to a file named

coffee.txt , as shown in the following code:

using Microsoft.Win32.SafeHandles; // To use SafeFileHandle. using System.Text; // To use Encoding.

using SafeFileHandle handle = File.OpenHandle(path: "coffee.txt",

mode: FileMode.OpenOrCreate, access: FileAccess.ReadWrite);

1. Write some text encoded as a byte array, and then store it in a read-only memory buffer to the file, as shown in the following code:

string message = "Café £4.39";

ReadOnlyMemory buffer = new(Encoding.UTF8.GetBytes(message)); await RandomAccess.WriteAsync(handle, buffer, fileOffset: 0);

1. To read from the file, get the length of the file, allocate a memory buffer for the contents using that length, and then read the file, as shown in the following code:

long length = RandomAccess.GetLength(handle); Memory contentBytes = new(new byte[length]);

await RandomAccess.ReadAsync(handle, contentBytes, fileOffset: 0); string content = Encoding.UTF8.GetString(contentBytes.ToArray()); WriteLine($"Content of file: {content}");

1. Run the code, and note the content of the file, as shown in the following output:

Content of file: Café £4.39

Encoding and decoding text‌

Text characters can be represented in different ways. For example, the alphabet can be encoded using Morse code into a series of dots and dashes for transmission over a telegraph line.In a similar way, text inside a computer is stored as bits (ones and zeros) representing a code point within a code space. Most code points represent a single character, but they can also have other meanings like formatting.For example, ASCII has a code space with 128 code points. .NET uses a standard called Unicode to encode text internally. Unicode has more than 1 million code points.Sometimes, you will need to move text outside .NET for use by systems that do not use Unicode or a variation of it, so it is important to learn how to convert between encodings.Some common text encodings used by computers are shown in Table 9.6:

Encoding Description

ASCII This encodes a limited range of characters using the lower seven bits of a byte. UTF-8 This represents each Unicode code point as a sequence of one to four bytes.

UTF-7 This is designed to be more efficient over 7-bit channels than UTF-8, but it has security and robustness issues, so UTF-8 is recommended over UTF-7.

UTF-16 This represents each Unicode code point as a sequence of one or two 16-bit integers.

UTF-32 This represents each Unicode code point as a 32-bit integer and is, therefore, a fixed-length encoding unlike the other Unicode encodings, which are all variable- length encodings.

ANSI/ISO This provides support for a variety of code pages that are used to support a

encodings specific language or group of languages.

Table 9.6: Common text encodings

Good Practice: In most cases today, UTF-8 is a good default, which is why it is literally the default encoding, that is, Encoding.Default . You should avoid using Encoding.UTF7 because it is not secure. Due to this, the C# compiler will warn you when you try to use UTF-7. Of course, you might need to generate text using that encoding for compatibility with another system, so it needs to remain an option in .NET.

Encoding strings as byte arrays‌

Let's explore text encodings:

Use your preferred code editor to add a new Console App / console project named

WorkingWithEncodings to the Chapter09 solution.

In the project file, add an element to statically and globally import the System.Console

class.

In Program.cs , delete the existing statements, import the System.Text namespace, add statements to encode a string using an encoding chosen by the user, loop through each byte, and then decode it back into a string and output it, as shown in the following code:

using System.Text; // To use Encoding. WriteLine("Encodings");

WriteLine("[1] ASCII");

WriteLine("[2] UTF-7");

WriteLine("[3] UTF-8");

WriteLine("[4] UTF-16 (Unicode)");

WriteLine("[5] UTF-32");

WriteLine("[6] Latin1");

WriteLine("[any other key] Default encoding"); WriteLine();

Write("Press a number to choose an encoding."); ConsoleKey number = ReadKey(intercept: true).Key; WriteLine(); WriteLine();

Encoding encoder = number switch

{

ConsoleKey.D1 or ConsoleKey.NumPad1 => Encoding.ASCII, ConsoleKey.D2 or ConsoleKey.NumPad2 => Encoding.UTF7, ConsoleKey.D3 or ConsoleKey.NumPad3 => Encoding.UTF8, ConsoleKey.D4 or ConsoleKey.NumPad4 => Encoding.Unicode, ConsoleKey.D5 or ConsoleKey.NumPad5 => Encoding.UTF32, ConsoleKey.D6 or ConsoleKey.NumPad6 => Encoding.Latin1,

_ => Encoding.Default

};

// Define a string to encode string message = "Café £4.39";

WriteLine($"Text to encode: {message} Characters: {message.Length}.");

// Encode the string into a byte array. byte[] encoded = encoder.GetBytes(message);

// Check how many bytes the encoding needed. WriteLine("{0} used {1:N0} bytes.",

encoder.GetType().Name, encoded.Length); WriteLine();

// Enumerate each byte. WriteLine("BYTE | HEX | CHAR"); foreach (byte b in encoded)

{

WriteLine($"{b,4} | {b,3:X} | {(char)b,4}");

}

// Decode the byte array back into a string and display it. string decoded = encoder.GetString(encoded); WriteLine($"Decoded: {decoded}");

1. Run the code, press 1 to choose ASCII, and note that when outputting the bytes, the pound sign ( £ ) and accented e ( é ) cannot be represented in ASCII, so it uses a question mark instead:

Text to encode: Café £4.39 Characters: 10 ASCIIEncodingSealed used 10 bytes.

BYTE

|

HEX

|

CHAR

67

|

43

|

C

97

|

61

|

a

102

|

66

|

f

63

|

3F

|

?

32

|

20

|

63

|

3F

|

?

52

|

34

|

4

46

|

2E

|

.

51

|

33

|

3

57

|

39

|

9

Decoded: Caf? ?4.39

1. Rerun the code and press 3 to choose UTF-8. Note that UTF-8 requires 2 extra bytes for the two characters that need 2 bytes each (12 bytes instead of 10 bytes in total), but

it can encode and decode the é and £ characters:

Text to encode: Café £4.39 Characters: 10 UTF8EncodingSealed used 12 bytes.

BYTE

|

HEX

|

CHAR

67

|

43

|

C

97

|

61

|

a

102

|

66

|

f

195

|

C3

|

Ã

169

|

A9

|

©

32

|

20

|

194

|

C2

|

Â

163

|

A3

|

£

52

|

34

|

4

46

|

2E

|

.

51

|

33

|

3

57

|

39

|

9

Decoded: Café £4.39

1. Rerun the code and press 4 to choose Unicode (UTF-16). Note that UTF-16 requires 2 bytes for every character, so 20 bytes in total, and it can encode and decode the é and £ characters. This encoding is used internally by .NET to store char and string values.

Encoding and decoding text in files‌

When using stream helper classes, such as StreamReader and StreamWriter , you can specify the encoding you want to use. As you write to the helper, the text will be automatically encoded, and as you read from the helper, the bytes will be automatically decoded.To specify an encoding, pass the encoding as a second parameter to the helper type's constructor, as shown in the following code:

StreamReader reader = new(stream, Encoding.UTF8); StreamWriter writer = new(stream, Encoding.UTF8);

Good Practice: Often, you won't have the choice of which encoding to use because you will generate a file for use by another system. However, if you do, pick one that uses the least number of bytes but can store every character you need.

Serializing object graphs‌

An object graph is a structure of multiple objects that are related to each other, either through a direct reference or indirectly through a chain of references.Serialization is the process of converting a live object graph into a sequence of bytes using a specified format. Deserialization is the reverse process. You would use serialization to save the current state of a live object so that you can recreate it in the future, for example, saving the current state of a game so that you can continue at the same place tomorrow. The stream produced from a serialized object is usually stored in a file or database.There are dozens of formats you can choose for serialization, but the two most common text-based human- readable formats are eXtensible Markup Language (XML) and JavaScript Object Notation (JSON). There are also more efficient binary formats like Protobuf used by gRPC.

Good Practice: JSON is more compact and is best for web and mobile applications. XML is more verbose but is better supported in more legacy systems. Use JSON to minimize the size of serialized object graphs. JSON is also a good choice when sending object graphs to web applications and mobile applications because JSON is the native serialization format for JavaScript, and mobile apps often make calls over limited bandwidth, so the number of bytes is important.

.NET has multiple classes that will serialize to and from XML and JSON. We will start by looking at XmlSerializer and JsonSerializer .

Serializing as XML‌

Let's start by looking at XML, probably the world's most used serialization format (for now). To show a typical example, we will define a custom class to store information about a person and then create an object graph, using a list of Person instances with nesting:

Use your preferred code editor to add a new Console App / console project named

WorkingWithSerialization to the Chapter09 solution.

In the project file, add elements to statically and globally import the System.Console ,

System.Environment , and System.IO.Path classes.

Add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , add a partial Program class with a SectionTitle and an

OutputFileInfo method, as shown in the following code:

// null namespace to merge with auto-generated Program. partial class Program

{

private static void SectionTitle(string title)

{

ConsoleColor previousColor = ForegroundColor; ForegroundColor = ConsoleColor.DarkYellow; WriteLine($"*** {title} ***"); ForegroundColor = previousColor;

}

private static void OutputFileInfo(string path)

{

WriteLine("**** File Info ****"); WriteLine($"File: {GetFileName(path)}"); WriteLine($"Path: {GetDirectoryName(path)}");

WriteLine($"Size: {new FileInfo(path).Length:N0} bytes."); WriteLine("/ ");

WriteLine(File.ReadAllText(path)); WriteLine(" /");

}

}

1. Add a new class file named Person.cs to define a Person class with a Salary property that is protected , meaning it is only accessible to itself and derived classes. To populate the salary, the class has a constructor with a single parameter to set the initial salary, as shown in the following code:

namespace Packt.Shared; public class Person

{

public Person(decimal initialSalary)

{

Salary = initialSalary;

}

public string? FirstName { get; set; } public string? LastName { get; set; } public DateTime DateOfBirth { get; set; }

public HashSet? Children { get; set; } protected decimal Salary { get; set; }

}

1. In Program.cs , delete the existing statements, and then import namespaces to work with XML serialization and the Person class, as shown in the following code:

using System.Xml.Serialization; // To use XmlSerializer. using Packt.Shared; // To use Person.

1. In Program.cs , add statements to create an object graph of Person instances, as shown in the following code:

List people = new()

{

new(initialSalary: 30_000M)

{

FirstName = "Alice", LastName = "Smith",

DateOfBirth = new(year: 1974, month: 3, day: 14)

},

new(initialSalary: 40_000M)

{

FirstName = "Bob", LastName = "Jones",

DateOfBirth = new(year: 1969, month: 11, day: 23)

},

new(initialSalary: 20_000M)

{

FirstName = "Charlie", LastName = "Cox",

DateOfBirth = new(year: 1984, month: 5, day: 4), Children = new()

{

new(initialSalary: 0M)

{

FirstName = "Sally", LastName = "Cox",

DateOfBirth = new(year: 2012, month: 7, day: 12)

}

}

}

};

SectionTitle("Serializing as XML");

// Create serializer to format a "List of Person" as XML. XmlSerializer xs = new(type: people.GetType());

// Create a file to write to.

string path = Combine(CurrentDirectory, "people.xml"); using (FileStream stream = File.Create(path))

{

// Serialize the object graph to the stream. xs.Serialize(stream, people);

} // Closes the stream.

OutputFileInfo(path);

1. Run the code, view the result, and note that an exception is thrown, as shown in the following output:

Unhandled Exception: System.InvalidOperationException: Packt.Shared.Person cannot be serialized

1. In Person.cs , add a statement to define a parameterless constructor , as shown in the following code:

// A parameterless constructor is required for XML serialization. public Person() { }

The constructor does not need to do anything, but it must exist so that the XmlSerializer can call it to instantiate new Person instances during the deserialization process.

1. Run the code and view the result, and note that the object graph is serialized as XML elements, like Bob , and that the Salary property is not included because it is not a public property, as shown in the following output:

**** File Info **** File: people.xml

Path: C:\cs12dotnet8\Chapter09\WorkingWithSerialization\bin\Debug\net8.0

Size: 793 bytes.

/------------------

public string? FirstName { get; set; } [XmlAttribute("lname")]

public string? LastName { get; set; } [XmlAttribute("dob")]

public DateTime DateOfBirth { get; set; }

1. Run the code, and note that the size of the file has reduced from 793 to 488 bytes, a space-saving of more than a third. This reduction was achieved by outputting property values as XML attributes, as shown in the following output:

**** File Info **** File: people.xml

Path: C:\cs12dotnet8\Chapter09\WorkingWithSerialization\bin\Debug\net8.0 Size: 488 bytes.

/------------------

------------------/

Deserializing XML files‌

Now, let's try deserializing the XML file back into live objects in memory:

1. In Program.cs , add statements to open the XML file, and then deserialize it, as shown in the following code:

SectionTitle("Deserializing XML files");

using (FileStream xmlLoad = File.Open(path, FileMode.Open))

{

// Deserialize and cast the object graph into a "List of Person". List? loadedPeople =

xs.Deserialize(xmlLoad) as List; if (loadedPeople is not null)

{

foreach (Person p in loadedPeople)

{

WriteLine("{0} has {1} children.", p.LastName, p.Children?.Count ?? 0);

}

}

}

1. Run the code, and note that the people are loaded successfully from the XML file and then enumerated, as shown in the following output:

Smith has 0 children. Jones has 0 children. Cox has 1 children.

More Information: There are many other attributes defined in the System.Xml.Serialization namespace that can be used to control the XML generated. A good place to start is the official documentation for the XmlAttributeAttribute class found here: https://learn.microsoft.com/en- us/dotnet/api/system.xml.serialization.xmlattributeattribute. Do not get this class confused with the XmlAttribute class in the System.Xml namespace. That is used to represent an XML attribute when reading and writing XML, using XmlReader and XmlWriter .

If you don't use any annotations, XmlSerializer performs a case-insensitive match using the property name when deserializing.

Good Practice: When using XmlSerializer , remember that only the public fields and properties are included, and the type must have a parameterless constructor. You can customize the output with attributes.

Serializing with JSON‌

One of the most popular .NET libraries to work with the JSON serialization format is Newtonsoft.Json, known as Json.NET. It is mature and powerful. Newtonsoft.Json is so popular that it overflowed the bounds of the 32-bit integer used for the download count in the NuGet package manager, as shown in the following tweet in Figure 9.5:

image

Figure 9.5: Negative 2 billion downloads for Newtonsoft.Json in August 2022

Let's see it in action:

1. In the WorkingWithSerialization project, add a package reference for the latest version of Newtonsoft.Json , as shown in the following markup:

Build the WorkingWithSerialization project to restore packages.

In Program.cs , add statements to create a text file, and then serialize the people into the file as JSON, as shown in the following code:

SectionTitle("Serializing with JSON");

// Create a file to write to.

string jsonPath = Combine(CurrentDirectory, "people.json"); using (StreamWriter jsonStream = File.CreateText(jsonPath))

{

Newtonsoft.Json.JsonSerializer jss = new();

// Serialize the object graph into a string. jss.Serialize(jsonStream, people);

} // Closes the file stream and release resources. OutputFileInfo(jsonPath);

1. Run the code, and note that JSON requires fewer than half the number of bytes compared to XML with elements. It's even smaller than the XML file, which uses attributes (366 compared to 488), as shown in the following output:

**** File Info **** File: people.json

Path: C:\cs12dotnet8\Chapter09\WorkingWithSerialization\bin\Debug\net8.0 Size: 366 bytes.

/------------------

[{"FirstName":"Alice","LastName":"Smith","DateOfBirth":"1974-03-14T00:00:00","Children":null},{"

------------------/

High-performance JSON processing‌

.NET Core 3 introduced a new namespace to work with JSON, System.Text.Json , which is optimized for performance by leveraging APIs like Span .Also, older libraries like Json.NET are implemented by reading UTF-16. It would be more performant to read and write JSON documents using UTF-8 because most network protocols, including HTTP, use UTF-8, and you can avoid transcoding UTF-8 to and from Json.NET's Unicode string values.With the new API, Microsoft achieved between 1.3x and 5x improvement, depending on the scenario.The

original author of Json.NET, James Newton-King, joined Microsoft and is working with them to develop their new JSON types. As he says in a comment discussing the new JSON APIs, "Json.NET isn't going away," as shown in Figure 9.6:

image

Figure 9.6: A comment by the original author of Json.NET

Deserializing JSON files‌

Let's see how to use the modern JSON APIs to deserialize a JSON file:

1. In the WorkingWithSerialization project, at the top of Program.cs , import the new JSON class to perform serialization, using an alias to avoid conflicting names with the Json.NET one we used before, as shown in the following code:

using FastJson = System.Text.Json.JsonSerializer;

1. In Program.cs , add statements to open the JSON file, deserialize it, and output the names and counts of the children of the people, as shown in the following code:

SectionTitle("Deserializing JSON files");

await using (FileStream jsonLoad = File.Open(jsonPath, FileMode.Open))

{

// Deserialize object graph into a "List of Person". List? loadedPeople =

await FastJson.DeserializeAsync(utf8Json: jsonLoad, returnType: typeof(List)) as List;

if (loadedPeople is not null)

{

foreach (Person p in loadedPeople)

{

WriteLine("{0} has {1} children.", p.LastName, p.Children?.Count ?? 0);

}

}

}

1. Run the code and view the result, as shown in the following output:

Smith has 0 children. Jones has 0 children. Cox has 1 children.

Good Practice: Choose Json.NET for developer productivity and a large feature set, or System.Text.Json for performance. You can review a list of the differences at the following link: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system- text-json-migrate-from-newtonsoft-how-to#table-of-differences-between-newtonsoftjson- and-systemtextjson.

Controlling JSON processing‌

There are many options to take control of how JSON is processed, as shown in the following list:

image

image

Including and excluding fields. Setting a casing policy.

image

Selecting a case-sensitivity policy.

image

Choosing between compact and prettified whitespace.

Let's see some in action:

Use your preferred code editor to add a new Console App / console project named

ControllingJson to the Chapter09 solution.

In the project file, add elements to statically and globally import the System.Console ,

System.Environment , and System.IO.Path classes.

In the ControllingJson project, add a new class file named Book.cs .

In Book.cs , define a class named Book , as shown in the following code:

using System.Text.Json.Serialization; // To use [JsonInclude]. namespace Packt.Shared;

public class Book

{

// Constructor to set non-nullable property. public Book(string title)

{

Title = title;

}

// Properties.

public string Title { get; set; } public string? Author { get; set; }

// Fields.

[JsonInclude] // Include this field. public DateTime PublishDate; [JsonInclude] // Include this field. public DateTimeOffset Created; public ushort Pages;

}

1. In Program.cs , delete the existing statements, and then import the namespaces to work with high-performance JSON and Book , as shown in the following code:

using Packt.Shared; // To use Book.

using System.Text.Json; // To use JsonSerializer.

1. In Program.cs , add statements to create an instance of the Book class and serialize it to JSON, as shown in the following code:

Book csharpBook = new(title:

"C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals")

{

Author = "Mark J Price",

PublishDate = new(year: 2023, month: 11, day: 14),

Pages = 823,

Created = DateTimeOffset.UtcNow,

};

JsonSerializerOptions options = new()

{

IncludeFields = true, // Includes all fields. PropertyNameCaseInsensitive = true, WriteIndented = true,

PropertyNamingPolicy = JsonNamingPolicy.CamelCase,

};

string path = Combine(CurrentDirectory, "book.json"); using (Stream fileStream = File.Create(path))

{

JsonSerializer.Serialize(

utf8Json: fileStream, value: csharpBook, options);

}

WriteLine("**** File Info ****"); WriteLine($"File: {GetFileName(path)}"); WriteLine($"Path: {GetDirectoryName(path)}");

WriteLine($"Size: {new FileInfo(path).Length:N0} bytes."); WriteLine("/ ");

WriteLine(File.ReadAllText(path)); WriteLine(" /");

1. Run the code and view the result, as shown in the following output:

**** File Info **** File: book.json

Path: C:\cs12dotnet8\Chapter09\ControllingJson\bin\Debug\net8.0 Size: 221 bytes.

/------------------

{

"title": "C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals", "author": "Mark J Price",

"publishDate": "2023-11-14T00:00:00", "created": "2023-07-13T14:29:07.119631+00:00",

"pages": 823

}

------------------/

Note the following:

image

The JSON file is 221 bytes.

image

The member names use camelCasing, for example, publishDate . This is best for subsequent processing in a browser with JavaScript.

image

image

All fields are included due to the options set, including pages . JSON is prettified for easier human legibility.

image

DateTime and DateTimeOffset values are stored as a single standard string format.

In Program.cs , when setting the JsonSerializerOptions , comment out the setting of casing policy, write with an indent, and include fields.

Run the code and view the result, as shown in the following output:

**** File Info **** File: book.json

Path: C:\cs12dotnet8\Chapter09\ControllingJson\bin\Debug\net8.0 Size: 184 bytes.

/------------------

{"Title":"C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals","Author":"Mark J Pr

------------------/

Note the following:

image

The JSON file has about a 20% reduction.

image

The member names use normal casing, for example, PublishDate .

image

The Pages field is missing. The other fields are included due to the [JsonInclude]

attribute on the PublishDate and Created fields.

Working with environment variables‌

Environment variables are system and user-definable values that can affect the way running processes behave. They are commonly used to set options like toggling between development and production configurations in ASP.NET Core web projects, or to pass values needed by a process like service keys and passwords for database connection strings.Environment variables on Windows can be set at three scope levels: machine (aka system), user, and process. The methods for setting and getting environment variables assume process scope

level by default and have overloads to specify the EnvironmentVariableTarget of Process ,

User , and Machine , as shown in Table 9.7:

Method Description

GetEnvironmentVariables Returns an IDictionary of all environment variables at a

specified scope level or for the current process by default.

GetEnvironmentVariable Returns the value for a named environment variable.

SetEnvironmentVariable Sets the value for a named environment variable.

ExpandEnvironmentVariables Converts any environment variables in a string to their values identified with %% . For example,

"My computer is named %COMPUTER_NAME%" .

Table 9.7: Methods to work with environment variables

Reading all environment variables‌

Let's start by looking at how to list all current environment variables at various levels of scope:

Use your preferred code editor to add a new Console App / console project named

WorkingWithEnvVars to the Chapter09 solution.

In the project file, add a package reference for Spectre.Console , and then add elements to statically and globally import the System.Console and System.Environment classes, and finally import the namespaces to work with Spectre.Console and System.Collections , as shown in the following configuration:

Add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , add a partial Program class with a SectionTitle and an

DictionaryToTable method, as shown in the following code:

// null namespace to merge with auto-generated Program. partial class Program

{

private static void SectionTitle(string title)

{

ConsoleColor previousColor = ForegroundColor; ForegroundColor = ConsoleColor.DarkYellow; WriteLine($"*** {title} ***"); ForegroundColor = previousColor;

}

private static void DictionaryToTable(IDictionary dictionary)

{

Table table = new(); table.AddColumn("Key"); table.AddColumn("Value");

foreach (string key in dictionary.Keys)

{

table.AddRow(key, dictionary[key]!.ToString()!);

}

AnsiConsole.Write(table);

}

}

1. In Program.cs , delete any existing statements, and write statements to show all the environment variables at the three different scopes, as shown in the following code:

SectionTitle("Reading all environment variables for process"); IDictionary vars = GetEnvironmentVariables(); DictionaryToTable(vars);

SectionTitle("Reading all environment variables for machine"); IDictionary varsMachine = GetEnvironmentVariables(

EnvironmentVariableTarget.Machine); DictionaryToTable(varsMachine);

SectionTitle("Reading all environment variables for user"); IDictionary varsUser = GetEnvironmentVariables(

EnvironmentVariableTarget.User); DictionaryToTable(varsUser);

1. Run the code and view the result, as shown in the following partial output:

*** Reading all environment variables for process ***

┌─────────────────┬──────────────────────────────────────────────────┐

│ Key │ Value │

├─────────────────┼──────────────────────────────────────────────────┤

│ HOMEPATH │ \Users\markj │

...

└─────────────────┴──────────────────────────────────────────────────┘

Expanding, setting, and getting an environment variables‌

Often you need to format a string that contains embedded environment variables. They are defined by surrounding the variable name with percent symbols, as shown in the following text:

My username is %USERNAME%. My CPU is %PROCESSOR_IDENTIFIER%.

To set an environment variable at the command prompt on Windows, use the set or setx

commands, as shown in Table 9.8:

Scope Level Command

Session/Shell set MY_ENV_VAR="Alpha" User setx MY_ENV_VAR "Beta" Machine setx MY_ENV_VAR "Gamma" /M

Table 9.8: Commands to set an environment variable on Windows

The set command defines a temporary environment variable that can be read immediately in the current shell or session. Note that it uses an equal sign = to assign the value.The setx command defines a permanent environment variable but after defining it, you must close the current shell or session and restart the shell for the environment variable to be read. Note that it does not use an equal sign to assign the value!

More Information: You can learn more about the setx command here: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/setx.

You can also manage environment variables with a user interface on Windows: Navigate to Settings | System | About | Advanced system settings, and then in the System Properties dialog box, click Environment Variables.To temporarily set an environment variable at the command prompt or terminal on macOS or Linux, you can use the export command, as shown in the following command:

export MY_ENV_VAR=Delta

More Information: You can learn more about the export command here: https://ss64.com/bash/export.xhtml.

Let's see some examples of expanding, setting in various ways, and getting environment variables:

1. In Program.cs , add statements to define a string that contains a couple of environment variables (if the ones I picked are not defined on your computer, then pick any other two that you do have defined) and then expand them and output them to the console, as shown in the following code:

string myComputer = "My username is %USERNAME%. My CPU is %PROCESSOR_IDENTIFIER%."; WriteLine(ExpandEnvironmentVariables(myComputer));

1. Run the code and view the result, as shown in the following output:

My username is markj. My CPU is Intel64 Family 6 Model 140 Stepping 1, GenuineIntel.

1. In Program.cs , add statements to set a process scoped environment variable named

MY_PASSWORD and then get it and output it, as shown in the following code:

string password_key = "MY_PASSWORD"; SetEnvironmentVariable(password_key, "Pa$$w0rd"); string? password = GetEnvironmentVariable(password_key); WriteLine($"{password_key}: {password}");

1. Run the code and view the result, as shown in the following output:

MY_PASSWORD: Pa$$w0rd

In a real-world app, you might pass an argument to the console that is then used to set the process scope environment variable on startup for reading later in the process lifetime.

1. In Program.cs , add statements to try to get an environment variable named MY_PASSWORD at all three potential scope levels, and then output them, as shown in the following code:

string secret_key = "MY_SECRET";

string? secret = GetEnvironmentVariable(secret_key, EnvironmentVariableTarget.Process);

WriteLine($"Process - {secret_key}: {secret}"); secret = GetEnvironmentVariable(secret_key,

EnvironmentVariableTarget.Machine); WriteLine($"Machine - {secret_key}: {secret}"); secret = GetEnvironmentVariable(secret_key,

EnvironmentVariableTarget.User); WriteLine($"User - {secret_key}: {secret}");

If you are using Visual Studio 2022, then navigate to Project | WorkingWithEnvVars Properties, click the Debug tab, and then click Open debug launch profiles UI. In the Environment variables section, add an entry with Name MY_SECRET and Value of Alpha .

In the Properties folder, open launchSettings.json , and note the configured environment variables, as shown in the following configuration:

{

"profiles": { "WorkingWithEnvVars": {

"commandName": "Project", "environmentVariables": {

"MY_SECRET": "Alpha"

}

}

}

}

If you are using a different code editor, then you can manually create the launchSettings.json file. Environment variables defined in the launchSettings.json file are set at process scope.

1. At the command prompt or terminal with administrator permissions, set some environment variables at the user and machine scope levels on Windows, as shown in the following commands:

setx MY_SECRET "Beta" setx MY_SECRET "Gamma" /M

1. Note the result for each command, as shown in the following output:

SUCCESS: Specified value was saved.

On macOS or Linux, use the export command instead.

1. Run the code and view the result, as shown in the following output:

Process - MY_SECRET: Alpha Machine - MY_SECRET: Gamma User - MY_SECRET: Beta

Now that you have seen how to work with environment variables, we can use them in future chapters to set options like passwords rather than store those sensitive values in code.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring this chapter's topics with more in-depth research.

Exercise 9.1 – Test your knowledge‌

Answer the following questions:

What is the difference between using the File class and the FileInfo class?

What is the difference between the ReadByte method and the Read method of a stream?

When would you use the StringReader , TextReader , and StreamReader classes?

What does the DeflateStream type do?

How many bytes per character does UTF-8 encoding use?

What is an object graph?

What is the best serialization format to choose to minimize space requirements?

What is the best serialization format to choose for cross-platform compatibility?

Why is it bad to use a string value like "\Code\Chapter01" to represent a path, and what should you do instead?

Where can you find information about NuGet packages and their dependencies?

Exercise 9.2 – Practice serializing as XML‌

In the Chapter09 solution, create a console app named Ch09Ex02SerializingShapes that creates a list of shapes, uses serialization to save it to the filesystem with XML, and then deserializes it back:

// Create a list of Shapes to serialize. List listOfShapes = new()

{

new Circle { Colour = "Red", Radius = 2.5 },

new Rectangle { Colour = "Blue", Height = 20.0, Width = 10.0 }, new Circle { Colour = "Green", Radius = 8.0 },

new Circle { Colour = "Purple", Radius = 12.3 },

new Rectangle { Colour = "Blue", Height = 45.0, Width = 18.0 }

};

Shapes should have a read-only property named Area so that when you deserialize, you can output a list of shapes, including their areas, as shown here:

List loadedShapesXml = serializerXml.Deserialize(fileXml) as List;

foreach (Shape item in loadedShapesXml)

{

WriteLine("{0} is {1} and has an area of {2:N2}", item.GetType().Name, item.Colour, item.Area);

}

This is what your output should look like when you run your console application:

Loading shapes from XML:

Circle is Red and has an area of 19.63 Rectangle is Blue and has an area of 200.00 Circle is Green and has an area of 201.06 Circle is Purple and has an area of 475.29 Rectangle is Blue and has an area of 810.00

Exercise 9.3 – Working with Tar archives‌

If you use Linux, then you will be interested in how to programmatically work with Tar archives. I have written an online-only section to introduce you to them that is found at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch09-tar- archives.md

Exercise 9.4 – Migrating from Newtonsoft to new JSON‌

If you have existing code that uses the Newtonsoft Json.NET library and you want to migrate to the new System.Text.Json namespace, then Microsoft has specific documentation for that that you can find at the following link:https://learn.microsoft.com/en- us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to

Exercise 9.5 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-9--- working-with-files-streams-and-serialization

Summary‌

In this chapter, you learned how to:

image

image

image

Read from and write to text files. Read from and write to XML files. Compress and decompress files.

image

Encode and decode text.

image

image

image

Serialize an object graph into JSON and XML. Deserialize an object graph from JSON and XML. Work with environment variables.

In the next chapter, you will learn how to work with databases using Entity Framework Core.

Working with Data Using Entity Framework Core‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about reading from and writing to relational data stores, such as SQLite and SQL Server, by using the object-to-data store mapping technology named Entity Framework Core (EF Core).This chapter will cover the following topics:

image

image

image

Understanding modern databases Setting up EF Core in a .NET project Defining EF Core models

image

Querying EF Core models

image

image

Loading and tracking patterns with EF Core Modifying data with EF Core

Understanding modern databases‌

Two of the most common places to store data are in a Relational Database Management System (RDBMS), such as SQL Server, PostgreSQL, MySQL, and SQLite, or in a NoSQL database such as Azure Cosmos DB, Redis, MongoDB, and Apache Cassandra.Relational databases were invented in the 1970s. They are queried with Structured Query Language (SQL). At the time, data storage costs were high, so they reduced data duplication as much as possible. Data is stored in tabular structures with rows and columns that are tricky to refactor once in production.

They can be difficult and expensive to scale.NoSQL databases do not just mean “no SQL”; they can also mean “not only SQL.” They were invented in the 2000s, after the Internet and the web had become popular, and adopted many of the learnings from that era of software.

They are designed for massive scalability and high performance and to make programming easier by providing maximum flexibility and allowing schema changes at any time because they do not enforce a structure.If you know nothing about relational databases, then you should read the database primer that I wrote at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch10-database-primer.md

Understanding legacy Entity Framework‌

Entity Framework (EF) was first released as part of .NET Framework 3.5 with Service Pack 1 back in late 2008. Since then, Entity Framework has evolved, as Microsoft has observed how programmers use an object-relational mapping (ORM) tool in the real world.ORMs use a mapping definition to associate columns in tables to properties in classes. Then, a programmer can interact with objects of different types in a way that they are familiar with, instead of having to deal with knowing how to store the values in a relational table or another structure provided by a NoSQL data store.The version of EF included with .NET Framework is Entity Framework 6 (EF6). It is mature and stable and supports an EDMX (XML file) way of defining the model, as well as complex inheritance models and a few other

advanced features.EF 6.3 and later have been extracted from .NET Framework as a separate package, so they can be supported on .NET Core 3 and later. This enables existing projects like web applications and services to be ported and run cross-platform. However, EF6 should be considered a legacy technology because it has some limitations when running cross- platform and no new features will be added to it.

Using the legacy Entity Framework 6.3 or later‌

To use the legacy Entity Framework in a .NET Core 3 or later project, you must add a package reference to it in your project file, as shown in the following markup:

Good Practice: Only use legacy EF6 if you must; for example, to migrate a WPF app that uses EF6 on .NET Framework to modern .NET. This book is about modern cross-platform development, so in the rest of this chapter, I will only cover the modern EF Core. You will not need to reference the legacy EF6 package as shown above in the projects for this chapter.

Understanding Entity Framework Core‌

The truly cross-platform version, EF Core, is different from the legacy Entity Framework. Although EF Core has a similar name, you should be aware of how it varies from EF6. The latest EF Core is version 8, to match .NET 8.

EF Core 8 targets .NET 8 or later. EF Core 9 will also target .NET 8 or later because the EF Core team wants as many developers as possible to benefit from new features in future releases even if you must target only long-term support releases of .NET. This means that you can use all the new features of EF Core 9 with either .NET 8 or .NET 9. But when EF Core 10 is released in November 2025, your projects will need to target .NET

10 to use it.

EF Core 3 and later only work with platforms that support .NET Standard 2.1, meaning .NET Core 3 and later. EF Core 3 and later do not support .NET Standard 2.0 platforms like .NET Framework 4.8.As well as traditional RDBMSs, EF Core supports modern cloud-based, nonrelational, schema-less data stores, such as Azure Cosmos DB and MongoDB, sometimes with third-party providers.EF Core has so many improvements in each release that this chapter cannot cover them all. In this chapter, I will focus on the fundamentals that all .NET developers should know and some of the most useful new features. You can learn more about EF Core and how to use it with SQL Server in my companion book, Apps and Services with .NET 8, or by reading the official documentation, found at the following link: https://learn.microsoft.com/en-us/ef/core/.You can keep up with the latest EF Core news at the following link: https://aka.ms/efnews.

Understanding Database First and Code First‌

There are two approaches to working with EF Core:

Database First: A database already exists, so you build a model that matches its structure and features. This is the most common scenario in real life. You will see an example of this throughout this chapter.

Code First: No database exists, so you build a model and then use EF Core to create a database that matches its structure and features. You will see an example of this if you complete the online-only section linked to in one of the exercises at the end of this chapter.

Performance improvements in EF Core‌

The EF Core team continues to work hard on improving the performance of EF Core. For example, if EF Core identifies that only a single statement will be executed against the database when SaveChanges is called, then it does not create an explicit transaction as earlier versions do. That gives a 25% performance improvement to a common scenario.There is too much detail about all the recent performance improvements to cover in this chapter, and you get all the benefits without needing to know how they work anyway. If you are interested (and it is fascinating what they looked at and how they took advantage of some cool SQL Server features in particular), then I recommend that you read the following posts from the EF Core team:

image

Announcing Entity Framework Core 7 Preview 6: Performance Edition: https://devblogs.microsoft.com/dotnet/announcing-ef-core-7-preview6-performance- optimizations/

image

Announcing Entity Framework Core 6.0 Preview 4: Performance Edition: https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-6-0-preview-4- performance-edition/

Using a sample relational database‌

To learn how to manage an RDBMS using .NET, it would be useful to have a sample one so that you can practice on one that has a medium complexity and a decent number of sample records. Microsoft offers several sample databases, most of which are too complex for our needs, so instead, we will use a database that was first created in the early 1990s known as Northwind.Let's take a minute to look at a diagram of the Northwind database. You can use the diagram in Figure 10.1 to refer to as we write code and queries throughout this book:

image

Figure 10.1: The Northwind database tables and relationships

You will write code to work with the Categories and Products tables later in this chapter, and other tables in later chapters. But before we do, note that:

image

Each category has a unique identifier, name, description, and picture.

image

Each product has a unique identifier, name, unit price, units in stock, and other fields.

image

image

Each product is associated with a category by storing the category's unique identifier. The relationship between Categories and Products is one-to-many, meaning each category can have zero or more products. This is indicated in Figure 10.1 by an infinity symbol at one end (meaning many) and a yellow key at the other end (meaning one).

Using SQLite‌

SQLite is a small, fast, cross-platform, self-contained RDBMS that is available in the public domain. It's the most common RDBMS for mobile platforms such as iOS (iPhone and iPad) and Android. SQLite is the most used database engine in the world and there are more than one trillion SQLite databases in active use. You can read more about this at the following link: https://www.sqlite.org/mostdeployed.xhtml.

I decided to demonstrate databases using SQLite in this book, since important themes are cross-platform development and fundamental skills that only need basic database capabilities. I recommend that you initially complete the book code tasks using SQLite. If you also want to try the code tasks using SQL Server, then I provide documentation to do so in the online-only sections in the GitHub repository for this book.

Using SQL Server or other SQL systems‌

Enterprises that standardize on Windows tend to also use SQL Server as their database. If you would prefer to use SQL Server, please see the online instructions at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/sql-server/README.mdIf you would prefer to use a different SQL system, then the SQL scripts that I provide should work with most SQL systems, for example, PostgreSQL or MySQL, but I have not written step-by-step instructions for them and I make no guarantees it will work.My recommendation is to complete this book using SQLite, so you focus on learning what's taught in the book about EF Core rather than adding the complication of trying to use a different database system. Learning is hard enough; don't bite off more than you can chew; don't make it harder on yourself.

Once you've learned what's in the book, you can always repeat it with a different database system.

Setting up SQLite for Windows‌

On Windows, we need to add the folder for SQLite to the system path so it will be found when we enter commands at a command prompt or terminal:

Start your favorite browser and navigate to the following link: https://www.sqlite.org/download.xhtml.

Scroll down the page to the Precompiled Binaries for Windows section.

Click sqlite-tools-win32-x86-3420000.zip (or the file might have a higher version number), as shown in the following screenshot:

image

Figure 10.2: Downloading SQLite for Windows

Extract the ZIP file into a folder named C:\Sqlite\ . Make sure that the three extracted files including sqlite3.exe are directly inside the C:\SQLite folder or the executable will not be found later when you try to use it.

In the Windows Start menu, navigate to Settings.

Search for environment and choose Edit the system environment variables. On non- English versions of Windows, please search for the equivalent word in your local language to find the setting.

Click the Environment Variables button.

In System variables, select Path in the list, and then click Edit….

If C:\SQLite is not already in the path, then click New, enter C:\Sqlite , and press Enter.

Click OK, then OK, then OK again, and then close Settings.

To confirm that the path to SQLite has been configured correctly, at any command prompt or terminal, enter a command to start SQLite, as shown in the following command:

sqlite3

1. Note the result, as shown in the following output:

SQLite version 3.42.0 2023-05-16 12:36:15

Enter ".help" for usage hints.

Connected to a transient in-memory database.

Use ".open FILENAME" to reopen on a persistent database. sqlite>

1. To exit the SQLite command prompt:

image

image

On Windows, press Ctrl + C twice. On macOS, press Ctrl + D.

Setting up SQLite for macOS and Linux‌

On macOS, SQLite is included in the /usr/bin/ directory as a command-line application named

sqlite3 .On Linux, you can get set up with SQLite using the following command:

sudo apt-get install sqlite3

SQLite can be downloaded and installed for other OSs from the following link: https://www.sqlite.org/download.xhtml.

Setting up EF Core in a .NET project‌

Now that we have a database system set up, we can create a database and .NET project that use it.

Creating a console app for working with EF Core‌

First, we will create a console app project for this chapter:

1. Use your preferred code editor to create a new project, as defined in the following list:

image

image

image

Project template: Console App / console Project file and folder: WorkingWithEFCore Solution file and folder: Chapter10

Creating the Northwind sample database for SQLite‌

Now we can create the Northwind sample database for SQLite using an SQL script:

If you have not previously cloned or downloaded the ZIP for the GitHub repository for this book, then do so now using the following link: https://github.com/markjprice/cs12dotnet8.

Copy the script to create the Northwind database for SQLite from the following path in your local Git repository or where you extracted the ZIP:

/scripts/sql-scripts/Northwind4SQLite.sql into the WorkingWithEFCore folder.

Start a command prompt or terminal in the WorkingWithEFCore project folder:

image

On Windows, start File Explorer, right-click the WorkingWithEFCore folder, and select New Command Prompt at Folder or Open in Windows Terminal.

image

On macOS, start Finder, right-click the WorkingWithEFCore folder, and select New Terminal at Folder.

Enter the command to execute the SQL script using SQLite to create the Northwind.db

database, as shown here:

sqlite3 Northwind.db -init Northwind4SQLite.sql

1. Be patient because this command might take a while to create the database structure. Eventually, you will see the SQLite command prompt, as shown in the following output:

-- Loading resources from Northwind4SQLite.sql SQLite version 3.42.0 2023-05-16 12:36:15

Enter ".help" for usage hints. sqlite>

To exit the SQLite command prompt:

image

image

On Windows, press Ctrl + C twice. On macOS or Linux, press Ctrl + D.

You can leave the command prompt or terminal window open because you will use it again soon.

If you are using Visual Studio 2022‌

If you are using Visual Studio Code and the dotnet run command, the compiled application executes in the WorkingWithEFCore folder, allowing it to locate the database file stored therein. But if you are using Visual Studio 2022, or JetBrains Rider, then the compiled application executes in the WorkingWithEFCore\bin\Debug\net8.0 folder, so it will not find the database file because it is not in that directory.Let's tell Visual Studio 2022 to copy the database file to the directory that it runs the code in so that it can find the file, but only if the database file is newer or is missing so it will not overwrite any database changes we make during runtime:

In Solution Explorer, right-click the Northwind.db file and select Properties.

In Properties, set Copy to Output Directory to Copy if newer.

In WorkingWithEFCore.csproj , note the new elements, as shown in the following markup:

PreserveNewest

If you prefer to overwrite the data changes every time you start the project, then set

CopyToOutputDirectory to Always .

Managing the Northwind sample database with SQLiteStudio‌

You can use a cross-platform graphical database manager named SQLiteStudio to easily manage SQLite databases:

Navigate to the following link, https://sqlitestudio.pl, and then download and install the application.

Start SQLiteStudio.

Navigate to Database | Add a database.

In the Database dialog, in the File section, click on the yellow folder button to browse for an existing database file on the local computer, select the Northwind.db file in the WorkingWithEFCore project folder, and then click OK, as shown in Figure 10.3:

image

Figure 10.3: Adding the Northwind.db database file to SQLiteStudio

If you cannot see the database, then navigate to View | Databases.

In the Databases window, right-click on the Northwind database and choose Connect to the database (or just double-click Northwind). You will see the 10 tables that were created by the script. (The script for SQLite is simpler than the one for SQL Server; it does not create as many tables or other database objects.)

Right-click on the Products table and choose Edit the table, or just double-click the table.

In the table editor window, note the structure of the Products table, including column names, data types, keys, and constraints, as shown in Figure 10.4:

image

Figure 10.4: The table editor in SQLiteStudio showing the structure of the Products table

In the table editor window, click the Data tab, and you will see 77 products, as shown in Figure 10.5:

image

Figure 10.5: The Data tab showing the 77 rows in the Products table

In the Database window, right-click Northwind and select Disconnect from the database.

Quit SQLiteStudio.

Using the lightweight ADO.NET database providers‌

Before Entity Framework, there was ADO.NET. Compared to EF, this is a simpler and more efficient API for working with databases. It provides abstract classes like DbConnection , DbCommand , and DbReader , and provider-specific implementations of them like SqlConnection and SqlCommand . In this chapter, if you choose to use SQL Server then you should use the SqlConnectionStringBuilder class to help write a valid connection string. This is because it has properties for all possible parts of a database connection string that you set individually and then it returns the complete string. You should also get sensitive secrets like passwords from an environment variable or a secret management system instead of writing them in your source code.For SQLite, the connection string is so simple you do not need to use the SqliteConnectionStringBuilder class.The EF Core database providers for SQLite and SQL Server are built on top of the ADO.NET libraries, so EF Core is always inherently slower than ADO.NET. ADO.NET can be used independently for better performance because the EF Core database providers are "closer to the metal."If you want to use native ahead-of-time (AOT) publishing, be aware that EF Core does not yet support it. This means you can only use the ADO.NET libraries if you plan to compile to native code. The EF Core team is investigating how they can support native AOT, but it is challenging so it is unlikely to happen with EF Core 8 this year. Hopefully, it will happen for EF Core 9 in 2024 or EF Core 10 in 2025.Apart from SqlConnectionStringBuilder , this book does not cover using the ADO.NET library, but I do cover examples of how to publish native AOT minimal API web services using the ADO.NET for SQL Server library in the companion book, Apps and Services with .NET 8. You can learn more about the ADO.NET for SQLite library at the following link:https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/You can learn more about the ADO.NET for SQL Server library at the following link:https://learn.microsoft.com/en- us/sql/connect/ado-net/microsoft-ado-net-sql-server

Choosing an EF Core database provider‌

Before we dive into the practicalities of managing data using EF Core, let's briefly talk about choosing between EF Core database providers. To manage data in a specific database, we need classes that know how to efficiently talk to that database.EF Core database providers are sets of classes that are optimized for a specific data store. There is even a provider for storing the data in the memory of the current process, which can be useful for high- performance unit testing since it avoids hitting an external system.They are distributed as NuGet packages, as shown in Table 10.1:

To manage this data store Reference this NuGet package

SQL Server 2012 or later Microsoft.EntityFrameworkCore.SqlServer SQLite 3.7 or later Microsoft.EntityFrameworkCore.SQLite In-memory Microsoft.EntityFrameworkCore.InMemory Azure Cosmos DB SQL API Microsoft.EntityFrameworkCore.Cosmos MySQL MySQL.EntityFrameworkCore

Oracle DB 11.2 Oracle.EntityFrameworkCore

PostgreSQL Npgsql.EntityFrameworkCore.PostgreSQL

Table 10.1: NuGet packages for common EF Core database providers

You can reference as many EF Core database providers in the same project as you need. Each package includes the common shared types as well as provider-specific types.

Connecting to a named SQLite database‌

To connect to an SQLite database, we just need to know the database path and filename, set using the legacy parameter Filename or the modern equivalent Data Source . The path can be relative to the current directory or an absolute path. We specify this information in a connection string.

Defining the Northwind database context class‌

A class named Northwind will be used to represent the database. To use EF Core, the class must inherit from DbContext . The DbContext class understands how to communicate with databases and dynamically generate SQL statements to query and manipulate data.Your DbContext -derived class should have an overridden method named OnConfiguring , which will set the database connection string.We will create a project that uses SQLite, but feel free to use SQL Server or some other database system if you feel comfortable doing so instead:

1. In the WorkingWithEFCore project, add a package reference to the EF Core provider for SQLite and globally and statically import the System.Console class for all C# files, as shown in the following markup:

Build the WorkingWithEFCore project to restore packages.

After February 2024, you will be able to try out previews of EF Core 9 by specifying version 9.0-* . The target framework for your project should continue to use net8.0 . By using a wildcard, you will automatically download the latest monthly preview when you restore the packages for the project. Once the EF Core 9 GA version is released in November 2024, change the package version to 9.0.0 . After February 2025, you will be able to do the same with EF Core 10 but that will likely require a project targeting net10.0 .

In the project folder, add a new class file named NorthwindDb.cs .

In NorthwindDb.cs , import the main namespace for EF Core, define a class named

Northwind , and make the class inherit from DbContext . Then, in an OnConfiguring method, configure the options builder to use SQLite with an appropriate database connection string, as shown in the following code:

using Microsoft.EntityFrameworkCore; // To use DbContext and so on. namespace Northwind.EntityModels;

// This manages interactions with the Northwind database. public class NorthwindDb : DbContext

{

protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder)

{

string databaseFile = "Northwind.db"; string path = Path.Combine(

Environment.CurrentDirectory, databaseFile); string connectionString = $"Data Source={path}"; WriteLine($"Connection: {connectionString}"); optionsBuilder.UseSqlite(connectionString);

}

}

1. In Program.cs , delete the existing statements. Then, import the Northwind.EntityModels

namespace and output the database provider, as shown in the following code:

using Northwind.EntityModels; // To use Northwind. using NorthwindDb db = new();

WriteLine($"Provider: {db.Database.ProviderName}");

// Disposes the database context.

1. Run the console app and note the output showing the database connection string and which database provider you are using, as shown in the following output:

Connection: Data Source=C:\cs12dotnet8\Chapter10\WorkingWithEFCore\bin\Debug\net8.0\Northwind.db Provider: Microsoft.EntityFrameworkCore.Sqlite

You now know how to connect to a database by defining an EF Core data context. Next, we need to define a model that represents the tables in the database.

Defining EF Core models‌

EF Core uses a combination of conventions, annotation attributes, and Fluent API statements to build an entity model at runtime, which enables any actions performed on the classes to later be automatically translated into actions performed on the actual database. An entity class represents the structure of a table, and an instance of the class represents a row in that table.First, we will review the three ways to define a model, with code examples, and then we will create some classes that implement those techniques.

Using EF Core conventions to define the model‌

The code we will write will use the following conventions:

image

The name of a table is assumed to match the name of a DbSet property in the

DbContext class, for example, Products .

image

The names of the columns are assumed to match the names of properties in the entity model class, for example, ProductId .

image

image

The string .NET type is assumed to be a nvarchar type in the database. The int .NET type is assumed to be an int type in the database.

image

The primary key is assumed to be a property that is named Id or ID , or when the entity model class is named Product , then the property can be named ProductId or ProductID . If this property is an integer type or the Guid type, then it is also assumed to be an IDENTITY column (a column type that automatically assigns a value when inserting).

Good Practice: There are many other conventions that you should know, and you can even define your own, but that is beyond the scope of this book. You can read about them at

the following link: https://learn.microsoft.com/en-us/ef/core/modeling/.

Using EF Core annotation attributes to define the model‌

Conventions often aren't enough to completely map the classes to the database objects. A simple way of making your model smarter is to apply annotation attributes. Some common attributes recognized by EF Core are shown in Table 10.2:

Attribute Description

[Required] Ensures the value is not null. In .NET 8, it has a DisallowAllDefaultValues parameter to prevent value types having their default value. For example, an int cannot be 0 .

[StringLength(50)] Ensures the value is up to 50 characters in length.

[Column(TypeName = "money", Name = "UnitPrice")] Specifies the column type and column name

used in the table.

Table 10.2: Common EF Core annotation attributes

Some additional attributes that can be used to validate entities and are recognized by platforms like ASP.NET Core and Blazor for validation are shown in Table 10.3:

Attribute Description

[RegularExpression(expression)] Ensures the value matches the specified regular expression.

[EmailAddress] Ensures the value contains one @ symbol, but not as the

first or last character. It does not use a regular expression.

[Range(1, 10)] Ensures a double , int , or string value within a specified range. New in .NET 8 are parameters MinimumIsExclusive and MaximumIsExclusive .

[Length(10, 20)] Ensures a string or collection is within a specified length

range, for example, minimum 10 characters or items, maximum

20 characters or items.

[Base64String] Ensures the value is a well-formed Base64 string.

[AllowedValues] Ensures value is one of the items in the params array of

objects. For example, "alpha", "beta", "gamma", or 1, 2, 3.

[DeniedValues] Ensures value is not one of the items in the params array of

objects. For example, "alpha", "beta", "gamma", or 1, 2, 3.

Table 10.3: Validation annotation attributes

Why does the EmailAddress attribute seem so basic? “The check is intentionally naive because doing something infallible is very hard. The email really should be validated in some other way, such as through an email confirmation flow where an email is actually sent. The validation attribute is designed only to catch egregiously wrong values such as for a U.I.” You can read the debate at the following link: https://github.com/dotnet/runtime/issues/27592.

For example, in the database, the maximum length of a product name is 40, and the value cannot be null, as shown highlighted in the following Data Definition Language (DDL) code from the Northwind4SQLite.sql script file, which defines how to create a table named Products with its columns, data types, keys, and other constraints:

CREATE TABLE Products (

ProductId INTEGER PRIMARY KEY,

ProductName NVARCHAR (40) NOT NULL,

SupplierId "INT",

CategoryId "INT", QuantityPerUnit NVARCHAR (20),

UnitPrice "MONEY" CONSTRAINT DF_Products_UnitPrice DEFAULT (0), UnitsInStock "SMALLINT" CONSTRAINT DF_Products_UnitsInStock DEFAULT (0), UnitsOnOrder "SMALLINT" CONSTRAINT DF_Products_UnitsOnOrder DEFAULT (0), ReorderLevel "SMALLINT" CONSTRAINT DF_Products_ReorderLevel DEFAULT (0), Discontinued "BIT" NOT NULL

CONSTRAINT DF_Products_Discontinued DEFAULT (0), CONSTRAINT FK_Products_Categories FOREIGN KEY (

CategoryId

)

REFERENCES Categories (CategoryId),

CONSTRAINT FK_Products_Suppliers FOREIGN KEY ( SupplierId

)

REFERENCES Suppliers (SupplierId),

CONSTRAINT CK_Products_UnitPrice CHECK (UnitPrice >= 0), CONSTRAINT CK_ReorderLevel CHECK (ReorderLevel >= 0), CONSTRAINT CK_UnitsInStock CHECK (UnitsInStock >= 0), CONSTRAINT CK_UnitsOnOrder CHECK (UnitsOnOrder >= 0)

);

In a Product class, we could apply attributes to specify this, as shown in the following code:

[Required] [StringLength(40)]

public string ProductName { get; set; }

When there isn't an obvious map between .NET types and database types, an attribute can be used.For example, in the database, the column type of UnitPrice for the Products table is money . .NET does not have a money type, so it should use decimal instead, as shown in the following code:

[Column(TypeName = "money")]

public decimal? UnitPrice { get; set; }

Using the EF Core Fluent API to define the model‌

The last way that the model can be defined is by using the Fluent API. This API can be used instead of attributes, as well as being used in addition to them. For example, to define the ProductName property, instead of decorating the property with two attributes, an equivalent Fluent API statement could be written in the OnModelCreating method of the database context class, as shown in the following code:

modelBuilder.Entity()

.Property(product => product.ProductName)

.IsRequired()

.HasMaxLength(40);

This keeps the entity model class simpler.

Understanding data seeding with the Fluent API‌

Another benefit of the Fluent API is to provide initial data to populate a database. EF Core automatically works out what insert, update, or delete operations must be executed.For example, if we wanted to make sure that a new database has at least one row in the Product table, then we would call the HasData method, as shown in the following code:

modelBuilder.Entity()

.HasData(new Product

{

ProductId = 1, ProductName = "Chai", UnitPrice = 8.99M

});

Calls to HasData take effect either during a data migration executed by the command

dotnet ef database update or when you call the Database.EnsureCreated method.Our model will map to an existing database that is already populated with data, so we will not need to use this technique in our code.

Building EF Core models for the Northwind tables‌

Now that you've learned about ways to define EF Core models, let's build models to represent two of the tables in the Northwind database. For reuse, we will do this in a separate class library project.The two entity classes will refer to each other, so to avoid compiler errors, we will create the classes without any members first:

Use your preferred code editor to create a new project, as defined in the following list:

image

image

image

Project template: Class Library / classlib Project file and folder: Northwind.EntityModels Solution file and folder: Chapter10

In the Northwind.EntityModels project, delete the file named Class1.cs and then add two class files named Category.cs and Product.cs .

In Category.cs , define a class named Category , as shown in the following code:

namespace Northwind.EntityModels; public class Category

{

}

1. In Product.cs , define a class named Product , as shown in the following code:

namespace Northwind.EntityModels; public class Product

{

}

1. In the WorkingWithEFCore project, add a project reference to the Northwind.EntityModels

project, as shown in the following markup:

The project reference path and filename must all go on one line.

1. Build the WorkingWithEFCore project.

Defining the Category and Product entity classes‌

The Category class, also known as an entity model, will be used to represent a row in the Categories table. This table has four columns, as shown in the following DDL taken from the Northwind4SQLite.sql script file:

CREATE TABLE Categories (

CategoryId INTEGER PRIMARY KEY,

CategoryName NVARCHAR (15) NOT NULL, Description "NTEXT",

Picture "IMAGE"

);

We will use conventions to define:

image

image

Three of the four properties (we will not map the Picture column). The primary key.

image

The one-to-many relationship to the Products table.

To map the Description column to the correct database type, we will need to decorate the string property with the Column attribute.Later in this chapter, we will use the Fluent API to define that CategoryName cannot be null and is limited to a maximum of 15 characters.Let's go:

1. In the Northwind.EntityModels project, modify the Category entity model class, as shown highlighted in the following code:

using System.ComponentModel.DataAnnotations.Schema; // To use [Column]. namespace Northwind.EntityModels;

public class Category

{

// These properties map to columns in the database. public int CategoryId { get; set; } // The primary key. public string CategoryName { get; set; } = null!; [Column(TypeName = "ntext")]

public string? Description { get; set; }

// Defines a navigation property for related rows.

public virtual ICollection Products { get; set; }

// To enable developers to add products to a Category, we must

// initialize the navigation property to an empty collection.

// This also avoids an exception if we get a member like Count.

= new HashSet();

}

image

Note the following:

image

The Category class will be in the Northwind.EntityModels namespace.

image

The CategoryId property follows the primary key naming convention, so it will be mapped to a column marked as the primary key with an index.

image

The CategoryName property maps to a column that does not allow database NULL values so it is a non-nullable string, and to disable nullability warnings we have assigned the null-forgiving operator.

image

The CategoryName property maps to a column with the ntext data type instead of the default mapping for string values to nvarchar .

image

We initialize the collection of Product objects to a new, empty HashSet . A hash set is more efficient than a list because it is unordered. If you do not initialize Products , then it will be null and if you try to get its Count then you will get an exception.

1. Modify the Product class, as shown highlighted in the following code:

using System.ComponentModel.DataAnnotations; // To use [Required]. using System.ComponentModel.DataAnnotations.Schema; // To use [Column]. namespace Northwind.EntityModels;

public class Product

{

public int ProductId { get; set; } // The primary key. [Required]

[StringLength(40)]

public string ProductName { get; set; } = null!;

// Property name is different from the column name. [Column("UnitPrice", TypeName = "money")]

public decimal? Cost { get; set; } [Column("UnitsInStock")]

public short? Stock { get; set; }

public bool Discontinued { get; set; }

// These two properties define the foreign key relationship

// to the Categories table.

public int CategoryId { get; set; }

public virtual Category Category { get; set; } = null!;

}

Note the following:

image

The Product class will be used to represent a row in the Products table, which has ten columns.

image

You do not need to include all columns from a table as properties of a class. We will only map six properties: ProductId , ProductName , UnitPrice , UnitsInStock , Discontinued , and CategoryId .

image

Columns that are not mapped to properties cannot be read or set using the class instances. If you use the class to create a new object, then the new row in the table will have NULL or some other default value for the unmapped column values in that row. You must make sure that those missing columns are optional or have default values set by the database or an exception will be thrown at runtime. In this scenario, the rows already have data values and I have decided that I do not need to read those values in this application.

image

We can rename a column by defining a property with a different name, like Cost , and then decorating the property with the [Column] attribute and specifying its column name, like UnitPrice .

image

The final property, CategoryId , is associated with a Category property that will be used to map each product to its parent category.

The two properties that relate the two entities, Category.Products and Product.Category , are both marked as virtual . This allows EF Core to inherit and override the properties to provide extra features, such as lazy loading.

Adding tables to the Northwind database context class‌

Inside your DbContext -derived class, you must define at least one property of the DbSet type. These properties represent the tables. To tell EF Core what columns each table has, the DbSet properties use generics to specify a class that represents a row in the table. That entity model class has properties that represent its columns.The DbContext -derived class can optionally have an overridden method named OnModelCreating . This is where you can write Fluent API statements as an alternative to decorating your entity classes with attributes.Let's write the code:

1. In the WorkingWithEFCore project, modify the NorthwindDb class to add statements to define two properties for the two tables and an OnModelCreating method, as shown highlighted in the following code:

public class NorthwindDb : DbContext

{

// These two properties map to tables in the database. public DbSet? Categories { get; set; } public DbSet? Products { get; set; } protected override void OnConfiguring(

DbContextOptionsBuilder optionsBuilder)

{

...

}

protected override void OnModelCreating( ModelBuilder modelBuilder)

{

// Example of using Fluent API instead of attributes

// to limit the length of a category name to 15. modelBuilder.Entity()

.Property(category => category.CategoryName)

.IsRequired() // NOT NULL

.HasMaxLength(15);

// Some SQLite-specific configuration.

if (Database.ProviderName?.Contains("Sqlite") ?? false)

{

// To "fix" the lack of decimal support in SQLite. modelBuilder.Entity()

.Property(product => product.Cost)

.HasConversion();

}

}

}

The decimal type is not supported by the SQLite database provider for sorting and other operations. We can fix this by telling the model that decimal values can be treated as double values when using the SQLite database provider. This does not actually perform any conversion at runtime.Now that you have seen some examples of defining an entity model manually, let's see a tool that can do some of the work for you.

Setting up the dotnet-ef tool‌

The .NET CLI tool named dotnet can be extended with capabilities useful for working with EF Core. It can perform design-time tasks like creating and applying migrations from an older model to a newer model and generating code for a model from an existing database.The dotnet ef command-line tool is not automatically installed. You must install this package as either a global or local tool. If you have already installed an older version of the tool, then you should update it to the latest version:

1. At a command prompt or terminal, check if you have already installed dotnet-ef as a global tool, as shown in the following command:

dotnet tool list --global

1. Check in the list if an older version of the tool has been installed, like the one for

.NET 7, as shown in the following output:

Package Id

Version

Commands

dotnet-ef

7.0.0

dotnet-ef

1. If an old version is already installed, then update the tool, as shown in the following command:

dotnet tool update --global dotnet-ef

1. If it is not already installed, then install the latest version, as shown in the following command:

dotnet tool install --global dotnet-ef

If necessary, follow any OS-specific instructions to add the dotnet tools directory to your PATH environment variable, as described in the output of installing the dotnet-ef tool.By default, the latest GA release of .NET will be used to install the tool. To explicitly set a version, for example, to use a preview, add the --version switch. For example, to update to the latest .NET 9 preview version available from February 2024 to October 2024, use the following command with a version wildcard:

dotnet tool update --global dotnet-ef --version 9.0-*

Once the .NET 9 GA release happens in November 2024, you can just use the command without the --version switch to upgrade.You can also remove the tool, as shown in the following command:

dotnet tool uninstall --global dotnet-ef

Scaffolding models using an existing database‌

Scaffolding is the process of using a tool to create classes that represent the model of an existing database using reverse engineering. A good scaffolding tool allows you to extend the automatically generated classes because they are partial and then regenerate those classes without losing your partial classes.If you know that you will never regenerate the classes using the tool, then feel free to change the code for the automatically generated classes as much as you want. The code generated by the tool is just the best approximation.

Good Practice: Do not be afraid to overrule a tool when you know better. Let's see if the tool generates the same model as we did manually:

1. Add the latest version of the Microsoft.EntityFrameworkCore.Design package to the

WorkingWithEFCore project, as shown highlighted in the following markup:

all

runtime; build; native; contentfiles; analyzers; buildtransitive

<PackageReference Version="8.0.0"

Include="Microsoft.EntityFrameworkCore.Sqlite" />

More Information: If you are unfamiliar with how packages like Microsoft.EntityFrameworkCore.Design can manage their assets, then you can learn more at the following link: https://learn.microsoft.com/en-us/nuget/consume-packages/package- references-in-project-files#controlling-dependency-assets.

Build the WorkingWithEFCore project to restore packages.

Start a command prompt or terminal in the WorkingWithEFCore project folder. For example:

image

If you are using Visual Studio 2022, in Solution Explorer, right-click the

WorkingWithEFCore project and select Open in Terminal.

image

On Windows, start File Explorer, right-click the WorkingWithEFCore folder, and select New Command Prompt at Folder or Open in Windows Terminal.

image

On macOS, start Finder, right-click the WorkingWithEFCore folder, and select New Terminal at Folder.

image

If you are using JetBrains Rider, in Solution Explorer, right-click the

WorkingWithEFCore project and select Open In | Terminal.

Warning! When I say the WorkingWithEFCore project folder, I mean the folder that contains the WorkingWithEFCore.csproj project file. If you enter the command in a folder that does not contain a project file, then you will see the following error: No project was found. Change the current working directory or use the --project option.

Good Practice: You are about to enter a long command. I recommend that you type from the print book or copy and paste long commands like this from the eBook into a plain text editor like Notepad. Then make sure that the whole command is properly formatted as a single line with correct spacing. Only then should you

copy and paste it into the command prompt or terminal. Copying and pasting directly from the eBook is likely to include newline characters and missing spaces that break the command if you aren't careful. Also remember that all commands are available to copy from at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/command-lines.md.

At a command prompt or terminal, use the dotnet-ef tool to generate a model for the Categories and Products tables in a new folder named AutoGenModels , as shown in the following command and in Figure 10.6:

dotnet ef dbcontext scaffold "Data Source=Northwind.db" Microsoft.EntityFrameworkCore.Sqlite --t

Note the following:

image

The command action: dbcontext scaffold

image

The connection string: "Data Source=Northwind.db"

image

The database provider: Microsoft.EntityFrameworkCore.Sqlite

image

The tables to generate models for: --table Categories --table Products

image

The output folder: --output-dir AutoGenModels

image

The namespace: --namespace WorkingWithEFCore.AutoGen

image

To use data annotations as well as the Fluent API: --data-annotations

image

To rename the context from [database_name]Context : --context NorthwindDb

If you prefer to use SQL Server, then the equivalent command is found at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/sql- server/README.md#scaffolding-models-using-an-existing-database.

image

Figure 10.6: Entering a dotnet-ef command in Terminal using Visual Studio 2022

  1. Note the build messages and warnings, as shown in the following output:

Build started...

Build succeeded.

To protect potentially sensitive information in your connection string, you should move it out o Skipping foreign key with identity '0' on table 'Products' since principal table 'Suppliers' was

In the AutoGenModels folder, note the three class files that were automatically generated: Category.cs , NorthwindDb.cs , and Product.cs .

In the AutoGenModels folder, in Category.cs , note the differences compared to the one you created manually. I have not included namespace imports to save space, as shown in the following code:

namespace WorkingWithEFCore.AutoGen; [Index("CategoryName", Name = "CategoryName")] public partial class Category

{

[Key]

public int CategoryId { get; set; } [Column(TypeName = "nvarchar (15)")]

public string CategoryName { get; set; } = null!; [Column(TypeName = "ntext")]

public string? Description { get; set; } [Column(TypeName = "image")]

public byte[]? Picture { get; set; } [InverseProperty("Category")]

public virtual ICollection Products { get; set; }

= new List();

}

Note the following:

image

It decorates the entity class with the [Index] attribute, which was introduced in EF Core 5. This indicates properties that should have an index when using the Code First approach to generate a database at runtime. Since we are using Database First with an existing database, this is not needed. But if we wanted to recreate a new, empty database from our code, then this information would be needed.

image

The table name in the database is Categories but the dotnet-ef tool uses the Humanizer third-party library to automatically singularize the class name to Category , which is a more natural name when creating a single entity that represents a row in the table.

image

The entity class is declared using the partial keyword so that you can create a matching partial class for adding additional code. This allows you to rerun the tool and regenerate the entity class without losing that extra code.

image

The CategoryId property is decorated with the [Key] attribute to indicate that it is the primary key for this entity. The data type for this property is int for SQL Server and long for SQLite. We did not do this because we followed the naming primary key convention.

image

image

The CategoryName property is decorated with the [Column(TypeName = "nvarchar (15)")] attribute, which is only needed if you want to generate a database from the model. We chose not to include the Picture column as a property because this is a binary object that we will not use in our console app.

image

The Products property uses the [InverseProperty] attribute to define the foreign key relationship to the Category property on the Product entity class, and it initializes the collection to a new empty list.

In the AutoGenModels folder, in Product.cs , note the differences compared to the one you created manually.

In the AutoGenModels folder, in NorthwindDb.cs , note the differences compared to the one you created manually, as shown in the following edited-for-space code:

using Microsoft.EntityFrameworkCore; namespace WorkingWithEFCore.AutoGen;

public partial class NorthwindDb : DbContext

{

public NorthwindDb()

{

}

public NorthwindDb(DbContextOptions options)

: base(options)

{

}

public virtual DbSet Categories { get; set; } public virtual DbSet Products { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

warning To protect potentially sensitive information in your connection string, you should move

=> optionsBuilder.UseSqlite("Data Source=Northwind.db"); protected override void OnModelCreating(ModelBuilder modelBuilder)

{

modelBuilder.Entity(entity =>

{

entity.Property(e => e.CategoryId).ValueGeneratedNever();

});

modelBuilder.Entity(entity =>

{

entity.Property(e => e.ProductId).ValueGeneratedNever(); entity.Property(e => e.Discontinued).HasDefaultValueSql("0"); entity.Property(e => e.ReorderLevel).HasDefaultValueSql("0"); entity.Property(e => e.UnitPrice).HasDefaultValueSql("0"); entity.Property(e => e.UnitsInStock).HasDefaultValueSql("0"); entity.Property(e => e.UnitsOnOrder).HasDefaultValueSql("0");

});

OnModelCreatingPartial(modelBuilder);

}

partial void OnModelCreatingPartial(ModelBuilder modelBuilder);

}

Note the following:

image

The NorthwindDb data context class is partial to allow you to extend it and regenerate it in the future.

image

It has two constructors: a default parameter-less one and one that allows options to be passed in. This is useful in apps where you want to specify the connection string at runtime.

image

In the OnConfiguring method, if options have not been specified in the constructor, then it defaults to using a connection string that looks for the database file in the current folder. It has a compiler warning to remind you that you should not hardcode security information in this connection string.

image

In the OnModelCreating method, the Fluent API is used to configure the two entity classes, and then a partial method named OnModelCreatingPartial is invoked. This allows you to implement that partial method in your own partial Northwind class to add your own Fluent API configuration that will not be lost if you regenerate the model classes.

  1. Close the automatically generated class files.

Customizing the reverse engineering templates‌

One of the features introduced with EF Core 7 was the ability to customize the code that is automatically generated by the dotnet-ef scaffolding tool. This is an advanced technique, so I do not cover it in this book. Usually, it is easier to just modify the code that is generated by default anyway. If you would like to learn how to modify the T4 templates used by the dotnet-ef scaffolding tool, then you can find that information at the following link:https://learn.microsoft.com/en-us/ef/core/managing-schemas/scaffolding/templates

Configuring preconvention models‌

Along with support for the DateOnly and TimeOnly types for use with the SQLite database provider, one of the features introduced with EF Core 6 was configuring preconvention models.As models become more complex, relying on conventions to discover entity types and their properties and successfully map them to tables and columns becomes harder. It would be useful if you could configure the conventions themselves before they are used to analyze and build a model.For example, you might want to define a convention to say that all string properties should have a maximum length of 50 characters as a default, or any property types that implement a custom interface should not be mapped, as shown in the following code:

protected override void ConfigureConventions( ModelConfigurationBuilder configurationBuilder)

{

configurationBuilder.Properties().HaveMaxLength(50); configurationBuilder.IgnoreAny();

}

In the rest of this chapter, we will use the classes that you manually created.

Querying EF Core models‌

Now that we have a model that maps to the Northwind database and two of its tables, we can write some simple LINQ queries to fetch data. You will learn much more about writing LINQ queries in Chapter 11, Querying and Manipulating Data Using LINQ.For now, just write the code and view the results:

In the WorkingWithEFCore project, add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , add a partial Program class with a SectionTitle method, as shown in the following code:

partial class Program

{

private static void ConfigureConsole(string culture = "en-US", bool useComputerCulture = false)

{

// To enable Unicode characters like Euro symbol in the console. OutputEncoding = System.Text.Encoding.UTF8;

if (!useComputerCulture)

{

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);

}

WriteLine($"CurrentCulture: {CultureInfo.CurrentCulture.DisplayName}");

}

private static void WriteLineInColor(string text, ConsoleColor color)

{

ConsoleColor previousColor = ForegroundColor; ForegroundColor = color;

WriteLine(text);

ForegroundColor = previousColor;

}

private static void SectionTitle(string title)

{

WriteLineInColor($" {title} ", ConsoleColor.DarkYellow);

}

private static void Fail(string message)

{

WriteLineInColor($"Fail > {message}", ConsoleColor.Red);

}

private static void Info(string message)

{

WriteLineInColor($"Info > {message}", ConsoleColor.Cyan);

}

}

Add a new class file named Program.Queries.cs .

In Program.Queries.cs , define a partial Program class with a QueryingCategories method, and add statements to do these tasks, as shown in the following code:

image

Create an instance of the Northwind class that will manage the database. Database context instances are designed for short lifetimes in a unit of work. They should be disposed of as soon as possible. So, we will wrap our instance in a using statement. In Chapter 13, Building Websites Using ASP.NET Core Razor Pages, you will learn how to get a database context using dependency injection.

image

Create a query for all categories that includes their related products. Include is an extension method that requires you to import the Microsoft.EntityFrameworkCore namespace.

image

Enumerate through the categories, outputting the name and number of products for each one:

using Microsoft.EntityFrameworkCore; // To use Include method.

using Northwind.EntityModels; // To use Northwind, Category, Product. partial class Program

{

private static void QueryingCategories()

{

using NorthwindDb db = new();

SectionTitle("Categories and how many products they have");

// A query to get all categories and their related products. IQueryable? categories = db.Categories?

.Include(c => c.Products);

if (categories is null || !categories.Any())

{

Fail("No categories found."); return;

}

// Execute query and enumerate results. foreach (Category c in categories)

{

WriteLine($"{c.CategoryName} has {c.Products.Count} products.");

}

}

}

Note that the order of the clauses in the if statement is important. We must check that categories is null first. If this is true , then the code will never execute the second clause and therefore won't throw a NullReferenceException when accessing the Any() member.

  1. In Program.cs , comment out the two statements that create a Northwind instance and output the database provider name, and then call the ConfigureConsole and QueryingCategories methods, as shown in the following code:

ConfigureConsole();

QueryingCategories();

  1. Run the code and view the result, as shown in the following partial output:

Beverages has 12 products. Condiments has 12 products. Confections has 13 products. Dairy Products has 10 products. Grains/Cereals has 7 products. Meat/Poultry has 6 products.

Produce has 5 products.

Seafood has 12 products.

Warning! If you see the following exception, the most likely problem is that the

Northwind.db file is not being copied to the output directory:

Unhandled exception. Microsoft.Data.Sqlite.SqliteException (0x80004005): SQLite Error 1: 'no s

. Make sure that Copy to Output Directory is set, but even when it is, some code editors do not always copy the file when they should. You might need to manually copy the Northwind.db file to the appropriate directory.

Filtering included entities‌

EF Core 5 introduced filtered includes, which means you can specify a lambda expression in the Include method call to filter which entities are returned in the results:

  1. In Program.Queries.cs , define a FilteredIncludes method, and add statements to do these tasks, as shown in the following code:

image

image

Create an instance of the Northwind class that will manage the database. Prompt the user to enter a minimum value for units in stock.

image

Create a query for categories that have products with that minimum number of units in stock.

image

Enumerate through the categories and products, outputting the name and units in stock for each one:

private static void FilteredIncludes()

{

using NorthwindDb db = new();

SectionTitle("Products with a minimum number of units in stock"); string? input;

int stock; do

{

Write("Enter a minimum for units in stock: "); input = ReadLine();

} while (!int.TryParse(input, out stock)); IQueryable? categories = db.Categories?

.Include(c => c.Products.Where(p => p.Stock >= stock)); if (categories is null || !categories.Any())

{

Fail("No categories found."); return;

}

foreach (Category c in categories)

{

WriteLine(

"{0} has {1} products with a minimum {2} units in stock.", arg0: c.CategoryName, arg1: c.Products.Count, arg2: stock);

foreach(Product p in c.Products)

{

WriteLine($" {p.ProductName} has {p.Stock} units in stock.");

}

}

}

  1. In Program.cs , call the FilteredIncludes method, as shown in the following code:

FilteredIncludes();

  1. Run the code, enter a minimum value for units in stock, like 100 , and view the result, as shown in the following partial output:

Enter a minimum for units in stock: 100

Beverages has 2 products with a minimum of 100 units in stock.

Sasquatch Ale has 111 units in stock. Rhönbräu Klosterbier has 125 units in stock.

Condiments has 2 products with a minimum of 100 units in stock.

Grandma's Boysenberry Spread has 120 units in stock. Sirop d'érable has 113 units in stock.

Confections has 0 products with a minimum of 100 units in stock. Dairy Products has 1 products with a minimum of 100 units in stock.

Geitost has 112 units in stock.

Grains/Cereals has 1 products with a minimum of 100 units in stock.

Gustaf's Knäckebröd has 104 units in stock.

Meat/Poultry has 1 products with a minimum of 100 units in stock.

Pâté chinois has 115 units in stock.

Produce has 0 products with a minimum of 100 units in stock. Seafood has 3 products with a minimum of 100 units in stock.

Inlagd Sill has 112 units in stock. Boston Crab Meat has 123 units in stock. Röd Kaviar has 101 units in stock.

Unicode characters in the Windows console: There is a limitation with the console provided by Microsoft on versions of Windows before the Windows 10 Fall Creators Update. By default, the console cannot display Unicode characters, for example, in the name Rhönbräu.

If you have this issue, then you can temporarily change the code page (also known as the character set) in a console to Unicode UTF-8 by entering the following command at the prompt before running the app:

chcp 65001

Filtering and sorting products‌

Let's explore a more complex query that will filter and sort data:

  1. In Program.Queries.cs , define a QueryingProducts method, and add statements to do the following, as shown in the following code:

image

image

Create an instance of the Northwind class that will manage the database. Prompt the user for a price for products.

image

Create a query for products that cost more than the price using LINQ.

image

Loop through the results, outputting the ID, name, cost (formatted in US dollars), and the number of units in stock:

private static void QueryingProducts()

{

using NorthwindDb db = new();

SectionTitle("Products that cost more than a price, highest at top"); string? input;

decimal price; do

{

Write("Enter a product price: "); input = ReadLine();

} while (!decimal.TryParse(input, out price)); IQueryable? products = db.Products?

.Where(product => product.Cost > price)

.OrderByDescending(product => product.Cost); if (products is null || !products.Any())

{

Fail("No products found."); return;

}

foreach (Product p in products)

{

WriteLine(

"{0}: {1} costs {2:$#,##0.00} and has {3} in stock.", p.ProductId, p.ProductName, p.Cost, p.Stock);

}

}

In Program.cs , call the QueryingProducts method.

Run the code, enter 50 when prompted to enter a product price, view the result, and note the descending order by cost, as shown in the following partial output:

Enter a product price: 50

38: Côte de Blaye costs $263.50 and has 17 in stock.

29: Thüringer Rostbratwurst costs $123.79 and has 0 in stock. 9: Mishi Kobe Niku costs $97.00 and has 29 in stock.

20: Sir Rodney's Marmalade costs $81.00 and has 40 in stock. 18: Carnarvon Tigers costs $62.50 and has 42 in stock.

59: Raclette Courdavault costs $55.00 and has 79 in stock. 51: Manjimup Dried Apples costs $53.00 and has 20 in stock.

  1. Run the code, enter 500 when prompted to enter a product price, and view the result, as shown in the following output:

Fail > No products found.

Getting the generated SQL‌

You might be wondering how well written the SQL statements are that are generated from the C# queries we write. EF Core 5 introduced a quick and easy way to see the SQL generated:

  1. In the FilteredIncludes method, before using the foreach statement to enumerate the query, add a statement to output the generated SQL, as shown in the following code:

Info($"ToQueryString: {categories.ToQueryString()}");

  1. In the QueryingProducts method, before using the foreach statement to enumerate the query, add a statement to output the generated SQL, as shown in the following code:

Info($"ToQueryString: {products.ToQueryString()}");

  1. Run the code, enter a minimum value for units in stock, like 99 , and view the result, as shown in the following partial output:

Enter a minimum for units in stock: 95

Connection: Data Source=C:\cs12dotnet8\Chapter10\WorkingWithEFCore\bin\Debug\net8.0\Northwind.db Info > ToQueryString: .param set @ stock_0 95

SELECT "c"."CategoryId", "c"."CategoryName", "c"."Description", "t"."ProductId", "t"."CategoryId FROM "Categories" AS "c"

LEFT JOIN (

SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."ProductN FROM "Products" AS "p"

WHERE "p"."UnitsInStock" >= @ stock_0

) AS "t" ON "c"."CategoryId" = "t"."CategoryId" ORDER BY "c"."CategoryId"

Beverages has 2 products with a minimum of 95 units in stock.

Sasquatch Ale has 111 units in stock. Rhönbräu Klosterbier has 125 units in stock.

...

Note the SQL parameter named @ stock_0 has been set to a minimum stock value of 95 .If you used SQL Server, the generated SQL will be slightly different. For example, it uses square brackets instead of double quotes around object names, as shown in the following output:

Info > ToQueryString: DECLARE @ stock_0 smallint = CAST(95 AS smallint);

SELECT .[CategoryId], .[CategoryName], .[Description], [t].[ProductId], [t].[CategoryId FROM [Categories] AS

LEFT JOIN (

SELECT [p].[ProductId], [p].[CategoryId], [p].[UnitPrice], [p].[Discontinued], [p].[ProductN FROM [Products] AS [p]

WHERE [p].[UnitsInStock] >= @ stock_0

) AS [t] ON .[CategoryId] = [t].[CategoryId] ORDER BY .[CategoryId]

Logging EF Core‌

To monitor the interaction between EF Core and the database, we can enable logging. Logging could be to the console, to Debug or Trace , or to a file.

Good Practice: By default, EF Core logging will exclude any data in case it is sensitive. You can include this data by calling the EnableSensitiveDataLogging method, especially during development. You should disable it before deploying to production. You can also call EnableDetailedErrors .

Let's see an example of this in action:

  1. In NorthwindDb.cs , at the bottom of the OnConfiguring method, add statements to log to the console and to include sensitive data like parameter values for commands being sent to the database if we compile the debug configuration, as shown in the following code:

optionsBuilder.LogTo(WriteLine) // This is the Console method. #if DEBUG

.EnableSensitiveDataLogging() // Include SQL parameters.

.EnableDetailedErrors() #endif

;

LogTo requires an Action delegate. EF Core will call this delegate, passing a string value for each log message. Passing the Console class WriteLine method, therefore, tells the logger to write each method to the console.

Note that when the solution configuration is Debug, the calls to the EnableSensitiveDataLogging and EnableDetailedErrors methods are included in the compilation, but if you change the solution configuration to Release, the method calls are grayed out to indicate that they are not compiled, as shown in Figure 10.7:

image

Figure 10.7: Including SQL parameters in logging for debug configuration

Run the code and view the log messages, which are shown in the following partial output:

warn: 7/16/2023 14:03:40.255 CoreEventId.SensitiveDataLoggingEnabledWarning[10400] (Microsoft.En Sensitive data logging is enabled. Log entries and exception messages may include sensitiv

...

dbug: 05/03/2023 12:36:11.702 RelationalEventId.ConnectionOpening[20000] (Microsoft.EntityFramew Opening connection to database 'main' on server 'C:\cs12dotnet8\Chapter10\WorkingWithEFCor

dbug: 05/03/2023 12:36:11.718 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFramewo Opened connection to database 'main' on server 'C:\cs12dotnet8\Chapter10\WorkingWithEFCore

dbug: 05/03/2023 12:36:11.721 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT "c"."CategoryId", "c"."CategoryName", "c"."Description", "p"."ProductId", "p"."Cate FROM "Categories" AS "c"

LEFT JOIN "Products" AS "p" ON "c"."CategoryId" = "p"."CategoryId" ORDER BY "c"."CategoryId"

...

Your logs might vary from those shown above based on your chosen database provider and code editor and future improvements to EF Core. For now, note that different events like opening a connection or executing a command have different event IDs, as shown in the following list:

image

20000 RelationalEventId.ConnectionOpening : Includes the database file path

image

20001 RelationalEventId.ConnectionOpened : Includes the database file path

image

20100 RelationalEventId.CommandExecuting : Includes the SQL statement

Filtering logs by provider-specific values‌

The event ID values and what they mean will be specific to the EF Core provider. If we want to know how the LINQ query has been translated into SQL statements and is executing, then the event ID to output has an Id value of 20100 :

  1. At the top of NorthwindDb.cs , import the namespace for working for EF Core diagnostics, as shown in the following code:

// To use RelationalEventId.

using Microsoft.EntityFrameworkCore.Diagnostics;

  1. Modify the LogTo method call to only output events with an Id of 20100 , as shown highlighted in the following code:

optionsBuilder.LogTo(WriteLine, // This is the Console method. new[] { RelationalEventId.CommandExecuting })

if DEBUG

.EnableSensitiveDataLogging()

.EnableDetailedErrors() #endif

;

  1. Run the code and note the following SQL statements that were logged, as shown in the following output, which has been edited for space:

dbug: 05/03/2022 12:48:43.153 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT "c"."CategoryId", "c"."CategoryName", "c"."Description", "p"."ProductId", "p"."Cate FROM "Categories" AS "c"

LEFT JOIN "Products" AS "p" ON "c"."CategoryId" = "p"."CategoryId" ORDER BY "c"."CategoryId"

Beverages has 12 products. Condiments has 12 products. Confections has 13 products. Dairy Products has 10 products. Grains/Cereals has 7 products. Meat/Poultry has 6 products.

Produce has 5 products.

Seafood has 12 products.

Logging with query tags‌

When logging LINQ queries, it can be tricky to correlate log messages in complex scenarios. EF Core 2.2 introduced the query tags feature to help by allowing you to add SQL comments to the log.You can annotate a LINQ query using the TagWith method, as shown in the following code:

IQueryable? products = db.Products?

.TagWith("Products filtered by price and sorted.")

.Where(product => product.Cost > price)

.OrderByDescending(product => product.Cost);

This will add a SQL comment to the log, as shown in the following output:

-- Products filtered by price and sorted.

Getting a single entity‌

There are two LINQ methods to get a single entity: First and Single . It is important to understand the difference between them when using an EF Core database provider. Let’s see an example:

  1. In Program.Queries.cs , define a GettingOneProduct method, and add statements to do the following, as shown in the following code:

image

image

Create an instance of the Northwind class that will manage the database. Prompt the user for a product ID.

image

Create a query for products with that product ID using the First and Single

methods.

image

Write to SQL statement for each query to the console:

private static void GettingOneProduct()

{

using NorthwindDb db = new(); SectionTitle("Getting a single product"); string? input;

int id; do

{

Write("Enter a product ID: "); input = ReadLine();

} while (!int.TryParse(input, out id)); Product? product = db.Products?

.First(product => product.ProductId == id); Info($"First: {product?.ProductName}");

if (product is null) Fail("No product found using First."); product = db.Products?

.Single(product => product.ProductId == id); Info($"Single: {product?.ProductName}");

if (product is null) Fail("No product found using Single.");

}

In Program.cs , call the GettingOneProduct method.

Run the code, enter 1 when prompted to enter a product ID, view the result, and note the SQL statements used by First and Single , as shown in the following output:

Enter a product ID: 1

Connection: Data Source=C:\cs12dotnet8\Chapter10\WorkingWithEFCore\bin\Debug\net8.0\Northwind.db dbug: 9/17/2023 18:04:14.210 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewor

Executing DbCommand [Parameters=[@ id_0='1'], CommandType='Text', CommandTimeout='30'] SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND "p"."ProductId" > @ id_0 LIMIT 1

Info > First: Chang

dbug: 9/17/2023 18:04:14.286 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewor Executing DbCommand [Parameters=[@ id_0='1'], CommandType='Text', CommandTimeout='30'] SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND "p"."ProductId" > @ id_0 LIMIT 2

Info > Single: Chang

Note that both methods execute the same SQL statement except for the LIMIT clauses highlighted in the preceding code. For First , it sets LIMIT 1 but for Single , it sets LIMIT 2 . Why?For First , the query can match one or more entities and only the first will be returned. If there are no matches, an exception is thrown, but you can call FirstOrDefault to return null if there are no matches.For Single , the query must match only one entity and it will be returned. If there are more than one match, an exception must be thrown. But the only way for EF Core to know if there are more than one match is to request more than one and check. So, it has to set LIMIT 2 and check if there is a second entity match.

Good Practice: If you do not need to make sure that only one entity matches, use First

instead of Single to avoid retrieving two records.

Pattern matching with Like‌

EF Core supports common SQL statements including Like for pattern matching:

  1. In Program.Queries.cs , add a method named QueryingWithLike , as shown in the following code, and note:

image

We have enabled logging.

image

We prompt the user to enter part of a product name and then use the

EF.Functions.Like method to search anywhere in the ProductName property.

image

For each matching product, we output its name, stock, and if it is discontinued:

private static void QueryingWithLike()

{

using NorthwindDb db = new(); SectionTitle("Pattern matching with LIKE"); Write("Enter part of a product name: "); string? input = ReadLine();

if (string.IsNullOrWhiteSpace(input))

{

Fail("You did not enter part of a product name."); return;

}

IQueryable? products = db.Products?

.Where(p => EF.Functions.Like(p.ProductName, $"%{input}%")); if (products is null || !products.Any())

{

Fail("No products found."); return;

}

foreach (Product p in products)

{

WriteLine("{0} has {1} units in stock. Discontinued: {2}", p.ProductName, p.Stock, p.Discontinued);

}

}

In Program.cs , comment out the existing methods and call QueryingWithLike .

Run the code, enter a partial product name such as che , and view the result, as shown in the following edited output:

Enter part of a product name: che

dbug: 07/16/2023 13:03:42.793 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ Format_1='%che%' (Size = 5)], CommandType='Text', Comm SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE "p"."ProductName" LIKE @ Format_1

Chef Anton's Cajun Seasoning has 53 units in stock. Discontinued: False Chef Anton's Gumbo Mix has 0 units in stock. Discontinued: True

Queso Manchego La Pastora has 86 units in stock. Discontinued: False

More Information: You can learn more about wildcards with LIKE at the following link: https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language- reference/like-entity-sql.

Generating a random number in queries‌

EF Core 6 introduced a useful function, EF.Functions.Random , that maps to a database function returning a pseudo-random number between 0 and 1, exclusive. For example, you could

multiply the random number by the count of rows in a table to select one random row from that table.

  1. In Program.Queries.cs , add a method named GetRandomProduct , as shown in the following code:

private static void GetRandomProduct()

{

using NorthwindDb db = new(); SectionTitle("Get a random product"); int? rowCount = db.Products?.Count(); if (rowCount is null)

{

Fail("Products table is empty."); return;

}

Product? p = db.Products?.FirstOrDefault(

p => p.ProductId == (int)(EF.Functions.Random() * rowCount)); if (p is null)

{

Fail("Product not found."); return;

}

WriteLine($"Random product: {p.ProductId} - {p.ProductName}");

}

In Program.cs , add a call to GetRandomProduct .

Run the code and view the result, as shown in the following output:

dbug: 05/03/2023 13:19:01.783 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT COUNT(*)

FROM "Products" AS "p"

dbug: 05/03/2022 13:19:01.848 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ p_1='77' (Nullable = true)], CommandType='Text', Comma SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE "p"."ProductId" = CAST((abs(random() / 9.2233720368547799E+18) * @ p_1) AS INTEGER) LIMIT 1

Random product: 42 - Singaporean Hokkien Fried Mee

Defining global filters‌

Northwind products can be discontinued, so it might be useful to ensure that discontinued products are never returned in results, even if the programmer does not use Where to filter them out in their queries:

  1. In NorthwindDb.cs , at the bottom of the OnModelCreating method, add a global filter to remove discontinued products, as shown in the following code:

// A global filter to remove discontinued products. modelBuilder.Entity()

.HasQueryFilter(p => !p.Discontinued);

In Program.cs , uncomment the call to QueryingWithLike , and comment out all the other method calls.

Run the code, enter the partial product name che , view the result, and note that Chef Anton's Gumbo Mix is now missing, because the SQL statement generated includes a filter for the Discontinued column, as shown highlighted in the following output:

Enter part of a product name: che

dbug: 05/03/2022 13:34:27.290 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ Format_1='%che%' (Size = 5)], CommandType='Text', Comm SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND ("p"."ProductName" LIKE @ Format_1) Chef Anton's Cajun Seasoning has 53 units in stock. Discontinued? False

Queso Manchego La Pastora has 86 units in stock. Discontinued? False Gumbär Gummibärchen has 15 units in stock. Discontinued? False

You've now seen many common ways to query data using EF Core. Next, we will look at how data is loaded and tracked and why you might want to control how EF Core does that.

Loading and tracking patterns with EF Core‌

There are three loading patterns that are commonly used with EF Core:

image

Eager loading: Load data early.

image

Lazy loading: Load data automatically just before it is needed.

image

Explicit loading: Load data manually.

In this section, we're going to introduce each of them.

Eager loading entities using the Include extension method‌

In the QueryingCategories method, the code currently uses the Categories property to loop through each category, outputting the category name and the number of products in that category.This works because when we wrote the query, we enabled eager loading by calling the Include method for the related products.Let's see what happens if we do not call Include :

  1. In Program.Queries.cs , in the QueryingCategories method, modify the query to comment out the Include method call, as shown highlighted in the following code:

IQueryable? categories = db.Categories;

//.Include(c => c.Products);

In Program.cs , comment out all method calls except ConfigureConsole and

QueryingCategories .

Run the code and view the result, as shown in the following partial output:

Beverages has 0 products. Condiments has 0 products. Confections has 0 products. Dairy Products has 0 products. Grains/Cereals has 0 products. Meat/Poultry has 0 products.

Produce has 0 products.

Seafood has 0 products.

Each item in foreach is an instance of the Category class, which has a property named Products , that is, the list of products in that category. Since the original query is only selected from the Categories table, this property is empty for each category.

Enabling lazy loading‌

Lazy loading was introduced in EF Core 2.1, and it can automatically load missing related data. To enable lazy loading, developers must:

image

image

Reference a NuGet package for proxies. Configure lazy loading to use a proxy.

Let's see this in action:

  1. In the WorkingWithEFCore project, add a package reference for EF Core proxies, as shown in the following markup:

Build the WorkingWithEFCore project to restore packages.

In NorthwindDb.cs , at the bottom of the OnConfiguring method, call an extension method to use lazy loading proxies, as shown in the following code:

optionsBuilder.UseLazyLoadingProxies();

Now, every time the loop enumerates and an attempt is made to read the Products property, the lazy loading proxy will check if they are loaded. If they're not loaded, it will load them for us "lazily" by executing a SELECT statement to load just that set of products for the current category, and then the correct count will be returned to the output.

  1. Run the code and note that the product counts are now correct. But you will see that the problem with lazy loading is that multiple round trips to the database server are required to eventually fetch all the data. For example, getting all the categories and then getting the products for the first category, Beverages , requires the execution of two SQL commands, as shown in the following partial output:

dbug: 05/03/2022 13:41:40.221 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT "c"."CategoryId", "c"."CategoryName", "c"."Description" FROM "Categories" AS "c"

dbug: 05/03/2022 13:41:40.331 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ p_0='1'], CommandType='Text', CommandTimeout='30'] SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND "p"."CategoryId" = @ p_0 Beverages has 11 products.

...

Explicit loading entities using the Load method‌

Another type of loading is explicit loading. It works in a similar way to lazy loading, with the difference being that you are in control of exactly what related data is loaded and when:

  1. At the top of Program.Queries.cs , import the change tracking namespace to enable us to use the CollectionEntry class to manually load related entities, as shown in the following code:

// To use CollectionEntry.

using Microsoft.EntityFrameworkCore.ChangeTracking;

  1. In QueryingCategories , modify the statements to disable lazy loading and then prompt the user as to whether they want to enable eager loading and explicit loading, as shown highlighted in the following code:

IQueryable? categories;

// = db.Categories;

// .Include(c => c.Products); db.ChangeTracker.LazyLoadingEnabled = false; Write("Enable eager loading? (Y/N): ");

bool eagerLoading = (ReadKey().Key == ConsoleKey.Y); bool explicitLoading = false;

WriteLine();

if (eagerLoading)

{

categories = db.Categories?.Include(c => c.Products);

}

else

{

categories = db.Categories;

Write("Enable explicit loading? (Y/N): "); explicitLoading = (ReadKey().Key == ConsoleKey.Y); WriteLine();

}

  1. In the foreach loop, before the WriteLine method call, add statements to check if explicit loading is enabled, and if so, prompt the user as to whether they want to explicitly load each individual category, as shown in the following code:

if (explicitLoading)

{

Write($"Explicitly load products for {c.CategoryName}? (Y/N): "); ConsoleKeyInfo key = ReadKey();

WriteLine();

if (key.Key == ConsoleKey.Y)

{

CollectionEntry<Category, Product> products = db.Entry(c).Collection(c2 => c2.Products);

if (!products.IsLoaded) products.Load();

}

}

Run the code:

Press N to disable eager loading.

Then, press Y to enable explicit loading.

For each category, press Y or N to load its products as you wish.

I chose to load products for only two of the eight categories, Beverages and Seafood , as shown in the following output, which has been edited for space:

Enable eager loading? (Y/N): n Enable explicit loading? (Y/N): y

dbug: 05/03/2023 13:48:48.541 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT "c"."CategoryId", "c"."CategoryName", "c"."Description" FROM "Categories" AS "c"

Explicitly load products for Beverages? (Y/N): y

dbug: 05/03/2023 13:49:07.416 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ p_0='1'], CommandType='Text', CommandTimeout='30'] SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND "p"."CategoryId" = @ p_0 Beverages has 11 products.

Explicitly load products for Condiments? (Y/N): n Condiments has 0 products.

Explicitly load products for Confections? (Y/N): n Confections has 0 products.

Explicitly load products for Dairy Products? (Y/N): n Dairy Products has 0 products.

Explicitly load products for Grains/Cereals? (Y/N): n Grains/Cereals has 0 products.

Explicitly load products for Meat/Poultry? (Y/N): n Meat/Poultry has 0 products.

Explicitly load products for Produce? (Y/N): n Produce has 0 products.

Explicitly load products for Seafood? (Y/N): y

dbug: 05/03/2023 13:49:16.682 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ p_0='8'], CommandType='Text', CommandTimeout='30'] SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND "p"."CategoryId" = @ p_0 Seafood has 12 products.

Good Practice: Carefully consider which loading pattern is best for your code. Lazy loading could literally make you a lazy database developer! Read more about loading patterns at the following link: https://learn.microsoft.com/en- us/ef/core/querying/related-data.

Controlling the tracking of entities‌

We need to start with the definition of entity identity resolution. EF Core resolves each entity instance by reading its unique primary key value. This ensures no ambiguities about the identities of entities or relationships between them.By default, EF Core assumes that you want to track entities in local memory so that if you make changes, like adding a new entity, modifying an existing entity, or removing an existing entity, then you can call SaveChanges and all those changes will be made in the underlying data store.

EF Core can only track entities with keys because it uses the key to uniquely identify the entity in the database. Keyless entities, like those returned by views, are never tracked in any scenario.

In the Northwind database, in the Customers table, there is a customer, as shown in the following record:

CustomerId: ALFKI

CompanyName: Alfreds Futterkiste Country: Germany

Phone: 030-0074321

If you execute a query within a data context, like getting all customers in Germany, and then execute another query within the same data context, like getting all customers whose name starts with A, if one of those customer entities already exists in the context, it will be identified and not loaded again, which improves performance.However, if the telephone number of that customer is updated in the database between the executions of the two queries, then the entity being tracked in the data context is not refreshed with the new telephone number.If you do not need to track local changes, or you want to load new instances of an entity for every query execution with the latest data values, even if the entity is already loaded, then you can disable tracking.To disable tracking for an individual query, call the AsNoTracking method as part of the query, as shown in the following code:

var products = db.Products

.AsNoTracking()

.Where(p => p.UnitPrice > price)

.Select(p => new { p.ProductId, p.ProductName, p.UnitPrice });

To disable tracking by default for an instance of a data context, set the change tracker's query-tracking behavior to NoTracking , as shown in the following code:

db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

To disable tracking for an individual query, but retain identity resolution, call the AsNoTrackingWithIdentityResolution method as part of the query, as shown in the following code:

var products = db.Products

.AsNoTrackingWithIdentityResolution()

.Where(p => p.UnitPrice > price)

.Select(p => new { p.ProductId, p.ProductName, p.UnitPrice });

To disable tracking but perform identity resolution by default for an instance of a data context, set the change tracker's query tracking behavior to

NoTrackingWithIdentityResolution , as shown in the following code:

db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTrackingWithIdentityResolution;

To set defaults for all new instances of a data context, in its OnConfiguring method, call the UseQueryTrackingBehavior method, as shown in the following code:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

{

optionsBuilder.UseSqlServer(connectionString)

.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);

}

Three tracking scenarios‌

First, let’s review a scenario using default tracking. The default is tracking with identity resolution. Once an entity is loaded into the data context, underlying changes are not reflected and only one copy exists locally. Entities have local changes tracked and a call to SaveChanges updates the database. Actions and states for scenario 1 are illustrated in Table 10.4:

Action

Query for customers in Germany

Entity in data context

Alfreds Futterkiste, 030-

Row in database

Alfreds Futterkiste,

030-

7432

7432

Change telephone in database

Alfreds Futterkiste, 030-

7432

Alfreds Futterkiste, 9876

030-

Query for customers starting with A

Alfreds Futterkiste, 030-

7432

Alfreds Futterkiste, 030-

9876

Query for customers in Germany Alfreds Futterkiste, 030-

7432

Change telephone in local entity Alfreds Futterkiste, 030-

1928

Save changes Alfreds Futterkiste, 030- 1928

Alfreds Futterkiste, 030-

9876

Alfreds Futterkiste, 030-

9876

Alfreds Futterkiste, 030-

1928

Table 10.4: Scenario 1, change tracking with identity resolution

Second, let's compare the same set of actions using no tracking and no identity resolution. Every query loads another instance of a database row into the data context, including underlying changes, allowing duplicates and mixed out-of-date and updated data. No local entity changes are tracked, so SaveChanges does nothing. Actions and states for scenario 2 are illustrated in Table 10.5:

Action Entities in data context Row in database

Query for customers in Germany Alfreds Futterkiste, 030-7432 Alfreds Futterkiste, 030-7432 Change telephone in database Alfreds Futterkiste, 030-7432 Alfreds Futterkiste, 030-9876

Query for customers starting

with A Alfreds Futterkiste, 030-7432

Alfreds Futterkiste, 030-9876

Alfreds Futterkiste, 030-9876

Query for customers in Germany

Alfreds Futterkiste, 030-7432

Alfreds Futterkiste, 030-9876

Alfreds Futterkiste, 030-9876

Alfreds Futterkiste, 030-9876

Change telephone in local

entity Alfreds Futterkiste, 030-7432

Alfreds Futterkiste, 030-9876

Alfreds Futterkiste, 030-1928

Alfreds Futterkiste, 030-9876

Save changes

Alfreds Futterkiste, 030-7432

Alfreds Futterkiste, 030-9876

Alfreds Futterkiste, 030-1928

Alfreds Futterkiste, 030-9876

Table 10.5: Scenario 2, no change tracking with no identity resolution

Third, let's compare the same set of actions using no tracking with identity resolution. Once an entity is loaded into the data context, underlying changes are not reflected and only one copy exists. No local entity changes are tracked, so SaveChanges does nothing. Actions and states for scenario 3 are illustrated in Table 10.6:

Action

Query for customers in Germany

Entities in data context

Alfreds Futterkiste, 030-

Row in database

Alfreds Futterkiste,

030-

7432

7432

Change telephone in database

Alfreds Futterkiste, 030-

7432

Alfreds Futterkiste, 9876

030-

Query for customers starting with A

Alfreds Futterkiste, 030-

7432

Alfreds Futterkiste, 030-

9876

Query for customers in Germany Alfreds Futterkiste, 030-

7432

Change telephone in local entity Alfreds Futterkiste, 030-

1928

Save changes Alfreds Futterkiste, 030- 1928

Alfreds Futterkiste, 030-

9876

Alfreds Futterkiste, 030-

9876

Alfreds Futterkiste, 030-

9876

Table 10.6: Scenario 3, no change tracking with identity resolution

Lazy loading for no tracking queries‌

In EF Core 7 or earlier, if you enable no tracking, then you cannot use the lazy-loading pattern. If you try, then you will see the following exception at runtime:

Unhandled exception. System.InvalidOperationException: An error was generated for warning 'Micro

EF Core 8 enables support for the lazy loading of entities that are not being tracked.Let's try an example:

  1. In Program.Queries.cs , add a method to request a no tracking query for products, and when you enumerate the products, use lazy loading to fetch the related category name, as shown in the following code:

private static void LazyLoadingWithNoTracking()

{

using NorthwindDb db = new(); SectionTitle("Lazy-loading with no tracking");

IQueryable? products = db.Products?.AsNoTracking();

if (products is null || !products.Any())

{

Fail("No products found."); return;

}

foreach (Product p in products)

{

WriteLine("{0} is in category named {1}.", p.ProductName, p.Category.CategoryName);

}

}

In Program.cs , add a call to LazyLoadingWithNoTracking . You might want to comment out any other method calls except ConfigureConsole , which ensures you see the same currency and other formatting as shown in the book.

Run the code and note that it works without throwing an exception as it would have done with previous versions of EF Core.

If you want to see the runtime exception for yourself, in the project file, change the version numbers of the three EF Core packages from 8.0.0 to any older package version, like 7.0.0 or 6.0.0.

Summary of tracking‌

Which should you choose? Of course, it depends on your specific scenarios.You will sometimes read blogs or LinkedIn posts that excitedly tell you that "no one knows this one amazing trick to dramatically improve your EF Core queries" by calling AsNoTracking . But if you run a query that returns thousands of entities, and then run the same query again within the same data context, you’ll now have thousands of duplicates! This wastes memory and negatively impacts performance, so the advice to "call AsNoTracking " to improve performance is not always true.Understand how the three tracking choices work and select the best for your data context or individual queries.

Modifying data with EF Core‌

Inserting, updating, and deleting entities using EF Core is an easy task to accomplish. This is often referred to as CRUD, an acronym that includes the following operations:

image

C for Create

image

R for Retrieve (or Read)

image

U for Update

image

D for Delete

By default, DbContext maintains change tracking automatically, so the local entities can have multiple changes tracked, including adding new entities, modifying existing entities, and removing entities.When you are ready to send those changes to the underlying database, call the SaveChanges method. The number of entities successfully changed will be returned.

Inserting entities‌

Let's start by looking at how to add a new row to a table:

In the WorkingWithEFCore project, add a new class file named Program.Modifications.cs .

In Program.Modifications.cs , create a partial Program class with a method named

ListProducts that outputs the ID, name, cost, stock, and discontinued properties of each product, sorted with the costliest first, and highlights any that match an array of int values that can be optionally passed to the method, as shown in the following code:

using Microsoft.EntityFrameworkCore; // To use ExecuteUpdate, ExecuteDelete. using Microsoft.EntityFrameworkCore.ChangeTracking; // To use EntityEntry. using Northwind.EntityModels; // To use Northwind, Product.

partial class Program

{

private static void ListProducts( int[]? productIdsToHighlight = null)

{

using NorthwindDb db = new();

if (db.Products is null || !db.Products.Any())

{

Fail("There are no products."); return;

}

WriteLine("| {0,-3} | {1,-35} | {2,8} | {3,5} | {4} |",

"Id", "Product Name", "Cost", "Stock", "Disc."); foreach (Product p in db.Products)

{

ConsoleColor previousColor = ForegroundColor; if (productIdsToHighlight is not null &&

productIdsToHighlight.Contains(p.ProductId))

{

ForegroundColor = ConsoleColor.Green;

}

WriteLine("| {0:000} | {1,-35} | {2,8:$#,##0.00} | {3,5} | {4} |",

p.ProductId, p.ProductName, p.Cost, p.Stock, p.Discontinued); ForegroundColor = previousColor;

}

}

}

Remember that 1,-35 means left-align argument 1 within a 35-character-wide column, and

3,5 means right-align argument 3 within a 5-character-wide column.

  1. In Program.Modifications.cs , add a method named AddProduct that returns a two-integer- tuple, as shown in the following code:

private static (int affected, int productId) AddProduct(

int categoryId, string productName, decimal? price, short? stock)

{

using NorthwindDb db = new();

if (db.Products is null) return (0, 0); Product p = new()

{

CategoryId = categoryId, ProductName = productName, Cost = price,

Stock = stock

};

// Set product as added in change tracking. EntityEntry entity = db.Products.Add(p); WriteLine($"State: {entity.State}, ProductId: {p.ProductId}");

// Save tracked change to database. int affected = db.SaveChanges();

WriteLine($"State: {entity.State}, ProductId: {p.ProductId}"); return (affected, p.ProductId);

}

  1. In Program.cs , comment out previous method calls, and then call AddProduct and

ListProducts , as shown in the following code:

var resultAdd = AddProduct(categoryId: 6,

productName: "Bob's Burgers", price: 500M, stock: 72); if (resultAdd.affected == 1)

{

WriteLine($"Add product successful with ID: {resultAdd.productId}.");

}

ListProducts(productIdsToHighlight: new[] { resultAdd.productId });

  1. Run the code, view the result, and note the new product has been added, as shown in the following partial output:

State: Added, ProductId: 0

dbug: 05/03/2022 14:21:37.818 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@p0='6', @p1='500' (Nullable = true), @p2='False', @p3='B INSERT INTO "Products" ("CategoryId", "UnitPrice", "Discontinued", "ProductName", "UnitsIn VALUES (@p0, @p1, @p2, @p3, @p4);

SELECT "ProductId" FROM "Products"

WHERE changes() = 1 AND "rowid" = last_insert_rowid(); State: Unchanged, ProductId: 78

Add product successful with ID: 78.

| Id | Product Name

|

Cost |

Stock | Disc. |

| 001 | Chai

|

$18.00 |

39 | False |

| 002 | Chang

|

$19.00 |

17 | False |

...

| 078 | Bob's Burgers

|

$500.00 |

72 | False |

When the new product is first created in memory and tracked by the EF Core change tracker, it has a state of Added and its ID is 0. After the call to SaveChanges , it has a state of Unchanged and its ID is 78, the value assigned by the database.

Updating entities‌

Now, let's modify an existing row in a table.We will find a product to update by specifying the start of a product name and only return the first match. In a real application, if you need to update a specific product, then you must use a unique identifier like ProductId .

I do not know what the product ID will be for the products that you add. I do know that there are no products that start with "Bob" in the existing Northwind database. Finding a product to update using its name avoids having to tell you to first discover what the product ID is for a product that you've added. It is likely to be 78 because there are already 77 products in the table, but once you've added that and then deleted it, the next product to be added would be 79 and it all gets out of sync.

Let's go:

  1. In Program.Modifications.cs , add a method to increase the price of the first product with a name that begins with a specified value (we'll use Bob in our example) by a specified amount, like $20 , as shown in the following code:

private static (int affected, int productId) IncreaseProductPrice( string productNameStartsWith, decimal amount)

{

using NorthwindDb db = new();

if (db.Products is null) return (0, 0);

// Get the first product whose name starts with the parameter value. Product updateProduct = db.Products.First(

p => p.ProductName.StartsWith(productNameStartsWith)); updateProduct.Cost += amount;

int affected = db.SaveChanges();

return (affected, updateProduct.ProductId);

}

  1. In Program.cs , comment out the statements to add a new product, and then add statements to call IncreaseProductPrice and then ListProducts , as shown in the following code:

var resultUpdate = IncreaseProductPrice( productNameStartsWith: "Bob", amount: 20M);

if (resultUpdate.affected == 1)

{

WriteLine($"Increase price success for ID: {resultUpdate.productId}.");

}

ListProducts(productIdsToHighlight: new[] { resultUpdate.productId });

  1. Run the code, view the result, and note that the existing entity for Bob's Burgers has increased in price by $20, as shown in the following partial output:

dbug: 05/03/2022 14:44:47.024 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ productNameStartsWith_0='Bob' (Size = 3)], CommandType SELECT "p"."ProductId", "p"."CategoryId", "p"."UnitPrice", "p"."Discontinued", "p"."Produc FROM "Products" AS "p"

WHERE NOT ("p"."Discontinued") AND (@ productNameStartsWith_0 = '' OR (("p"."ProductName"

LIMIT 1

dbug: 05/03/2022 14:44:47.028 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@p1='78', @p0='520' (Nullable = true)], CommandType='Text UPDATE "Products" SET "UnitPrice" = @p0

WHERE "ProductId" = @p1; SELECT changes();

Increase price success for ID: 78.

| Id | Product Name

|

Cost | Stock | Disc. |

| 001 | Chai

|

$18.00 | 39 | False |

...

| 078 | Bob's Burgers

Deleting entities‌

|

$520.00 | 72 | False |

You can remove individual entities with the Remove method. RemoveRange is more efficient when you want to delete multiple entities.Let's see how to delete rows from a table:

  1. In Program.Modifications.cs , add a method to delete all products with a name that begins with a specified value ( Bob in our example), as shown in the following code:

private static int DeleteProducts(string productNameStartsWith)

{

using NorthwindDb db = new();

IQueryable? products = db.Products?.Where(

p => p.ProductName.StartsWith(productNameStartsWith)); if (products is null || !products.Any())

{

WriteLine("No products found to delete."); return 0;

}

else

{

if (db.Products is null) return 0; db.Products.RemoveRange(products);

}

int affected = db.SaveChanges(); return affected;

}

  1. In Program.cs , comment out the statements to update the product, and then add statements to call DeleteProducts , as shown in the following code:

WriteLine("About to delete all products whose name starts with Bob."); Write("Press Enter to continue or any other key to exit: ");

if (ReadKey(intercept: true).Key == ConsoleKey.Enter)

{

int deleted = DeleteProducts(productNameStartsWith: "Bob"); WriteLine($"{deleted} product(s) were deleted.");

}

else

{

WriteLine("Delete was canceled.");

}

  1. Run the code, press Enter, and view the result, as shown in the following partial output:

1 product(s) were deleted.

If multiple product names started with Bob , then they would all be deleted. As an optional challenge, modify the statements to add three new products that start with Bob and then delete them.

More efficient updates and deletes‌

You have now seen the traditional way of modifying data using EF Core, as summarized in the following steps:

Create a database context. Change tracking is enabled by default.

To insert data, create a new instance of an entity class and then pass it as an argument to the Add method of the appropriate collection, for example, db.Products.Add(product) .

To update data, retrieve the entities that you want to modify and then change their properties.

To delete data, retrieve the entities that you want to remove and then pass them as an argument to the Remove or RemoveRange methods of the appropriate collection, for example, db.Products.Remove(product) .

Call the SaveChanges method of the database context. This uses the change tracker to generate SQL statements to perform the needed inserts, updates, and deletes, and then returns the number of entities affected.

EF Core 7 introduced two methods that can make updates and deletes more efficient because they do not require entities to be loaded into memory and have their changes tracked. The methods are named ExecuteDelete and ExecuteUpdate (and their Async equivalents). They are called on a LINQ query and affect the entities in the query result, although the query is not used to retrieve entities, so no entities are loaded into the data context.For example, to delete all products, call the ExecuteDelete or ExecuteDeleteAsync method on any table, as shown in the following code:

await db.Products.ExecuteDeleteAsync();

The preceding code would execute an SQL statement in the database, as shown in the following code:

DELETE FROM Products

To delete all products that have a unit price greater than 50, use the following code:

await db.Products

.Where(product => product.UnitPrice > 50)

.ExecuteDeleteAsync();

The preceding code would execute an SQL statement in the database, as shown in the following code:

DELETE FROM Products p WHERE p.UnitPrice > 50

ExecuteUpdate and ExecuteDelete can only act on a single table, so although you can write quite complex LINQ queries, they can only update or delete from a single table.

To update all products that are not discontinued to increase their unit price by 10% due to inflation, use the following code:

await db.Products

.Where(product => !product.Discontinued)

.ExecuteUpdateAsync(s => s.SetProperty(

p => p.UnitPrice, // Selects the property to update.

p => p.UnitPrice * 0.1)); // Sets the value to update it to.

You can chain multiple calls to SetProperty in the same query to update multiple properties in one command.

Let's see some examples:

  1. In Program.Modifications.cs , add a method to update all products with a name that begins with a specified value using ExecuteUpdate , as shown in the following code:

private static (int affected, int[]? productIds) IncreaseProductPricesBetter(

string productNameStartsWith, decimal amount)

{

using NorthwindDb db = new();

if (db.Products is null) return (0, null);

// Get products whose name starts with the parameter value. IQueryable? products = db.Products.Where(

p => p.ProductName.StartsWith(productNameStartsWith)); int affected = products.ExecuteUpdate(s => s.SetProperty(

p => p.Cost, // Property selector lambda expression.

p => p.Cost + amount)); // Value to update to lambda expression. int[] productIds = products.Select(p => p.ProductId).ToArray(); return (affected, productIds);

}

  1. In Program.cs , comment out the statements to delete products, and then add statements to call IncreaseProductPricesBetter , as shown in the following code:

var resultUpdateBetter = IncreaseProductPricesBetter( productNameStartsWith: "Bob", amount: 20M);

if (resultUpdateBetter.affected > 0)

{

WriteLine("Increase product price successful.");

}

ListProducts(productIdsToHighlight: resultUpdateBetter.productIds);

Uncomment the statements that add a new product.

Run the console app multiple times and note that, each time, the existing products with the “Bob” prefix are each updated with an incrementing cost, as shown in the following output:

...

| 078 | Bob's Burgers | $560.00 | 72 | False |

| 079 | Bob's Burgers | $540.00 | 72 | False |

| 080 | Bob's Burgers | $520.00 | 72 | False |

  1. In Program.Modifications.cs , add a method to delete any products with a name that begins with a specified value using ExecuteDelete , as shown in the following code:

private static int DeleteProductsBetter( string productNameStartsWith)

{

using NorthwindDb db = new(); int affected = 0;

IQueryable? products = db.Products?.Where(

p => p.ProductName.StartsWith(productNameStartsWith)); if (products is null || !products.Any())

{

WriteLine("No products found to delete."); return 0;

}

else

{

affected = products.ExecuteDelete();

}

return affected;

}

  1. In Program.cs , comment out previous method calls except ConfigureConsole , and then add statements to call DeleteProductsBetter , as shown in the following code:

WriteLine("About to delete all products whose name starts with Bob."); Write("Press Enter to continue or any other key to exit: ");

if (ReadKey(intercept: true).Key == ConsoleKey.Enter)

{

int deleted = DeleteProductsBetter(productNameStartsWith: "Bob"); WriteLine($"{deleted} product(s) were deleted.");

}

else

{

WriteLine("Delete was canceled.");

}

  1. Run the console app and confirm that the products are deleted, as shown in the following output:

3 product(s) were deleted.

Warning! If you mix traditional tracked changes with the ExecuteUpdate and ExecuteDelete methods, then note that they are not kept synchronized. The change tracker will not know what you have updated and deleted using those methods.

Pooling database contexts‌

The DbContext class is disposable and is designed following the single-unit-of-work principle. In the previous code examples, we created all the DbContext -derived NorthwindDb instances in a using block so that Dispose is properly called at the end of each unit of work.A feature of ASP.NET Core that is related to EF Core is that it makes your code more efficient by pooling database contexts when building websites and services. This allows you to create and dispose of as many DbContext -derived objects as you want, knowing that your code is still as efficient as possible.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring this chapter's topics with deeper research.

Exercise 10.1 – Test your knowledge‌

Answer the following questions:

What type would you use for the property that represents a table, for example, the

Products property of a database context?

What type would you use for the property that represents a one-to-many relationship, for example, the Products property of a Category entity?

What is the EF Core convention for primary keys?

When might you use an annotation attribute in an entity class?

Why might you choose the Fluent API in preference to annotation attributes?

What does a transaction isolation level of Serializable mean?

What does the DbContext.SaveChanges() method return?

What is the difference between eager loading and explicit loading?

How should you define an EF Core entity class to match the following table?

CREATE TABLE Employees( EmpId INT IDENTITY,

FirstName NVARCHAR(40) NOT NULL,

Salary MONEY

)

  1. What benefit do you get from declaring entity navigation properties as virtual ?

Exercise 10.2 – Exporting data using different serialization formats‌

In the Chapter10 solution, create a console app named Ch10Ex02DataSerialization that queries the Northwind database for all the categories and products, and then serializes the data using at least three formats of serialization available to .NET. Which format of serialization uses the least number of bytes?

Exercise 10.3 – Working with transactions‌

Add transactions to the modification code: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch10-transactions.md.

Exercise 10.4 – Explore a Code First EF Core model‌

Work through an example of a Code First model that generates an empty database, seeds it with sample data, and then queries the data: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch10-code-first.md.

Exercise 10.5 – Explore app secrets‌

When connecting to a database you often need to include sensitive secret values like a username or password. These values should never be stored in source code or even in a separate file that might be added to a code repository.Secrets should be stored locally during development and in secure systems for production. You can use Secret Manager during local development and Azure Key Vault for cloud production systems. To learn more about app secrets, I have written an online-only section that you can read at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch10-app-secrets.md

Exercise 10.6 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:

https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-10---working- with-data-using-entity-framework-core

Exercise 10.7 – Explore NoSQL databases‌

This chapter focused on RDBMSs such as SQL Server and SQLite. If you wish to learn more about NoSQL databases, such as Cosmos DB and MongoDB, and how to use them with EF Core, then I recommend the following links:

image

Welcome to Azure Cosmos DB: https://learn.microsoft.com/en-us/azure/cosmos- db/introduction

image

Use NoSQL databases as a persistence infrastructure: https://learn.microsoft.com/en- us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/nosql- database-persistence-infrastructure

image

Document Database Providers for Entity Framework Core: https://github.com/BlueshiftSoftware/EntityFrameworkCore

Summary‌

In this chapter, you learned how to:

image

image

Connect to and build entity data models for an existing database. Execute a simple LINQ query and process the results.

image

Use filtered includes.

image

image

Control loading and tracking patterns. Add, modify, and delete data.

In the next chapter, you will learn how to write more advanced LINQ queries to select, filter, sort, join, and group.

11 Querying and Manipulating Data Using LINQ‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about Language INtegrated Query (LINQ) expressions. LINQ is a set of language extensions that add the ability to work with sequences of data and then filter, sort, and project them into different outputs.This chapter will cover the following topics:

image

image

Writing LINQ expressions LINQ in practice

image

Sorting and more

image

image

Using LINQ with EF Core Joining, grouping, and lookups

image

Aggregating and paging sequences

Writing LINQ expressions‌

The first question we need to answer is a fundamental one: why does LINQ exist?

Comparing imperative and declarative language features‌

LINQ was introduced in 2008 with C# 3 and .NET Framework 3. Before that, if a C# and .NET programmer wanted to process a sequence of items, they had to use procedural, aka imperative, code statements. For example, a loop:

Set the current position to the first item.

Check if the item is one that should be processed by comparing one or more properties against specified values. For example, is the unit price greater than 50, or is the country equal to Belgium?

If there’s a match, process that item. For example, output one or more of its properties to the user, update one or more properties to new values, delete the item, or perform an aggregate calculation like counting or summing values.

Move on to the next item. Repeat until all items have been processed.

Procedural code tells the compiler how to achieve a goal. Do this, then do that. Since the compiler does not know what you are trying to achieve, it cannot help you as much. You are 100% responsible for ensuring that every how-to step is correct.LINQ makes these common tasks much easier with less opportunity to introduce subtle bugs. Instead of needing to explicitly state each individual action, like move, read, update, and so on, LINQ enables the programmer to use a declarative aka functional style of writing statements. Declarative, aka functional, code tells the compiler what goal to achieve. The compiler works out the best way to achieve the goal. The statements also tend to be more concise.

Good Practice: If you do not fully understand how LINQ works, then the statements you write can introduce their own subtle bugs! A code teaser doing the rounds recently involves a sequence of tasks and understanding when they are executed (https://twitter.com/amantinband/status/1559187912218099714). Most experienced developers got it wrong! To be fair, it is the combination of LINQ behavior with multi- threading behavior that confused most of them. But by the end of this chapter, you will be better informed to understand why the code was dangerous due to LINQ behavior.

Although we wrote a few LINQ expressions in Chapter 10, Working with Data Using Entity Framework Core, they weren't the focus, so I didn't properly explain how LINQ works. Let's now take time to properly understand them.

LINQ components‌

LINQ has several parts; some are required, and some are optional:

image

Extension methods (required): These include examples such as Where , OrderBy , and

Select . These are what provide the functionality of LINQ.

image

LINQ providers (required): These include LINQ to Objects for processing in-memory objects, LINQ to Entities for processing data stored in external databases and modeled with EF Core, and LINQ to XML for processing data stored as XML. These providers are the part of LINQ that executes LINQ expressions in a way specific to different types of data.

image

image

Lambda expressions (optional): These can be used instead of named methods to simplify LINQ queries, for example, for the conditional logic of the Where method for filtering. LINQ query comprehension syntax (optional): These include C# keywords like from , in , where , orderby , descending , and select . These are aliases for some of the LINQ extension methods, and their use can simplify the queries you write, especially if you already have experience with other query languages, such as Structured Query Language (SQL).

When programmers are first introduced to LINQ, they often believed that LINQ query comprehension syntax was LINQ but, ironically, that is one of the parts of LINQ that is optional!

Building LINQ expressions with the Enumerable class‌

The LINQ extension methods, such as Where and Select , are appended by the Enumerable static class to any type, known as a sequence, that implements IEnumerable . A sequence contains zero, one, or more items.For example, an array of any type implements the IEnumerable class, where T is the type of item in the array. This means that all arrays support LINQ to query and manipulate them.All generic collections, such as List , Dictionary<TKey, TValue> , Stack , and Queue , implement IEnumerable , so they can be queried and manipulated with LINQ too. Enumerable defines more than 50 extension methods, as summarized in Table 11.1:

This table will be useful for you for future reference, but for now, you might want to briefly scan it to get a feel for what extension methods exist and come back later to review it properly. An online version of this table is available at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch11-linq-methods.md.

Method(s) Description

First , FirstOrDefault

, Last , LastOrDefault

Get the first or last item in the sequence or throw an exception, or return the default value for the type, for example, 0 for an

int and null for a reference type, if there is not a first or last item.

Where Return a sequence of items that match a specified filter.

Single ,

SingleOrDefault

ElementAt ,

ElementAtOrDefault

Return an item that matches a specific filter or throw an exception, or return the default value for the type if there is not exactly one match.

Return an item at a specified index position or throw an exception, or return the default value for the type if there is not an item at that position. New in .NET 6 are overloads that can be passed an Index instead of an int , which is more efficient when working with Span sequences.

Select , SelectMany Project items into a different shape, that is, a different type, and flatten a nested hierarchy of items.

OrderBy , OrderByDescending , ThenBy , ThenByDescending

Sort items by a specified field or property.

Order , OrderDescending Sort items by the item itself.

Reverse Reverse the order of the items.

GroupBy , GroupJoin ,

Join

Group and/or join two sequences.

Skip , SkipWhile Skip a number of items; or skip when an expression is true .

Take , TakeWhile Take a number of items, or take items while an expression is true

. Introduced in .NET 6 is an overload that can be passed a Range , for example, Take(range: 3..^5) , meaning take a subset starting 3 items in from the start and ending 5 items in from the end, or instead of Skip(4) you could use Take(4..) .

Aggregate , Average , Count , LongCount , Max , Min , Sum

Calculate aggregate values.

TryGetNonEnumeratedCount Count() checks if a Count property is implemented on the sequence and returns its value, or it enumerates the entire sequence to count its items. Introduced in .NET 6, this method only checks for Count ; if it is missing, it returns false and sets the out parameter to 0 to avoid a potentially poor-performing operation.

All , Any , Contains Return true if all or any of the items match the filter, or if the sequence contains a specified item.

Cast Cast items into a specified type. It is useful to convert non- generic objects to a generic type in scenarios where the compiler would otherwise complain.

OfType Remove items that do not match a specified type.

Distinct Remove duplicate items.

Except , Intersect ,

Union

Perform operations that return sets. Sets cannot have duplicate items. Although the inputs can be any sequence and so the inputs can have duplicates, the result is always a set.

DistinctBy , ExceptBy , Allow the comparison to be performed on a subset of the items

IntersectBy , UnionBy , rather than the entire items. For example, instead of removing

MinBy , MaxBy

duplicates with Distinct by comparing an entire Person object, you could remove duplicates with DistinctBy by comparing just their LastName and DateOfBirth .

Chunk Divide a sequence into sized batches. The size parameter specified the number of items in each chunk. The last chunk will contain the remaining items and could be less than size .

Append , Concat ,

Prepend

Perform sequence-combining operations.

Zip Perform a match operation on two or three sequences based on the position of items, for example, the item at position 1 in the first sequence matches the item at position 1 in the second sequence.

ToArray , ToList , ToDictionary , ToHashSet , ToLookup

Convert the sequence into an array or collection. These are the only extension methods that force the execution of a LINQ expression immediately rather than wait for deferred execution, which you will learn about shortly.

Table 11.1: LINQ extension methods

Good Practice: Make sure that you understand and remember the difference between LINQ extension methods that start with As and To . Methods that start with As , like AsEnumerable , cast the sequence into a different type but do not allocate memory so those methods are fast. Methods that start with To , like ToList , allocate memory for a new sequence of items so can be slow and will always use more memory resources.

The Enumerable class also has some methods that are not extension methods, as shown in

Table 11.2:

Method Description

Empty Returns an empty sequence of the specified type T . It is useful for passing an empty sequence to a method that requires an IEnumerable .

Range Returns a sequence of integers from the start value with count items. For example, Enumerable.Range(start: 5, count: 3) would contain the integers 5, 6, and

7.

Repeat Returns a sequence that contains the same element repeated count times. For example, Enumerable.Repeat(element: "5", count: 3) would contain the string values "5" , "5" , and "5" .

Table 11.2: Enumerable non-extension methods

LINQ in practice‌

Now we can build a console app to explore practical examples of using LINQ.

Understanding deferred execution‌

LINQ uses deferred execution. It is important to understand that calling most of the above extension methods does not execute the query and get the results. Most of these extension methods return a LINQ expression that represents a question, not an answer. Let's explore:

Use your preferred code editor to create a new project, as defined in the following list:

image

image

image

Project template: Console App / console Project file and folder: LinqWithObjects Solution file and folder: Chapter11

In the project file, globally and statically import the System.Console class.

Add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , delete any existing statements and then define a partial Program

class with a method to output a section title, as shown in the following code:

partial class Program

{

private static void SectionTitle(string title)

{

ConsoleColor previousColor = ForegroundColor; ForegroundColor = ConsoleColor.DarkYellow; WriteLine($" {title} "); ForegroundColor = previousColor;

}

}

Add a new class file named Program.Functions.cs .

In Program.Functions.cs , delete any existing statements and then define a partial Program class with a method named DeferredExecution that is passed an array of string values, and then define two queries, as shown in the following code:

partial class Program

{

private static void DeferredExecution(string[] names)

{

SectionTitle("Deferred execution");

// Question: Which names end with an M?

// (using a LINQ extension method)

var query1 = names.Where(name => name.EndsWith("m"));

// Question: Which names end with an M?

// (using LINQ query comprehension syntax)

var query2 = from name in names where name.EndsWith("m") select name;

}

}

  1. In Program.cs , delete the existing statements and then add statements to define a sequence of string values for people who work in an office, and then pass it as an argument to the DeferredExecution method, as shown in the following code:

// A string array is a sequence that implements IEnumerable. string[] names = { "Michael", "Pam", "Jim", "Dwight",

"Angela", "Kevin", "Toby", "Creed" }; DeferredExecution(names);

  1. In Program.Functions.cs , in the DeferredExecution method, to get the answer – in other words, to execute the query – you must materialize it by either calling one of the "To" methods like ToArray , ToDictionary , or ToLookup , or by enumerating the query. Add statements to do this, as shown in the following code:

// Answer returned as an array of strings containing Pam and Jim. string[] result1 = query1.ToArray();

// Answer returned as a list of strings containing Pam and Jim. List result2 = query2.ToList();

// Answer returned as we enumerate over the results. foreach (string name in query1)

{

WriteLine(name); // outputs Pam

names[2] = "Jimmy"; // Change Jim to Jimmy.

// On the second iteration Jimmy does not

// end with an "m" so it does not get output.

}

  1. Run the console app and note the result, as shown in the following output:

Deferred execution Pam

Due to deferred execution, after outputting the first result, Pam , if the original array values change, then by the time we loop back around, there are no more matches because Jim has become Jimmy and does not end with an m , so only Pam is output.Before we get too deep into the weeds, let's slow down and look at some common LINQ extension methods and how to use them, one at a time.

Filtering entities using Where‌

The most common reason for using LINQ is to filter items in a sequence using the Where extension method. Let's explore filtering by defining a sequence of names and then applying LINQ operations to it:

  1. In the project file, add an element to remove the System.Linq namespace from automatically being imported globally, as shown highlighted in the following markup:

  1. In Program.Functions.cs , add a new method named FilteringUsingWhere , as shown in the following code:

private static void FilteringUsingWhere(string[] names)

{

}

If you are using Visual Studio 2022, navigate to Tools | Options, in the Options dialog box, navigate to Text Editor | C# | IntelliSense, clear the Show items from unimported namespaces check box, and then click OK.

In FilteringUsingWhere , attempt to call the Where extension method on the array of names, as shown in the following code:

SectionTitle("Filtering entities using Where"); var query = names.W

As you type the W , note that in older code editors (or code editors with the option to show items from unimported namespaces is disabled) the Where method is missing from the IntelliSense list of members of a string array, as shown in Figure 11.1:

image

Figure 11.1: IntelliSense with the Where extension method missing

This is because Where is an extension method. It does not exist on the array type. To make the Where extension method available, we must import the System.Linq namespace. This is implicitly imported by default in new .NET 6 and later projects, but we removed it to illustrate the point. Recent versions of code editors are smart enough to suggest using the Where method anyway and indicate that they will import the System.Linq namespace for you automatically.

If you are using Visual Studio 2022, navigate to Tools | Options, in the Options dialog box, navigate to Text Editor | C# | IntelliSense, select the Show items from unimported namespaces check box, and then click OK.

In the project file, comment out the element that removed System.Linq , as shown in the following code:

Save the change and build the project.

Retype the W for the Where method and note that the IntelliSense list now includes the extension methods added by the Enumerable class, as shown in Figure 11.2:

image

Figure 11.2: IntelliSense showing LINQ extension methods when System.Linq is imported

Interestingly, as you can see in the screenshot of Visual Studio 2022 on my computer, GitHub Copilot is even suggesting autocompleting with a lambda expression very similar to the one that we will eventually end up writing. But there are some important intermediate steps you need to see before we get to that so do not press Tab, Tab to insert any GitHub Copilot suggestions if you have that feature enabled.

As you type the parentheses for the Where method, IntelliSense tells us that, to call

Where , we must pass in an instance of a Func<string, bool> delegate.

Enter an expression to create a new instance of a Func<string, bool> delegate, and for now note that we have not yet supplied a method name because we will define it in the next step, as shown in the following code:

var query = names.Where(new Func<string, bool>( ))

  1. Leave the statement unfinished for now.

The Func<string, bool> delegate tells us that for each string variable passed to the method, the method must return a bool value. If the method returns true , it indicates that we should include the string in the results, and if the method returns false , it indicates that we should exclude it.

Targeting a named method‌

Let's define a method that only includes names that are longer than four characters:

  1. In Program.Functions.cs , add a method that will return true only for names longer than four characters, as shown in the following code:

static bool NameLongerThanFour(string name)

{

// Returns true for a name longer than four characters. return name.Length > 4;

}

  1. In the FilteringUsingWhere method, pass the method's name into the Func<string, bool>

delegate, as shown highlighted in the following code:

var query = names.Where(

new Func<string, bool>(NameLongerThanFour));

  1. In the FilteringUsingWhere method, add statements to enumerate the names array using

foreach , as shown in the following code:

foreach (string item in query)

{

WriteLine(item);

}

  1. In Program.cs , comment out the call to DeferredExecution and then pass names as an argument to the FilteringUsingWhere method, as shown in the following code:

// DeferredExecution(names); FilteringUsingWhere(names);

  1. Run the code and view the results, noting that only names longer than four letters are listed, as shown in the following output:

Michael Dwight Angela Kevin Creed

Simplifying the code by removing the explicit delegate instantiation‌

We can simplify the code by deleting the explicit instantiation of the Func<string, bool>

delegate because the C# compiler can instantiate the delegate for us:

  1. To help you learn by seeing progressively improved code, in the FilteringUsingWhere method, comment out the query and add a comment about how it works, as shown in the following code:

// Explicitly creating the required delegate.

// var query = names.Where(

// new Func<string, bool>(NameLongerThanFour));

  1. Enter the query a second time, but this time without the explicit instantiation of the delegate, as shown in the following code:

// The compiler creates the delegate automatically. var query = names.Where(NameLongerThanFour);

  1. Run the code and note that it has the same behavior.

Targeting a lambda expression‌

We can simplify our code even further using a lambda expression in place of a named method.Although it can look complicated at first, a lambda expression is simply a nameless function. It uses the => (read as "goes to") symbol to indicate the return value:

  1. Comment out the second query, and then add a third version of the query that uses a lambda expression, as shown in the following code:

// Using a lambda expression instead of a named method. var query = names.Where(name => name.Length > 4);

Note that the syntax for a lambda expression includes all the important parts of the NameLongerThanFour method, but nothing more. A lambda expression only needs to define the following:

image

The names of input parameters: name

image

A return value expression: name.Length > 4

The type of the name input parameter is inferred from the fact that the sequence contains string values, and the return type must be a bool value as defined by the delegate for Where to work, so the expression after the => symbol must return a bool value. The compiler does most of the work for us, so our code can be as concise as possible.

  1. Run the code and note that it has the same behavior.

Lambda expressions with default parameter values‌

Introduced with C# 12, you can now provide default values for parameters in lambda expressions, as shown in the following code:

var query = names.Where((string name = "Bob") => name.Length > 4);

For what we are using this lambda expression for, setting a default value is not necessary, but later you will see more useful examples.

Sorting and more‌

Other commonly used extension methods are OrderBy and ThenBy , used for sorting a sequence.

Sorting by a single property using OrderBy‌

Extension methods can be chained if the previous method returns another sequence, that is, a type that implements the IEnumerable interface.Let's continue working with the current project to explore sorting:

  1. In the FilteringUsingWhere method, append a call to OrderBy to the end of the existing query, as shown in the following code:

var query = names

.Where(name => name.Length > 4)

.OrderBy(name => name.Length);

Good Practice: Format the LINQ statement so that each extension method call happens on its own line, to make it easier to read.

  1. Run the code and note that the names are now sorted by shortest first, as shown in the following output:

Kevin Creed Dwight

Angela Michael

To put the longest name first, you would use OrderByDescending .

Sorting by a subsequent property using ThenBy‌

We might want to sort by more than one property, for example, to sort names of the same length in alphabetical order:

  1. In the FilteringUsingWhere method, append a call to the ThenBy method at the end of the existing query, as highlighted in the following code:

var query = names

.Where(name => name.Length > 4)

.OrderBy(name => name.Length)

.ThenBy(name => name);

  1. Run the code and note the slight difference in the following sort order. Within a group of names of the same length, the names are sorted alphabetically by the full value of the string , so Creed comes before Kevin , and Angela comes before Dwight , as shown in the following output:

Creed Kevin Angela Dwight Michael

Sorting by the item itself‌

Introduced in .NET 7 are the Order and OrderDescending extension methods. These simplify ordering by the item itself. For example, if you have a sequence of string values, then before .NET 7 you would have to call the OrderBy method and pass a lambda that selects the items themselves, as shown in the following code:

var query = names.OrderBy(name => name);

With .NET 7 or later, we can simplify the statement, as shown in the following code:

var query = names.Order();

OrderDescending does a similar thing but in descending order.Remember that the names array contains instances of the string type, which implements the IComparable interface. This is why they can be ordered, aka sorted. If the array contained instances of a complex type like Person or Product , then those types would have to implement the IComparable interface so they could be ordered too.

Declaring a query using var or a specified type‌

While writing a LINQ expression, it is convenient to use var to declare the query object. This is because the return type frequently changes as you work on a LINQ expression. For example, our query started as an IEnumerable and is currently an IOrderedEnumerable :

Hover your mouse over the var keyword and note that its type is

IOrderedEnumerable , as shown in Figure 11.3:

image

Figure 11.3: Hover over var to see the actual implied type of the query expression

In Figure 11.3, I added extra vertical space between names and .Where so that the tooltip did not cover up the query.

Replace var with the actual type, as shown highlighted in the following code:

IOrderedEnumerable query = names

.Where(name => name.Length > 4)

.OrderBy(name => name.Length)

.ThenBy(name => name);

Good practice: Once you have finished working on a query, you could change the declared type from var to the actual type to make it clearer what the type is. This is easy because your code editor can tell you what it is. Doing this is just for clarity. It has no effect on performance because C# changes all var declarations to the actual types at compile time.

  1. Run the code and note that it has the same behavior.

Filtering by type‌

The Where extension method is great for filtering by values, such as text and numbers. But what if the sequence contains multiple types, and you want to filter by a specific type and respect any inheritance hierarchy?Imagine that you have a sequence of exceptions. There are hundreds of exception types that form a complex inheritance hierarchy, as partially shown in Figure 11.4:

image

Figure 11.4: A partial exception inheritance hierarchy

Let's explore filtering by type:

  1. In Program.Functions.cs , define a new method to list and then filter exception-derived objects using the OfType extension method to remove exceptions that are not arithmetic exceptions and write only the arithmetic exceptions to the console, as shown in the following code:

static void FilteringByType()

{

SectionTitle("Filtering by type"); List exceptions = new()

{

new ArgumentException(), new SystemException(),

new IndexOutOfRangeException(), new InvalidOperationException(), new NullReferenceException(), new InvalidCastException(),

new OverflowException(), new DivideByZeroException(), new ApplicationException()

};

IEnumerable arithmeticExceptionsQuery = exceptions.OfType();

foreach (ArithmeticException exception in arithmeticExceptionsQuery)

{

WriteLine(exception);

}

}

  1. In Program.cs , comment out the call to FilteringUsingWhere , and then add a call to the

FilteringByType method, as shown in the following code:

// FilteringUsingWhere(names); FilteringByType();

  1. Run the code and note that the results only include exceptions of the ArithmeticException type, or the ArithmeticException -derived types, as shown in the following output:

System.OverflowException: Arithmetic operation resulted in an overflow. System.DivideByZeroException: Attempted to divide by zero.

Working with sets and bags‌

Sets are one of the most fundamental concepts in mathematics. A set is a collection of one or more unique objects. A multiset, aka a bag, is a collection of one or more objects that can have duplicates.You might remember being taught about Venn diagrams in school. Common set operations include the intersect or union between sets.Let's write some code that will define three arrays of string values for cohorts of apprentices and then perform some common set and multiset operations on them:

  1. In Program.Functions.cs , add a method that outputs any sequence of string variables as a comma-separated single string to the console output, along with an optional description, as shown in the following code:

static void Output(IEnumerable cohort, string description = "")

{

if (!string.IsNullOrEmpty(description))

{

WriteLine(description);

}

Write(" ");

WriteLine(string.Join(", ", cohort.ToArray())); WriteLine();

}

  1. In Program.Functions.cs , add a method that defines three arrays of names, outputs them, and then performs various set operations on them, as shown in the following code:

static void WorkingWithSets()

{

string[] cohort1 =

{ "Rachel", "Gareth", "Jonathan", "George" }; string[] cohort2 =

{ "Jack", "Stephen", "Daniel", "Jack", "Jared" }; string[] cohort3 =

{ "Declan", "Jack", "Jack", "Jasmine", "Conor" }; SectionTitle("The cohorts");

Output(cohort1, "Cohort 1");

Output(cohort2, "Cohort 2");

Output(cohort3, "Cohort 3"); SectionTitle("Set operations");

Output(cohort2.Distinct(), "cohort2.Distinct()"); Output(cohort2.DistinctBy(name => name.Substring(0, 2)),

"cohort2.DistinctBy(name => name.Substring(0, 2)):"); Output(cohort2.Union(cohort3), "cohort2.Union(cohort3)"); Output(cohort2.Concat(cohort3), "cohort2.Concat(cohort3)"); Output(cohort2.Intersect(cohort3), "cohort2.Intersect(cohort3)"); Output(cohort2.Except(cohort3), "cohort2.Except(cohort3)"); Output(cohort1.Zip(cohort2,(c1, c2) => $"{c1} matched with {c2}"),

"cohort1.Zip(cohort2)");

}

  1. In Program.cs , comment out the call to FilteringByType , and then add a call to the

WorkingWithSets method, as shown in the following code:

// FilteringByType();

WorkingWithSets();

  1. Run the code and view the results, as shown in the following output:

Cohort 1

Rachel, Gareth, Jonathan, George Cohort 2

Jack, Stephen, Daniel, Jack, Jared Cohort 3

Declan, Jack, Jack, Jasmine, Conor cohort2.Distinct()

Jack, Stephen, Daniel, Jared cohort2.DistinctBy(name => name.Substring(0, 2)):

Jack, Stephen, Daniel cohort2.Union(cohort3)

Jack, Stephen, Daniel, Jared, Declan, Jasmine, Conor cohort2.Concat(cohort3)

Jack, Stephen, Daniel, Jack, Jared, Declan, Jack, Jack, Jasmine, Conor cohort2.Intersect(cohort3)

Jack cohort2.Except(cohort3)

Stephen, Daniel, Jared cohort1.Zip(cohort2)

Rachel matched with Jack, Gareth matched with Stephen, Jonathan matched with Daniel, George ma

With Zip , if there are unequal numbers of items in the two sequences, then some items will not have a matching partner. Those without a partner, like Jared , will not be included in the result.For the DistinctBy example, instead of removing duplicates by comparing the whole name, we define a lambda key selector to remove duplicates by comparing the first two characters, so Jared is removed because Jack already is a name that starts with Ja .So far, we have used the LINQ to Objects provider to work with in-memory objects. Next, we will use the LINQ to Entities provider to work with entities stored in a database.

Using LINQ with EF Core‌

We have looked at LINQ queries that filter and sort, but none that change the shape of the items in the sequence. This is called projection because it's about projecting items of one

shape into another shape. To learn about projection, it is best to have some more complex types to work with, so in the next project, instead of using string sequences, we will use sequences of entities from the Northwind sample database that you were introduced to in Chapter 10, Working with Data Using Entity Framework Core.I will give instructions to use SQLite because it is cross-platform, but if you prefer to use SQL Server then feel free to do so. I have included some commented code to enable SQL Server if you choose.

Creating a console app for exploring LINQ to Entities‌

First, we must create a console app and Northwind database to work with:

Use your preferred code editor to add a new Console App / console project named

LinqWithEFCore to the Chapter11 solution.

In the project file, globally and statically import the System.Console class.

In the LinqWithEFCore project, add a package reference to the EF Core provider for SQLite and/or SQL Server, as shown in the following markup:

Build the LinqWithEFCore project to restore packages.

Copy the Northwind4Sqlite.sql file to the LinqWithEFCore folder.

At a command prompt or terminal in the LinqWithEFCore folder, create the Northwind database by executing the following command:

sqlite3 Northwind.db -init Northwind4Sqlite.sql

  1. Be patient because this command might take a while to create the database structure. Eventually, you will see the SQLite command prompt, as shown in the following output:

-- Loading resources from Northwind4Sqlite.sql SQLite version 3.38.0 2022-02-22 15:20:15

Enter ".help" for usage hints. sqlite>

To exit SQLite command mode, press Ctrl + C twice on Windows or cmd + D on macOS or Linux.

If you prefer to work with SQL Server, then you should already have the Northwind database created in SQL Server from the previous chapter.

Building an EF Core model‌

We must define an EF Core model to represent the database and tables that we will work with. We will define the model manually to take complete control and to prevent a relationship from being automatically defined between the Categories and Products tables. Later, you will use LINQ to join the two entity sets:

In the LinqWithEFCore project, add a new folder named EntityModels .

In the EntityModels folder, add three class files to the project, named NorthwindDb.cs ,

Category.cs , and Product.cs .

Modify the class file named Category.cs , as shown in the following code:

// To use [Required] and [StringLength]. using System.ComponentModel.DataAnnotations; namespace Northwind.EntityModels;

public class Category

{

public int CategoryId { get; set; } [Required]

[StringLength(15)]

public string CategoryName { get; set; } = null!; public string? Description { get; set; }

}

  1. Modify the class file named Product.cs , as shown in the following code:

// To use [Required] and [StringLength]. using System.ComponentModel.DataAnnotations;

// To use [Column].

using System.ComponentModel.DataAnnotations.Schema; namespace Northwind.EntityModels;

public class Product

{

public int ProductId { get; set; } [Required]

[StringLength(40)]

public string ProductName { get; set; } = null!; public int? SupplierId { get; set; }

public int? CategoryId { get; set; } [StringLength(20)]

public string? QuantityPerUnit { get; set; }

// Required for SQL Server provider. [Column(TypeName = "money")]

public decimal? UnitPrice { get; set; } public short? UnitsInStock { get; set; } public short? UnitsOnOrder { get; set; } public short? ReorderLevel { get; set; } public bool Discontinued { get; set; }

}

We have deliberately not defined any relationships between Category and Product so that we can see how to manually associate them with each other using LINQ later.

  1. Modify the class file named NorthwindDb.cs , as shown in the following code:

using Microsoft.Data.SqlClient; // To use SqlConnectionStringBuilder. using Microsoft.EntityFrameworkCore; // To use DbContext, DbSet. namespace Northwind.EntityModels;

public class NorthwindDb : DbContext

{

public DbSet Categories { get; set; } = null!; public DbSet Products { get; set; } = null!; protected override void OnConfiguring(

DbContextOptionsBuilder optionsBuilder)

{

region To use SQLite

string database = "Northwind.db";

string dir = Environment.CurrentDirectory; string path = string.Empty;

// The database file will stay in the project folder.

// We will automatically adjust the relative path to

// account for running in VS2022 or from terminal. if (dir.EndsWith("net8.0"))

{

// Running in the \bin\<Debug|Release>\net8.0 directory. path = Path.Combine("..", "..", "..", database);

}

else

{

// Running in the directory. path = database;

}

path = Path.GetFullPath(path); // Convert to absolute path. WriteLine($"SQLite database path: {path}");

if (!File.Exists(path))

{

throw new FileNotFoundException(

message: $"{path} not found.", fileName: path);

}

// To use SQLite. optionsBuilder.UseSqlite($"Data Source={path}"); #endregion

region To use SQL Server SqlConnectionStringBuilder builder = new(); builder.DataSource = "."; builder.InitialCatalog = "Northwind"; builder.IntegratedSecurity = true; builder.Encrypt = true; builder.TrustServerCertificate = true; builder.MultipleActiveResultSets = true; string connection = builder.ConnectionString;

// WriteLine($"SQL Server connection: {connection}");

// To use SQL Server.

// optionsBuilder.UseSqlServer(connection); #endregion

}

protected override void OnModelCreating( ModelBuilder modelBuilder)

{

if (Database.ProviderName is not null && Database.ProviderName.Contains("Sqlite"))

{

// SQLite data provider does not directly support the

// decimal type so we can convert to double instead. modelBuilder.Entity()

.Property(product => product.UnitPrice)

.HasConversion();

}

}

}

If you want to use SQL Server, then comment out the statement that calls UseSqlite and uncomment the statement that calls UseSqlServer .

  1. Build the project and fix any compiler errors.

Filtering and sorting sequences‌

Now let's write statements to filter and sort sequences of rows from the tables:

In the LinqWithEFCore project, add a new class file named Program.Helpers.cs .

In Program.Helpers.cs , define a partial Program class with a method to configure the console to support special characters like the Euro currency symbol and control the current culture, and a method to output a section title, as shown in the following code:

using System.Globalization; // To use CultureInfo. partial class Program

{

private static void ConfigureConsole(string culture = "en-US", bool useComputerCulture = false)

{

// To enable Unicode characters like Euro symbol in the console. OutputEncoding = System.Text.Encoding.UTF8;

if (!useComputerCulture)

{

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture);

}

WriteLine($"CurrentCulture: {CultureInfo.CurrentCulture.DisplayName}");

}

private static void SectionTitle(string title)

{

ConsoleColor previousColor = ForegroundColor; ForegroundColor = ConsoleColor.DarkYellow; WriteLine($" {title} "); ForegroundColor = previousColor;

}

}

In the LinqWithEFCore project, add a new class file named Program.Functions.cs .

In Program.Functions.cs , define a partial Program class and add a method to filter and sort products, as shown in the following code:

using Northwind.EntityModels; // To use NorthwindDb, Category, Product. using Microsoft.EntityFrameworkCore; // To use DbSet.

partial class Program

{

private static void FilterAndSort()

{

SectionTitle("Filter and sort"); using NorthwindDb db = new();

DbSet allProducts = db.Products; IQueryable filteredProducts =

allProducts.Where(product => product.UnitPrice < 10M); IOrderedQueryable sortedAndFilteredProducts =

filteredProducts.OrderByDescending(product => product.UnitPrice); WriteLine("Products that cost less than $10:");

foreach (Product p in sortedAndFilteredProducts)

{

WriteLine("{0}: {1} costs {2:$#,##0.00}", p.ProductId, p.ProductName, p.UnitPrice);

}

WriteLine();

}

}

Note the following about the preceding code:

image

DbSet implements IEnumerable , so LINQ can be used to query and manipulate sequences of entities in models built for EF Core. (Actually, I should say TEntity instead of T , but the name of this generic type has no functional effect. The only requirement is that the type is a class . The name just indicates the class is expected to be an entity model.)

image

The sequences implement IQueryable (or IOrderedQueryable after a call to an ordering LINQ method) instead of IEnumerable or IOrderedEnumerable . This is an indication that we are using a LINQ provider that builds the query using expression trees. They represent code in a tree-like data structure and enable the creation of dynamic queries, which is useful for building LINQ queries for external data providers like SQLite.

image

The LINQ expression will be converted into another query language, such as SQL. Enumerating the query with foreach or calling a method such as ToArray will force the execution of the query and materialize the results.

  1. In Program.cs , delete any existing statements and then call the ConfigureConsole and

FilterAndSort methods, as shown in the following code:

ConfigureConsole(); // Sets US English by default. FilterAndSort();

  1. Run the project and view the result, as shown in the following output:

CurrentCulture: English (United States)

Filter and sort

SQLite database path: C:\cs12dotnet8\Chapter11\LinqWithEFCore\Northwind.db Products that cost less than $10:

41: Jack's New England Clam Chowder costs $9.65 45: Rogede sild costs $9.50

47: Zaanse koeken costs $9.50

19: Teatime Chocolate Biscuits costs $9.20 23: Tunnbröd costs $9.00

75: Rhönbräu Klosterbier costs $7.75 54: Tourtière costs $7.45

52: Filo Mix costs $7.00 13: Konbu costs $6.00

24: Guaraná Fantástica costs $4.50 33: Geitost costs $2.50

Although this query outputs the information we want, it does so inefficiently because it gets all columns from the Products table instead of just the three columns we need. Let's log the generated SQL:

  1. In the FilterAndSort method, before enumerating the results using foreach , add a statement to output the SQL, as shown highlighted in the following code:

WriteLine("Products that cost less than $10:"); WriteLine(sortedAndFilteredProducts.ToQueryString());

  1. Run the code and view the result that shows the SQL executed before the product details, as shown in the following partial output:

Products that cost less than $10:

SELECT "p"."ProductId", "p"."CategoryId", "p"."Discontinued", "p"."ProductName", "p"."QuantityPe FROM "Products" AS "p"

WHERE "p"."UnitPrice" < 10.0 ORDER BY "p"."UnitPrice" DESC

41: Jack's New England Clam Chowder costs $9.65

...

Projecting sequences into new types‌

Before we look at projection, we should review object initialization syntax. If you have a class defined, then you can instantiate an object using the class name, new() , and curly braces to set initial values for fields and properties, as shown in the following code:

// Person.cs

public class Person

{

public string Name { get; set; }

public DateTime DateOfBirth { get; set; }

}

// Program.cs

Person knownTypeObject = new()

{

Name = "Boris Johnson",

DateOfBirth = new(year: 1964, month: 6, day: 19)

};

C# 3 and later allow instances of anonymous types to be instantiated using the var keyword, as shown in the following code:

var anonymouslyTypedObject = new

{

Name = "Boris Johnson",

DateOfBirth = new DateTime(year: 1964, month: 6, day: 19)

};

Although we did not specify a type, the compiler can infer an anonymous type from the setting of two properties named Name and DateOfBirth . The compiler can infer the types of the two properties from the values assigned: a literal string and a new instance of a

date/time value.This capability is especially useful when writing LINQ queries to project an existing type into a new type without having to explicitly define the new type. Since the type is anonymous, this can only work with var -declared local variables.Let's make the SQL command executed against the database table more efficient by adding a call to the Select method to project instances of the Product class into instances of a new anonymous type with only three properties:

  1. In Program.Functions.cs , in the FilterAndSort method, add a statement to extend the LINQ query to use the Select method to return only the three properties (that is, table columns) that we need, and modify the call to ToQueryString to use the new projectedProducts query, and the foreach statement to use the var keyword and the new projectedProducts query, as shown highlighted in the following code:

IOrderedQueryable sortedAndFilteredProducts = filteredProducts.OrderByDescending(product => product.UnitPrice);

var projectedProducts = sortedAndFilteredProducts

.Select(product => new // Anonymous type.

{

product.ProductId, product.ProductName, product.UnitPrice

});

WriteLine("Products that cost less than $10:"); WriteLine(projectedProducts.ToQueryString()); foreach (var p in projectedProducts)

{

Hover your mouse over the new keyword in the Select method call, or the var keyword in the foreach statement, and note that it is an anonymous type, as shown in Figure 11.5:

image

Figure 11.5: An anonymous type used during LINQ projection

Run the project and confirm that the output is the same as before and the generated SQL is more efficient, as shown in the following output:

SELECT "p"."ProductId", "p"."ProductName", "p"."UnitPrice" FROM "Products" AS "p"

WHERE "p"."UnitPrice" < 10.0 ORDER BY "p"."UnitPrice" DESC

More information: You can learn more about projection using the Select method at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/programming- guide/concepts/linq/projection-operations.

Let's continue to look at common LINQ queries by learning how to join, group, and perform lookups.

Joining, grouping, and lookups‌

There are three extension methods for joining, grouping, and creating grouped lookups:

image

Join : This method has four parameters: the sequence that you want to join with, the property or properties on the left sequence to match on, the property or properties on the right sequence to match on, and a projection.

image

GroupJoin : This method has the same parameters, but it combines the matches into a group object with a Key property for the matching value and an IEnumerable type for the multiple matches.

image

ToLookup : This method creates a new data structure with the sequence grouped by a key.

Joining sequences‌

Let's explore these methods when working with two tables, Categories and Products :

  1. In Program.Functions.cs , add a method to select categories and products, join them, and output them, as shown in the following code:

private static void JoinCategoriesAndProducts()

{

SectionTitle("Join categories and products"); using NorthwindDb db = new();

// Join every product to its category to return 77 matches. var queryJoin = db.Categories.Join(

inner: db.Products,

outerKeySelector: category => category.CategoryId, innerKeySelector: product => product.CategoryId, resultSelector: (c, p) =>

new { c.CategoryName, p.ProductName, p.ProductId }); foreach (var p in queryJoin)

{

WriteLine($"{p.ProductId}: {p.ProductName} in {p.CategoryName}.");

}

}

In a join, there are two sequences, outer and inner . In the preceding example,

categories is the outer sequence and products is the inner sequence.

  1. In Program.cs , comment out the call to FilterAndSort , and then call the

JoinCategoriesAndProducts method, as shown highlighted in the following code:

ConfigureConsole(); // Sets US English by default.

// FilterAndSort(); JoinCategoriesAndProducts();

  1. Run the code and view the results. Note that there is a single line of output for each of the 77 products, as shown in the following output (edited to only include the first four items):

1: Chai in Beverages.

2: Chang in Beverages.

3: Aniseed Syrup in Condiments.

4: Chef Anton's Cajun Seasoning in Condiments.

...

  1. In Program.Functions.cs , in the JoinCategoriesAndProducts method, at the end of the existing query, call the OrderBy method to sort by CategoryName , as shown highlighted in the following code:

var queryJoin = db.Categories.Join( inner: db.Products,

outerKeySelector: category => category.CategoryId, innerKeySelector: product => product.CategoryId, resultSelector: (c, p) =>

new { c.CategoryName, p.ProductName, p.ProductId })

.OrderBy(cp => cp.CategoryName);

  1. Run the code and view the results. Note that there is a single line of output for each of the 77 products, and the results show all products in the Beverages category first, then the Condiments category, and so on, as shown in the following partial output:

1: Chai in Beverages.

2: Chang in Beverages.

24: Guaraná Fantástica in Beverages. 34: Sasquatch Ale in Beverages.

...

Group-joining sequences‌

Let's explore group joining when working with the same two tables as we used to explore joining, Categories and Products , so we can compare the subtle differences:

  1. In Program.Functions.cs , add a method to group and join, show the group name, and then show all the items within each group, as shown in the following code:

private static void GroupJoinCategoriesAndProducts()

{

SectionTitle("Group join categories and products"); using NorthwindDb db = new();

// Group all products by their category to return 8 matches. var queryGroup = db.Categories.AsEnumerable().GroupJoin(

inner: db.Products,

outerKeySelector: category => category.CategoryId, innerKeySelector: product => product.CategoryId, resultSelector: (c, matchingProducts) => new

{

c.CategoryName,

Products = matchingProducts.OrderBy(p => p.ProductName)

});

foreach (var c in queryGroup)

{

WriteLine($"{c.CategoryName} has {c.Products.Count()} products."); foreach (var product in c.Products)

{

WriteLine($" {product.ProductName}");

}

}

}

If we had not called the AsEnumerable method, then a runtime exception would have been thrown, as shown in the following output:

Unhandled exception. System.ArgumentException: Argument type 'System.Linq.IOrderedQueryable`1[P

This is because not all LINQ extension methods can be converted from expression trees into some other query syntax like SQL. In these cases, we can convert from IQueryable to IEnumerable by calling the AsEnumerable method, which forces query processing to use LINQ to EF Core only to bring the data into the application, and then use LINQ to Objects to execute more complex processing in memory. But, often, this is less efficient.

In Program.cs , call the GroupJoinCategoriesAndProducts method.

Run the code, view the results, and note that the products inside each category have been sorted by their name, as defined in the query, and as shown in the following

partial output:

Beverages has 12 products.

Chai Chang

...

Condiments has 12 products.

Aniseed Syrup

Chef Anton's Cajun Seasoning

...

Grouping for lookups‌

Instead of writing a LINQ query expression to join and group and running it once, you might want to use a LINQ extension method to create and then store a reusable in-memory collection that has entities that have been grouped.We have a table named Products in the Northwind database that includes a column for the category that they reside in, as shown in Table 11.3:

ProductName CategoryID

Chai 1

Chang 1

Aniseed Syrup 2

Chef Anton's Cajun Seasoning 2 Chef Anton's Gumbo Mix 2

... ...

Table 11.3: Simplified products table

You might want to create a data structure in memory that can group the Product entities by their category, and then provide a quick way to look up all the products in a specific category. You can create this using the ToLookup LINQ method, as shown in the following code:

ILookup<int, Product>? productsByCategoryId = db.Products.ToLookup(keySelector: category => category.CategoryId);

When you call the ToLookup method, you must specify a key selector to choose what value you want to group by. This value can then later be used to look up the group and its items.The ToLookup method creates a dictionary-like data structure of key-value pairs in memory that has unique category IDs for the key and a collection of Product objects for the value, as shown in Table 11.4:

Key Value (each one is a collection of Product objects)

[Chai] [Chang] and so on.

[Aniseed Syrup] [Chef Anton's Cajun Seasoning] [Chef Anton's Gumbo Mix] and so on.

... ...

Table 11.4: Simplified product lookup

Note the product names in square brackets like [Chai] represent an entire Product object.Instead of using the CategoryId values as the key to the lookup, we could use the category names from the related categories table.Let's do this in a code example:

  1. In Program.Functions.cs , add a method to join products to category names, and then convert them into a lookup, enumerate through the whole lookup, using an IGrouping<string, Product> to represent each row in the lookup dictionary, and then look

up an individual collection of products for a specific category, as shown in the following code:

private static void ProductsLookup()

{

SectionTitle("Products lookup"); using NorthwindDb db = new();

// Join all products to their category to return 77 matches. var productQuery = db.Categories.Join(

inner: db.Products,

outerKeySelector: category => category.CategoryId, innerKeySelector: product => product.CategoryId, resultSelector: (c, p) => new { c.CategoryName, Product = p });

ILookup<string, Product> productLookup = productQuery.ToLookup( keySelector: cp => cp.CategoryName,

elementSelector: cp => cp.Product);

foreach (IGrouping<string, Product> group in productLookup)

{

// Key is Beverages, Condiments, and so on. WriteLine($"{group.Key} has {group.Count()} products."); foreach (Product product in group)

{

WriteLine($" {product.ProductName}");

}

}

// We can look up the products by a category name. Write("Enter a category name: ");

string categoryName = ReadLine()!; WriteLine();

WriteLine($"Products in {categoryName}:");

IEnumerable productsInCategory = productLookup[categoryName]; foreach (Product product in productsInCategory)

{

WriteLine($" {product.ProductName}");

}

}

Selector parameters are lambda expressions that select sub-elements for different purposes. For example, ToLookup has a keySelector to select the part of each item that will be the key and an elementSelector to select the part of each item that will be the value. You can learn more at the following link: https://learn.microsoft.com/en- us/dotnet/api/system.linq.enumerable.tolookup.

In Program.cs , call the ProductsLookup method.

Run the code, view the results, enter a category name like Seafoods , and note that the products are looked up and listed for that category, as shown in the following partial output:

Beverages has 12 products.

Chai Chang

...

Condiments has 12 products.

Aniseed Syrup

Chef Anton's Cajun Seasoning

...

Enter a category name: Seafood Products in Seafood:

Ikura Konbu

Carnarvon Tigers

Nord-Ost Matjeshering Inlagd Sill

Gravad lax Boston Crab Meat

Jack's New England Clam Chowder Rogede sild

Spegesild

Escargots de Bourgogne Röd Kaviar

Aggregating and paging sequences‌

There are LINQ extension methods to perform aggregation functions, such as Average and Sum . Let's write some code to see some of these methods in action, aggregating information from the Products table:

  1. In Program.Functions.cs , add a method to show the use of the aggregation extension methods, as shown in the following code:

private static void AggregateProducts()

{

SectionTitle("Aggregate products"); using NorthwindDb db = new();

// Try to get an efficient count from EF Core DbSet.

if (db.Products.TryGetNonEnumeratedCount(out int countDbSet))

{

WriteLine($"{"Product count from DbSet:",-25} {countDbSet,10}");

}

else

{

WriteLine("Products DbSet does not have a Count property.");

}

// Try to get an efficient count from a List. List products = db.Products.ToList();

if (products.TryGetNonEnumeratedCount(out int countList))

{

WriteLine($"{"Product count from list:",-25} {countList,10}");

}

else

{

WriteLine("Products list does not have a Count property.");

}

WriteLine($"{"Product count:",-25} {db.Products.Count(),10}"); WriteLine($"{"Discontinued product count:",-27} {db.Products

.Count(product => product.Discontinued),8}"); WriteLine($"{"Highest product price:",-25} {db.Products

.Max(p => p.UnitPrice),10:$#,##0.00}"); WriteLine($"{"Sum of units in stock:",-25} {db.Products

.Sum(p => p.UnitsInStock),10:N0}");

WriteLine($"{"Sum of units on order:",-25} {db.Products

.Sum(p => p.UnitsOnOrder),10:N0}"); WriteLine($"{"Average unit price:",-25} {db.Products

.Average(p => p.UnitPrice),10:$#,##0.00}"); WriteLine($"{"Value of units in stock:",-25} {db.Products

.Sum(p => p.UnitPrice * p.UnitsInStock),10:$#,##0.00}");

}

Good practice: Getting a count can seem like a simple operation but counting can be costly. A DbSet like Products does not have a Count property so TryGetNonEnumeratedCount returns false . A List like products does have a Count property because it implements ICollection so TryGetNonEnumeratedCount returns true . (In this case, we had to instantiate a list, which is itself a costly operation, but if you already have a list and need to know the number of items, then this would be efficient.) You can always call Count() on a DbSet , but it can be slow because it might have to enumerate the sequence depending on the data provider implementation. For any array, use the Length property to get a count. You can pass a lambda expression to Count() to filter which items in the sequence should be counted, which you cannot do with either the Count or Length properties.

In Program.cs , call the AggregateProducts method.

Run the code and view the result, as shown in the following output:

Products DbSet does not have a Count property.

Product count from list:

77

Product count:

77

Discontinued product count:

8

Highest product price:

$263.50

Sum of units in stock:

3,119

Sum of units on order:

780

Average unit price:

$28.87

Value of units in stock: $74,050.85

Checking for an empty sequence‌

There are multiple ways to check if a sequence is empty or it contains any items:

image

Call the LINQ Count() method and see if it is greater than zero. This is sometimes the worst if it must enumerate the whole sequence to count the items. You will see more about this in the next section. But as we saw when we used ILSpy to decompile the Count method implementation, it is smart enough to check if the sequence implements ICollection or ICollection and therefore has a more efficient Count property.

image

Call the LINQ Any() method and see if it returns true . This is better than Count() but not as good as either of the next two options.

image

image

Get the sequence's Count property (if it has one) and see if it is greater than zero. Any sequence that implements ICollection or ICollection will have a Count property. Get the sequence's Length property (if it has one) and see if it is greater than zero. Any array will have a Length property.

Be careful with Count!‌

Amichai Mantinband is a Software Engineer at Microsoft, and he does a great job of highlighting interesting parts of the C# and .NET developer stack.Recently, he posted a code teaser on Twitter, LinkedIn, and YouTube, with a poll to find out what developers thought the code would do. Here's the code:

IEnumerable tasks = Enumerable.Range(0, 2)

.Select(_ => Task.Run(() => Console.WriteLine("*"))); await Task.WhenAll(tasks); Console.WriteLine($"{tasks.Count()} stars!");

What will the output be?

  1. **2 stars!

  2. 2 stars!

  3. ****2 stars!

  4. Something else

Most got it wrong, as shown in Figure 11.6:

image

Figure 11.6: A tricky LINQ and Task code teaser from Amichai Mantinband

By this point in this chapter, I would hope that you understand the LINQ parts of this tricky question. Don't worry! I would not expect you to understand the subtleties of multi- threading with tasks. It is still worth breaking down the code to make sure you understand the LINQ parts, as shown in Table 11.5:

Code Description

Enumerable.Range(0, 2) Returns a sequence of two integers, 0 and 1

. Personally, I would have added named parameters to make this clearer, as shown in the following code:

Enumerable.Range(start: 0, count: 2) .

Select( => Task.Run(...) Creates a task with its own thread for each of the two numbers. The parameter discards the number value. Each task outputs a star * to the console.

await Task.WhenAll(tasks); Blocks the main thread until both of the two tasks have completed. So, at this point, we know that two stars ** have been output to the console.

tasks.Count() For the LINQ Count() extension method to work in this scenario, it must enumerate the sequence . This triggers the two tasks to execute again! But we do not know when those two tasks will execute. The value 2 is returned from the method call.

2 stars! is output to the console.

Console.WriteLine(

$"... stars!");

Table 11.5: Code teaser steps explanation

So, we know that ** is output to the console first, then one or both tasks might output their star, then 2 stars! is output, and finally one or both tasks might output their star

if they did not have time to do so before, or the main thread might end, terminating the console app before either task can output their star:

*[each task could output here]2 stars![each task could output * here]

So, the best answer to Amichai's teaser is "Something else."

Good practice: Be careful when calling LINQ extension methods like Count that need to enumerate over the sequence to calculate their return value. Even if you are not working with a sequence of executable objects like tasks, re-enumerating the sequence is likely to be inefficient.

Paging with LINQ‌

Let's see how we could implement paging using the Skip and Take extension methods:

  1. In Program.Functions.cs , add a method to output to the console a table of products passed as an array, as shown in the following code:

private static void OutputTableOfProducts(Product[] products, int currentPage, int totalPages)

{

string line = new('-', count: 73); string lineHalf = new('-', count: 30); WriteLine(line);

WriteLine("{0,4} {1,-40} {2,12} {3,-15}",

"ID", "Product Name", "Unit Price", "Discontinued"); WriteLine(line);

foreach (Product p in products)

{

WriteLine("{0,4} {1,-40} {2,12:C} {3,-15}",

p.ProductId, p.ProductName, p.UnitPrice, p.Discontinued);

}

WriteLine("{0} Page {1} of {2} {3}",

lineHalf, currentPage + 1, totalPages + 1, lineHalf);

}

As usual in computing, our code will start counting from zero, so we need to add one to both the currentPage count and totalPages count before showing these values in a user interface.

  1. In Program.Functions.cs , add a method to create a LINQ query that creates a page of products, outputs the SQL generated from it, and then passes the results as an array of products to the method that outputs a table of products, as shown in the following code:

private static void OutputPageOfProducts(IQueryable products, int pageSize, int currentPage, int totalPages)

{

// We must order data before skipping and taking to ensure

// the data is not randomly sorted in each page.

var pagingQuery = products.OrderBy(p => p.ProductId)

.Skip(currentPage * pageSize).Take(pageSize); Clear(); // Clear the console/screen.

SectionTitle(pagingQuery.ToQueryString()); OutputTableOfProducts(pagingQuery.ToArray(),

currentPage, totalPages);

}

Why order when paging? The EF Core team gave a good example at the following link: https://devblogs.microsoft.com/dotnet/announcing-ef7-preview7-entity-framework/#linq- expression-tree-interception. I also cover ordering when paging in the companion book Apps and Services with .NET 8.

  1. In Program.Functions.cs , add a method to loop while the user presses either the left or right arrows to page through the products in the database, showing one page at a time, as shown in the following code:

private static void PagingProducts()

{

SectionTitle("Paging products"); using NorthwindDb db = new(); int pageSize = 10;

int currentPage = 0;

int productCount = db.Products.Count(); int totalPages = productCount / pageSize;

while (true) // Use break to escape this infinite loop.

{

OutputPageOfProducts(db.Products, pageSize, currentPage, totalPages); Write("Press <- to page back, press -> to page forward, any key to exit."); ConsoleKey key = ReadKey().Key;

if (key == ConsoleKey.LeftArrow)

currentPage = currentPage == 0 ? totalPages : currentPage - 1; else if (key == ConsoleKey.RightArrow)

currentPage = currentPage == totalPages ? 0 : currentPage + 1; else

break; // Break out of the while loop.

WriteLine();

}

}

In Program.cs , comment out any other methods and then call the PagingProducts method.

ID

Product Name

Unit Price

Discontinued

1

Chai

£18.00

False

2

Chang

£19.00

False

3

Aniseed Syrup

£10.00

False

4

Chef Anton's Cajun Seasoning

£22.00

False

5

Chef Anton's Gumbo Mix

£21.35

True

6

Grandma's Boysenberry Spread

£25.00

False

7

Uncle Bob's Organic Dried Pears

£30.00

False

8

Northwoods Cranberry Sauce

£40.00

False

9

Mishi Kobe Niku

£97.00

True

10

Ikura

£31.00

False

Run the code and view the result, as shown in the following output:

image

image

Page 1 of 8

Press <- to page back, press -> to page forward.

The preceding output excludes the SQL statement used to efficiently get the page of products by using ORDER BY , LIMIT , and OFFSET , as shown in the following code:

.param set @ p_1 10

.param set @ p_0 0

SELECT "p"."ProductId", "p"."CategoryId", "p"."Discontinued", "p"."ProductName", "p"."QuantityPe FROM "Products" AS "p"

ORDER BY "p"."ProductId" LIMIT @ p_1 OFFSET @ p_0

Make sure the command prompt or terminal window has the focus when you press keys.

Press the right arrow and note the second page of results, as shown in the following output:

image

ID Product Name Unit Price Discontinued

image

Queso Cabrales £21.00 False

Queso Manchego La Pastora £38.00 False

Konbu £6.00 False

Tofu £23.25 False

Genen Shouyu £15.50 False

Pavlova £17.45 False

Alice Mutton £39.00 True

Carnarvon Tigers £62.50 False

Teatime Chocolate Biscuits £9.20 False

image

image

Sir Rodney's Marmalade £81.00 False Page 2 of 8

Press <- to page back, press -> to page forward.

  1. Press the left arrow twice and note it loops around to the last page of results, as shown in the following output:

image

ID Product Name Unit Price Discontinued

image

Flotemysost £21.50 False

Mozzarella di Giovanni £34.80 False

Röd Kaviar £15.00 False

Longlife Tofu £10.00 False

Rhönbräu Klosterbier £7.75 False

Lakkalikööri £18.00 False

Original Frankfurter grüne Soße £13.00 False

image

image

Page 8 of 8

Press <- to page back, press -> to page forward.

Press any other key to end the loop.

Optionally, in Program.Functions.cs , in the OutputPageOfProducts method, comment out the statement to output the SQL used, as shown highlighted in the following code:

// SectionTitle(pagingQuery.ToQueryString());

As an optional task, explore how you might use the Chunk method to output pages of products. You can read more about partitioning items in a sequence using Skip , Take , and Chunk at the following link: https://learn.microsoft.com/en- us/dotnet/csharp/programming-guide/concepts/linq/partitioning-data.

Good practice: You should always order data before calling Skip and Take if you want to implement paging. This is because each time you execute a query, the LINQ provider does not have to guarantee to return the data in the same order unless you have specified it. Therefore, if the SQLite provider wanted to, the first time you request a page of products, they might be in ProductId order, but the next time you request a page of products, they might be in UnitPrice order, or a random order, and that would confuse the users! In practice, at least for relational databases, the default order is usually by its index on the primary key.

Sweetening LINQ syntax with syntactic sugar‌

C# 3 introduced some new language keywords in 2008 to make it easier for programmers with experience with SQL to write LINQ queries. This syntactic sugar is sometimes called the LINQ query comprehension syntax.Consider the following array of string values:

string[] names = new[] { "Michael", "Pam", "Jim", "Dwight",

"Angela", "Kevin", "Toby", "Creed" };

To filter and sort the names, you could use extension methods and lambda expressions, as shown in the following code:

var query = names

.Where(name => name.Length > 4)

.OrderBy(name => name.Length)

.ThenBy(name => name);

Or you could achieve the same results by using query comprehension syntax, as shown in the following code:

var query = from name in names where name.Length > 4 orderby name.Length, name select name;

The compiler changes the query comprehension syntax to the equivalent extension methods and lambda expressions for you.

The select keyword is always required for LINQ query comprehension syntax. The Select extension method is optional when using extension methods and lambda expressions because if you do not call Select , then the whole item is implicitly selected.

Not all extension methods have a C# keyword equivalent, for example, the Skip and Take extension methods, which are commonly used to implement paging for lots of data.A query that skips and takes cannot be written using only the query comprehension syntax, so we could write the query using all extension methods, as shown in the following code:

var query = names

.Where(name => name.Length > 4)

.Skip(80)

.Take(10);

Or, we could wrap query comprehension syntax in parentheses and then switch to using extension methods, as shown in the following code:

var query = (from name in names where name.Length > 4

select name)

.Skip(80)

.Take(10);

Good practice: Learn both extension methods with lambda expressions and the query comprehension syntax ways of writing LINQ queries, because you are likely to have to maintain code that uses both.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring with deeper research into the topics covered in this chapter.

Exercise 11.1 – Test your knowledge‌

Answer the following questions:

What are the two required parts of LINQ?

Which LINQ extension method would you use to return a subset of properties from a type?

Which LINQ extension method would you use to filter a sequence?

List five LINQ extension methods that perform aggregation.

What is the difference between the Select and SelectMany extension methods?

What is the difference between IEnumerable and IQueryable ? How do you switch between them?

What does the last type parameter T in generic Func delegates like Func<T1, T2, T>

represent?

What is the benefit of a LINQ extension method that ends with OrDefault ?

Why is query comprehension syntax optional?

How can you create your own LINQ extension methods?

Exercise 11.2 – Practice querying with LINQ‌

In the Chapter11 solution, create a console application, named Ch11Ex02LinqQueries , that prompts the user for a city and then lists the company names for Northwind customers in that city, as shown in the following output:

Enter the name of a city: London There are 6 customers in London:

Around the Horn B's Beverages

Consolidated Holdings Eastern Connection North/South

Seven Seas Imports

Then, enhance the application by displaying a list of all unique cities that customers already reside in as a prompt to the user before they enter their preferred city, as shown in the following output:

Aachen, Albuquerque, Anchorage, Århus, Barcelona, Barquisimeto, Bergamo, Berlin, Bern, Boise, Br

Exercise 11.3 – Using multiple threads with parallel LINQ‌

You can improve performance and scalability by using multiple threads to run LINQ queries. Learn how by completing the online-only section found at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch11-plinq.md

Exercise 11.4 – Working with LINQ to XML‌

If you want to process or generate XML using LINQ, then you can learn the basics of how by completing the online-only section found at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch11-linq-to-xml.md

Exercise 11.5 – Creating your own LINQ extension methods‌

If you want to create your own LINQ extension methods, then you can learn the basics of how by completing the online-only section found at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch11-custom-linq-methods.md

Exercise 11.6 – Explore topics‌

image

Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-11--- querying-and-manipulating-data-using-linq

Summary‌

In this chapter, you learned how to write LINQ queries to perform common tasks like:

image

image

Selecting just the properties of an item that you need. Filtering items based on conditions.

image

Sorting items.

image

image

Projecting items into different types. Joining and grouping items.

image

Aggregating items.

In the next chapter, you will be introduced to web development using ASP.NET Core. In the remaining chapters, you will learn how to implement major components of ASP.NET Core like Razor Pages, MVC, Web API, and Blazor.

12 Introducing Web Development Using ASP.NET Core‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

The third and final part of this book is about web development using ASP.NET Core. You will learn how to build cross-platform projects such as websites, web services, and web browser apps.Microsoft calls platforms for building applications app models or workloads. I recommend that you work through this and subsequent chapters sequentially because later chapters will reference projects in earlier chapters, and you will build up sufficient knowledge and skills to tackle the trickier problems in later chapters.In this chapter, we will cover the following topics:

image

image

image

Understanding ASP.NET Core New features in ASP.NET Core Structuring projects

image

image

Building an entity model for use in the rest of the book Understanding web development

Understanding ASP.NET Core‌

Since this book is about C# and .NET, we will learn about app models that use them to build the practical applications that we will encounter in the remaining chapters of this book.

More Information: Microsoft has extensive guidance for implementing app models in its

.NET Architecture Guides documentation, which you can read at the following link: https://dotnet.microsoft.com/en-us/learn/dotnet/architecture-guides.

Microsoft ASP.NET Core is part of a history of Microsoft technologies used to build websites and services that have evolved over the years:

image

Active Server Pages (ASP) was released in 1996 and was Microsoft's first attempt at a platform for dynamic server-side execution of website code. ASP files contain a mix of HTML and code that execute on the server written in the VBScript language.

image

ASP.NET Web Forms was released in 2002 with the .NET Framework and was designed to enable non-web developers, such as those familiar with Visual Basic, to quickly create websites by dragging and dropping visual components and writing event-driven code in Visual Basic or C#. Web Forms should be avoided for new .NET Framework web projects in favor of ASP.NET MVC.

image

Windows Communication Foundation (WCF) was released in 2006 and enables developers to build SOAP and REST services. SOAP is powerful but complex, so it should be avoided unless you need advanced features, such as distributed transactions and complex messaging topologies.

image

ASP.NET MVC was released in 2009 to cleanly separate the concerns of web developers between the models, which temporarily store the data; the views, which present the data

using various formats in the UI; and the controllers, which fetch the model and pass it to a view. This separation enables improved reuse and unit testing.

image

ASP.NET Web API was released in 2012 and enables developers to create HTTP services (aka REST services) that are simpler and more scalable than SOAP services.

image

ASP.NET SignalR was released in 2013 and enables real-time communication for websites by abstracting underlying technologies and techniques, such as WebSockets and long polling. This enables website features such as live chat or updates to time-sensitive data such as stock prices across a wide variety of web browsers, even when they do not support an underlying technology such as WebSockets.

image

ASP.NET Core was released in 2016 and combines modern implementations of .NET Framework technologies such as MVC, Web API, and SignalR, with newer technologies such as Razor Pages, gRPC, and Blazor, all running on modern .NET. Therefore, it can execute cross- platform. ASP.NET Core has many project templates to get you started with its supported technologies.

Good Practice: Choose ASP.NET Core to develop websites and web services because it includes web-related technologies that are modern and cross-platform.

Classic ASP.NET versus modern ASP.NET Core‌

Until modern .NET, ASP.NET was built on top of a large assembly in .NET Framework named System.Web.dll and it was tightly coupled to Microsoft's Windows-only web server named Internet Information Services (IIS). Over the years, this assembly has accumulated a lot of features, many of which are not suitable for modern cross-platform development.ASP.NET Core is a major redesign of ASP.NET. It removes the dependency on the System.Web.dll assembly and IIS and is composed of modular lightweight packages, just like the rest of modern .NET. Using IIS as the web server is still supported by ASP.NET Core, but there is a better option.You can develop and run ASP.NET Core applications cross-platform on Windows, macOS, and Linux. Microsoft has even created a cross-platform, super-performant web server named Kestrel, and the entire stack is open source.ASP.NET Core 2.2 or later projects default to the new in-process hosting model. This gives a 400% performance improvement when hosting in Microsoft IIS, but Microsoft still recommends using Kestrel for even better performance.

Building websites using ASP.NET Core‌

Websites are made up of multiple web pages loaded statically from the filesystem or generated dynamically by a server-side technology such as ASP.NET Core. A web browser makes GET requests using Unique Resource Locators (URLs) that identify each page and can manipulate data stored on the server using POST , PUT , and DELETE requests.With many websites, the web browser is treated as a presentation layer, with almost all the processing performed on the server side. Some JavaScript might be used on the client side to implement form validation warnings and some presentation features, such as carousels.ASP.NET Core provides multiple technologies for building the user interface for websites:

image

ASP.NET Core Razor Pages is a simple way to dynamically generate HTML for simple websites. You will learn about them in detail in Chapter 13, Building Websites Using ASP.NET Core Razor Pages.

image

ASP.NET Core MVC is an implementation of the Model-View-Controller (MVC) design pattern that is popular for developing complex websites. You will learn about it in detail in Chapter 14, Building Websites Using the Model-View-Controller Pattern.

image

Blazor lets you build user interface components using C# and .NET instead of a JavaScript-based UI framework like Angular, React, and Vue. Early versions of Blazor required a developer to choose a hosting model. The Blazor WebAssembly hosting model runs your code in the browser like a JavaScript-based framework would. The Blazor Server hosting model runs your code on the server and updates the web page dynamically.

Introduced with .NET 8 is a unified, full-stack hosting model that allows individual components to execute either on the server- or client-side, or even adapt dynamically at runtime. You will learn about Blazor in detail in Chapter 16, Building User Interfaces Using Blazor.

Comparison of file types used in ASP.NET Core‌

It is useful to summarize the file types used by these technologies because they are similar but different. If the reader does not understand some subtle but important differences, it can cause much confusion when trying to implement their own projects. Please note the differences in Table 12.1:

Technology Special filename File extension Directive Razor Component (Blazor) .razor

Razor Component (Blazor with page routing) .razor @page

Razor Page .cshtml @page

Razor View (MVC) .cshtml

Razor Layout .cshtml

Razor View Start _ViewStart .cshtml

Razor View Imports _ViewImports .cshtml

Table 12.1: Comparison of file types used in ASP.NET Core

Directives like @page are added to the top of a file's contents.If a file does not have a special filename, then it can be named anything. For example, you might create a Razor Component for use in a Blazor project named Customer.razor , or you might create a Razor Layout for use in an MVC or Razor Pages project named _MobileLayout.cshtml .

The naming convention for shared Razor files like layouts and partial views is to prefix with an underscore _ . For example, _ViewStart.cshtml , _Layout.cshtml , or

_Product.cshtml (this might be a partial view for rendering a product).

A Razor Layout file like _MyCustomLayout.cshtml is identical to a Razor View. What makes the file a layout is being set as the Layout property of another Razor file, as shown in the following code:

@{

Layout = "_MyCustomLayout"; // File extension is not needed.

}

Warning! Be careful to use the correct file extension and directive at the top of the file or you will get unexpected behavior.

Building websites using a content management system‌

Most websites have a lot of content, and if developers had to be involved every time some content needed to be changed, that would not scale well. A Content Management System (CMS) enables developers to define content structure and templates to provide consistency and good design while making it easy for a non-technical content owner to manage the actual content. They can create new pages or blocks of content, and update existing content, knowing it will look great for visitors with minimal effort.There are a multitude of CMSs available for all web platforms, like WordPress for PHP or Django CMS for Python. CMSs that support modern

.NET include Optimizely Content Cloud, Umbraco, Piranha CMS, and Orchard Core.The key benefit of using a CMS is that it provides a friendly content management user interface. Content owners log in to the website and manage the content themselves. The content is then rendered and returned to visitors using ASP.NET Core MVC controllers and views, or via web

service endpoints, known as a headless CMS, to provide that content to "heads" implemented as mobile or desktop apps, in-store touchpoints, or clients built with JavaScript frameworks or Blazor.This book does not cover .NET CMSs, so I have included links where you can learn more about them in the GitHub repository: https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#net-content- management-systems.

Building web applications using SPA frameworks‌

Web applications are often built using technologies known as Single-Page Application (SPA) frameworks, such as Blazor, Angular, React, Vue, or a proprietary JavaScript library. They can make requests to a backend web service to get more data when needed and post updated data using common serialization formats such as XML and JSON. The canonical examples are Google web apps like Gmail, Maps, and Docs.With a web application, the client side uses JavaScript frameworks or Blazor to implement sophisticated user interactions, but most of the important processing and data access still happens on the server side, because the web browser has limited access to local system resources.JavaScript is loosely typed and is not designed for complex projects, so most JavaScript libraries these days use TypeScript, which adds strong typing to JavaScript and is designed with many modern language features for handling complex implementations..NET SDK has project templates for JavaScript and TypeScript-based SPAs, but we will not spend any time learning how to build JavaScript and TypeScript-based SPAs in this book. Even though these SPAs are commonly used with ASP.NET Core as the backend, the focus of this book is on C# and not on other languages.In summary, C# and .NET can be used on both the server side and the client side to build websites, as shown in Figure 12.1:

image

Figure 12.1: The use of C# and .NET to build websites on both the server and client side

Building web and other services‌

Although we will not learn about JavaScript and TypeScript-based SPAs, we will learn how to build a web service using the ASP.NET Core Web API, and then call that web service from the server-side code in our ASP.NET Core websites. Later, we will call that web service from Blazor components and cross-platform mobile and desktop apps.There are no formal definitions, but services are sometimes described based on their complexity:

image

image

image

Service: All functionality needed by a client app in one monolithic service. Microservice: Multiple services that each focus on a smaller set of functionalities. Nanoservice: A single function provided as a service. Unlike services and microservices that are hosted 24/7/365, nanoservices are often inactive until called upon to reduce

resources and costs.

At the start of the first part of this book, we briefly reviewed C# language features and in which versions they were introduced. At the start of the second part of this book, we briefly reviewed .NET library features and in which versions they were introduced. Now, in the third and final part of this book, we will briefly review ASP.NET Core features and in which versions they were introduced.

New features in ASP.NET Core‌

Over the past few years, Microsoft has rapidly expanded the capabilities of ASP.NET Core. You should note which .NET platforms are supported, as shown in the following list:

image

image

ASP.NET Core 1.0 to 2.2 runs on either .NET Core or .NET Framework. ASP.NET Core 3.0 or later only runs on .NET Core 3.0 or later.

ASP.NET Core 1.0, June 2016‌

Implemented a minimum API suitable for building modern cross-platform web apps and services for Windows, macOS, and Linux.

ASP.NET Core 1.1, November 2016‌

Focused on bug fixes and general improvements to features and performance.

ASP.NET Core 2.0, August 2017‌

Added new features such as Razor Pages, bunded assemblies into a Microsoft.AspNetCore.All metapackage, targeted .NET Standard 2.0, provided a new authentication model, and added many performance improvements. The biggest new features introduced with ASP.NET Core 2.0 are ASP.NET Core Razor Pages, which is covered in Chapter 13, Building Websites Using ASP.NET Core Razor Pages, and ASP.NET Core OData support. OData is covered in an online-only chapter of the companion book, Apps and Services with .NET 8, and is available at the following link: https://github.com/markjprice/apps-services-net8/blob/main/docs/ch08-odata.md.

ASP.NET Core 2.1, May 2018‌

Added new features such as SignalR for real-time communication, Razor class libraries for reusing web components, ASP.NET Core Identity for authentication, and better support for HTTPS and the European Union's General Data Protection Regulation (GDPR), including the topics listed in Table 12.2:

Feature Chapter Topic

Razor class libraries 13 Using Razor class libraries

GDPR support 14 Creating and exploring an ASP.NET Core MVC website

Identity UI library and scaffolding

14 Exploring an ASP.NET Core MVC website

Integration tests 14 Testing an ASP.NET Core MVC website [ApiController] , ActionResult 15 Creating an ASP.NET Core Web API project Problem details 15 Implementing a Web API controller

IHttpClientFactory 15 Configuring HTTP clients using HttpClientFactory

Table 12.2: Features of ASP.NET Core 2.1 covered in this book

ASP.NET Core 2.2, December 2018‌

Improved the building of RESTful HTTP APIs, updated the project templates to Bootstrap 4 and Angular 6, an optimized configuration for hosting in Azure, including the topics listed in Table 12.3:

Feature Chapter Topic

HTTP/2 in Kestrel 13 Classic ASP.NET versus modern ASP.NET Core In-process hosting model 13 Creating an ASP.NET Core project

Endpoint routing 13 Understanding endpoint routing Health Checks Middleware 15 Implementing health checks

Open API analyzers 15 Implementing Open API analyzers and conventions Table 12.3: Features of ASP.NET Core 2.2 covered in this book

ASP.NET Core 3.0, September 2019‌

Focused on fully leveraging .NET Core 3.0 and .NET Standard 2.1, which meant it could not support .NET Framework, and it added useful refinements, including the topics listed in Table 12.4:

Feature Chapter Topic

Static assets in Razor class libraries 13 Using Razor class libraries

New options for MVC service registration 14 Understanding ASP.NET Core MVC startup Blazor Server 16 Building components using Blazor Server Table 12.4: Features of ASP.NET Core 3.0 covered in this book

ASP.NET Core 3.1, December 2019‌

Added refinements like partial class support for Razor components and a new tag helper.

Blazor WebAssembly 3.2, May 2020‌

It was a Current (now known as STS) release, meaning that projects had to be upgraded to the

.NET 5 version within three months of the .NET 5 release. Microsoft finally delivered on the promise of full-stack web development with .NET.

ASP.NET Core 5, November 2020‌

Focused on bug fixes, performance improvements, using caching for certificate authentication, HPACK dynamic compression of HTTP/2 response headers in Kestrel, nullable annotations for ASP.NET Core assemblies, and a reduction in container image sizes, including the topics listed in Table 12.5:

Feature Chapter Topic

Extension method to allow anonymous access to an 15 Securing web services endpoint

JSON extension methods for HttpRequest and 15 Getting customers as JSON in the

HttpResponse controller

Table 12.5: Features of ASP.NET Core 5 covered in this book

ASP.NET Core 6, November 2021‌

Added productivity improvements like minimizing code to implement basic websites and services, support for .NET Hot Reload, and new hosting options for Blazor, like hybrid apps using .NET MAUI, including the topics listed in Table 12.6:

Feature Chapter Topic

New empty web project template 13 Understanding the empty web template Minimal APIs 15 Implementing minimal Web APIs

Blazor WebAssembly AOT 16 Enabling Blazor WebAssembly ahead-of-time compilation Table 12.6: Features of ASP.NET Core 6 covered in this book

ASP.NET Core 7, November 2022‌

Filled well-known gaps in functionality like HTTP/3 support, output caching, and many quality-of-life improvements to Blazor, including the topics listed in Table 12.7:

Feature Chapter Topic

HTTP request decompression

13 Enabling request decompression support

HTTP/3 support 13 Enabling HTTP/3 support

Output caching 14 Using a filter to cache output

W3C log additional headers

Support for logging additional request headers in W3CLogger

HTTP/3 client support 15 Enabling HTTP/3 support for HttpClient Blazor Empty templates 16 Comparing Blazor project templates Location change support 16 Enabling location change event handling Table 12.7: Features of ASP.NET Core 7 covered in this book

ASP.NET Core 8, November 2023‌

Focused on unifying the Blazor hosting models, including the topics listed in Table 12.8: Feature Chapter Topic

Route short-circuiting 15 Short-circuit routes in ASP.NET Core 8 Route tooling enhancements 15 Improved route tooling in ASP.NET Core 8

Unification of Blazor hosting

All of Chapter 16 , Building User Interfaces Using Blazor .

Table 12.8: Features of ASP.NET Core 8 covered in this book

Structuring projects‌

How should you structure your projects? So far, we have mostly built small individual console apps to illustrate language or library features, with the occasional class library and unit test project to support them. In the rest of this book, we will build multiple projects using different technologies that work together to provide a single solution.With large, complex solutions, it can be difficult to navigate through all the code. So, the

primary reason to structure your projects is to make it easier to find components. It is good to have an overall name for your solution that reflects the application or solution.We will build multiple projects for a fictional company named Northwind. We will name the solution PracticalApps and use the name Northwind as a prefix for all the project names.There are many ways to structure and name projects and solutions, for example, using a folder hierarchy as well as a naming convention. If you work in a team, make sure you know how your team does it.

Structuring projects in a solution‌

It is good to have a naming convention for your projects in a solution so that any developer can tell what each one does instantly. A common choice is to use the type of project, for example, class library, console app, website, and so on.Since you might want to run multiple web projects at the same time, and they will be hosted on a local web server, we need to differentiate each project by assigning different port numbers for their endpoints for both HTTP and HTTPS. Commonly assigned local port numbers are 5000 for HTTP and 5001 for HTTPS. We will use a numbering convention of 50 for HTTP and 51 for HTTPS. For example, for an MVC website project we will create in Chapter 14, we will assign 5140 for HTTP and 5141 for HTTPS.We will therefore use the following project names and port numbers, as shown in Table 12.9:

Name

Ports

Description

Northwind.Common

n/a

A class library project for

common types like interfaces,

enums, classes, records, and

structs, used across multiple

projects.

Northwind.EntityModels

n/a

A class library project for

common EF Core entity models.

Entity models are often used

on both the server and client

side, so it is best to

separate dependencies on

specific database providers.

Northwind.DataContext

n/a

A class library project for

the EF Core database context

with dependencies on specific

database providers.

Northwind.UnitTests

n/a

An xUnit test project for the

solution.

Northwind.Web

http

5130 ,

An ASP.NET Core project for a simple website that uses a

mixture of static HTML files

Northwind.Mvc

Northwind.WebApi

https 5131

http 5140 ,

https 5141

http 5150 ,

https 5151

and dynamic Razor Pages.

An ASP.NET Core project for a complex website that uses the MVC pattern and can be more easily unit tested.

An ASP.NET Core project for a Web API aka HTTP service. A good choice for integrating with websites because it can use any JavaScript library or

Blazor to interact with the service.

Northwind.MinimalApi http 5152 An ASP.NET Core project for a Minimal API aka HTTP service. Unlike Web API projects, these can be compiled using native AOT for improved startup time and reduced memory footprint.

Northwind.Blazor

http 5160 ,

https 5161

An ASP.NET Core Blazor project.

Table 12.9: Example project names for various project types

Building an entity model for use in the rest of the book‌

Practical applications usually need to work with data in a relational database or another data store. In this section, we will define an entity data model for the Northwind database stored in SQL Server or SQLite. It will be used in most of the apps that we create in subsequent chapters.

Creating the Northwind database‌

The script files for creating the Northwind database for SQLite and SQL Server are different. The script for SQL Server creates 13 tables as well as related views and stored procedures. The script for SQLite is a simplified version that only creates 10 tables because SQLite does not support as many features. The main projects in this book only need those 10 tables so you can complete every task in this book with either database.The SQL scripts are found at the following link: https://github.com/markjprice/cs12dotnet8/tree/main/scripts/sql-scripts.There are multiple SQL scripts to choose from, as described in the following list:

image

Northwind4Sqlite.sql script: To use SQLite on a local Windows, macOS, or Linux computer. This script could probably also be used for other SQL systems like PostgreSQL or MySQL but has not been tested for use with those!

image

image

Northwind4SqlServer.sql script: To use SQL Server on a local Windows computer. The script checks if the Northwind database already exists and drops it before creating it. Northwind4AzureSqlDatabaseCloud.sql script: To use SQL Server with an Azure SQL Database resource created in the Azure cloud. These resources cost money as long as they exist! The script does not drop or create the Northwind database because you should manually create the Northwind database using the Azure portal user interface.

image

Northwind4AzureSqlEdgeDocker.sql script: To use SQL Server on a local computer in Docker. The script creates the Northwind database. It does not drop it if it already exists because the Docker container should be empty anyway as a fresh one will be spun up each time.

Instructions to install SQLite can be found in Chapter 10, Working with Data Using Entity Framework Core. In that chapter, you will also find instructions for installing the dotnet-ef tool, which you will use to scaffold an entity model from an existing

database.Instructions to install SQL Server Developer Edition (free) on your local Windows computer can be found in the GitHub repository for this book at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/sql-server/README.md.Instructions to setup Azure SQL Edge in Docker for Windows, macOS, or Linux can be found in the GitHub

repository for the companion book at the following link: https://github.com/markjprice/apps- services-net8/blob/main/docs/ch02-sql-edge.md.

Creating a class library for entity models using SQLite‌

You will now define entity data models in a class library so that they can be reused in other types of projects including client-side app models.

Good Practice: You should create a separate class library project for your entity data models. This allows easier sharing between backend web servers and frontend desktop, mobile, and Blazor clients.

We will automatically generate some entity models using the EF Core command-line tool:

Use your preferred code editor to create a new project and solution, as defined in the following list:

image

Project template: Class Library / classlib

image

Project file and folder: Northwind.EntityModels.Sqlite

image

Solution file and folder: PracticalApps

In the Northwind.EntityModels.Sqlite project, add package references for the SQLite database provider and EF Core design-time support, as shown in the following markup:

all

runtime; build; native; contentfiles; analyzers; buildtransitive

Delete the Class1.cs file.

Build the Northwind.EntityModels.Sqlite project to restore packages.

Copy the Northwind4Sqlite.sql file into the PracticalApps solution folder (not the project folder!).

At a command prompt or terminal in the PracticalApps folder, enter a command to create the Northwind.db file for SQLite, as shown in the following command:

sqlite3 Northwind.db -init Northwind4SQLite.sql

Be patient because this command might take a while to create the database structure.

To exit SQLite command mode, press Ctrl + C twice on Windows or Cmd + D on macOS or Linux.

At a command prompt or terminal in the Northwind.EntityModels.Sqlite project folder (the folder that contains the .csproj project file), generate entity class models for all tables, as shown in the following command:

dotnet ef dbcontext scaffold "Data Source=../Northwind.db" Microsoft.EntityFrameworkCore.Sqlite

Note the following:

image

The command to perform: dbcontext scaffold

image

The connection string refers to the database file in the solution folder, which is one folder up from the current project folder: "Data Source=../Northwind.db"

image

The database provider: Microsoft.EntityFrameworkCore.Sqlite

image

The namespace: --namespace Northwind.EntityModels

image

To use data annotations as well as the Fluent API: --data-annotations

Warning! dotnet-ef commands must be entered all on one line and in a folder that contains a project, or you will see the following error:

No project was found. Change the current working directory or use the --project option. Remember that all command lines can be found at and copied from the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/command-lines.md.

If you use SQLite, then you will see warnings about incompatible type mappings between the table columns and properties in the entity class models. For example,

The column 'BirthDate' on table 'Employees' should map to a property of type 'DateOnly', but

. This is due to SQLite using dynamic types. We will fix those issues in the next section.

Creating a class library for a database context using SQLite‌

You will now define a database context class library:

image

Add a new project to the solution, as defined in the following list: Project template: Class Library / classlib

image

Project file and folder: Northwind.DataContext.Sqlite

image

Solution file and folder: PracticalApps

In the Northwind.DataContext.Sqlite project, statically and globally import the Console class, add a package reference to the EF Core data provider for SQLite, and add a project reference to the Northwind.EntityModels.Sqlite project, as shown in the following markup:



Warning! The path to the project reference should not have a line break in your project file.

In the Northwind.DataContext.Sqlite project, delete the Class1.cs file.

Build the Northwind.DataContext.Sqlite project to restore packages.

In the Northwind.DataContext.Sqlite project, add a class named

NorthwindContextLogger.cs .

Modify its contents to define a static method named WriteLine that appends a string to the end of a text file named northwindlog.txt on the desktop, as shown in the following code:

using static System.Environment; namespace Northwind.EntityModels; public class NorthwindContextLogger

{

public static void WriteLine(string message)

{

string path = Path.Combine(GetFolderPath( SpecialFolder.DesktopDirectory), "northwindlog.txt");

StreamWriter textFile = File.AppendText(path); textFile.WriteLine(message);

textFile.Close();

}

}

Move the NorthwindContext.cs file from the Northwind.EntityModels.Sqlite project/folder to the Northwind.DataContext.Sqlite project/folder.

In Visual Studio 2022 Solution Explorer, if you drag and drop a file between projects, it will be copied. If you hold down Shift while dragging and dropping, it will be moved. In Visual Studio Code EXPLORER, if you drag and drop a file between projects, it will be moved. If you hold down Ctrl while dragging and dropping, it will be copied.

In NorthwindContext.cs , note the second constructor can have options passed as a parameter, which allows us to override the default database connection string in any projects such as websites that need to work with the Northwind database, as shown in the following code:

public NorthwindContext(DbContextOptions options)

: base(options)

{

}

  1. In NorthwindContext.cs , in the OnConfiguring method, remove the compiler #warning about the connection string and then add statements to check the end of the current directory to adjust for when running in Visual Studio 2022 compared to the command prompt with Visual Studio Code, as shown in the following code:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

{

if (!optionsBuilder.IsConfigured)

{

string database = "Northwind.db";

string dir = Environment.CurrentDirectory; string path = string.Empty;

if (dir.EndsWith("net8.0"))

{

// In the \bin\<Debug|Release>\net8.0 directory. path = Path.Combine("..", "..", "..", "..", database);

}

else

{

// In the directory.

path = Path.Combine("..", database);

}

path = Path.GetFullPath(path); // Convert to absolute path. NorthwindContextLogger.WriteLine($"Database path: {path}"); if (!File.Exists(path))

{

throw new FileNotFoundException(

message: $"{path} not found.", fileName: path);

}

optionsBuilder.UseSqlite($"Data Source={path}"); optionsBuilder.LogTo(NorthwindContextLogger.WriteLine,

new[] { Microsoft.EntityFrameworkCore

.Diagnostics.RelationalEventId.CommandExecuting });

}

}

The throwing of the exception is important because if the database file is missing, then the SQLite database provider will create an empty database file, and so if you test connecting to it, it works. But if you query it, then you will see an exception related to missing tables because it does not have any tables! After converting the relative path to an absolute path, you can set a breakpoint while debugging to more easily see where the database file is expected to be or add a statement to log that path.

Customizing the model and defining an extension method‌

Now, we will simplify the OnModelCreating method. I will briefly explain the individual steps and then show the complete final method. You can either try to perform the individual steps or just use the final method code:

  1. In the OnModelCreating method, remove all Fluent API statements that call the ValueGeneratedNever method, like the one shown in the following code. This will configure primary key properties like CategoryId to never generate a value automatically or call the HasDefaultValueSql method:

modelBuilder.Entity(entity =>

{

entity.Property(e => e. CategoryId).ValueGeneratedNever();

});

If we do not remove the configuration like the statements above, then when we add new suppliers, the CategoryId value will always be 0 and we will only be able to add one supplier with that value; all other attempts will throw an exception. You can compare your NorthwindContext.cs to the one in the GitHub repository at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/Northwind.DataCon text.Sqlite/NorthwindContext.cs.

  1. In the OnModelCreating method, for the Product entity, tell SQLite that the UnitPrice

can be converted from decimal to double , as shown in the following code:

entity.Property(product => product.UnitPrice)

.HasConversion();

  1. The OnModelCreating method should now be simpler, as shown in the following code:

protected override void OnModelCreating(ModelBuilder modelBuilder)

{

modelBuilder.Entity(entity =>

{

entity.Property(e => e.Freight).HasDefaultValueSql("0");

});

modelBuilder.Entity(entity =>

{

entity.Property(e => e.Quantity).HasDefaultValueSql("1"); entity.Property(e => e.UnitPrice).HasDefaultValueSql("0"); entity.HasOne(d => d.Order).WithMany(p =>

p.OrderDetails).OnDelete(DeleteBehavior.ClientSetNull); entity.HasOne(d => d.Product).WithMany(p =>

p.OrderDetails).OnDelete(DeleteBehavior.ClientSetNull);

});

modelBuilder.Entity(entity =>

{

entity.Property(e => e.Discontinued).HasDefaultValueSql("0"); entity.Property(e => e.ReorderLevel).HasDefaultValueSql("0"); entity.Property(e => e.UnitPrice).HasDefaultValueSql("0"); entity.Property(e => e.UnitsInStock).HasDefaultValueSql("0"); entity.Property(e => e.UnitsOnOrder).HasDefaultValueSql("0"); entity.Property(product => product.UnitPrice)

.HasConversion();

});

OnModelCreatingPartial(modelBuilder);

}

  1. In the Northwind.DataContext.Sqlite project, add a class named NorthwindContextExtensions.cs . Modify its contents to define an extension method that adds the Northwind database context to a collection of dependency services, as shown in the following code:

using Microsoft.EntityFrameworkCore; // To use UseSqlite.

using Microsoft.Extensions.DependencyInjection; // To use IServiceCollection. namespace Northwind.EntityModels;

public static class NorthwindContextExtensions

{

///

/// Adds NorthwindContext to the specified IServiceCollection. Uses the Sqlite database provid

///

/// The service collection.

/// Default is ".."

/// Default is "Northwind.db"

/// An IServiceCollection that can be used to add more services. public static IServiceCollection AddNorthwindContext(

this IServiceCollection services, // The type to extend. string relativePath = "..",

string databaseName = "Northwind.db")

{

string path = Path.Combine(relativePath, databaseName); path = Path.GetFullPath(path); NorthwindContextLogger.WriteLine($"Database path: {path}"); if (!File.Exists(path))

{

throw new FileNotFoundException(

message: $"{path} not found.", fileName: path);

}

services.AddDbContext(options =>

{

// Data Source is the modern equivalent of Filename. Options.UseSqlite($"Data Source={path}"); options.LogTo(NorthwindContextLogger.WriteLine,

new[] { Microsoft.EntityFrameworkCore

.Diagnostics.RelationalEventId.CommandExecuting });

},

// Register with a transient lifetime to avoid concurrency

// issues in Blazor server-side projects. contextLifetime: ServiceLifetime.Transient, optionsLifetime: ServiceLifetime.Transient); return services;

}

}

  1. Build the two class libraries and fix any compiler errors.

Registering the scope of a dependency service‌

By default, a DbContext class is registered using Scope lifetime, meaning that multiple threads can share the same instance. But DbContext does not support multiple threads. If more than one thread attempts to use the same NorthwindContext class instance at the same time, then you will see the following runtime exception thrown:

A second operation started on this context before a previous operation completed. This is usuall

.This happens in Blazor projects with components set to run on the server side because, whenever interactions on the client side happen, a SignalR call is made back to the server where a single instance of the database context is shared between multiple clients. This issue does not occur if a component is set to run on the client side.

Creating class libraries for entity models using SQL Server‌

If you would like to use SQL Server instead of SQLite, then there are instructions at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/sql- server/README.md#chapter-12---introducing-web-development-using-aspnet-core

Improving the class-to-table mapping‌

The dotnet-ef command-line tool generates different code for SQL Server and SQLite because they support different levels of functionality, and SQLite uses dynamic typing. For example,

with EF Core 7, all integer columns in SQLite were mapped to nullable long properties for maximum flexibility.With EF Core 8 and later, the actual stored values are checked and if they are all storable in an int , it will declare the mapped property as an int . If they are all column values storable in a short , it will declare the mapped property as a

short .In this edition, we need to do less work to improve the mapping. Hooray!As another example, SQL Server text columns can have limits on the number of characters. SQLite does not support this. So, dotnet-ef will generate validation attributes to ensure string properties are limited to a specified number of characters for SQL Server but not for SQLite, as shown in the following code:

// SQLite database provider-generated code. [Column(TypeName = "nvarchar (15)")]

public string CategoryName { get; set; } = null!;

// SQL Server database provider-generated code. [StringLength(15)]

public string CategoryName { get; set; } = null!;

We will make some small changes to improve the entity model mapping and validation rules for SQLite. Similar ones for SQL Server are in the online-only instructions.

Remember that all code is available in the GitHub repository for the book. Although you will learn more by typing the code yourself, you never have to. Go to the following link and press . to get a live code editor in your browser: https://github.com/markjprice/cs12dotnet8.

First, we will add a regular expression to validate that a CustomerId value is exactly five uppercase letters. Second, we will add string length requirements to validate that multiple properties throughout the entity models know the maximum length allowed for their text values:

Activate your code editor's Find and Replace feature:

image

In Visual Studio 2022, navigate to Edit | Find and Replace | Quick Replace, and then toggle on Use Regular Expressions.

Type a regular expression in the Find box, as shown in Figure 12.2 and in the following expression:

[Column(TypeName = "(nchar|nvarchar) ((.*))")]

  1. In the Replace box, type a replacement regular expression, as shown in the following expression:

$0\n [StringLength($2)]

After the newline character, \n , I have included four space characters to indent correctly on my system, which uses two space characters per indentation level. You can insert as many as you wish.

Set the Find and Replace to search files in the Current project.

Execute the Find and Replace to replace all, as shown in Figure 12.2:

image

Figure 12.2: Find and replace all matches using regular expressions in Visual Studio 2022

Change any date/time columns, for example, in Employee.cs , to use a nullable DateTime

instead of a string, as shown in the following code:

// Before:

[Column(TypeName = "datetime")]

public string? BirthDate { get; set; }

// After:

[Column(TypeName = "datetime")]

public DateTime? BirthDate { get; set; }

Use your code editor's Find feature to search for "datetime" to find all the properties that need changing. There should be two in Employee.cs and three in Order.cs .

  1. Change any money columns, for example, in Order.cs , to use a nullable decimal instead of a double , as shown in the following code:

// Before:

[Column(TypeName = "money")]

public double? Freight { get; set; }

// After:

[Column(TypeName = "money")]

public decimal? Freight { get; set; }

Use your code editor's Find feature to search for "money" to find all the properties that need changing. There should be one in Order.cs , one in Orderdetail.cs , and one in Product.cs .

  1. In Category.cs , make the CategoryName property required, as shown highlighted in the following code:

[Required]

[Column(TypeName = "nvarchar (15)")] [StringLength(15)]

public string CategoryName { get; set; }

  1. In Customer.cs , add a regular expression to validate its primary key CustomerId to only allow uppercase Western characters and make the CompanyName property required, as shown highlighted in the following code:

[Key]

[Column(TypeName = "nchar (5)")] [StringLength(5)] [RegularExpression("[A-Z]{5}")]

public string CustomerId { get; set; } = null!; [Required]

[Column(TypeName = "nvarchar (40)")] [StringLength(40)]

public string CompanyName { get; set; }

In Employee.cs , make the FirstName and LastName properties required.

In EmployeeTerritory.cs , make the TerritoryId property required.

In Order.cs , decorate the CustomerId property with a regular expression to enforce five uppercase characters.

In Product.cs , make the ProductName property required.

In Shipper.cs , make the CompanyName property required.

In Supplier.cs , make the CompanyName property required.

In Territory.cs , make the TerritoryId and TerritoryDescription properties required.

Testing the class libraries‌

Now let's build some unit tests to ensure the class libraries are working correctly.

Warning! If you are using the SQLite database provider, then when you call the CanConnect method with a wrong or missing database file, the provider creates a Northwind.db with 0 bytes! This is why it is so important that, in our NorthwindContext class, we explicitly check if the database file exists and throw an exception when it is instantiated if it does not exist to prevent this behavior.

Let's write the tests:

Use your preferred coding tool to add a new xUnit Test Project [C#] / xunit project named Northwind.UnitTests to the PracticalApps solution.

In the Northwind.UnitTests project, add a project reference to the Northwind.DataContext project for either SQLite or SQL Server, as shown highlighted in the following configuration:

Warning! The project reference must go all on one line with no line break.

Build the Northwind.UnitTests project to build referenced projects.

Rename UnitTest1.cs to EntityModelTests.cs .

Modify the contents of the file to define two tests, the first to connect to the database and the second to confirm there are eight categories in the database, as shown in the following code:

using Northwind.EntityModels; // To use NorthwindContext. namespace Northwind.UnitTests

{

public class EntityModelTests

{

[Fact]

public void DatabaseConnectTest()

{

using NorthwindContext db = new(); Assert.True(db.Database.CanConnect());

}

[Fact]

public void CategoryCountTest()

{

using NorthwindContext db = new(); int expected = 8;

int actual = db.Categories.Count(); Assert.Equal(expected, actual);

}

[Fact]

public void ProductId1IsChaiTest()

{

using NorthwindContext db = new(); string expected = "Chai";

Product? product = db.Products.Find(keyValues: 1); string actual = product?.ProductName ?? string.Empty; Assert.Equal(expected, actual);

}

}

}

Run the unit tests:

image

If you are using Visual Studio 2022, then navigate to Test | Run All Tests, and then view the results in Test Explorer.

image

If you are using Visual Studio Code, then in the Northwind.UnitTests project's TERMINAL window, run the tests, as shown in the following command: dotnet test , or use the TESTING window if you have installed C# Dev Kit.

Note that the results should indicate that three tests ran, and all passed. If any of the tests fail, then fix the issue. For example, if you are using SQLite, then check that the Northwind.db file is in the solution directory (one up from the project directories). Check the database path in the northwindlog.txt file on your desktop which should output the database path it used three times for the three tests, as shown in the following log:

Database path: C:\cs12dotnet8\PracticalApps\Northwind.db Database path: C:\cs12dotnet8\PracticalApps\Northwind.db

dbug: 18/09/2023 14:20:16.712 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[@ p_0='?' (DbType = Int32)], CommandType='Text', Command SELECT "p"."ProductId", "p"."CategoryId", "p"."Discontinued", "p"."ProductName", "p"."Quan FROM "Products" AS "p"

WHERE "p"."ProductId" = @ p_0 LIMIT 1

Database path: C:\cs12dotnet8\PracticalApps\Northwind.db

dbug: 18/09/2023 14:20:16.832 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFramewo Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']

SELECT COUNT(*)

FROM "Categories" AS "c"

Finally in this chapter, let's review some key concepts about web development so we will be better prepared to dive into ASP.NET Core Razor Pages in the next chapter.

Understanding web development‌

Developing for the web means developing with Hypertext Transfer Protocol (HTTP), so we will start by reviewing this important foundational technology.

Understanding Hypertext Transfer Protocol‌

To communicate with a web server, the client, also known as the user agent, makes calls over the network using HTTP. As such, HTTP is the technical underpinning of the web. So, when we talk about websites and web services, we mean that they use HTTP to communicate between a client (often a web browser) and a server.A client makes an HTTP request for a

resource, such as a page, uniquely identified by a Uniform Resource Locator (URL), and the server sends back an HTTP response, as shown in Figure 12.3:

image

Figure 12.3: An HTTP request and response

You can use Google Chrome and other browsers to record requests and responses.

Good Practice: Google Chrome is currently used by about two-thirds of website visitors worldwide, and it has powerful, built-in developer tools, so it is a good first choice for testing your websites. Test your websites with Chrome and at least two other browsers, for example, Firefox and Safari for macOS and iPhone. Microsoft Edge switched from using Microsoft's own rendering engine to using Chromium in 2019, so it is less important to test with it, although some say Edge has the best developer tools. If Microsoft's Internet Explorer is used at all, it tends to mostly be inside organizations for intranets.

Understanding the components of a URL‌

A Uniform Resource Locator (URL) is made up of several components:

image

Scheme: http (clear text) or https (encrypted).

image

Domain: For a production website or service, the top-level domain (TLD) might be example.com . You might have subdomains such as www , jobs , or extranet . During development, you typically use localhost for all websites and services.

image

Port number: For a production website or service, 80 for http , 443 for https . These port numbers are usually inferred from the scheme. During development, other port numbers are commonly used, such as 5000 , 5001 , and so on, to differentiate between websites and services that all use the shared domain localhost .

image

Path: A relative path to a resource, for example, /customers/germany .

image

Query string: A way to pass parameter values, for example,

?country=Germany&searchtext=shoes .

image

Fragment: A reference to an element on a web page using its id , for example, #toc .

URL is a subset of Uniform Resource Identifier (URI). A URL specifies where a resource is located and how to get it. A URI identifies a resource either by URL or URN (Uniform Resource Name).

Using Google Chrome to make HTTP requests‌

Let's explore how to use Google Chrome to make HTTP requests:

Start Google Chrome.

Navigate to More tools | Developer tools.

Click the Network tab, and Chrome should immediately start recording the network traffic between your browser and any web servers (note the red circle), as shown in Figure 12.4:

image

Figure 12.4: Chrome Developer tools recording network traffic

In Chrome's address box, enter the address of Microsoft's website for learning ASP.NET, as shown in the following URL:

https://dotnet.microsoft.com/en-us/learn/aspnet

In Developer Tools, in the list of recorded requests, scroll to the top and click on the first entry, the row where the Type is document, as shown in Figure 12.5:

image

Figure 12.5: Recorded requests in Developer Tools

On the right-hand side, click on the Headers tab, and you will see details about

Request Headers and Response Headers, as shown in Figure 12.6:

image

Figure 12.6: Request and response headers

Note the following aspects:

image

Request Method is GET . Other HTTP methods that you could see here include POST , PUT ,

DELETE , HEAD , and PATCH .

image

Status Code is 200 OK. This means that the server found the resource that the browser requested and has returned it in the body of the response. Other status codes that you might see in response to a GET request include 301 Moved Permanently , 400 Bad Request ,

401 Unauthorized , and 404 Not Found .

image

Request Headers sent by the browser to the web server include:

image

accept, which lists what formats the browser accepts. In this case, the browser is saying it understands HTML , XHTML , XML , and some image formats, but it will accept all other files ( / ). Default weightings, also known as quality values, are 1.0 . XML is specified with a quality value of 0.9 so it is preferred less than HTML or XHTML. All other file types are given a quality value of 0.8 so are least preferred.

image

accept-encoding, which lists what compression algorithms the browser understands, in this case, GZIP , DEFLATE , and Brotli .

image

accept-language, which lists the human languages it would prefer the content to use. In this case, US English, which has a default quality value of 1.0 , then any dialect of English that has an explicitly specified quality value of 0.9 , and then any dialect of Swedish that has an explicitly specified quality value of 0.8 .

image

Response Headers, content-encoding , which tells me the server has sent back the HTML web page response compressed using the GZIP algorithm because it knows that the client can decompress that format. (This is not visible in Figure 12.6 because there is not enough space to expand the Response Headers section.)

  1. Close Chrome.

Understanding client-side web development technologies‌

When building websites, a developer needs to know more than just C# and .NET. On the client (that is, in the web browser), you will use a combination of the following technologies:

image

HTML5: This is used for the content and structure of a web page.

image

image

CSS3: This is used for the styles applied to elements on the web page. JavaScript: This is used to code any business logic needed on the web page, for example, validating form input or making calls to a web service to fetch more data needed by the web page.

Although HTML5, CSS3, and JavaScript are the fundamental components of frontend web development, there are many additional technologies that can make frontend web development more productive, including:

image

Bootstrap, the world's most popular frontend open-source toolkit.

image

SASS and LESS, CSS preprocessors for styling.

image

image

Microsoft's TypeScript language for writing more robust code. JavaScript libraries such as Angular, jQuery, React, and Vue.

All these higher-level technologies ultimately translate or compile to the underlying three core technologies, so they work across all modern browsers.As part of the build and deploy process, you will likely use technologies such as:

image

Node.js, a framework for server-side development using JavaScript.

image

Node Package Manager (npm) and Yarn, both client-side package managers.

image

Webpack, a popular module bundler, and a tool for compiling, transforming, and bundling website source files.

Practicing and exploring‌

Test your knowledge and understanding by answering some questions and exploring this chapter's topics with deeper research.

Exercise 12.1 – Test your knowledge‌

Answer the following questions:

What was the name of Microsoft's first dynamic server-side executed web page technology and why is it still useful to know this history today?

What are the names of two Microsoft web servers?

What are some differences between a microservice and a nanoservice?

What is Blazor?

What was the first version of ASP.NET Core that could not be hosted on .NET Framework?

What is a user agent?

What impact does the HTTP request-response communication model have on web developers?

Name and describe four components of a URL.

What capabilities does Developer Tools give you?

What are the three main client-side web development technologies and what do they do?

Exercise 12.2 – Know your webbreviations‌

What do the following web abbreviations stand for and what do they do?

URI

URL

WCF

TLD

API

SPA

CMS

Wasm

SASS

REST

Exercise 12.3 – Explore topics‌

Use the links on the following page to learn more details about the topics covered in this chapter:

https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-12--- introducing-web-development-using-aspnet-core

Summary‌

In this chapter, you have:

image

Been introduced to some of the app models that you can use to build websites and web services using C# and .NET.

image

Created class libraries to define an entity data model for working with the Northwind database using SQLite, SQL Server, or both.

In the following chapters, you will learn the details about how to build the following:

image

Simple websites using static HTML pages and dynamic Razor Pages.

image

Complex websites using the MVC design pattern.

image

Web services that can be called by any platform that can make an HTTP request, and client websites that call those web services.

image

Blazor user interface components that can be hosted on a web server, in the browser, or on hybrid web-native mobile and desktop apps.

13 Building Websites Using ASP.NET Core Razor Pages‌‌‌

Join our book community on Discord

https://packt.link/EarlyAccess

image

This chapter is about building websites with a modern HTTP architecture on the server side using Microsoft ASP.NET Core. You will learn about building simple websites using the ASP.NET Core Razor Pages feature introduced with ASP.NET Core 2 and the Razor class library feature introduced with ASP.NET Core 2.1.This chapter covers the following topics:

image

Exploring ASP.NET Core

image

Exploring ASP.NET Core Razor Pages

image

image

Using Entity Framework Core with ASP.NET Core Configuring services and the HTTP request pipeline

Exploring ASP.NET Core‌

We will start by creating an empty ASP.NET Core project and explore how to enable it to serve simple web pages.

Creating an empty ASP.NET Core project‌

We will create an ASP.NET Core project that will show a list of suppliers from the Northwind database.The dotnet tool has many project templates that do a lot of work for you, but it can be difficult to know which works best for a given situation, so we will start with the empty website project template and then add features step by step so that you can understand all the pieces:

Use your preferred code editor to open the PracticalApps solution and then add a new project, as defined in the following list:

image

Project template: ASP.NET Core Empty [C#] / web . For JetBrains Rider, select the project template named ASP.NET Core Web Application, and then set the Type to Empty.

image

image

Project file and folder: Northwind.Web . Solution file and folder: PracticalApps .

image

For Visual Studio 2022, leave all other options as their defaults, for example, Framework: .NET 8.0 (Long Term Support), Configure for HTTPS: selected, Enable Docker: cleared, and Do not use top-level statements: cleared.

image

For Visual Studio Code and the dotnet new web command, the defaults are the options we want. In future projects, if you want to change from top-level statements to the old Program class style, then specify the switch --use-program-main .

Summaries of Visual Studio 2022 and dotnet new options when creating new projects can be found in the GitHub repository at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch01-project-options.md.

Build the Northwind.Web project.

In Northwind.Web.csproj , note the project is like a class library except that the SDK is

Microsoft.NET.Sdk.Web , as shown highlighted in the following markup:

net8.0

enable

enable

Add an element to import the System.Console class globally and statically.

If you are using Visual Studio 2022, in Solution Explorer, toggle Show All Files. If you are using JetBrains Rider, then mouse cursor over the Solution pane, then click the "eyeball" icon.

Expand the obj folder, expand the Debug folder, expand the net8.0 folder, select the Northwind.Web.GlobalUsings.g.cs file, and note how the implicitly imported namespaces include all the ones for a console app or class library, as well as some ASP.NET Core ones, such as Microsoft.AspNetCore.Builder , as shown in the following code:

//

global using global::Microsoft.AspNetCore.Builder; global using global::Microsoft.AspNetCore.Hosting; global using global::Microsoft.AspNetCore.Http; global using global::Microsoft.AspNetCore.Routing;

global using global::Microsoft.Extensions.Configuration; global using global::Microsoft.Extensions.DependencyInjection; global using global::Microsoft.Extensions.Hosting;

global using global::Microsoft.Extensions.Logging; 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.Net.Http.Json; global using global::System.Threading; global using global::System.Threading.Tasks; global using static global::System.Console;

Close the file and collapse the obj folder.

In the Northwind.Web project/folder, expand the folder named Properties , open the file named launchSettings.json , and note the profiles named http and https . They have randomly assigned port numbers that you will change in the next step, so for now just note their locations, as shown highlighted in the following configuration:

{

"$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": {

"windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": {

"applicationUrl": "http://localhost:14842", "sslPort": 44352

}

},

"profiles": {

"http": {

"commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true,

"applicationUrl": "http://localhost:5122",

"environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development"

}

},

"https": {

"commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true,

"applicationUrl": "https://localhost:7155;http://localhost:5122", "environmentVariables": {

"ASPNETCORE_ENVIRONMENT": "Development"

}

},

"IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": {

"ASPNETCORE_ENVIRONMENT": "Development"

}

}

}

}

The launchSettings.json file is only for use during development. It has no effect on the build process. It is not deployed with the compiled website project, so it has no effect on the production runtime. It is only processed by code editors like Visual Studio 2022 and JetBrains Rider to set up environment variables and define URLs for the web server to listen on when the project is started by a code editor.

  1. For the https profile, for its applicationUrl , change the assigned port numbers for http to 5130 and https to 5131 , and swap the order so http is first in the list so it will be used by default, as shown highlighted in the following markup:

"applicationUrl": "http://localhost:5130;https://localhost:5131",

The http and https launch profiles have a commandName of Project , meaning they use the web server configured in the project to host the website, which is Kestrel by default.

There is also a profile and settings for IIS, which is a Windows-only web server. In this book, we will only use Kestrel as the web server since it is cross-platform. To declutter your launchSettings.json file you could even delete the iisSettings and IIS Express sections.

  1. In Program.cs , note the following:

image

An ASP.NET Core project is like a top-level console app, with a hidden

$

method as its entry point that has an argument passed using the name args .

image

It calls WebApplication.CreateBuilder , which creates a host for the website using defaults for a web host that is then built.

image

image

The website will respond to all HTTP GET requests with plain text: Hello World! . The call to the Run method is a blocking call, so the hidden

$ method does not return until the web server stops running.

The contents of Program.cs are shown in the following code:

var builder = WebApplication.CreateBuilder(args); var app = builder.Build();

app.MapGet("/", () => "Hello World!"); app.Run();

  1. At the bottom of Program.cs , add a comment to explain the Run method and a statement to write a message to the console after Run and therefore after the web server has stopped, as shown highlighted in the following code:

// Start the web server, host the website, and wait for requests. app.Run(); // This is a thread-blocking call.

WriteLine("This executes after the web server has stopped!");

Testing and securing the website‌

We will now test the functionality of the ASP.NET Core Empty website project. We will also enable encryption of all traffic between the browser and web server for privacy by switching from HTTP to HTTPS. HTTPS is the secure encrypted version of HTTP.

For Visual Studio 2022:

In the toolbar, make sure that the https profile is selected (rather than http, IIS Express, or WSL), and then change Web Browser to Google Chrome, as shown in Figure 13.1:

image

Figure 13.1: Selecting the https profile with its Kestrel web server in Visual Studio

Navigate to Debug | Start Without Debugging….

On Windows, if you see a Windows Security Alert saying Windows Defender Firewall has blocked some features of this app, then click the Allow access button.

The first time you start a secure website, you might be prompted that your project is configured to use SSL, and to avoid warnings in the browser, you can choose to trust the self-signed certificate that ASP.NET Core has generated. Click Yes. When you see the Security Warning dialog box, click Yes again.

For Visual Studio Code, enter the command to start the project with the https profile, like this: dotnet run --launch-profile https . Then start Chrome.

For JetBrains Rider:

Navigate to Run | Edit Configurations….

In the Run/Debug Configurations dialog box, select Northwind.Web: https.

At the bottom of the dialog box, to the right of the After launch check box, select

Chrome and then click OK.

Navigate to Run | Run 'Northwind.Web: https'.

In either Visual Studio's command prompt window or Visual Studio Code's terminal, note the following, as shown in the following output:

image

image

The web server has started listening on the ports we assigned for HTTP and HTTPS. You can press Ctrl + C to shut down the Kestrel web server.

image

The hosting environment is Development .

info: Microsoft.Hosting.Lifetime[14]

Now listening on: http://localhost:5130

info: Microsoft.Hosting.Lifetime[14]

Now listening on: https://localhost:5131 info: Microsoft.Hosting.Lifetime[0]

Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0]

Hosting environment: Development info: Microsoft.Hosting.Lifetime[0]

Content root path: C:\cs12dotnet8\PracticalApps\Northwind.Web

Visual Studio 2022 will also start your chosen browser automatically and navigate to the first URL. If you are using Visual Studio Code, you will have to start Chrome manually.

Leave the Kestrel web server running in the command prompt or terminal.

In Chrome, show Developer Tools, and click the Network tab.

Request the home page for the website project:

image

If you are using Visual Studio 2022 and Chrome launched automatically with the URL already entered for you, then click the Reload this page button or press F5.

image

If you are using Visual Studio Code and the command prompt or terminal, then in the Chrome address bar, manually enter the address http://localhost:5130/ .

In the Network tab, click localhost, and note the response is Hello World! in plain text, from the cross-platform Kestrel web server, as shown in Figure 13.2:

image

Figure 13.2: Plain text response from the website project

Browsers like Chrome might also request a favicon.ico file to show in their browser window or tab, but this file does not exist in our project, so it shows as a 404 Not Found error. If this annoys you, then you can generate a favicon.ico file for free at the following link and put it in the project folder: https://favicon.io/. On a web page, you can also specify one in the meta tags, for example, a blank one using Base64 encoding, as shown in the following markup:

Enter the address https://localhost:5131/ and note that if you are not using Visual Studio 2022 or if you clicked No when prompted to trust the SSL certificate, then the response is a privacy error. You will see this error when you have not configured a certificate that the browser can trust to encrypt and decrypt HTTPS traffic (if you do not see this error, it is because you have already configured a certificate). In a production environment, you would want to pay a company such as Verisign for an SSL certificate because they provide liability protection and technical support. During development, you can tell your OS to trust a temporary development certificate provided by ASP.NET Core.

For Linux Developers: If you use a Linux variant that cannot create self-signed certificates or you do not mind reapplying for a new certificate every 90 days, then you can get a free certificate from the following link: https://letsencrypt.org.

At the command prompt or terminal, press Ctrl + C to shut down the web server, and note the message that is written, as shown highlighted in the following output:

info: Microsoft.Hosting.Lifetime[0] Application is shutting down...

This executes after the web server has stopped!

C:\cs12dotnet8\PracticalApps\Northwind.Web\bin\Debug\net8.0\Northwind.Web.exe (process 19888) ex

If you need to trust a local self-signed SSL certificate, then at the command line or terminal, enter the following command: dotnet dev-certs https --trust .

Note the message Trusting the HTTPS development certificate was requested. You might be prompted to enter your password and a valid HTTPS certificate may already be present.

Enabling stronger security and redirecting to a secure connection‌

It is good practice to enable stricter security and automatically redirect requests for HTTP to HTTPS.

Good Practice: An optional but recommended security enhancement is HTTP Strict Transport Security (HSTS), which you should always enable. If a website specifies it and a browser supports it, then it forces all communication over HTTPS and prevents the visitor from using untrusted or invalid certificates.

Let's do that now:

  1. In Program.cs , after the statement that builds the app , add a region and an if statement to enable HSTS when not in development, and redirect HTTP requests to HTTPS, as shown highlighted in the following code:

var builder = WebApplication.CreateBuilder(args); var app = builder.Build();

region Configure the HTTP pipeline and routes if (!app.Environment.IsDevelopment())

{

app.UseHsts();

}

app.UseHttpsRedirection(); app.MapGet("/", () => "Hello World!"); #endregion

// Start the web server, host the website, and wait for requests. app.Run(); // This is a thread-blocking call.

WriteLine("This executes after the web server has stopped!");

Start the Northwind.Web website project without debugging using the https launch profile.

If Chrome is still running, close and restart it.

In Chrome, show Developer Tools, and click the Network tab.

Enter the address http://localhost:5130/ , and note how the server responds with a 307 Temporary Redirect to https://localhost:5131/ , and that the certificate is valid and trusted, as shown in Figure 13.3:

image

Figure 13.3: The connection is now secured using a valid certificate and a 307 redirect

Close Chrome and shut down the web server.

Good Practice: Remember to shut down the Kestrel web server by switching to the command prompt or terminal and pressing Ctrl + C whenever you have finished testing a website.

Controlling the hosting environment‌

In ASP.NET Core 5 and earlier, the project template sets a rule to say that while in development mode, any unhandled exceptions will be shown in the browser window for the developer to see the details of the exception, as shown in the following code:

if (app.Environment.IsDevelopment())

{

app.UseDeveloperExceptionPage();

}

With ASP.NET Core 6 and later, this code is executed automatically so it is no longer included in the project template Program.cs source code.How does ASP.NET Core know when we are running in development mode so that the IsDevelopment method returns true , and this extra code executes to set up the developer exception page? Let's find out.ASP.NET Core can read from settings files and environment variables to determine what hosting environment to use, for example, DOTNET_ENVIRONMENT or ASPNETCORE_ENVIRONMENT .You can override these settings during local development:

  1. In the Northwind.Web folder, expand the folder named Properties , and open the file named launchSettings.json . Note the https launch profile sets the environment variable for the hosting environment to Development , as shown highlighted in the following configuration:

"https": {

"commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true,

"applicationUrl": "https://localhost:5131;http://localhost:5130", "environmentVariables": {

"ASPNETCORE_ENVIRONMENT": "Development"

}

},

Change the ASPNETCORE_ENVIRONMENT environment variable from Development to Production .

If you are using Visual Studio 2022, optionally, change launchBrowser to false to prevent Visual Studio from automatically launching a browser. This setting is ignored when you start a website project using dotnet run or JetBrains Rider.

In Program.cs , modify the MapGet statement, as shown highlighted in the following code:

app.MapGet("/", () =>

$"Environment is {app.Environment.EnvironmentName}");

  1. Start the website project using the https launch profile and note the hosting environment is Production , as shown in the following output:

info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production

In Chrome, note that the plain text is Environment is Production .

Shut down the web server.

In launchSettings.json , change the environment variable back to Development .

More Information: You can learn more about environments at the following link: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments.

Enabling a website to serve static content‌

A website that only ever returns a single plain text message isn't very useful!At a minimum, it ought to return static HTML pages, CSS that the web pages will use for styling, and any other static resources, such as images and videos.By convention, these files should be stored in a directory named wwwroot to keep them separate from the dynamically executing parts of your website project.

Creating a folder for static files and a web page‌

You will now create a folder for your static website resources and a basic index page that uses Bootstrap for styling:

In the Northwind.Web project/folder, create a folder named wwwroot . Note that Visual Studio 2022 recognizes it as a special type of folder by giving it a globe icon

image

.

In the wwwroot folder, add a new file named index.xhtml . (In Visual Studio 2022, the project item template is named HTML Page.)

In index.xhtml , modify its markup to link to CDN-hosted Bootstrap for styling, and use modern good practices such as setting the viewport, as shown in the following markup:

<!doctype html>

Welcome to Northwind B2B

We supply products to our customers.


This is a static HTML page.

Our customers include restaurants, hotels, and cruise lines.

Learn more

Good Practice: Check the latest version at the following link: https://getbootstrap.com/docs/versions/. Click the latest version to go to its Get started with Bootstrap page. Scroll down the page to Step 2 to find the latest and

@await RenderSectionAsync("Scripts", required: false)

1. In _LoginPartial.cshtml , note the log-in functionality is implemented using the ASP.NET Core Identity system as Razor Pages using an asp-area named Identity , as shown in the following markup:

@using Microsoft.AspNetCore.Identity

@inject SignInManager SignInManager @inject UserManager UserManager

1. In _ValidationScriptsPartial.cshtml , note this partial view has references to a pair of jQuery scripts for performing validation, as shown in the following markup:

Good Practice: If you create a Razor View that uses a model with validation attributes like [Required] and [StringLength] , then add this partial view to the Scripts block to enable validation on the client side by the browser, as shown in the following markup:

@section Scripts {

}

How cache busting with Tag Helpers works‌

When asp-append-version is specified with a true value in a , , or

You can see this for yourself in the current project because the _Layout.cshtml file has the element.

If even a single byte within the site.js file changes, then its hash value will be different, and therefore if a browser or CDN is caching the script file, then it will bust the cached copy and replace it with the new version.The src attribute must be set to a static file stored on the local web server, usually in the wwwroot folder, but you can configure additional locations. Remote references are not supported.

Customizing an ASP.NET Core MVC website‌

Now that you've reviewed the structure of a basic MVC website, you will customize and extend it. You have already registered an EF Core model for the Northwind database, so the next task is to output some of that data on the home page.

Defining a custom style‌

The home page will show a list of the 77 products in the Northwind database. To make efficient use of space, we want to show the list in three columns. To do this, we need to customize the stylesheet for the website:

In the wwwroot\css folder, open the site.css file.

At the bottom of the file, add a new style that will apply to an element with the

product-columns ID, as shown in the following code:

#product-columns

{

column-count: 3;

}

Setting up the category images‌

The Northwind database includes a table of eight categories, but they do not have images, and websites look better with some colorful pictures:

In the wwwroot folder, create a folder named images .

In the images folder, add eight image files named category1.jpeg , category2.jpeg , and so on, up to category8.jpeg .

You can download images from the GitHub repository for this book at the following link: https://github.com/markjprice/cs12dotnet8/tree/main/code/images/Categories

Razor syntax and expressions‌

Before we customize the home page view, let's review an example Razor file. The file has an initial Razor code block that instantiates an order with the price and quantity, and then outputs information about the order on the web page, as shown in the following markup:

@{

Order order = new()

{

OrderId = 123, Product = "Sushi", Price = 8.49M, Quantity = 3

};

}

Your order for @order.Quantity of @order.Product has a total cost of $@ order.Price * @orde

The preceding Razor file would result in the following incorrect output:

Your order for 3 of Sushi has a total cost of $8.49 * 3

Although Razor markup can include the value of any single property using the @object.property syntax, you should wrap expressions in parentheses, as shown in the following markup:

Your order for @order.Quantity of @order.Product has a total cost of $@ (order.Price * orde

The preceding Razor expression results in the following correct output:

Your order for 3 of Sushi has a total cost of $25.47

Defining a typed view‌

To improve the IntelliSense when writing a view, you can define what type the view can expect using an @model directive at the top:

In the Views\Home folder, open Index.cshtml .

At the top of the file, add statements to import the namespace for Northwind entities and set the model type to use the HomeIndexViewModel , as shown in the following code:

@using Northwind.EntityModels @model HomeIndexViewModel

Now, whenever we type Model in this view, your code editor will know the correct type for the model and will provide IntelliSense for it.While entering code in a view, remember the following:

image

Declare the type for the model using @model (with a lowercase m).

image

Interact with the instance of the model using @Model (with an uppercase M).

Let's continue customizing the view for the home page.

1. In the initial Razor code block, add a statement to declare a string variable for the current item, as shown highlighted in the following markup:

@{

ViewData["Title"] = "Home Page"; string currentItem = "";

}

1. Under the existing

element, after its closing

, add new markup to output categories in a carousel and products as an unordered list, as shown in the following markup:

@if (Model is not null)

{

}

Northwind

We have had @Model?.VisitorCount visitors this month.

@if (Model is not null)

{

Products

}

While reviewing the preceding Razor markup, note the following:

image

JetBrains Rider might tell you that Model is never null , so you do not need to check for null . Visual Studio 2022 will warn you the opposite, which is why I put in the null check. Unfortunately, it is a common programmer error to pass an object for the model that is null .

image

It is easy to mix static HTML elements such as

    and

  • with C# code to output the carousel of categories and the list of product names.

    image

    The

    element with the id attribute of product-columns will use the custom style that we defined earlier, so all the content in that element will display in three columns.

    image

    The element for each category uses parentheses around a Razor expression to ensure that the compiler does not include the .jpeg as part of the expression, as shown in the following markup: "~/images/category@(Model.Categories.CategoryID).jpeg" .

    image

    The elements for the product links use tag helpers to generate URL paths. Clicks on these hyperlinks will be handled by the HomeController and its ProductDetail action method. This action method does not exist yet, but you will add it later in this chapter. The ID of the product is passed as a route segment named id , as shown in the following URL path for Ipoh Coffee: https://localhost:5141/Home/ProductDetail/43 .

    Let's see the result of our customized home page:

    Start the Northwind.Mvc website project using the https launch profile.

    Note the home page has a rotating carousel showing categories, a random number of visitors, and a list of products in three columns, as shown in Figure 14.4:

    image

    Figure 14.4: The updated Northwind MVC website home page

    For now, clicking on any of the categories or product links gives 404 Not Found errors, so let's see how we can implement pages that use the parameters passed to them to see the details of a product or category.

    1. Close Chrome and shut down the web server.

    Passing parameters using a route value‌

    One way to pass a simple parameter is to use the id segment defined in the default route:

    1. In HomeController , import the namespace for calling the Include extension method so that we can get related entities, as shown in the following code:

    using Microsoft.EntityFrameworkCore; // To use Include method.

    1. Add an action method named ProductDetail , as shown in the following code:

    public IActionResult ProductDetail(int? id)

    {

    if (!id.HasValue)

    {

    return BadRequest("You must pass a product ID in the route, for example, /Home/ProductDetail

    }

    Product? model = _db.Products.Include(p => p.Category)

    .SingleOrDefault(p => p.ProductId == id); if (model is null)

    {

    return NotFound($"ProductId {id} not found.");

    }

    return View(model); // Pass model to view and then return result.

    }

    Note the following:

    image

    This method uses a feature of ASP.NET Core called model binding to automatically match the id passed in the route to the parameter named id in the method.

    image

    Inside the method, we check to see whether id does not have a value, and if so, we call the BadRequest method to return a 400 status code with a custom message explaining the correct URL path format.

    image

    Otherwise, we can connect to the database and try to retrieve a product using the id

    value and include the related category information so we can see its name.

    image

    If we find a product, we pass it to a view; otherwise, we call the NotFound method to return a 404 status code and a custom message explaining that a product with that ID was not found in the database.

    In the Views/Home folder, add a new Razor View file named ProductDetail.cshtml . (In Visual Studio, the item template is named Razor View - Empty. In Visual Studio Code, at the command prompt or terminal, enter dotnet new view -n ProductDetail .)

    Warning! Be careful not to add a Razor Page. If you do, then the file will have an @page directive at the top, which will prevent the model from being passed from the controller to the view and you will get a NullReferenceException !

    Modify the contents, as shown in the following markup:

    @model Northwind.EntityModels.Product @{

    ViewData["Title"] = $"Product Detail - {Model.ProductName}";

    }

    Product Detail


    Product Id
    @Model.ProductId
    Product Name
    @Model.ProductName
    Category
    @Model.CategoryId - @Model.Category?.CategoryName
    Unit Price
    @(Model.UnitPrice is null ? "zero" : Model.UnitPrice.Value.ToString("C"))
    Units In Stock
    @Model.UnitsInStock

    Start the Northwind.Mvc project using the https launch profile.

    When the home page appears with the list of products, click on one of them, for example, the second product, Chang.

    Note the URL path in the browser's address bar, the page title shown in the browser tab, and the product details page, as shown in Figure 14.5:

    image

    Figure 14.5: The product details page for Schoggi Schokolade

    View Developer Tools.

    Edit the URL in the address box of Chrome to request a product ID that does not exist, like 99, and note the 404 Not Found status code and custom error response.

    Close Chrome and shut down the web server.

    Disambiguating action methods‌

    Although the C# compiler can differentiate between the two methods by noting that the signatures are different, from the point of view of routing an HTTP request, both methods are potential matches. We need an HTTP-specific way to disambiguate the action methods.We could do this by creating different names for the actions or by specifying that one method should be used for a specific HTTP verb, like GET , POST , or DELETE . You do this by decorating the action method with one of the following attributes: [HttpPost] , [HttpPut] , and so on.

    Model binders in detail‌

    Model binders are a powerful yet easy way to set parameters of action methods based on values passed in an HTTP request, and the default one does a lot for you. After the default route identifies a controller class to instantiate and an action method to call, if that method has parameters, then those parameters need to have values set.Model binders do this

    by looking for parameter values passed in the HTTP request as any of the following types of parameters:

    image

    Route parameter, like id as we used in the previous section, as shown in the following URL path: /Home/ProductDetail/2

    image

    Query string parameter, as shown in the following URL path: /Home/ProductDetail?id=2

    image

    Form parameter, as shown in the following markup:

    Model binders can populate almost any type:

    image

    image

    image

    Simple types, like int , string , DateTime , and bool Complex types defined by class , record , or struct Collection types, like arrays and lists

    The process of model binding can cause errors, for example, data type conversions or validation errors if the model has been decorated with validation rules. What data has been bound and any binding or validation errors are stored in ControllerBase.ModelState .Let's create a somewhat artificial example to illustrate what can be achieved using the default model binder and what we can do with the model state by applying some validation rules to the bound model and showing invalid data messages in the view:

    In the Models folder, add a new file named Thing.cs .

    Modify the contents to define a record with three properties; a nullable integer named Id , a string named Color , and a string named Email , each with appropriate validation attributes, as shown in the following code:

    // To use [Range], [Required], [EmailAddress]. using System.ComponentModel.DataAnnotations; namespace Northwind.Mvc.Models;

    public record Thing( [Range(1, 10)] int? Id, [Required] string? Color,

    [EmailAddress] string? Email

    );

    In the Models folder, add a new class file named HomeModelBindingViewModel.cs .

    Modify its contents to define a record with properties to store the bound model, a flag to indicate that there are errors, and a sequence of error messages, as shown in the following code:

    namespace Northwind.Mvc.Models;

    public record HomeModelBindingViewModel(Thing Thing, bool HasErrors, IEnumerable ValidationErrors);

    1. In HomeController , add two new action methods, one to show a page with a form and one to display a thing with a parameter using your new model type, as shown in the following code:

    // This action method will handle GET and other requests except POST. public IActionResult ModelBinding()

    {

    return View(); // The page with a form to submit.

    }

    [HttpPost] // This action method will handle POST requests. public IActionResult ModelBinding(Thing thing)

    {

    HomeModelBindingViewModel model = new(

    Thing: thing, HasErrors: !ModelState.IsValid,

    ValidationErrors: ModelState.Values

    .SelectMany(state => state.Errors)

    .Select(error => error.ErrorMessage)

    );

    return View(model); // Show the model bound thing.

    }

    The first ModelBinding action method will implicitly be used for all other types of HTTP requests, like GET , PUT , DELETE , and so on, because the second ModelBinding action method is decorated with [HttpPost] .

    In the Views\Home folder, add a new file named ModelBinding.cshtml .

    Modify its contents, as shown in the following markup:

    @model HomeModelBindingViewModel @{

    ViewData["Title"] = "Model Binding Demo";

    }

    @ViewData["Title"]

    Enter values for your thing in the following form:

    @if (Model is not null)

    {

    Submitted Thing


    Model.Thing.Id
    @Model.Thing.Id
    Model.Thing.Color
    @Model.Thing.Color
    Model.Thing.Email
    @Model.Thing.Email

    @if (Model.HasErrors)

    {

    @foreach(string errorMessage in Model.ValidationErrors)

    {

    }

    }

    }

    @section Scripts {

    }

    1. In Views/Home , in Index.cshtml , in the first

    , after rendering the heading, add a new paragraph with a link to the model binding page, as shown in the following markup:

    Binding

    Start the Northwind.Mvc website project using the https launch profile.

    On the home page, click Binding.

    Click the Submit button and note the value for the Id property is set from the query string parameter in the action of the form, and the values for the color and email properties are set from the form elements, as shown in Figure 14.6:

    image

    Figure 14.6: The Model Binding Demo page

    Close Chrome and shut down the web server.

    Passing a route parameter‌

    Now we will set the property using a route parameter:

    1. In the Views\Home folder, in ModelBinding.cshtml , modify the action for the form to pass the value 2 as an MVC route parameter, as shown highlighted in the following markup:

    Start the Northwind.Mvc website project using the https launch profile.

    On the home page, click Binding.

    Click the Submit button and note the value for the Id property is set from the route parameter.

    Close Chrome and shut down the web server.

    Passing a form parameter‌

    Now we will set the property using a form parameter:

    1. In the Views\Home folder, in ModelBinding.cshtml , modify the action for the form to pass the value 1 as a form element parameter, as shown highlighted in the following markup:

    Start the Northwind.Mvc website project using the https launch profile.

    On the home page, click Binding.

    Click the Submit button and note the values for all the properties are both set from the form element parameters.

    Good Practice: If you have multiple parameters with the same name, then remember that form parameters have the highest priority and query string parameters have the lowest priority for automatic model binding.

    Enter an Id of 13 , clear the color textbox, delete the @ from the email address, click the Submit button, and note the error messages, as shown in Figure 14.7:

    image

    Figure 14.7: The Model Binding Demo page with field validations

    Close Chrome and shut down the web server.

    Good Practice: What regular expression does Microsoft use for the implementation of the EmailAddress validation attribute? Find out at the following link: https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64e cd/System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs#L54

    Defining views with HTML Helper methods‌

    While creating a view for ASP.NET Core MVC, you can use the Html object and its methods to generate markup. When Microsoft first introduced ASP.NET MVC in 2009, these HTML Helper methods were the way to programmatically render HTML. Modern ASP.NET Core retains these HTML Helper methods for backward compatibility and provides Tag Helpers that are usually easier to read and write in most scenarios. But there are notable situations where Tag Helpers cannot be used, like in Razor components.Some useful methods include the following:

    image

    ActionLink : Use this to generate an anchor element that contains a URL path to the specified controller and action. For example,

    Html.ActionLink(linkText: "Binding", actionName: "ModelBinding", controllerName: "Home") would generate Binding . You can achieve the same result using the anchor tag helper

    Binding .

    image

    AntiForgeryToken : Use this inside a

    to insert a element containing an anti-forgery token that will be validated when the form is submitted.

    image

    Display and DisplayFor : Use this to generate HTML markup for the expression relative to the current model using a display template. There are built-in display templates for

    .NET types and custom templates can be created in the DisplayTemplates folder. The folder name is case-sensitive on case-sensitive filesystems.

    image

    DisplayForModel : Use this to generate HTML markup for an entire model instead of a single expression.

    image

    image

    Editor and EditorFor : Use this to generate HTML markup for the expression relative to the current model using an editor template. There are built-in editor templates for .NET types that use

    image

    Encode : Use this to safely encode an object or string into HTML. For example, the string value "

    While reviewing the preceding markup, note the following:

    image

    image

    A Blazor component for injecting additional content into the section. This is one of the built-in components available in all Blazor projects. A Blazor component for defining the custom routes in this project. This component can be completely customized by the developer because it is part of the current project in a file named Routes.razor .

    image

    A script block for blazor.web.js that manages communication back to the server for Blazor's dynamic features like downloading WebAssembly components in the background and later switching from server-side to client-side component execution.

    1. In the Components folder, in Routes.razor , note that a enables routing for all Blazor components found in the current assembly, and that if a matching route is found, then RouteView is executed, which sets the default layout for the component to MainLayout and passes any route data parameters to the component. For that component, the first

    element in it will get the focus, as shown in the following code:

    In the Components folder, in _Imports.razor , note that this file imports some useful namespaces for use in all your custom Blazor components.

    In the Components\Layout folder, in MainLayout.razor , note that it defines

    for a sidebar containing a navigation menu that is implemented by the NavMenu.razor component file in this project, and HTML5 elements like
    and

    for the content, as shown in the following code:

    @inherits LayoutComponentBase

    @Body

    In the Components\Layout folder, in MainLayout.razor.css , note that it contains isolated CSS styles for the component. Due to the naming convention, styles defined in this file take priority over others defined elsewhere that might affect the component.

    Blazor components often need to provide their own CSS to apply styling or JavaScript for activities that cannot be performed purely in C#, like access to browser APIs. To ensure this does not conflict with site-level CSS and JavaScript, Blazor supports CSS and JavaScript isolation. If you have a component named Home.razor , simply create a CSS file named Home.razor.css . The styles defined within this file will override any other styles in the project.

    In the Components\Layout folder, in NavMenu.razor , note that it has three menu items for Home, Counter, and Fetch data. These are created by using a component named NavLink , as shown in the following markup:

    Note NavMenu.razor has its own isolated stylesheet named NavMenu.razor.css .

    In the Components\Pages folder, in Home.razor , note that it defines a component that sets the page title, and then renders a heading and a welcome message, as shown in the following code:

    @page "/"

    Home

    Hello, world!

    Welcome to your new app.

    1. In the Components\Pages folder, in Weather.razor , note that it defines a component that fetches weather forecasts from an injected dependency weather service and then renders them in a table, as shown in the following code:

    @page "/weather"

    @attribute [StreamRendering(true)]

    Weather

    Weather

    This component demonstrates showing data from the server.

    @if (forecasts == null)

    {

    Loading...

    }

    else

    {

    @foreach (var forecast in forecasts)

    {

    }

    Date Temp. (C) Temp. (F) Summary
    @forecast.Date.ToShortDateString() @forecast.TemperatureC @forecast.TemperatureF @forecast.Summary

    }

    @code {

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()

    {

    // Simulate asynchronous loading to demonstrate streaming rendering await Task.Delay(500);

    var startDate = DateOnly.FromDateTime(DateTime.Now);

    var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };

    forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast

    {

    Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55),

    Summary = Summaries[Random.Shared.Next(Summaries.Length)]

    }).ToArray();

    }

    private class WeatherForecast

    {

    public DateOnly Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    }

    }

    How to define a routable page component‌

    To create a routable page component, add the @page directive to the top of a component's

    .razor file, as shown in the following markup:

    @page "customers"

    The preceding code is the equivalent of an MVC controller decorated with the [Route]

    attribute, as shown in the following code:

    [Route("customers")]

    public class CustomersController

    {

    A page component can have multiple @page directives to register multiple routes, as shown in the following code:

    @page "/weather" @page "/forecast"

    The Router component scans the assembly specifically in its AppAssembly parameter for components decorated with the [Route] attribute and registers their URL paths.At runtime, the page component is merged with any specific layout that you have specified, just like an MVC view or Razor Page would be. By default, the Blazor Web App project template defines MainLayout.razor as the layout for page components.

    Good Practice: By convention, put routable page Blazor components in the

    Components\Pages folder.

    How to navigate routes and pass route parameters‌

    Microsoft provides a dependency service named NavigationManager that understands Blazor routing and the NavLink component. The NavigateTo method is used to go to the specified URL.Blazor routes can include case-insensitive named parameters, and your code can most easily access the passed values by binding the parameter to a property in the code block using the [Parameter] attribute, as shown in the following markup:

    @page "/customers/{country}"

    Country parameter as the value: @Country

    @code {

    [Parameter]

    public string Country { get; set; }

    }

    The recommended way to handle a parameter that should have a default value when it is missing is to suffix the parameter with ? and use the null coalescing operator in the OnParametersSet method, as shown in the following markup:

    @page "/customers/{country?}"

    Country parameter as the value: @Country

    @code {

    [Parameter]

    public string Country { get; set; } protected override void OnParametersSet()

    {

    // If the automatically set property is null, then

    // set its value to USA. Country = Country ?? "USA";

    }

    }

    How to use the navigation link component with routes‌

    In HTML, you use the element to define navigation links, as shown in the following markup:

    Customers

    In Blazor, use the component, as shown in the following markup:

    Customers

    The NavLink component is better than an anchor element because it automatically sets its class to active if its href is a match on the current location URL. If your CSS uses a different class name, then you can set the class name in the NavLink.ActiveClass property.By default, in the matching algorithm, the href is a path prefix, so if NavLink has an href of /customers , as shown in the preceding code example, then it would match all the following paths and set them all to have the active class style:

    /customers

    /customers/USA

    /customers/Germany/Berlin

    To ensure that the matching algorithm only performs matches on all of the text in the path, in other words, there is only a match when the whole complete text matches but not when just part of the path matches, then set the Match parameter to NavLinkMatch.All , as shown in the following code:

    Customers

    If you set other attributes such as target , they are passed through to the underlying

    element that is generated.

    Understanding base component classes‌

    The OnParametersSet method is defined by the base class that components inherit from by default named ComponentBase , as shown in the following code:

    using Microsoft.AspNetCore.Components;

    public abstract class ComponentBase : IComponent, IHandleAfterRender, IHandleEvent

    {

    // Members not shown.

    }

    ComponentBase has some useful methods that you can call and override, as shown in Table 16.1:

    Method(s) Description

    InvokeAsync Call this method to execute a function on the associated renderer's synchronization context.

    Override these methods to invoke code after

    OnAfterRender ,

    OnAfterRenderAsync

    each time the component has been rendered.

    OnInitialized ,

    OnInitializedAsync

    Override these methods to invoke code after the component has received its initial parameters from its parent in the render tree.

    OnParametersSet ,

    OnParametersSetAsync

    Override these methods to invoke code after the component has received parameters and the values have been assigned to properties.

    ShouldRender Override this method to indicate if the component should render.

    StateHasChanged Call this method to cause the component to re-render.

    Table 16.1: Useful methods to override in ComponentBase

    Blazor components can have shared layouts in a similar way to MVC views and Razor Pages. You would create a .razor component file and make it explicitly inherit from

    LayoutComponentBase , as shown in the following markup:

    @inherits LayoutComponentBase

    ...

    @Body

    ...

    The base class has a property named Body that you can render in the markup at the correct place within the layout.You can set a default layout for components in the App.razor file and its Router component. To explicitly set a layout for a component, use the @layout directive, as shown in the following markup:

    @page "/customers" @layout AlternativeLayout

    ...

    Running the Blazor Web App project template‌

    Now that we have reviewed the project template and the important parts that are specific to Blazor Server, we can start the website and review its behavior:

    1. In the Properties folder, in launchSettings.json , for the https profile, modify the applicationUrl to use port 5161 for HTTPS and port 5160 for HTTP, as shown highlighted in the following markup:

    "applicationUrl": "https://localhost:5161;http://localhost:5160",

    Start the Northwind.Blazor project using the https launch profile.

    Start Chrome and navigate to https://localhost:5161/ .

    In the left navigation menu, click Weather, as shown in Figure 16.1:

    image

    Figure 16.1: Fetching weather data into a Blazor Web App project

    Close Chrome and shut down the web server.

    Building components using Blazor‌

    In this section, we will build a component to list, create, and edit customers in the Northwind database. We will build it over several steps:

    Make a Blazor component that renders the name of a country set as a parameter.

    Make it work as a routable page as well as a component.

    Implement the functionality to perform CRUD operations on customers in a database.

    Defining and testing a simple Blazor component‌

    We will add the new component to the existing Blazor Web App project:

    In the Northwind.Blazor project, in the Components\Pages folder, add a new file named Customers.razor . In Visual Studio 2022, the project item template is named Razor Component. In JetBrains Rider, the project item template is named Blazor Component.

    Good Practice: Blazor component filenames must start with an uppercase letter, or you will have compile errors!

    Add statements to output a heading for the Customers component and define a code block that defines a property to store the name of a country, as shown highlighted in the following markup:

    Customers @(string.IsNullOrWhiteSpace(Country)

    ? "Worldwide" : "in " + Country)

    @code {

    [Parameter]

    public string? Country { get; set; }

    }

    1. In the Components\Pages folder, in Home.razor , add statements to the bottom of the file to instantiate the Customers component twice, once setting Germany as the Country parameter, and once without setting the country, as shown in the following markup:

    Start the Northwind.Blazor project using the https launch profile.

    Start Chrome, navigate to https://localhost:5161/ , and note the Customers components, as shown in Figure 16.2:

    image

    Figure 16.2: The Customers components with the Country parameter set to Germany, and not set

    Close Chrome and shut down the web server.

    Using Bootstrap icons‌

    In the older Blazor project templates, they included all Bootstrap icons. In the new project template, only three icons are defined using SVG. Let's see who the team defined those icons, and then add some more for our own use:

    1. In the Components\Layout folder, in NavMenu.razor.css , find the text bi-house , and note the three icons defined using SVG, as partially shown in the following code:

    .bi-house-door-fill {

    background-image: url("data:image/svg+xml,...");

    }

    .bi-plus-square-fill {

    background-image: url("data:image/svg+xml,...");

    }

    .bi-list-nested {

    background-image: url("data:image/svg+xml,...");

    }

    In your favorite browser, navigate to: https://icon-sets.iconify.design/bi/, and note that Bootstrap Icons have an MIT license and contain more than 2000 icons.

    In the Search Bootstrap Icons box, enter globe , and note that six globe icons are found.

    Click the first globe, scroll down the page, click the SVG as data: URI button, and note that you could copy and paste the definition of this icon for use in the CSS stylesheet, but you do not need to because I have already created a CSS file for you to use with five icons defined for you to use in your Blazor project.

    In your favorite browser, navigate to: https://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/Northwind.Blazor/ wwwroot/icons.css, download the file, and save it in your own project in its wwwroot folder.

    In the Components folder, in the App.razor component, in the , add a element to reference the icons.css stylesheet, as shown in the following markup:

    1. Save and close the file.

    Making the component a routable page component‌

    It is simple to turn this component into a routable page component with a route parameter for the country:

    1. In the Components\Pages folder, in the Customers.razor component, add a statement at the top of the file to register /customers as its route with an optional country route parameter, as shown in the following markup:

    @page "/customers/{country?}"

    1. In the Components\Layout folder, in NavMenu.razor , at the bottom of the existing list item elements, add two list item elements for our routable page component to show customers worldwide and in Germany that both use an icon of people, as shown in the following markup:

    In the Components\Pages folder, in Home.razor , remove the two components because we can test them using their navigation menu items from now on and we want to keep the home page as simple as possible.

    Start the Northwind.Blazor project using the https launch profile.

    Start Chrome and navigate to https://localhost:5161/ .

    In the left navigation menu, click Customers in Germany. Note that the country name is correctly passed to the page component and that the component uses the same layout as the other page components, like Home.razor . Also note the URL: https://localhost:5161/customers/Germany .

    Close Chrome and shut down the web server.

    Getting entities into a component‌

    Now that you have seen the minimum implementation of a component, we can add some useful functionality to it. In this case, we will use the Northwind database context to fetch customers from the database:

    1. In Northwind.Blazor.csproj , add a reference to the Northwind database context project for either SQL Server or SQLite, and globally import the namespace for working with Northwind entities, as shown in the following markup:

    Build the Northwind.Blazor project.

    In Program.cs , before the call to Build , add a statement to register the Northwind database context in the dependency services collection, as shown in the following code:

    builder.Services.AddNorthwindContext();

    Abstracting a service for a Blazor component‌

    We could implement the Blazor component so that it directly calls the Northwind database context to fetch the customers using an entity model. This would work if the Blazor component executes on the server. But if the component ran in the browser using WebAssembly, then it would not work.We will now create a local dependency service to enable better reuse of the components:

    image

    Use your preferred coding tool to add a new project, as defined in the following list: Project template: Class Library / classlib

    image

    Project file and folder: Northwind.Blazor.Services

    image

    Solution file and folder: PracticalApps

    In the Northwind.Blazor.Services.csproj project file, add a project reference to the Northwind entity models library, as shown in the following markup:

    Build the Northwind.Blazor.Services project.

    In the Northwind.Blazor.Services project, rename Class1.cs to INorthwindService.cs .

    In INorthwindService.cs , define a contract for a local service that abstracts CRUD operations, as shown in the following code:

    using Northwind.EntityModels; // To use Customer. namespace Northwind.Blazor.Services;

    public interface INorthwindService

    {

    Task> GetCustomersAsync(); Task> GetCustomersAsync(string country); Task GetCustomerAsync(string id); Task CreateCustomerAsync(Customer c); Task UpdateCustomerAsync(Customer c);

    Task DeleteCustomerAsync(string id);

    }

    1. In the Northwind.Blazor.csproj project file, add a project reference to the services library, as shown highlighted in the following markup:

    Build the Northwind.Blazor project.

    In the Northwind.Blazor project, add a new folder named Services .

    In the Services folder, add a new file named NorthwindServiceServerSide.cs and modify its contents to implement the INorthwindService interface by using the Northwind database context, as shown in the following code:

    using Microsoft.EntityFrameworkCore; // To use ToListAsync. namespace Northwind.Blazor.Services;

    public class NorthwindServiceServerSide : INorthwindService

    {

    private readonly NorthwindContext _db;

    public NorthwindServiceServerSide(NorthwindContext db)

    {

    _db = db;

    }

    public Task> GetCustomersAsync()

    {

    return _db.Customers.ToListAsync();

    }

    public Task> GetCustomersAsync(string country)

    {

    return _db.Customers.Where(c => c.Country == country).ToListAsync();

    }

    public Task GetCustomerAsync(string id)

    {

    return _db.Customers.FirstOrDefaultAsync (c => c.CustomerId == id);

    }

    public Task CreateCustomerAsync(Customer c)

    {

    _db.Customers.Add(c);

    _db.SaveChangesAsync(); return Task.FromResult(c);

    }

    public Task UpdateCustomerAsync(Customer c)

    {

    _db.Entry(c).State = EntityState.Modified;

    _db.SaveChangesAsync(); return Task.FromResult(c);

    }

    public Task DeleteCustomerAsync(string id)

    {

    Customer? customer = _db.Customers.FirstOrDefaultAsync (c => c.CustomerId == id).Result;

    if (customer == null)

    {

    return Task.CompletedTask;

    }

    else

    {

    _db.Customers.Remove(customer); return _db.SaveChangesAsync();

    }

    }

    }

    1. In Program.cs , import the namespace for our service, as shown in the following code:

    using Northwind.Blazor.Services; // To use INorthwindService.

    1. In Program.cs , before the call to Build , add a statement to register NorthwindServiceServerSide as a transient service that implements the INorthwindService interface, as shown in the following code:

    builder.Services.AddTransient();

    A transient service is one that creates a new instance for each request. You can read more about the different lifetimes for services at the following link: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#service- lifetimes.

    1. In the Components folder, in _Imports.razor , import the namespace for working with the Northwind entities and our service so that Blazor components that we build do not need to import the namespaces individually, as shown in the following markup:

    @using Northwind.Blazor.Services @* To use INorthwindService. *@ @using Northwind.EntityModels @* To use Northwind entities. *@

    The _Imports.razor file only applies to .razor files. If you use code-behind .cs files to implement component code, then they must have namespaces imported separately or use global usings to implicitly import the namespaces.

    1. In the Components\Pages folder, in Customers.razor , add statements to inject the service and then use it to output a table of all customers using synchronous database operations, as shown highlighted in the following code:

    @page "/customers/{country?}" @inject INorthwindService _service

    Customers @(string.IsNullOrWhiteSpace(Country)

    ? "Worldwide" : "in " + Country)

    @if (customers is null)

    {

    Loading...

    }

    else

    {

    @foreach (Customer c in customers)

    {

    }

    Id Company Name Address Phone
    @c.CustomerId @c.CompanyName

    @c.Address
    @c.City
    @c.PostalCode
    @c.Country

    @c.Phone

    }

    @code { [Parameter]

    public string? Country { get; set; } private IEnumerable? customers;

    protected override async Task OnParametersSetAsync()

    {

    if (string.IsNullOrWhiteSpace(Country))

    {

    customers = await _service.GetCustomersAsync();

    }

    else

    {

    customers = await _service.GetCustomersAsync(Country);

    }

    }

    }

    Start the Northwind.Blazor project using the https launch profile.

    Start Chrome and navigate to https://localhost:5161/ .

    In the left navigation menu, click Customers in Germany, and note that the table of customers loads from the database and renders in the web page, as shown in Figure 16.3:

    image

    Figure 16.3: The list of customers in Germany

    In the browser address bar, change Germany to UK , and note that the table of customers is filtered to only show UK customers.

    In the left navigation menu, click Customers Worldwide, and note that the table of customers is unfiltered by country.

    Click any of the edit or delete buttons and note that they return a message saying Error: 404 because we have not yet implemented that functionality, and note the link, for example, to edit a customer identified by the five-character Id : https://localhost:5161/editcustomer/ALFKI .

    Close Chrome and shut down the web server.

    Enabling streaming rendering‌

    Now, let's improve the rendering of the customers table by enabling it to happen after the page has appeared to the visitor. We are already using an asynchronous operation to fetch the data, but this operation must finish before the web page response is sent back to the browser. This is why we never see the Loading… message on the page. To see it, we must enable streaming rendering. But if you are fetching data from a local database it might still happen too quickly, so to make sure we see it, we will also slow down the fetching of the data by adding a delay:

    1. In the Components\Pages folder, at the top of Customers.razor , add an attribute to enable streaming rendering, as shown in the following code:

    @attribute [StreamRendering(true)]

    1. In Customers.razor , in the OnParametersSetAsync method, add a statement to asynchronously delay for one second, as shown highlighted in the following code:

    protected override async Task OnParametersSetAsync()

    {

    await Task.Delay(1000); // Delay for one second.

    ...

    }

    Start the Northwind.Blazor project using the https launch profile.

    Start Chrome and navigate to https://localhost:5161/ .

    In the left navigation menu, click Customers in Germany, and note that the Loading… message appears for a second, and then is replaced by the table of customers.

    So far, the component provides only a read-only table of customers. Now, we will extend it with full CRUD operations.

    Defining forms using the EditForm component‌

    Microsoft provides ready-made components for building forms. We will use them to provide create, edit, and delete functionality for customers.Microsoft provides the EditForm component and several form elements such as InputText to make it easier to use forms with Blazor. EditForm can have a model set to bind it to an object with properties and event handlers for custom validation, as well as to recognize standard Microsoft validation attributes on the model class, as shown in the following code:

    @code {

    private Customer customer = new(); private void ExtraValidation()

    {

    // Perform any extra validation you want.

    }

    }

    As an alternative to a ValidationSummary component, you can use the ValidationMessage component to show a message next to an individual form element. To bind the validation message to a property, you use a lambda expression to select the property, as shown in the following code:

    Building a customer detail component‌

    Next, we will create a component to show the details of a customer. This will only be a component, never a page:

    In the Northwind.Blazor project, in the Components folder, create a new file named CustomerDetail.razor . (The Visual Studio 2022 project item template is named Razor Component. The JetBrains Rider project item template is named Blazor Component.)

    Modify its contents to define a form to edit the properties of a customer, as shown in the following markup:

    @code {

    [Parameter]

    public Customer Customer { get; set; } = null!; [Parameter]

    public string ButtonText { get; set; } = "Save Changes"; [Parameter]

    public string ButtonStyle { get; set; } = "info"; [Parameter]

    public EventCallback OnValidSubmit { get; set; }

    }

    Building customer create, edit, and delete components‌

    Now we can create three routable page components that use the component:

    In the Components\Pages folder, create a new file named CreateCustomer.razor .

    In CreateCustomer.razor , modify its contents to use the customer detail component to create a new customer, as shown in the following code:

    @page "/createcustomer"

    @inject INorthwindService _service @inject NavigationManager _navigation

    Create Customer

    @code {

    private Customer customer = new(); private async Task Create()

    {

    await _service.CreateCustomerAsync(customer);

    _navigation.NavigateTo("customers");

    }

    }

    1. In the Components\Pages folder, in Customers.razor , after the

    element, add a

    element with a button to navigate to the create customer page component, as shown in the following markup:

    1. In the Components\Pages folder, create a new file named EditCustomer.razor and modify its contents to use the customer detail component to edit and save changes to an existing customer, as shown in the following code:

    @page "/editcustomer/{customerid}" @inject INorthwindService _service @inject NavigationManager _navigation

    Edit Customer

    @code { [Parameter]

    public string CustomerId { get; set; } = null!; private Customer? customer = new();

    protected override async Task OnParametersSetAsync()

    {

    customer = await _service.GetCustomerAsync(CustomerId);

    }

    private async Task Update()

    {

    if (customer is not null)

    {

    await _service.UpdateCustomerAsync(customer);

    }

    _navigation.NavigateTo("customers");

    }

    }

    1. In the Components\Pages folder, create a new file named DeleteCustomer.razor and modify its contents to use the customer detail component to show the customer that is about to be deleted, as shown in the following code:

    @page "/deletecustomer/{customerid}" @inject INorthwindService _service @inject NavigationManager _navigation

    Delete Customer

    Warning! This action cannot be undone!

    @code { [Parameter]

    public string CustomerId { get; set; } = null!; private Customer? customer = new();

    protected override async Task OnParametersSetAsync()

    {

    customer = await _service.GetCustomerAsync(CustomerId);

    }

    private async Task Delete()

    {

    if (customer is not null)

    {

    await _service.DeleteCustomerAsync(CustomerId);

    }

    _navigation.NavigateTo("customers");

    }

    }

    Enabling server-side interactions‌

    In our Blazor project, neither server-side nor client-side interactions are enabled. We will enable server-side interactions first so that when we attempt to add a new customer, the validation will run and show error messages:

    1. In Program.cs , at the end of the statement that adds Razor components, add a call to a method to enable server-side interactivity, as shown highlighted in the following code:

    builder.Services.AddRazorComponents()

    .AddServerComponents();

    1. In Program.cs , at the end of the statement that maps Razor components, add a call to a method to enable server-side interactivity, as shown highlighted in the following code:

    app.MapRazorComponents()

    .AddServerRenderMode();

    1. In the Components\Pages folder, in CreateCustomer.razor , at the top of the file, add a declaration to enable server-side rendering, as shown in the following code:

    @attribute [RenderModeServer]

    1. In the Components\Pages folder, in EditCustomer.razor , at the top of the file, add a declaration to enable server-side rendering, as shown in the following code:

    @attribute [RenderModeServer]

    1. In the Components\Pages folder, in DeleteCustomer.razor , at the top of the file, add a declaration to enable server-side rendering, as shown in the following code:

    @attribute [RenderModeServer]

    Testing the customer components‌

    Now we can test the customer components and how to use them to create, edit, and delete customers:

    Start the Northwind.Blazor project using the https launch profile.

    Start Chrome and navigate to https://localhost:5161/ .

    Navigate to Customers in Germany and click the + Create New button.

    Enter an invalid Customer Id like ABCDEF , leave the textbox, and note the validation message, as shown in Figure 16.4:

    image

    Figure 16.4: Creating a new customer and entering an invalid customer ID

    Change Customer Id to ABCDE , enter values for the other textboxes like Alpha Corp ,

    Main Street , and Germany , and then click the Create Customer button.

    When the list of customers appears, click Customers in Germany, scroll down to the bottom of the page to see the new customer.

    On the ABCDE customer row, click the Edit icon button, change the address to something like Upper Avenue , click the Update button, and note that the customer record has been updated.

    On the ABCDE customer row, click the Delete icon button, note the warning, click the

    Delete Customer button, and note that the customer record has been deleted.

    Close Chrome and shut down the web server.

    Enabling client-side execution using WebAssembly‌

    .NET 8 Release Candidate 1 had a Go Live license but the changes to Blazor were so ambitious that the client-side support was not finished in time for the final drafts of this book. We put the final section of this book online so that it could be updated to use the general availability release of .NET 8 which should complete the client-side functionality of Blazor. You can find the final section at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch16-blazor-wasm.md

    Practicing and exploring‌

    Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring this chapter's topics with deeper research.

    Exercise 16.1 – Test your knowledge‌

    Answer the following questions:

    What are the four Blazor render modes, and how are they different?

    In a Blazor Web App project, compared to an ASP.NET Core MVC project, what extra configuration is required?

    Why should you avoid the Blazor Server and Blazor Server Empty project templates?

    In a Blazor Web App project, what does the App.razor file do?

    What is the main benefit of using the component?

    How can you pass a value into a component?

    What is the main benefit of using the component?

    How can you execute some statements when parameters are set?

    How can you execute some statements when a component appears?

    One of the benefits of Blazor is being able to implement client-side components using C# and .NET instead of JavaScript. Does a Blazor component need any JavaScript?

    Exercise 16.2 – Practice by creating a times table component‌

    In the Northwind.Blazor project, create a routable page component that renders a times table based on a parameter named Number and then test your component in two ways.First, by adding an instance of your component to the Home.razor file, as shown in the following markup to generate the 6 times table with a default size of 12 rows or the 7 times table with a size of 10 rows:

    Second, by entering a path in the browser address bar, as shown in the following links:https://localhost:5161/timestable/6https://localhost:5161/timestable/7/10

    Exercise 16.3 – Practice by creating a country navigation item‌

    In the Northwind.Blazor project, in the NavMenu component, call the customer's web service to get the list of country names and loop through them, creating a menu item for each country. For example:

    1. In the Northwind.Blazor project, in INorthwindService.cs , add the following code:

    List GetCountries();

    1. In NorthwindServiceServerSide.cs , add the following code:

    public List GetCountries()

    {

    return _db.Customers.Select(c => c.Country)

    .Distinct().OrderBy(country => country).ToList();

    }

    1. In NavMenu.razor , add the following markup:

    @inject INorthwindService _service

    ...

    @foreach(string? country in _service.GetCountries())

    {

    string countryLink = "customers/" + country;

    }

    You cannot use because Blazor does not allow combined text and @ Razor expressions in components. That is why the code above creates a local variable to do the combining to make the country URL.

    Exercise 16.4 – Enhancing Blazor apps‌

    To learn how to enhance Blazor apps using AOT native publish, interop with JavaScript, and handle location-changing events, you can read an optional online-only section, found at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch16-enhanced- blazor.md

    Exercise 16.5 – Leveraging open source Blazor component libraries‌

    To learn how to use some common Blazor open source components, I have written an online-only section for my Apps and Services with .NET 8 companion book, found at the following link: https://github.com/markjprice/apps-services-net8/blob/main/docs/ch15-blazor-libraries.md.

    Exercise 16.6 – Explore topics‌

    Use the links on the following page to learn more details about the topics covered in this chapter:https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-16--- building-user-interfaces-using-blazor

    Summary‌

    In this chapter, you learned:

    image

    About the concepts of Blazor components.

    image

    How to build Blazor components that execute on the server side.

    image

    image

    How to build Blazor components that execute on the client side using WebAssembly. Some of the key differences between the two hosting models, like how data should be managed using dependency services.

    In the Epilogue, I will make some suggestions for books to take you deeper into C# and .NET.

    Epilogue‌‌‌

    Join our book community on Discord

    https://packt.link/EarlyAccess

    image

    I wanted this book to be different from others on the market. I hope that you found it to be a brisk, fun read, packed with practical, hands-on walk-throughs of each subject. This epilogue contains the following short sections:

    image

    image

    Next steps on your C# and .NET learning journey The ninth edition, coming November 2024

    image

    Good luck!

    Next steps on your C# and .NET learning journey‌

    For subjects that I didn’t have space to include in this book but you might want to learn more about, I hope that the notes, good practice tips, and links in the GitHub repository point you in the right direction: https://github.com/markjprice/cs12dotnet8/blob/main/docs/book

    -links.md

    Polishing your skills with design guidelines‌

    Now that you have learned the fundamentals of developing using C# and .NET, you are ready to improve the quality of your code by learning more detailed design guidelines. Back in the early .NET Framework era, Microsoft published a book that gave good practices in all areas of .NET development. Those recommendations are still very much applicable to modern .NET development. The following topics are covered:

    image

    Naming Guidelines

    image

    image

    image

    Type Design Guidelines Member Design Guidelines Designing for Extensibility

    image

    image

    Design Guidelines for Exceptions Usage Guidelines

    image

    Common Design Patterns

    To make the guidance as easy to follow as possible, the recommendations are simply labeled with the terms Do, Consider, Avoid, and Do not. Microsoft has made excerpts of the book available at the following link:https://learn.microsoft.com/en- us/dotnet/standard/design-guidelines/I strongly recommend that you review all the guidelines and apply them to your code.

    Companion books to continue your learning journey‌

    Soon, I will have written two more books to continue your learning journey with .NET 8 that you started with this fundamentals book. The two other books act as companions to this book, and together they all form a .NET 8 trilogy.

    The first book (the one you're reading now) covers the fundamentals of C#, .NET, and ASP.NET Core for web

    development.

    The second book covers more specialized topics, like internationalization and popular third-party packages including Serilog and Noda Time. You will learn how to build native AOT-compiled services with ASP.NET Core Minimal APIs and how to improve performance, scalability, and reliability using caching, queues, and background services. You will implement more services using GraphQL, gRPC, SignalR, and Azure Functions. Finally, you will learn how to build graphical user interfaces for websites and desktop and mobile apps with Blazor and .NET MAUI.

    The third book covers important tools and skills you should learn to become a well-rounded professional .NET developer. These include design patterns and solution architecture, debugging, memory analysis, all the important types of testing, from unit to performance and web and mobile, and then hosting and deployment topics like Docker and Azure Pipelines. Finally, we will look at how to prepare for an interview to get the .NET developer career that you want.

    A summary of the .NET 8 trilogy and their most important topics is shown in Figure 17.1:

    image

    Figure 17.1: Companion books for learning C# and .NET

    Tools and Skills for .NET 8 Pros is scheduled to be published in the first half of 2024. Look out for it in your favorite bookstore to complete your .NET 8 trilogy.

    image

    To see a list of all the books that I have published with Packt, you can use the following link:https://subscription.packtpub.com/search? query=mark+j.+price

    Other books to take your learning further‌

    If you are looking for other books from my publisher that cover related subjects, there are many to choose from, as shown in Figure 17.2:

    image

    Figure 17.2: Packt books to take your C# and .NET learning further

    You will also find a list of Packt books in the GitHub repository at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs

    /book-links.md#learn-from-other-packt-books

    The ninth edition, coming November 2024‌

    I have already started work identifying areas for improvement for the ninth edition, which we plan to publish with the release of .NET 9 in November 2024. While I do not expect major new features at the level of the unification of Blazor hosting models, I do expect .NET 9 to make worthwhile improvements to all aspects of .NET. You can learn how to use

    .NET 9 with this book at the following link:https://github.com/markjprice/cs12dotnet8/blob/main/docs

    /dotnet9.mdIf you have suggestions for topics that you would

    like to see covered or expanded upon, or you spot mistakes that need fixing in the text or code, then please let me know the details via chat in the Discord channel or the GitHub repository for this book, found at the following link: https://github.com/markjprice/cs12dotnet8

    Good luck!‌

    I wish you the best of luck with all your C# and .NET projects!

    Appendix: Answers to the Test Your Knowledge Questions‌‌‌

    Join our book community on Discord‌

    https://packt.link/EarlyAccess

    image

    This appendix has the answers to the questions in the Test Your Knowledge section at the end of each chapter.

    Chapter 1 – Hello, C#! Welcome, .NET!

    Exercise 1.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. Is Visual Studio 2022 better than Visual Studio Code?

    Answer: No. Each is optimized for different tasks. Visual Studio 2022 for Windows is large, heavyweight, and can create applications with graphical user interfaces, for example, Windows Forms, WPF, UWP, and .NET MAUI apps, but it is only available on Windows. Visual Studio 2022 is an Interactive Development Environment (IDE) rather than a code editor.

    Visual Studio Code is smaller, more lightweight, code-focused, supports many more languages, and is available cross-platform.

    1. Is .NET 5 and later better than .NET Framework?

    Answer: For modern development, yes, but it depends on what you need. .NET 5 and later are modern, cross-platform, performance-oriented versions of the legacy, mature .NET Framework. Modern .NET is more frequently improved. .NET Framework has better support for legacy applications; however, .NET Framework 4.8 will be the last release apart from security and bug fixes. It will never support some language features of C# 8 and later.

    1. What is .NET Standard and why is it still important?

    Answer: .NET Standard defines an API, aka contract, that a .NET platform can implement. The latest versions of .NET Framework, Xamarin, and modern .NET implement .NET Standard 2.0 to provide a single, standard API that developers can target for maximum reuse. .NET Core 3.0 or later implement .NET Standard 2.1, which has some new features not supported by .NET Framework. If you want to create a new class library that supports all .NET platforms, you will need it to be .NET Standard 2.0-compatible.

    1. Why can a programmer use different languages, for example, C# and F#, to write applications that run on .NET?

    Answer: Multiple languages are supported on .NET because each one has a compiler that translates the source code into intermediate language (IL) code. This IL code is then compiled to native CPU instructions at runtime by the Common Language Runtime (CLR).

    1. What is a top-level program and how do you access any command-line arguments?

    Answer: A top-level program is a project that does not need to explicitly define a Program class with a Main method entry point, with a parameter named args , to access any command- line arguments. These are implicitly defined for you so that you can type statements without boilerplate code.

    1. What is the name of the entry point method of a .NET console app and how should it be explicitly declared if you are not using the top-level program feature?

    Answer: The entry point of a .NET console app is the Main method. An optional string array for command-line arguments and a return type of int are recommended, but they are not required. They can be explicitly declared, as shown in the following code:

    public static void Main() // minimum

    public static int Main(string[] args) // recommended

    With .NET 6 and later, it is implicitly declared using the top-level program feature, as shown in the following code:‌

    public static int

    $(String[] args) // compiler-generated

    1. What namespace is the Program class defined in with a top-level program?

    Answer: With a top-level program, the Program class is defined in a null namespace.

    1. Where would you look for help with a C# keyword?

    Answer: The Microsoft Learn website. Specifically, C# keywords are documented at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language- reference/keywords/.

    1. Where would you look first for solutions to common programming problems?

    Answer: https://stackoverflow.com/.

    1. What should you do after getting an AI to write code for you?

    Answer: You should never trust the code written by an AI so test it thoroughly.

    Chapter 2 – Speaking C#

    Exercise 2.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What statement can you type in a C# file to discover the compiler and language version?

    Answer: #error version

    1. What are the two types of comments in C#?

    Answer: The two types of comments in C# are a single-line comment prefixed with // and a multi-line comment starting with /* and ending with */ . There are also XML comments, which are introduced in Chapter 4, Writing, Debugging, and Testing Functions, so I would not expect you to know about them yet.

    1. What is the difference between a verbatim string and an interpolated string ?

    Answer: A verbatim string is prefixed with the @ symbol and each character (except " ) is interpreted as itself; for example, a backslash \ is a backslash \ . An interpolated string is prefixed with the $ symbol and can include expressions surrounded with braces like this:

    {expression} .

    1. Why should you be careful when using float and double values?

    Answer: You should be careful when using float and double values because they are not guaranteed to be accurate, especially when performing equality comparisons.

    1. How can you determine how many bytes a type like double uses in memory?

    Answer: You can determine how many bytes a type like double uses in memory by using the

    sizeof() operator, for example, sizeof(double) .

    1. When should you use the var keyword?

    Answer: You should only use the var keyword to declare local variables when you cannot specify a known type. It is easy to overuse var due to its convenience when initially writing code, but its use can make it harder to maintain code later.

    1. What is the newest syntax to create an instance of a class like XmlDocument ?

    Answer: The newest way to create an instance of a class like XmlDocument is to use a

    target-typed new expression, as shown in the following code:

    XmlDocument doc = new();

    1. Why should you be careful when using the dynamic type?

    Answer: You should be careful when using the dynamic type because the type of object stored in it is not checked until runtime, which can mean runtime exceptions being thrown if you attempt to use a member that does not exist on the type.

    1. How do you right-align a format string ?

    Answer: To right-align a format string , after the index or expression, add a comma and an integer value to specify a column width within which to align the value. Positive integers

    mean right-aligned and negative integers mean left-aligned.

    1. What character separates arguments for a console app?

    Answer: The space character separates arguments for a console app.

    Exercise 2.2 – Test your knowledge of number types‌

    What type would you choose for the following "numbers"?

    1. A person's telephone number.

    Answer: string .

    1. A person's height.

    Answer: float or double .

    1. A person's age.

    Answer: int for best performance on most CPUs or byte (0 to 255) for the smallest size.

    1. A person's salary.

    Answer: decimal .

    1. A book's ISBN.

    Answer: string .

    1. A book's price.

    Answer: decimal .

    1. A book's shipping weight.

    Answer: float or double .

    1. A country's population.

    Answer: uint (0 to about 4 billion).

    1. The number of stars in the universe.

    Answer: ulong (0 to about 18 quadrillion) or System.Numerics.BigInteger (allows an arbitrarily large integer).

    1. The number of employees in each of the small or medium businesses in the United Kingdom (up to about 50,000 employees per business).

    Answer: Since there are hundreds of thousands of small or medium businesses, we need to take memory size as the determining factor, so choose ushort , because it only takes 2 bytes compared to an int , which takes 4 bytes.

    Exercise 2.3 – Practice number sizes and ranges‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter02/Ch02Ex03Numbers

    Chapter 3 – Controlling Flow, Converting Types, and Handling Exceptions‌

    Exercise 3.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What happens when you divide an int variable by 0 ?

    Answer: DivideByZeroException is thrown when dividing an integer or decimal by 0 .

    1. What happens when you divide a double variable by 0 ?

    Answer: The double type contains a special value of Infinity . Instances of floating-point numbers can have the special values of NaN (not a number), or in the case of dividing by 0 , either PositiveInfinity or NegativeInfinity .

    1. What happens when you overflow an int variable, that is, set it to a value beyond its range?

    Answer: It will loop unless you wrap the statement in a checked block, in which case,

    OverflowException will be thrown.

    1. What is the difference between x = y++; and x = ++y; ?

    Answer: In the statement x = y++; , the current value of y will be assigned to x and then y will be incremented, and in the statement x = ++y; , the value of y will be incremented and then the result will be assigned to x .

    1. What is the difference between break , continue , and return when used inside a loop statement?

    Answer: The break statement will end the whole loop and continue executing after the loop, the continue statement will end the current iteration of the loop and continue executing at the start of the loop block for the next iteration, and the return statement will end the current method call and continue executing after the method call.

    1. What are the three parts of a for statement and which of them are required?

    Answer: The three parts of a for statement are the initializer, condition, and incrementer expressions. All three parts are optional. If the condition is missing, then it will loop forever unless you break out of the loop using break , return , or something else.

    1. What is the difference between the = and == operators?

    Answer: The = operator is the assignment operator for assigning values to variables, while the == operator is the equality check operator that returns true or false .

    1. Does the following statement compile? for ( ; ; ) ;

    Answer: Yes. The for statement only requires the semicolons to separate the initializer expression, condition expression, and incrementer expressions. All three expressions are optional, so they can be missing. This for statement will execute the empty ; statement after the closing brace, forever. It is an example of an infinite loop.

    1. What does the underscore _ represent in a switch expression?

    Answer: The underscore _ represents the default return value. It is called a discard because it is used to represent a potential variable that is not needed. It can be used not just in switch expressions but also in other scenarios where a placeholder variable must be declared but its value is not needed, like parameters to lambda expressions and tuple deconstruction.

    1. What interface must an object "implement" to be enumerated over by using the foreach

    statement?

    Answer: An object must "implement" the IEnumerable interface. It must have the correct methods with the correct signatures even if the object does not actually implement the interface.

    Exercise 3.2 – Explore loops and overflow‌

    What will happen if this code executes?

    int max = 500;

    for (byte i = 0; i < max; i++) { WriteLine(i); } Answer:The code will loop forever because the value of i can only be between 0 and 255 . Once i gets incremented beyond 255 , it loops back to 0 and, therefore, will always be less than max ( 500 ).To prevent the infinite loop, you can add a checked statement around the code. This would cause an exception to be thrown after 255 due to the overflow, as shown in the following output: 254 255 System.OverflowException says Arithmetic operation resulted in an overflow. https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter03/Ch03Ex02LoopsAndOverflow Exercise 3.3 – Test your knowledge of operators‌ https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter03/Ch03Ex03Operators 1. What are the values of x and y after the following statements execute? x = 3; y = 2 + ++x; Answer: x is 4 and y is 6 . 1. What are the values of x and y after the following statements execute? x = 3 << 2; y = 10 >> 1;

    Answer: x is 12 and y is 5 .

    1. What are the values of x and y after the following statements execute?

    x = 10 & 8;

    y = 10 | 7;

    Answer: x is 8 and y is 15 .

    Exercise 3.4 – Practice loops and operators‌‌‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter03/Ch03Ex04FizzBuzz

    Exercise 3.5 – Practice exception handling‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter03/Ch03Ex05Exceptions

    Chapter 4 – Writing, Debugging, and Testing Functions

    Exercise 4.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What does the C# keyword void mean?

    Answer: It indicates that a method has no return value.

    1. What are some differences between imperative and functional programming styles?

    Answer: An imperative programming style means writing a sequence of statements that the runtime executes step by step like a recipe. Your code tells the runtime exactly how to perform the task. Do this. Now do that. It has variables meaning that the state can change at any time, including outside the current function. Imperative programming causes side effects, changing the value of some state somewhere in your program. Side effects are tricky to debug. A functional programming style describes what you want to achieve instead of how. It can also be described as declarative. But the most important point is that functional programming languages make all states immutable by default to avoid side effects.

    1. In Visual Studio Code or Visual Studio, what is the difference between pressing F5, Ctrl or Cmd + F5, Shift + F5, and Ctrl or Cmd + Shift + F5?

    Answer: F5 saves, compiles, runs, and attaches the debugger; Ctrl or Cmd + F5 saves, compiles, and runs the application without the debugger attached; Shift + F5 stops the debugger and running application; and Ctrl or Cmd + Shift + F5 restarts the application without the debugger attached.

    1. Where does the Trace.WriteLine method write its output to?

    Answer: Trace.WriteLine writes its output to any configured trace listeners. By default, this includes the terminal or the command line but can be configured to be a text file or any custom listener.

    1. What are the five trace levels?

    Answer: 0 = Off, 1 = Error, 2 = Warning, 3 = Info, and 4 = Verbose.

    1. What is the difference between the Debug and Trace classes?

    Answer: Debug is active only during development. Trace is active during development and after release into production.

    1. When writing a unit test, what are the three “A”s?

    Answer: Arrange, Act, and Assert.

    1. When writing a unit test using xUnit, what attribute must you decorate the test methods with?

    Answer: [Fact] or [Theory] .

    1. What dotnet command executes xUnit tests?

    Answer: dotnet test .

    1. What statement should you use to rethrow a caught exception named ex without losing the stack trace?‌

    Answer: Use throw; . Do not use throw ex; because this will lose stack trace information.

    Exercise 4.2 – Practice writing functions with debugging and unit testing‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter04/Ch04Ex02PrimeFactorsLibht tps://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter04/Ch04Ex02PrimeFactorsTestsht tps://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter04/Ch04Ex02PrimeFactorsApp

    Chapter 5 – Building Your Own Types with Object-Oriented Programming

    Exercise 5.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What are the seven access modifier keywords and combinations of keywords and what do they do?

    Answer: The seven combinations of access modifier keywords and their effects are described in the following list:

    image

    private : This modifier makes a type or member only visible inside the class.

    image

    image

    internal : This modifier makes a type or member only visible inside the same assembly. protected : This modifier makes a type or member only visible inside the class or derived classes.

    image

    internal protected : This modifier makes a type or member only visible inside the class, derived classes, or within the same assembly.

    image

    private protected : This modifier makes a type or member only visible inside the class or derived classes that are within the same assembly.

    image

    public : This modifier makes a type or member visible everywhere.

    image

    file : This modifier makes a type visible only within the same code file. It cannot be applied to a member.

    1. What is the difference between the static , const , and readonly keywords when applied to a type member?

    Answer: The difference between the static , const , and readonly keywords when applied to a type member is described in the following list:

    image

    static : This keyword makes the member shared by all instances and it must be accessed through the type, not an instance of the type.

    image

    const : This keyword makes the field a fixed literal value that must never change because, during the compilation, assemblies that use the field copy the literal value at the time of compilation.

    image

    readonly : This keyword restricts the field so that it can only be assigned to using a constructor or field initializer at runtime.

    1. What does a constructor do?

    Answer: A constructor allocates memory and initializes field values.

    1. Why do you need to apply the [Flags] attribute to an enum type when you want to store combined values?

    Answer: If you don't apply the [Flags] attribute to an enum type when you want to store combined values, then a stored enum value that is a combination will be returned by a call to ToString as the stored integer value, instead of one or more of the comma-separated list of text values.

    1. Why is the partial keyword useful?

    Answer: You can use the partial keyword to split the definition of a type over multiple files.

    1. What is a tuple?‌

    Answer: A tuple is a data structure consisting of multiple parts. It is used when you want to store multiple values as a unit without defining a type for them.

    1. What does the record keyword do?

    Answer: The record keyword defines a data structure that is immutable by default to enable a more functional programming style. Like a class, a record can have properties and methods, but the values of properties can only be set during initialization.

    1. What does overloading mean?

    Answer: Overloading is when you define more than one method with the same method name and different input parameters.

    1. What is the difference between the following two statements? (Do not just say a ">" character!)

    public List Children = new(); public List Children => new();

    Answer: The first statement defines a field named Children and initializes it to an empty list of Person objects. The second statement defines a read-only property named Children that returns an empty list of Person objects. A field is a data storage location that can be referenced. A property is one or a pair of methods that get and/or set a value. The value of a property is often stored in a private field. A read-only property only has a get method that can be implemented using lambda expression syntax.

    1. How do you make a method parameter optional?

    Answer: You make a method parameter optional by assigning a default value to it in the method signature.

    Exercise 5.2 – Practice with access modifiers‌

    The code given in this exercise will cause the compiler to show the following errors:

    Error CS0122 'Car' is inaccessible due to its protection level

    Error CS0122 'Car.Start()' is inaccessible due to its protection level Error CS0122 'Car.Wheels' is inaccessible due to its protection level

    In the class library project, in Car.cs , you would need to apply the public access modifier to the class because the default access modifier for a type is internal , and to the two properties and the method because the default access modifier for a member is private , as shown highlighted in the following code:

    public class Car

    {

    public int Wheels { get; set; } public public bool IsEV { get; set; } public void Start()

    {

    Console.WriteLine("Starting...");

    }

    }

    Chapter 6 – Implementing Interfaces and Inheriting Classes

    Exercise 6.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What is a delegate?

    Answer: A delegate is a type-safe method reference. It can be used to execute any method with a matching signature.

    1. What is an event?

    Answer: An event is a field that is a delegate having the event keyword applied. The keyword ensures that only += and -= are used; this safely combines multiple delegates without replacing any existing event handlers.

    1. How are a base class and a derived class related and how can the derived class access the base class?

    Answer: A derived class (or subclass) is a class that inherits from a base class (or superclass). Inside a derived class, you use the base keyword to access the class that the subclass inherits from.

    1. What is the difference between is and as operators?

    Answer: The is operator returns true if an object can be cast to the type; otherwise, it returns false . The as operator returns a reference to the object if an object can be cast to the type; otherwise, it returns null .

    1. Which keyword is used to prevent a class from being derived from or a method from being overridden?

    Answer: sealed .

    1. Which keyword is used to prevent a class from being instantiated with the new keyword?

    Answer: abstract .

    1. Which keyword is used to allow a member to be overridden?

    Answer: virtual .

    1. What's the difference between a destructor and a deconstruct method?

    Answer: A destructor, also known as a finalizer, must be used to release resources owned by the object. A deconstruct method is a feature of C# 7 or later that allows a complex object to be broken down into smaller parts. It is especially useful when working with tuples.

    1. What are the signatures of the constructors that all exceptions should have?

    Answer: The signatures of the three constructors that all exceptions should have are shown in the following list:

    image

    A constructor with no parameters.

    image

    A constructor with a string parameter, usually named message .

    image

    A constructor with a string parameter, usually named message , and an Exception‌

    parameter, usually named innerException .

    1. What is an extension method, and how do you define one?

    Answer: An extension method is a compiler trick that makes a static method of a static class appear to be one of the members of another type. You define which type you want to extend by prefixing the first parameter of that type in the method with the this keyword.

    Exercise 6.2 – Practice creating an inheritance hierarchy‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter06/Ch06Ex02Inheritance

    Chapter 7 – Packaging and Distributing .NET Types

    Exercise 7.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What is the difference between a namespace and an assembly?

    Answer: A namespace is the logical container of a type. An assembly is the physical container of a type. To use a type, the developer must reference its assembly. Optionally, the developer can import its namespace, or specify the namespace when specifying the type.

    1. How do you reference another project in a .csproj file?

    Answer: You reference another project in a .csproj file by adding a

    element that sets its Include attribute to a path to the reference project file inside an

    element, as shown in the following markup:

    1. What is the benefit of a tool like ILSpy?

    Answer: A benefit of a tool like ILSpy is learning how to write code in C# for the .NET platform by seeing how other packages are written. You should never steal their intellectual property, of course. But it is especially useful to see how the Microsoft developers have implemented key components of the base class libraries. Decompiling can also be useful when calling a third-party library and you need to better understand how it works to call it appropriately.

    1. Which .NET type does the C# float alias represent?

    Answer: System.Single .

    1. When porting an application from .NET Framework to modern .NET, what tool should you run before porting, and what tool could you run to perform much of the porting work?

    Answer: You should use the .NET Portability Analyzer before porting an application from .NET Framework to .NET 6. You could use the .NET Upgrade Assistant to perform much of the porting work.

    1. What is the difference between framework-dependent and self-contained deployments of

    .NET applications?

    Answer: Framework-dependent modern .NET applications require .NET to be installed for an operating system to execute. Self-contained .NET applications include everything necessary to execute on their own.

    1. What is a RID?

    Answer: RID is the acronym for Runtime Identifier. RID values are used to identify target platforms where a .NET application runs.

    1. What is the difference between the dotnet pack and dotnet publish commands?

    Answer: The dotnet pack command creates a NuGet package that could then be uploaded to a NuGet feed like Microsoft's. The dotnet publish command puts the application and its dependencies into a folder for deployment to a hosting system.‌

    1. What types of applications written for .NET Framework can be ported to modern .NET?

    Answer: Console, ASP.NET MVC, ASP.NET Web API, Windows Forms, and Windows Presentation Foundation (WPF) apps.

    1. Can you use packages written for .NET Framework with modern .NET?

    Answer: Yes, if they only call APIs in .NET Standard 2.0.

    Chapter 8 – Working with Common .NET Types

    Exercise 8.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What is the maximum number of characters that can be stored in a string variable?

    Answer: The maximum size of a string variable is 2 GB, or about 1 billion characters, because each character uses 2 bytes due to the internal use of Unicode (UTF-16) encoding for characters in a string .

    1. When and why should you use a SecureString type?

    Answer: The string type leaves text data in the memory for too long and it's too visible. The SecureString type encrypts its text and ensures that the memory is released immediately. For example, in WPF, the PasswordBox control stores its password as a SecureString variable, and when starting a new process, the Password parameter must be a SecureString variable.

    1. When is it appropriate to use a StringBuilder class?

    Answer: When concatenating more than three string variables, you will use less memory and get improved performance using StringBuilder than using the string.Concat method or the + operator.

    1. When should you use a LinkedList class?

    Answer: Each item in a linked list has a reference to its previous and next siblings as well as the list itself. A linked list should be used when items need to be inserted and removed from positions in the list without moving the items in memory.

    1. When should you use a SortedDictionary class rather than a SortedList class?

    Answer: The SortedList class uses less memory than SortedDictionary ; SortedDictionary has faster insertion and removal operations for unsorted data. If the list is populated all at once from sorted data, SortedList is faster than SortedDictionary .

    1. In a regular expression, what does $ mean?

    Answer: In a regular expression, $ represents the end of the input.

    1. In a regular expression, how can you represent digits?

    Answer: In a regular expression, you can represent digit characters using \d or [0-9] .

    1. Why should you not use the official standard for email addresses to create a regular expression to validate a user's email address?

    Answer: The effort is not worth the pain for you or your users. Validating an email address using the official specification doesn't check whether that address exists or whether the person entering the address is its owner.

    1. What characters are output when the following code runs?

    string city = "Aberdeen";‌

    ReadOnlySpan citySpan = city.AsSpan()[^5..^0]; WriteLine(citySpan.ToString());

    Answer: rdeen . ^5.. means the range is 5 characters long. ..^0 means the range ends zero characters in from the right end.

    1. How could you check that a web service is available before calling it?

    Answer: Use the Ping class to call the web service and check the Status of the reply.

    Exercise 8.2 – Practice regular expressions‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter08/Ch08Ex02RegularExpression s

    Exercise 8.3 – Practice writing extension methods‌

    image

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter08/Ch08Ex03NumbersAsWordsLib https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter08/Ch08Ex03NumbersAsWordsTes tshttps://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter08/Ch08Ex03NumbersAsWordsA pp

    Chapter 9 – Working with Files, Streams, and Serialization

    Exercise 9.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What is the difference between using the File class and the FileInfo class?

    Answer: The File class has static methods and it cannot be instantiated. It is best used for one-off tasks such as copying a file. The FileInfo class requires the instantiation of an object that represents a file. It is best used when you need to perform multiple operations on the same file.

    1. What is the difference between the ReadByte method and the Read method of a stream?

    Answer: The ReadByte method returns a single byte each time it is called and the Read method fills a temporary array with bytes up to a specified length. It is generally best to use Read to process multiple bytes at once.

    1. When would you use the StringReader , TextReader , and StreamReader classes?

    Answer:

    image

    image

    StringReader is used for efficiently reading from a string stored in memory. TextReader is an abstract class that StringReader and StreamReader both inherit from for their shared functionality.

    image

    StreamReader is used for reading strings from a stream that can be any type of text file, including XML and JSON.

    1. What does the DeflateStream type do?

    Answer: DeflateStream implements the same compression algorithm as GZIP, but without a cyclical redundancy check; so, although it produces smaller compressed files, it cannot perform integrity checks when decompressing.

    1. How many bytes per character does UTF-8 encoding use?

    Answer: The number of bytes per character used by the UTF-8 encoding depends on the character. Most Western alphabet characters are stored using one byte. Other characters may need two or more bytes.

    1. What is an object graph?

    Answer: An object graph is any set of connected instances of classes that reference each other. For example, a Customer object may have a property named Orders that references a collection of Order instances.

    1. What is the best serialization format to choose for minimizing space requirements?

    Answer: JavaScript Object Notation (JSON) has a good balance between space requirements and practical factors like human readability, but the protocol buffers (Protobuf) serialization format used by the gRPC standard is best for minimizing space requirements.

    1. What is the best serialization format to choose for cross-platform compatibility?

    Answer: There is still an argument for eXtensible Markup Language (XML) if you need maximum compatibility, especially with legacy systems, although JSON is better if you need

    to integrate with web systems, or Protobuf for best performance and minimum bandwidth use.‌

    1. Why is it bad to use a string value like "\Code\Chapter01" to represent a path, and what should you do instead?

    Answer: It is bad to use a string value like "\Code\Chapter01" to represent a path because it assumes that backslashes are used as a folder separator on all operating systems.

    Instead, you should use the Path.Combine method and pass separate string values for each folder, or a string array, as shown in the following code:

    string path = Path.Combine(new[] { "Code", "Chapter01" });

    1. Where can you find information about NuGet packages and their dependencies?

    Answer: You can find information about NuGet packages and their dependencies at the following link: https://www.nuget.org/.

    Exercise 9.2 – Practice serializing as XML‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter09/Ch09Ex02SerializingShapes

    Chapter 10 – Working with Data Using Entity Framework Core

    Exercise 10.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What type would you use for the property that represents a table, for example, the

    Products property of a database context?

    Answer: DbSet , where T is the entity type, for example, Product .

    1. What type would you use for the property that represents a one-to-many relationship, for example, the Products property of a Category entity?

    Answer: ICollection , where T is the entity type, for example, Product .

    1. What is the EF Core convention for primary keys?

    Answer: The property named ID or Id or ClassNameID or ClassNameId is assumed to be the primary key. If the type of that property is any of the following, then the property is also marked as being an IDENTITY column: tinyint , smallint , int , bigint , and guid .

    1. When might you use an annotation attribute in an entity class?

    Answer: You might use an annotation attribute in an entity class when the conventions cannot work out the correct mapping between the classes and tables, for example, if a class name does not match a table name or a property name does not match a column name. You might also define constraints, like a maximum length of characters in a text value or a range of numeric values, by decorating with validation attributes. These can be read by technologies like ASP.NET Core MVC and Blazor to provide automatic validation warnings to users.

    1. Why might you choose Fluent API in preference to annotation attributes?

    Answer: You might choose Fluent API in preference to annotation attributes when you want to keep your entity classes free from extraneous code that is not needed in all scenarios. For example, when creating a .NET Standard 2.0 class library for entity classes, you might want to only use validation attributes so that the metadata can be read by EF Core and by technologies like ASP.NET Core model binding validation and .NET MAUI desktop and mobile apps. However, you might want to use Fluent API to define EF Core-specific functionality like mapping to a different table or column name.

    1. What does a transaction isolation level of Serializable mean?

    Answer: Maximum locks are applied to ensure complete isolation from any other processes working with the affected data.

    1. What does the DbContext.SaveChanges method return?

    Answer: An int value for the number of entities affected.

    1. What is the difference between eager loading and explicit loading?

    Answer: Eager loading means related entities are included in the original query to the database so that they do not have to be loaded later. Explicit loading means related entities are not included in the original query to the database, and they must be explicitly loaded just before they are needed.

    1. How should you define an EF Core entity class to match the following table?‌

    CREATE TABLE Employees( EmpId INT IDENTITY,

    FirstName NVARCHAR(40) NOT NULL,

    Salary MONEY

    )

    Answer: Use the following class:

    public class Employee

    {

    [Column("EmpId")]

    public int EmployeeId { get; set; } [Required]

    [StringLength(40)]

    public string FirstName { get; set; } [Column(TypeName = "money")]

    public decimal? Salary { get; set; }

    }

    1. What benefit do you get from declaring entity navigation properties as virtual ?

    Answer: You can enable lazy loading if you declare entity navigation properties as virtual .

    Exercise 10.2 – Exporting data using different serialization formats‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter10/Ch10Ex02DataSerialization

    Chapter 11 – Querying and Manipulating Data Using LINQ

    Exercise 11.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What are the two required parts of LINQ?

    Answer: A LINQ provider and the LINQ extension methods. You must import the System.Linq namespace to make the LINQ extension methods available and reference a LINQ provider assembly for the type of data that you want to work with.

    1. Which LINQ extension method would you use to return a subset of properties from a type?

    Answer: The Select method allows the projection (aka selection) of properties.

    1. Which LINQ extension method would you use to filter a sequence?

    Answer: The Where method allows filtering by supplying a delegate (or lambda expression) that returns a Boolean to indicate whether the value should be included in the results.

    1. List five LINQ extension methods that perform aggregation.

    Answer: Any five of the following: Max , Min , Count , LongCount , Average , Sum , and

    Aggregate .

    1. What is the difference between the Select and SelectMany extension methods?

    Answer: Select returns exactly what you specify to return. SelectMany checks that the items you have selected are themselves IEnumerable and then breaks them down into smaller parts. For example, if the type you select is a string value (which is IEnumerable ), SelectMany will break each string value returned into its individual char values and combine them into a single sequence.

    1. What is the difference between IEnumerable and IQueryable ? How do you switch between them?

    Answer: The IEnumerable interface indicates a LINQ provider that will execute the query locally like LINQ to Objects. These providers have no limitations but can be less efficient. The IQueryable interface indicates a LINQ provider that first builds an expression tree to represent the query and then converts it into another query syntax before executing it, like Entity Framework Core converts LINQ into SQL statements. These providers sometimes have limitations, like a lack of support for some expressions, and may throw exceptions. You can convert from an IQueryable provider to an IEnumerable provider by calling the AsEnumerable method.

    1. What does the last type parameter T in generic Func delegates like Func

    represent?

    Answer: The last type parameter T in generic Func delegates like Func represents the type of the return value. For example, for Func , the delegate or lambda function used must return a Boolean value.

    1. What is the benefit of a LINQ extension method that ends with OrDefault ?

    Answer: The benefit of a LINQ extension method that ends with OrDefault is that it returns the default value instead of throwing an exception if it cannot return a value. For example,

    calling the First method on a sequence of int values would throw an exception if the collection is empty, but the FirstOrDefault method would return 0 .‌

    1. Why is query comprehension syntax optional?

    Answer: Query comprehension syntax is optional because it is just syntactic sugar. It makes code easier for humans to read but it does not add any additional functionality except the let keyword.

    You can learn more about the let keyword at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/let-clause

    1. How can you create your own LINQ extension methods?

    Answer: Create a static class with a static method, with an IEnumerable parameter prefixed with this , as shown in the following code:

    namespace System.Linq

    {

    public static class MyLinqExtensionMethods

    {

    public static IEnumerable MyChainableExtensionMethod( this IEnumerable sequence)

    {

    // return something IEnumerable

    }

    public static int? MyAggregateExtensionMethod( this IEnumerable sequence)

    {

    // return some int value

    }

    }

    }

    Exercise 11.2 – Practice querying with LINQ‌

    https://github.com/markjprice/cs12dotnet8/tree/main/code/Chapter11/Ch11Ex02LinqQueries

    Chapter 12 – Introducing Web Development Using ASP.NET Core

    Exercise 12.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What was the name of Microsoft's first dynamic server-side executed web page technology, and why is it still useful to know this history today?

    Answer: Active Server Pages (ASP). The name ASP.NET Core derives from ASP, and it is still used in new features like Tag Helpers, as shown in the following markup:

    Home

    1. What are the names of two Microsoft web servers?

    Answer: Kestrel (cross-platform) and Internet Information Services (IIS), which is Windows-only.

    1. What are some differences between a microservice and a nanoservice?

    Answer: A microservice implements more than one function grouped into a small domain and is always running. A nanoservice implements a single function and is not always running, i.e., it can be "serverless."

    1. What is Blazor?

    Answer: Blazor is a .NET-based technology for implementing client-side web user interfaces. It is designed to be used instead of single-page applications (SPAs) like React, Angular, and Vue.

    1. What was the first version of ASP.NET Core that cannot be hosted on .NET Framework?

    Answer: ASP.NET Core 3.0 requires .NET Standard 2.1, which is not supported by .NET Framework.

    1. What is a user agent?

    Answer: A user agent is a client to a web server, for example, a web browser or a search engine web crawler.

    1. What impact does the HTTP request-response communication model have on web developers?

    Answer: Website dynamic code resides on and executes on a web server. The server code cannot trigger communication. A web browser must make an HTTP request to trigger code on the server, which can then generate an HTTP response. This makes updating a web page difficult because it is under the control of the client, not the server, when requests for more data are made.

    1. Name and describe four components of a URL.

    Answer: The scheme determines if you are using HTTP or HTTPS. The domain is the unique address of the computer (or a web farm of servers acting as a single logical computer). The port number is the port on which the server(s) are listening (usually 80 for HTTP and 443 for HTTPS). The path is the relative path to a resource like a folder or file. The query string is for optional parameters passed along with the request. The fragment is an identified element within a resource like a web page.

    1. What capabilities does Developer Tools give you?‌

    Answer: Developer Tools allows you to see every HTTP request and response, view client-side application data like cookies, sessions, and local storage, provides a console for logging, and so on.

    1. What are the three main client-side web development technologies and what do they do?

    Answer: HTML5 (structure), CSS3 (styles), and JavaScript (execute actions).

    Exercise 12.2 – Know your webbreviations‌

    What do the following web abbreviations stand for and what do they do?

    URI (Uniform Resource Identifier) is a unique sequence of characters that identifies a resource.

    URL (Uniform Resource Location) is the address of a unique resource.

    WCF (Windows Communication Foundation) is a framework for building service-oriented applications. It is part of .NET Framework and an open-source implementation named WCF Core is available for modern .NET.

    TLD (Top-Level Domain) is one of the domains at the highest level in the hierarchical

    DNS (Domain Name System) of the internet, for example, packt.com .

    API (Application Programming Interface) is a mechanism for a computer system to allow other computer systems to communicate with it. They should be well-documented.

    SPA (Single-Page Application) is a web app that loads only a single web page and then updates the content dynamically via JavaScript APIs when needed. SPA frameworks include Angular, React, Vue, and Blazor.

    CMS (Content Management System) is a software application that allows users to build and manage a website without having to write code.

    Wasm (WebAssembly) is a binary instruction format and is designed as a compilation target for programming languages like C# for deployment on the web, on the client, and server.

    SASS (Syntactically Awesome Style Sheets) is a CSS preprocessor. SASS has features that don't exist in CSS yet like nesting, mixins, and inheritance that help you write maintainable CSS.

    REST (REpresentational State Transfer) is an architectural style for distributed hypermedia systems. Roy Fielding presented it in 2000 in his famous dissertation.

    In case you missed it (ICYMI): Acronyms and initialisms are types of abbreviation. Learn more at the following link: https://www.rd.com/article/acronym-vs-abbreviation- whats-the-difference/.

    Chapter 13 – Building Websites Using ASP. NET Core Razor Pages

    Exercise 13.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. List six method names that can be specified in an HTTP request.

    Answer: GET , HEAD , POST , PUT , PATCH , and DELETE . Others include TRACE , OPTIONS , and

    CONNECT .

    1. List six status codes and their descriptions that can be returned in an HTTP response.

    Answer: 200 OK, 201 Created, 301 Moved Permanently, 400 Bad Request, 404 Not Found (missing resource), and 500 Internal Server Error. Others include 101 Switching Protocols (e.g., from HTTP to WebSocket), 202 Accepted, 204 No Content, 304 Not Modified, 401 Unauthorized, 403 Forbidden, 406 Not Acceptable (for example, requesting a response format that is not supported by a website), and 503 Service Unavailable.

    1. In ASP.NET Core, what is the Program class used for?

    Answer: In ASP.NET Core, the Program class is where you add and configure dependency services like Razor Pages, MVC, and Entity Framework Core data contexts. It is also where you configure middleware in the request and response pipeline. This might include error handling, security options, static files, default files, and endpoint routing.

    1. What does the acronym HSTS stand for and what does it do?

    Answer: HTTP Strict Transport Security (HSTS) is an opt-in security enhancement. If a website specifies it and a browser supports it, then it forces all communication over HTTPS and prevents the visitor from using untrusted or invalid certificates.

    1. How do you enable static HTML pages for a website?

    Answer: To enable static HTML pages for a website, you must add statements to use default files and then static files (this order is important!), as shown in the following code:

    app.UseDefaultFiles(); // index.xhtml, default.xhtml, and so on app.UseStaticFiles();

    1. How do you mix C# code into the middle of HTML to create a dynamic page?

    Answer: To mix C# code into the middle of HTML to create a dynamic page, you can create a Razor file with the .cshtml file extension, and then prefix any C# expressions with the @ symbol, and for C# statements, wrap them in braces or create a @functions section, as shown in the following markup:

    @page @functions

    {

    public string[] DaysOfTheWeek

    {

    get => System.Threading.Thread.CurrentThread

    .CurrentCulture.DateTimeFormat.DayNames;

    }

    public string WhatDayIsIt

    {

    get => System.DateTime.Now.ToString("dddd");

    }

    }

    Today is @WhatDayIsIt

    Days of the week in your culture

      @{

      // to add a block of statements: use braces foreach (string dayName in DaysOfTheWeek)

      {

    • @dayName
    • }

      }

    1. How can you define shared layouts for Razor Pages?

    Answer: To define shared layouts for Razor Pages, create at least two files: _Layout.cshtml will define the markup for the shared layout, and _ViewStart.cshtml will set the default layout, as shown in the following markup:

    @{

    Layout = "_Layout";

    }

    1. How can you separate the markup from the code-behind in a Razor Page?

    Answer: To separate the markup from the code-behind in a Razor Page, create two files: MyPage.cshtml contains the markup and MyPage.cshtml.cs contains a class that inherits from PageModel . In MyPage.cshtml , set the model to use the class, as shown in the following markup:

    @page

    @model MyProject.Pages.MyPageModel

    1. How do you configure an Entity Framework Core data context for use with an ASP.NET Core website?

    Answer: To configure an Entity Framework Core data context for use with an ASP.NET Core website:

    image

    image

    In the project file, reference the assembly that defines the data context class. In Program.cs or the Startup class, import the namespaces for Microsoft.EntityFrameworkCore and the data context class.

    image

    In the ConfigureServices method or the section of Program.cs that configures services, add a statement that configures the data context with a database connection string for use with a specified database provider, like SQLite or SQL Server, as shown in the following code:

    services.AddDbContext(options => // or UseSqlServer() options.UseSqlite("my database connection string"));

    image

    In the Razor Page model class or @functions section, declare a private field to store the data context and then set it in the constructor, as shown in the following code:

    private MyDataContext db;

    public SuppliersModel(MyDataContext injectedContext)

    {

    db = injectedContext;

    }

    1. How can you reuse Razor Pages with ASP.NET Core 2.2 or later?‌

    Answer: To reuse Razor Pages with ASP.NET Core 2.2 or later, everything related to a Razor page can be compiled into a class library. To create one, enter the following command:

    dotnet new razorclasslib -s

    Exercise 13.4 – Practice building a data-driven web page‌

    My suggested solution can be found at the following links:https://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/Northwind.Web/P ages/Customers.cshtmlhttps://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/ Northwind.Web/Pages/Customers.cshtml.cshttps://github.com/markjprice/cs12dotnet8/blob/main/c ode/PracticalApps/Northwind.Web/Pages/CustomerOrders.cshtmlhttps://github.com/markjprice/cs1 2dotnet8/blob/main/code/PracticalApps/Northwind.Web/Pages/CustomerOrders.cshtml.cs

    Exercise 13.5 – Practice building web pages for functions‌

    My suggested solution can be found at the following links:https://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/Northwind.Web/P ages/Functions.cshtmlhttps://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/ Northwind.Web/Pages/Functions.cshtml.cs

    Chapter 14 – Building Websites Using the Model-View-Controller Pattern

    Exercise 14.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What do the files with the special names _ViewStart and _ViewImports do when created in the Views folder?

    Answer:

    image

    A _ViewStart file contains a block of statements that are executed when the View method is executed, when a controller action method passes a model to a view, for example, to set a default layout.

    image

    A _ViewImports file contains @using statements to import namespaces for all views, to avoid having to add the same import statements at the top of all views.

    1. What are the names of the three segments defined in the default ASP.NET Core MVC route, what do they represent, and which are optional?

    Answer:

    image

    {controller} : For example, /shippers represents a controller class to instantiate, for example, ShippersController . It is optional because it can use the default value: Home .

    image

    {action} : For example, /privacy represents an action method to execute, for example,

    Privacy . It is optional because it can use the default value: Index .

    image

    {id} : For example, /5 represents a parameter in the action method, for example,

    int id . It is optional because it is suffixed with ? .

    1. What does the default model binder do, and what data types can it handle?

    Answer: The default model binder sets parameters in the action method. It can handle the following data types:

    image

    image

    Simple types like int , string , and DateTime , including nullable types. Complex types like Person and Product .

    image

    Collection types like ICollection or IList .

    1. In a shared layout file like _Layout.cshtml , how do you output the content of the current view?

    Answer: To output the content of the current view in a shared layout, call the RenderBody

    method, as shown in the following markup:

    @RenderBody()

    1. In a shared layout file like _Layout.cshtml , how do you output a section that the current view can supply content for, and how does the view supply the contents for that section?

    Answer: To output the content of a section in a shared layout, call the RenderSection method, specifying a name for the section if it is required, as shown in the following markup:

    @RenderSection("Scripts", required: false)

    To define the contents of the section in the view, create a named section, as shown in the following markup:

    @section Scripts

    1. When calling the View method inside a controller's action method, what paths are searched for the view by convention?

    Answer: When calling the View method inside a controller's action method, three paths are searched for the view by default, based on combinations of the names of the controller and action method and a special Shared folder, as shown in the following example output:

    InvalidOperationException: The view 'Index' was not found. The following locations were searched

    /Views/Home/Index.cshtml

    /Views/Shared/Index.cshtml

    /Pages/Shared/Index.cshtml

    This can be generalized as follows:

    image

    /Views/[controller]/[action].cshtml

    image

    /Views/Shared/[action].cshtml

    image

    /Pages/Shared/[action].cshtml

    1. How can you instruct the visitor's browser to cache the response for 24 hours?

    Answer: To instruct the visitor's browser to cache the response for 24 hours, decorate the controller class or action method with the [ResponseCache] attribute, and set the Duration parameter to 86400 seconds and the Location parameter to ResponseCacheLocation.Client .

    1. Why might you enable Razor Pages even if you are not creating any yourself?

    Answer: If you have used features like ASP.NET Core Identity UI, then they require Razor Pages.

    1. How does ASP.NET Core MVC identify classes that can act as controllers?

    Answer: ASP.NET Core MVC identifies classes that can act as controllers by looking to see if the class (or a class that it derives from) is decorated with the [Controller] attribute.

    1. In what ways does ASP.NET Core MVC make it easier to test a website?

    Answer: The Model-View-Controller (MVC) design pattern separates the technical concerns. These consist of:

    image

    The shape of the data (model).

    image

    The executable statements to process the incoming request and outgoing response (controller).

    image

    The generation of the response in a format requested by the user agent like HTML or JSON (view).

    This makes it easier to write unit tests. ASP.NET Core also makes it easy to implement the Inversion-of-Control (IoC) and dependency injection (DI) design patterns to remove dependencies when testing a component like a controller.

    Exercise 14.2 – Practice implementing MVC by implementing a category detail page‌

    https://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/Northwind.Mvc/Control lers/HomeController.cs#L249https://github.com/markjprice/cs12dotnet8/blob/main/code/Practica lApps/Northwind.Mvc/Views/Home/CategoryDetail.cshtml‌

    Exercise 14.4 – Practice unit testing MVC controllers‌

    https://github.com/markjprice/cs12dotnet8/blob/main/code/PracticalApps/Northwind.UnitTests/C ontrollerUnitTests.cs

    Chapter 15 – Building and Consuming Web Services

    Exercise 15.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. Which class should you inherit from to create a controller class for an ASP.NET Core Web API service?

    Answer: To create a controller class for an ASP.NET Core Web API service, you should inherit from ControllerBase . Do not inherit from Controller as you would in MVC, because this class includes methods like View that use Razor files to render HTML that is not needed for a web service.

    1. When configuring an HTTP client, how do you specify the format of data that you prefer in the response from the web service?

    Answer: When configuring an HTTP client, you specify the format of the response that you prefer by adding an Accept header to the HTTP request that specifies the document format you prefer, and if you specify multiple and want to give them different weights, then set quality values between 0.0 and 1.0 , as shown highlighted in the following code:

    builder.Services.AddHttpClient(name: "Northwind.WebApi", configureClient: options =>

    {

    options.BaseAddress = new Uri("https://localhost:5002/"); options.DefaultRequestHeaders.Accept.Add(

    new MediaTypeWithQualityHeaderValue( mediaType: "application/json", quality: 1.0));

    });

    1. What must you do to specify which controller action method will be executed in response to an HTTP request?

    Answer: To specify which controller action method will be executed in response to a request, you must decorate the action method with an attribute. For example, to respond to an HTTP POST request, decorate the action method with [HttpPost] .

    1. What must you do to specify what responses should be expected when calling an action method?

    Answer: To specify what responses should be expected when calling an action method, decorate the action method with the [ProducesResponseType] attribute, as shown in the following code:

    // GET: api/customers/[id]

    [HttpGet("{id}", Name = nameof(Get))] // named route [ProducesResponseType(200, Type = typeof(Cu public IActionResult Get(string id)

    {

    1. List three methods that can be called to return responses with different status codes.

    Answer: Three methods that can be called to return responses with different status codes include:

    image

    image

    Ok : This returns the 200 status code and the object passed to this method in the body. CreatedAtRoute : This returns the 201 status code and the object passed to this method in the body.

    image

    NoContentResult : This returns the 204 status code and an empty body.

    image

    BadRequest : This returns the 400 status code and an optional error message.

    image

    NotFound : This returns the 404 status code and an optional error message.‌

    1. List four ways that you can test a web service.

    Answer: Four ways that you can test a web service include:

    image

    image

    Using a browser to test simple HTTP GET requests. Installing the REST Client extension for Visual Studio Code.

    image

    Installing the Swagger NuGet package in your web service project, enabling Swagger, and then using the Swagger testing user interface.

    image

    Installing the Postman tool from the following link: https://www.postman.com.

    I listed the four above because they are techniques that I covered or mentioned in the chapter. There are, of course, many other ways to test a web service, for example, curl. Give yourself an extra point for any valid additional methods beyond the ones I have listed.

    1. Why should you not wrap your use of HttpClient in a using statement to dispose of it when you are finished even though it implements the IDisposable interface, and what should you use instead?

    Answer: HttpClient is shared, reentrant, and partially thread-safe, so it is tricky to use correctly in many scenarios. You should use HttpClientFactory , which was introduced in .NET Core 2.1.

    1. What are the benefits of HTTP/2 and HTTP/3 compared to HTTP/1.1?

    Answer: HTTP/2 benefits from full multiplexing, which reduces latency, supports request prioritization, and minimizes overhead in the protocol using header compression. HTTP/3 benefits from being based on UDP rather than TCP so any packet loss does not block all streams. HTTP/3 also has 0-RTT support meaning subsequent connections do not repeat the TLS acknowledgement so the client can start requesting data faster.

    1. How can you enable clients to detect if your web service is healthy with ASP.NET Core

    2.2 and later?

    Answer: To enable clients to detect if your web service is healthy, you can install health check APIs including database health checks for Entity Framework Core data contexts. Health checks can be extended to report detailed information back to the client.

    1. What benefits does endpoint routing provide?

    Answer: Endpoint routing provides improved performance of routing and action method selection, and a link generation service.

    Exercise 15.2 – Practice creating and deleting customers with HttpClient‌

    For hints on how to complete this exercise, please see the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch15-exercise-2.md

    Chapter 16 – Building User Interfaces Using Blazor

    Exercise 16.1 – Test your knowledge‌

    Suggested answers to these questions:

    1. What are the four Blazor render modes, and how are they different?

    Answer: The four Blazor render modes are Server-Side, Streaming, Interactive Server, and Interactive WebAssembly:

    image

    Server-Side Rendering (SSR): Executes code on the server side like Razor Pages and MVC do. The complete response is sent to the browser for display to the visitor.

    image

    Streaming Rendering: Executes code on the server side, HTML markup can be displayed in the browser, and while the connection is still open, more markup is sent by the server to update the contents of the page. This improves the experience for the visitor because they see some content while waiting for the rest.

    image

    Interactive Server Rendering: Executes code on the server side, which means the code has full and easy access to server-side resources like databases. This can simplify implementing functionality. Interactive requests are made using SignalR, which is more efficient than a full request. A permanent connection is needed between the browser and server, which limits scalability.

    image

    Interactive WebAssembly Rendering: Executes code on the client side, which means the code only has access to resources within the browser. This can complicate the implementation because a callback to the server must be made whenever new data is required.

    1. In a Blazor Web App project, compared to an ASP.NET Core MVC project, what extra configuration is required?

    Answer: In the section that configures services, you must call AddRazorComponents . To use interactive server rendering, you must also call AddServerComponents . To use interactive client rendering, you must also call AddWebAssemblyComponents . In the section that configures the HTTP pipeline, you must call MapRazorComponents when setting up endpoint mapping to all the components configured in the App.razor file.

    1. Why should you avoid the Blazor Server and Blazor Server Empty project templates?

    Answer: The Blazor Server and Blazor Server Empty project templates are now legacy since they create projects where all the components are rendered on the server and interactions are handled by SignalR. Use the Blazor Web App project template instead and then enable interactive server rendering in the components that need it.

    1. In a Blazor Web App project, what does the App.razor file do?

    Answer: The App.razor file configures a Router used by all Blazor components in the current assembly. For example, it sets a default shared layout for components that match a route and a view to use when no match is found.

    1. What is the main benefit of using the component?

    Answer: The main benefit of using the component is that it integrates with the Blazor routing system, so it can automatically apply a current style to visually indicate when the current route matches the component.

    1. How can you pass a value into a component?

    Answer: You can pass a value into a component by decorating a public property in the component with the [Parameter] attribute and then setting the attribute in the component when using it, as shown in the following code:

    // defining the component @code {

    [Parameter]

    public string ButtonText { get; set; };

    }

    // using the component

    1. What is the main benefit of using the component?

    Answer: The main benefit of using the component is automatic validation messages.

    1. How can you execute some statements when parameters are set?

    Answer: You can execute some statements when parameters are set by defining an

    OnParametersSet method or an OnParametersSetAsync method to handle that event.

    1. How can you execute some statements when a component appears?

    Answer: You can execute some statements when a component appears by defining an

    OnInitialized method or an OnInitializedAsync method to handle that event.

    1. One of the benefits of Blazor is being able to implement client-side components using C# and .NET instead of JavaScript. Does a Blazor component need any JavaScript?

    Answer: Yes, Blazor components need some minimal JavaScript. For Blazor Server, this is provided by the file _framework/blazor.server.js . For Blazor WebAssembly, this is provided by the file _framework/blazor.webassembly.js . Blazor WebAssembly with progressive web application (PWA) also uses a JavaScript service worker file, service-worker.js . JavaScript is also needed to invoke browser and other client-side APIs, like getting the current geolocation or interacting with browser storage and alert dialog boxes.

    Leave a Reply

    Your email address will not be published. Required fields are marked *