C Sharp


Beginners To Experts


The site is under development.

C Saharp (C#)

Chapter 1: Introduction to C# and .NET

1.1 What is C#? History and Use Cases

C# (pronounced "C-sharp") is a modern, object-oriented programming language developed by Microsoft in the early 2000s as part of the .NET initiative. It's widely used for building Windows applications, web services, games (using Unity), and enterprise applications. It combines the power of C++ with the simplicity of Visual Basic.


// C# is often used for Windows desktop apps
// It is also popular in enterprise backend development
// Example of a simple class declaration
public class Product
{
public string Name;
public double Price;
}
Output:
(No console output - this is just a class definition.)

1.2 Setting Up the Environment (Visual Studio, .NET SDK)

To begin C# development, you need to install the .NET SDK and a development environment like Visual Studio. Visual Studio provides a graphical interface, while the .NET CLI allows for building apps via terminal. These tools help write, compile, and debug C# code.


// Steps to set up:
// 1. Download .NET SDK from https://dotnet.microsoft.com
// 2. Install Visual Studio or Visual Studio Code
// 3. Use terminal: dotnet new console -o HelloWorldApp
Output:
A folder HelloWorldApp is created with basic C# code.

1.3 Your First C# Program – Hello World

The classic first program in any language is "Hello World." In C#, it involves writing a static method inside a Program class. The program prints a message to the console.


using System; // Import base library
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!"); // Output message
}
}
Output:
Hello, World!

1.4 Compiling and Running C# Programs

C# programs are typically compiled using the .NET CLI or Visual Studio. Compilation translates C# code into Intermediate Language (IL), which runs on the .NET runtime.


// Using terminal:
// Navigate to project folder
// Run: dotnet build
// Then run: dotnet run
Output:
Build succeeded.
Hello, World!

1.5 Structure of a C# Project

A C# project usually contains a Program.cs file, a .csproj file for configuration, and possibly folders for additional classes or resources. This organization helps manage complexity in large applications.


// Basic structure:
// MyApp/
// ├── Program.cs
// └── MyApp.csproj
Output:
Structure helps the compiler understand how to build your project.

1.6 Understanding the Main Method

The Main method is the entry point of a C# program. It's where the program starts execution. It must be static and can optionally accept string array arguments for command-line inputs.


static void Main(string[] args)
{
Console.WriteLine("Starting program...");
}
Output:
Starting program...

1.7 Comments and Code Organization

C# supports single-line (//) and multi-line (/* */) comments. Proper use of comments improves code readability and maintenance. Code is usually organized using classes and namespaces.


// This is a single-line comment
/*
This is a
multi-line comment
*/
namespace MyApp
{
class Utilities
{
// Helper methods go here
}
}
Output:
(No output — this demonstrates comments and structure.)

Chapter 2: Variables and Data Types

2.1 Declaring and Initializing Variables

In C#, variables are containers for storing data values. Before you use a variable, you must declare it with a specific data type. Initialization means assigning a value at the time of declaration. This helps the compiler allocate the right amount of memory and enforce type safety.

int age = 25;
// Declare and initialize an integer variable
string name = "Alice";
// Declare and initialize a string
bool isStudent = true;
// Declare and initialize a boolean
Console.WriteLine($"Name: {name}, Age: {age}, Student: {isStudent}");
// Output the variables
Output: Name: Alice, Age: 25, Student: True

2.2 Value Types vs Reference Types

Value types hold data directly and are stored on the stack (e.g., int, float, bool). Reference types store references to the data's memory address and are stored on the heap (e.g., string, arrays, classes). Copying a value type duplicates the value, while copying a reference type shares the same object.

int a = 5;
int b = a;
b = 10;
Console.WriteLine($"a: {a}, b: {b}");
// 'a' remains 5
string s1 = "Hello";
string s2 = s1;
s2 = "World";
Console.WriteLine($"s1: {s1}, s2: {s2}");
// 's1' remains "Hello"
Output:
a: 5, b: 10
s1: Hello, s2: World

2.3 Common Data Types (int, float, bool, char, string)

C# provides various built-in data types for storing values: `int` for integers, `float` for decimals, `bool` for true/false, `char` for single characters, and `string` for text. These data types are essential for almost any program.

int score = 90;
float price = 19.99f;
bool isAvailable = true;
char grade = 'A';
string city = "London";
Console.WriteLine($"{score}, {price}, {isAvailable}, {grade}, {city}");
Output: 90, 19.99, True, A, London

2.4 Type Conversion and Casting

Type conversion allows you to change a value from one data type to another. Implicit conversion happens automatically when there's no data loss. Explicit casting is required when converting between incompatible types. You can also use methods like `Convert.ToInt32()` or `ToString()`.

int x = 10;
double y = x; // Implicit conversion
double z = 9.8;
int w = (int)z; // Explicit casting
string s = w.ToString();
Console.WriteLine($"y: {y}, w: {w}, s: {s}");
Output: y: 10, w: 9, s: 9

2.5 Constants and ReadOnly

`const` and `readonly` are used for values that shouldn't change. `const` must be initialized at compile time and cannot change afterward. `readonly` can be assigned at declaration or in the constructor and is used mostly in classes.

const double PI = 3.14159;
// Must be set at compile time
readonly int year;
// Can be set in constructor only
class Circle
{
public const double Pi = 3.14;
}
Console.WriteLine($"Pi: {Circle.Pi}");
Output: Pi: 3.14

2.6 Nullable Types

Nullable types allow value types (like int, bool, etc.) to hold `null`. This is useful when working with databases or optional values. They are declared using `?` after the type, such as `int?`.

int? age = null;
if (age.HasValue)
{
Console.WriteLine($"Age: {age.Value}");
}
else
{
Console.WriteLine("Age not provided.");
}
Output: Age not provided.

2.7 var Keyword and Type Inference

The `var` keyword lets the compiler infer the variable’s type based on the assigned value. It's useful for cleaner code but should be used when the type is obvious. Once assigned, the type is fixed and cannot change.

var name = "John";
// Inferred as string
var age = 30;
// Inferred as int
Console.WriteLine($"Name: {name}, Age: {age}");
Output: Name: John, Age: 30

Chapter 3: Operators and Expressions

Arithmetic Operators

Arithmetic operators are used to perform basic mathematical operations such as addition (+), subtraction (-), multiplication (*), division (/), and modulus (%). These operators work with numerical values and return the result of the specified arithmetic computation.

<script>
// Example: Calculating the total cost of items
let price1 = 15;
// price of item 1
let price2 = 25;
// price of item 2
let total = price1 + price2;
// addition
let difference = price2 - price1;
// subtraction
let product = price1 * price2;
// multiplication
let quotient = price2 / price1;
// division
let remainder = price2 % price1;
// modulus (remainder)
document.write("Total: " + total + "<br>");
document.write("Difference: " + difference + "<br>");
document.write("Product: " + product + "<br>");
document.write("Quotient: " + quotient + "<br>");
document.write("Remainder: " + remainder + "<br>");
</script>

Comparison and Logical Operators

Comparison operators (==, ===, !=, <, >, <=, >=) are used to compare two values. Logical operators (&&, ||, !) combine or invert Boolean values. These are typically used in conditions to control program flow.

<script>
// Example: Checking age for voting eligibility
let age = 20;
let isCitizen = true;
let eligible = (age >= 18) && isCitizen;
// logical AND
let underage = (age < 18) || !isCitizen;
// logical OR and NOT
document.write("Eligible to vote: " + eligible + "<br>");
document.write("Underage or not a citizen: " + underage + "<br>");
</script>

Assignment and Compound Assignment

The assignment operator (=) is used to assign values to variables. Compound assignment operators like +=, -=, *=, /=, and %= simplify expressions by combining assignment with arithmetic.

<script>
// Example: Updating wallet balance
let balance = 100;
balance += 50; // same as balance = balance + 50
balance -= 30; // same as balance = balance - 30
document.write("Updated balance: " + balance + "<br>");
</script>

Increment and Decrement

The increment (++) and decrement (--) operators increase or decrease a variable’s value by one. They can be used as prefix (++x) or postfix (x++), affecting the order of evaluation.

<script>
// Example: Tracking items added to cart
let items = 0;
items++; // increment
items++;
items--; // decrement
document.write("Items in cart: " + items + "<br>");
</script>

Ternary Operator

The ternary operator (condition ? expr1 : expr2) is a shorthand for if-else statements. It evaluates a condition and returns one of two values based on whether the condition is true or false.

<script>
// Example: Check if a number is even or odd
let num = 7;
let result = (num % 2 === 0) ? "Even" : "Odd";
document.write("Number is: " + result + "<br>");
</script>

Operator Precedence

Operator precedence determines the order in which operations are evaluated. Multiplication and division have higher precedence than addition and subtraction. Parentheses can be used to control the evaluation order explicitly.

<script>
// Example: Calculating total with tax
let price = 100;
let taxRate = 0.1;
let totalCost = price + price * taxRate;
// multiplication happens first
let adjustedCost = (price + price) * taxRate;
// parentheses change order
document.write("Total Cost: " + totalCost + "<br>");
document.write("Adjusted Cost: " + adjustedCost + "<br>");
</script>

Expressions and Evaluation

An expression is a combination of values, variables, and operators that JavaScript evaluates to a result. Expressions can be arithmetic, logical, or involve function calls and assignments. Evaluation is the process of computing the result.

<script>
// Example: Calculate discounted price
let originalPrice = 200;
let discount = 20;
let finalPrice = originalPrice - (originalPrice * discount / 100);
// evaluated using arithmetic rules
document.write("Final Price: " + finalPrice + "<br>");
</script>

Chapter 4: Control Structures

1. if, else if, else Statements

The if, else if, and else statements help control decision-making in programs. Based on a condition's truth value, specific code blocks can be executed. This allows for branching logic, where different outcomes are handled differently depending on input or situation.

// Declare a variable for age
let age = 20;

// Check age group using if-else-if
if (age < 13) {
  console.log("You are a child."); // Executes if age is less than 13
} else if (age < 20) {
  console.log("You are a teenager."); // Executes if age is 13 to 19
} else {
  console.log("You are an adult."); // Executes if age is 20 or more
}

Output:
You are an adult.

2. switch-case Control

The switch-case statement offers a cleaner way to handle multiple conditions for the same variable. Instead of writing multiple if-else blocks, you can list out all possible values as "cases" and define actions for each.

let day = "Wednesday";

switch(day) {
  case "Monday":
    console.log("Start of the work week."); // Case for Monday
    break;
  case "Wednesday":
    console.log("Midweek hustle."); // Case for Wednesday
    break;
  case "Friday":
    console.log("Weekend is near!"); // Case for Friday
    break;
  default:
    console.log("Just another day."); // Default case if none match
}

Output:
Midweek hustle.

3. while and do-while Loops

while loops repeat code as long as a condition is true. do-while loops are similar, but they run the code block at least once before checking the condition.

let count = 0;

// while loop runs as long as count < 3
while (count < 3) {
  console.log("While loop count: " + count);
  count++;
}

// do-while loop runs once even if condition is false
do {
  console.log("Do-while loop runs once even if false.");
} while (false);

Output:
While loop count: 0
While loop count: 1
While loop count: 2
Do-while loop runs once even if false.

4. for and foreach Loops

for loops are ideal when you know how many times you want to repeat an action. forEach is great for looping through arrays and performing actions on each element.

// for loop to print numbers
for (let i = 0; i < 3; i++) {
  console.log("For loop iteration: " + i);
}

// forEach loop on an array
let fruits = ["Apple", "Banana", "Cherry"];
fruits.forEach(function(fruit) {
  console.log("Fruit: " + fruit);
});

Output:
For loop iteration: 0
For loop iteration: 1
For loop iteration: 2
Fruit: Apple
Fruit: Banana
Fruit: Cherry

5. break and continue Statements

break exits a loop early when a condition is met. continue skips the current loop iteration and moves to the next one.

for (let i = 0; i < 5; i++) {
  if (i === 2) continue; // Skip iteration when i is 2
  if (i === 4) break;    // Stop loop when i is 4
  console.log("Number: " + i);
}

Output:
Number: 0
Number: 1
Number: 3

6. Nested Loops and Conditions

Loops and conditions can be nested inside one another to handle complex situations like matrices, tables, or multi-level decision-making.

for (let i = 1; i <= 2; i++) {
  for (let j = 1; j <= 3; j++) {
    console.log("i = " + i + ", j = " + j);
  }
}

Output:
i = 1, j = 1
i = 1, j = 2
i = 1, j = 3
i = 2, j = 1
i = 2, j = 2
i = 2, j = 3

7. Best Practices for Control Flow

Use control structures wisely to keep your code readable and maintainable. Some tips:

  • Minimize deep nesting by using early returns.
  • Use meaningful condition names or extract logic into functions.
  • Comment non-obvious logic.
  • Group related conditions together logically.

// Use early return to reduce nesting
function checkLogin(isLoggedIn) {
  if (!isLoggedIn) {
    console.log("Please log in."); // Show message if not logged in
    return;
  }
  console.log("Welcome back!"); // Show message if logged in
}

checkLogin(false);
checkLogin(true);

Output:
Please log in.
Welcome back!

Chapter 5: Methods and Functions

Declaring and Calling Methods

A method is a reusable block of code that performs a specific task. Declaring a method means defining it with a name, return type, and (optionally) parameters. Once declared, a method can be called or invoked to execute the code it contains. This promotes reusability and modularity in your code.

// Declaring a method
void GreetUser() {
    // This line prints a greeting to the console
    Console.WriteLine("Hello, User!");
}

// Calling the method
GreetUser(); // This line invokes the method and displays the greeting

Method Parameters and Return Types

Methods can receive input through parameters and return a result using a return type. Parameters allow data to be passed into methods, and return types allow the method to output a result after processing.

// This method accepts two integers and returns their sum
int Add(int a, int b) {
    // Add the two input values and return the result
    return a + b;
}

// Calling the method
int result = Add(5, 3); // Calls Add with 5 and 3, stores result in 'result'
Console.WriteLine(result); // Output: 8

Named and Optional Parameters

Named parameters let you specify arguments by name rather than position. Optional parameters have default values, so you can omit them during the method call if needed. This adds flexibility and readability to method usage.

// Method with a default value for 'prefix'
void PrintMessage(string message, string prefix = "Info") {
    // Combines prefix and message into a formatted output
    Console.WriteLine($"{prefix}: {message}");
}

// Using only the required parameter
PrintMessage("System started"); // Uses default 'Info' as prefix

// Using both parameters with named arguments
PrintMessage(message: "Warning", prefix: "Alert"); // Overrides default prefix

Method Overloading

Method overloading allows multiple methods to have the same name but different parameter lists. The compiler determines which version to call based on the arguments provided. This improves code clarity and usability.

// First version: accepts an integer
void Show(int num) {
    Console.WriteLine("Integer: " + num);
}

// Second version: accepts a string
void Show(string text) {
    Console.WriteLine("String: " + text);
}

// Calling overloaded methods
Show(42); // Output: Integer: 42
Show("Hello"); // Output: String: Hello

Recursion

Recursion occurs when a method calls itself. Each call works on a smaller problem until a base case is reached. It’s useful for solving problems like factorials, tree traversals, and more. Always define a stopping condition to prevent infinite loops.

// Recursive method to calculate factorial
int Factorial(int n) {
    // Base case: factorial of 1 is 1
    if (n == 1) return 1;
    // Recursive case: n * factorial of n-1
    return n * Factorial(n - 1);
}

// Calling the recursive method
int result = Factorial(5); // Calculates 5 * 4 * 3 * 2 * 1
Console.WriteLine(result); // Output: 120

Pass by Value vs Pass by Reference

In pass by value, a copy of the variable is sent to the method, so changes don't affect the original. In pass by reference, the actual variable is sent, so changes do affect the original. Use ref or out keywords for reference passing.

// Pass by value
void ChangeValue(int x) {
    x = 100; // Only changes the local copy
}

// Pass by reference
void ChangeRef(ref int x) {
    x = 100; // Changes the actual variable
}

int num1 = 5;
ChangeValue(num1);
Console.WriteLine(num1); // Output: 5 (unchanged)

int num2 = 5;
ChangeRef(ref num2);
Console.WriteLine(num2); // Output: 100 (changed)

Static Methods

Static methods belong to the class itself rather than an instance of the class. You can call them using the class name without creating an object. Static methods are commonly used for utility or helper functions.

class Calculator {
    // A static method to return the square of a number
    public static int Square(int x) {
        return x * x;
    }
}

// Calling a static method using the class name
int result = Calculator.Square(4);
Console.WriteLine(result); // Output: 16

Chapter 6: Object-Oriented Programming Basics

1. Classes and Objects

A class is a blueprint that defines attributes (data) and methods (behavior) for creating objects. An object is an instance of a class, meaning it's a real representation based on the class.

public class Car 
{
public string brand; // Field to store the brand of the car

public void Drive() // Method that simulates driving
{
Console.WriteLine("The car is driving.");
}
}

Car myCar = new Car(); // Creating an object of Car class
myCar.brand = "Toyota"; // Assigning a brand to the car
myCar.Drive(); // Calling the Drive method
// Output: The car is driving.

2. Fields and Properties

Fields are variables that store data in a class. Properties provide access to those fields and allow you to define rules using get and set accessors.

public class Person 
{
private string name; // Private field

public string Name // Property to access the name field
{
get { return name; }
set { name = value; }
}
}

Person p = new Person();
p.Name = "Alice"; // Using set accessor
Console.WriteLine(p.Name); // Using get accessor
// Output: Alice

3. Constructors and Destructors

A constructor is a special method that runs when an object is created. A destructor is called when an object is destroyed to clean up resources.

public class Book 
{
public string Title;

public Book(string title) // Constructor
{
Title = title;
}

~Book() // Destructor
{
Console.WriteLine("Book is being destroyed.");
}
}

Book b = new Book("C# Programming");
Console.WriteLine(b.Title);
// Output: C# Programming

4. Access Modifiers

Access modifiers define the visibility of class members. Common ones include public, private, protected, and internal.

public class BankAccount 
{
private double balance; // Private - only accessible within the class

public void Deposit(double amount) // Public - accessible from outside
{
balance += amount;
}

public double GetBalance()
{
return balance;
}
}

BankAccount account = new BankAccount();
account.Deposit(1000);
Console.WriteLine(account.GetBalance());
// Output: 1000

5. this Keyword

The this keyword refers to the current instance of the class. It's useful when local variables have the same name as class fields.

public class Student 
{
private string name;

public Student(string name)
{
this.name = name; // Refers to the field, not the parameter
}

public void DisplayName()
{
Console.WriteLine("Student: " + this.name);
}
}

Student s = new Student("Bob");
s.DisplayName();
// Output: Student: Bob

6. Encapsulation Principles

Encapsulation means hiding the internal state of an object and only exposing what’s necessary using properties or methods.

public class Temperature 
{
private int celsius; // Internal field

public int Celsius
{
get { return celsius; }
set
{
if (value >= -273)
{
celsius = value; // Valid value
}
}
}
}

Temperature t = new Temperature();
t.Celsius = 25;
Console.WriteLine(t.Celsius);
// Output: 25

7. Object Initializers

Object initializers allow you to set properties at the time of object creation using a concise syntax.

public class Animal 
{
public string Name { get; set; }
public int Age { get; set; }
}

Animal dog = new Animal { Name = "Buddy", Age = 5 };
Console.WriteLine("Name: " + dog.Name + ", Age: " + dog.Age);
// Output: Name: Buddy, Age: 5

Chapter 7: Inheritance and Polymorphism

Base and Derived Classes

Inheritance allows a class (derived class) to inherit methods and properties from another class (base class). The derived class can extend or modify the functionality provided by the base class.

        // Base class
        public class Animal
        {
            public string Name { get; set; }

            public void Speak()
            {
                Console.WriteLine("Animal makes a sound");
            }
        }

        // Derived class
        public class Dog : Animal
        {
            public void Bark()
            {
                Console.WriteLine("Woof! Woof!");
            }
        }

        // In the main method
        Dog dog = new Dog();
        dog.Name = "Buddy";   // Inherited property from Animal
        dog.Speak();          // Inherited method from Animal
        dog.Bark();           // Specific method for Dog
    

Output:
Animal makes a sound
Woof! Woof!

Inheritance Syntax and Constructors

In a derived class, constructors from the base class can be called using the base keyword to ensure proper initialization. This is especially useful when the base class requires certain parameters in its constructor.

        // Base class with constructor
        public class Animal
        {
            public string Name { get; set; }

            public Animal(string name)
            {
                Name = name;
                Console.WriteLine("Animal is created");
            }

            public void Speak()
            {
                Console.WriteLine("Animal makes a sound");
            }
        }

        // Derived class with constructor calling base constructor
        public class Dog : Animal
        {
            public Dog(string name) : base(name)
            {
                Console.WriteLine("Dog is created");
            }

            public void Bark()
            {
                Console.WriteLine("Woof! Woof!");
            }
        }

        // In the main method
        Dog dog = new Dog("Buddy");
    

Output:
Animal is created
Dog is created

Method Overriding (virtual/override)

Method overriding allows a derived class to provide its own implementation of a method that is already defined in the base class. This is done using the virtual keyword in the base class and the override keyword in the derived class.

        // Base class with virtual method
        public class Animal
        {
            public virtual void Speak()
            {
                Console.WriteLine("Animal makes a sound");
            }
        }

        // Derived class overriding the method
        public class Dog : Animal
        {
            public override void Speak()
            {
                Console.WriteLine("Woof! Woof!");
            }
        }

        // In the main method
        Animal myAnimal = new Dog();
        myAnimal.Speak();  // Calls the overridden method in Dog
    

Output:
Woof! Woof!

Sealed Classes and Methods

The sealed keyword is used to prevent a class or method from being inherited or overridden. This is useful when you want to prevent further modifications to the class or method.

        // Sealed class cannot be inherited
        public sealed class Animal
        {
            public void Speak()
            {
                Console.WriteLine("Animal makes a sound");
            }
        }

        // Attempting to inherit from a sealed class will cause an error
        // public class Dog : Animal  // Error: 'Animal' is sealed and cannot be inherited
        // {
        // }
    

If you try to derive a class from a sealed class, you will receive a compile-time error.

Polymorphism in Action

Polymorphism allows you to treat objects of different derived types through a reference of their common base type. This enables the use of a single interface to represent different underlying forms (types).

        // Base class
        public class Animal
        {
            public virtual void Speak()
            {
                Console.WriteLine("Animal makes a sound");
            }
        }

        // Derived classes
        public class Dog : Animal
        {
            public override void Speak()
            {
                Console.WriteLine("Woof! Woof!");
            }
        }

        public class Cat : Animal
        {
            public override void Speak()
            {
                Console.WriteLine("Meow!");
            }
        }

        // In the main method
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        myDog.Speak();  // Outputs: Woof! Woof!
        myCat.Speak();  // Outputs: Meow!
    

Output:
Woof! Woof!
Meow!

Type Casting and Type Checking (is, as)

Type casting and type checking are important when dealing with polymorphism. The is keyword is used to check the type of an object, and the as keyword is used for safe casting.

        // Base class
        public class Animal
        {
            public void Speak()
            {
                Console.WriteLine("Animal makes a sound");
            }
        }

        // Derived class
        public class Dog : Animal
        {
            public void Bark()
            {
                Console.WriteLine("Woof! Woof!");
            }
        }

        // In the main method
        Animal myAnimal = new Dog();

        if (myAnimal is Dog)  // Checking if myAnimal is of type Dog
        {
            Dog dog = myAnimal as Dog;  // Safe casting to Dog
            dog.Bark();  // Outputs: Woof! Woof!
        }
    

Output:
Woof! Woof!

Abstract Classes

Abstract classes cannot be instantiated directly. They are meant to be inherited by other classes. Abstract methods in the base class must be overridden in the derived class.

        // Abstract class
        public abstract class Animal
        {
            public string Name { get; set; }

            public abstract void Speak();  // Abstract method, must be implemented by derived class
        }

        // Derived class
        public class Dog : Animal
        {
            public Dog(string name)
            {
                Name = name;
            }

            public override void Speak()
            {
                Console.WriteLine($"{Name} says Woof! Woof!");
            }
        }

        // In the main method
        Animal myDog = new Dog("Buddy");
        myDog.Speak();  // Outputs: Buddy says Woof! Woof!
    

Output:
Buddy says Woof! Woof!

Chapter 8: Interfaces and Dependency Injection

Defining and Implementing Interfaces

In object-oriented programming, an interface is a contract that defines a set of methods (and properties, events) that a class must implement. An interface only contains method signatures and does not provide the implementation itself. A class that implements the interface must provide the implementation for each method defined in the interface.

    // Defining an interface
    public interface IShape
    {
        double GetArea();  // Method signature without implementation
    }

    // Implementing the interface in a class
    public class Circle : IShape
    {
        public double Radius { get; set; }

        // Providing implementation for GetArea method
        public double GetArea()
        {
            return Math.PI * Radius * Radius;
        }
    }

    // Using the class
    var circle = new Circle { Radius = 5 };
    Console.WriteLine(circle.GetArea()); // Output: 78.53981633974483
    

In this example, the interface IShape defines a GetArea method, but does not implement it. The class Circle implements the interface and provides the actual implementation for the GetArea method.

Interface vs Abstract Classes

Both interfaces and abstract classes define a contract for other classes, but they differ in several key aspects:

  • Abstract Classes: Can have method implementations, fields, and constructors. A class can only inherit from one abstract class.
  • Interfaces: Cannot have method implementations (before C# 8.0). A class can implement multiple interfaces.
    // Abstract class example
    public abstract class Shape
    {
        public abstract double GetArea(); // Abstract method, no implementation
        public string Name { get; set; } // Regular property
    }

    // Implementing abstract class in a derived class
    public class Square : Shape
    {
        public double Side { get; set; }

        // Implementing the abstract method
        public override double GetArea()
        {
            return Side * Side;
        }
    }

    // Using the class
    var square = new Square { Side = 4 };
    Console.WriteLine(square.GetArea()); // Output: 16
    

The abstract class Shape contains both abstract methods and regular properties. The derived class Square must implement the abstract method GetArea.

Explicit Interface Implementation

In C#, explicit interface implementation allows you to define methods in a class that are only accessible through the interface type, not through the class type. This is useful when you want to prevent external code from directly accessing interface methods, ensuring they can only be accessed via the interface.

    // Explicit interface implementation
    public interface IDriveable
    {
        void Drive();
    }

    public class Car : IDriveable
    {
        // Explicitly implementing the interface method
        void IDriveable.Drive()
        {
            Console.WriteLine("Car is driving.");
        }
    }

    // Using explicit interface implementation
    var car = new Car();
    ((IDriveable)car).Drive();  // Output: Car is driving.
    

In this example, the Car class implements the IDriveable interface explicitly. The Drive method is only accessible through the interface, not directly from the class.

Using Interfaces for Polymorphism

Interfaces are widely used to achieve polymorphism. With interfaces, you can treat different types in a consistent way as long as they implement the same interface. This enables you to write code that works with any class that implements a particular interface.

    // Defining an interface
    public interface IAnimal
    {
        void Speak();
    }

    // Implementing the interface in different classes
    public class Dog : IAnimal
    {
        public void Speak()
        {
            Console.WriteLine("Bark");
        }
    }

    public class Cat : IAnimal
    {
        public void Speak()
        {
            Console.WriteLine("Meow");
        }
    }

    // Polymorphism in action
    List animals = new List { new Dog(), new Cat() };
    foreach (var animal in animals)
    {
        animal.Speak();  // Output: Bark
                          // Output: Meow
    }
    

The IAnimal interface defines the Speak method, which is implemented by both Dog and Cat. By using the interface, we can treat both objects as IAnimal and call the Speak method polymorphically.

Introduction to Dependency Injection

Dependency Injection (DI) is a design pattern used to implement Inversion of Control (IoC) by passing objects (dependencies) into a class, rather than having the class create the objects itself. This promotes loose coupling and enhances testability.

    // A class that requires a dependency
    public class Car
    {
        private readonly IEngine _engine;

        // Constructor injection: the dependency is passed via the constructor
        public Car(IEngine engine)
        {
            _engine = engine;
        }

        public void Start()
        {
            _engine.Run();
            Console.WriteLine("Car is starting.");
        }
    }

    // Dependency (interface)
    public interface IEngine
    {
        void Run();
    }

    // Concrete implementation of the dependency
    public class V8Engine : IEngine
    {
        public void Run()
        {
            Console.WriteLine("V8 engine is running.");
        }
    }

    // Using Dependency Injection
    var engine = new V8Engine();
    var car = new Car(engine); // The dependency is injected into the Car class
    car.Start();  // Output: V8 engine is running.
                  // Output: Car is starting.
    

In this example, the Car class depends on the IEngine interface. The dependency is injected into the class via the constructor, which makes it easier to swap the engine implementation for testing or changes in requirements.

Constructor and Method Injection

There are two primary ways to inject dependencies in C#: constructor injection and method injection.

  • Constructor Injection: Dependencies are passed through the constructor when the class is instantiated.
  • Method Injection: Dependencies are passed via method parameters.
    // Constructor Injection example (shown earlier)

    // Method Injection example
    public class CarWithMethodInjection
    {
        public void Start(IEngine engine)
        {
            engine.Run();
            Console.WriteLine("Car is starting.");
        }
    }

    // Using Method Injection
    var engine = new V8Engine();
    var car = new CarWithMethodInjection();
    car.Start(engine);  // Output: V8 engine is running.
                        // Output: Car is starting.
    

In the method injection example, the Start method of the CarWithMethodInjection class takes an IEngine as a parameter, providing a different way to inject dependencies.

Built-in DI in .NET Core

.NET Core provides built-in support for Dependency Injection, which can be configured in the Startup.cs file. The DI container is responsible for managing the lifecycle of dependencies.

    // In Startup.cs (ConfigureServices method)
    public void ConfigureServices(IServiceCollection services)
    {
        // Registering a dependency (Transient, Singleton, Scoped)
        services.AddTransient();
        services.AddTransient();
    }

    // Using DI in a controller or other service
    public class HomeController : Controller
    {
        private readonly Car _car;

        // Constructor injection
        public HomeController(Car car)
        {
            _car = car;
        }

        public IActionResult Index()
        {
            _car.Start(); // Output: V8 engine is running.
                          // Output: Car is starting.
            return View();
        }
    }
    

In this example, we register the IEngine and Car classes with the DI container in Startup.cs. .NET Core automatically resolves and injects the dependencies into the controller when it is created.

Chapter 9: Collections and Generics

9.1 Arrays vs Collections

Arrays are fixed-size collections, meaning their size must be defined at the time of creation and cannot be changed. Collections, on the other hand, are more flexible and can dynamically resize. Collections offer many useful methods for adding, removing, and sorting elements, unlike arrays.


// Arrays are static and have a fixed size.
// Collections are dynamic and provide more features for manipulating data.
Output:
(No output — This is a conceptual explanation.)

9.2 List, Dictionary, HashSet, Queue, Stack

These are common types of collections in C#. - List is an ordered collection that allows duplicates. - Dictionary is a collection of key-value pairs. - HashSet is an unordered collection that does not allow duplicates. - Queue represents a First-In-First-Out (FIFO) collection. - Stack represents a Last-In-First-Out (LIFO) collection.


using System;
using System.Collections.Generic;

class CollectionExamples
{
static void Main()
{
// List example
List names = new List { "John", "Jane", "Paul" };
names.Add("Alice");
Console.WriteLine(names[0]); // John

// Dictionary example
Dictionary userRoles = new Dictionary();
userRoles.Add(1, "Admin");
Console.WriteLine(userRoles[1]); // Admin

// HashSet example
HashSet countries = new HashSet { "USA", "Canada", "USA" };
Console.WriteLine(countries.Count); // 2 (duplicates are not allowed)

// Queue example
Queue queue = new Queue();
queue.Enqueue("First");
queue.Enqueue("Second");
Console.WriteLine(queue.Dequeue()); // First

// Stack example
Stack stack = new Stack();
stack.Push("First");
stack.Push("Second");
Console.WriteLine(stack.Pop()); // Second
}
}
Output:
John
Admin
2
First
Second

9.3 Generic Collections (List<T>, Dictionary<TKey, TValue>)

Generic collections allow you to define the data type of the elements, providing type safety and performance benefits. - List<T> stores a collection of items of the same type. - Dictionary<TKey, TValue> stores key-value pairs with specific types for both key and value.


using System;
using System.Collections.Generic;

class GenericCollections
{
static void Main()
{
// Generic List example
List numbers = new List { 1, 2, 3, 4, 5 };
numbers.Add(6);
Console.WriteLine(numbers[2]); // 3

// Generic Dictionary example
Dictionary ageDictionary = new Dictionary();
ageDictionary.Add("John", 30);
Console.WriteLine(ageDictionary["John"]); // 30
}
}
Output:
3
30

9.4 Custom Generic Classes

Custom generic classes allow developers to create data structures or methods that can operate on any data type, providing both flexibility and type safety. The type is specified when creating an instance of the class.


using System;

class Box<T>
{
private T item;
public Box(T item)
{
this.item = item;
}
public T GetItem() { return item; }
}

class Program
{
static void Main()
{
// Using the generic class with different types
Box<int> intBox = new Box<int>(10);
Console.WriteLine(intBox.GetItem()); // 10

Box<string> stringBox = new Box<string>("Hello, Generics!");
Console.WriteLine(stringBox.GetItem()); // Hello, Generics!
}
}
Output:
10
Hello, Generics!

9.5 LINQ with Generics

LINQ (Language Integrated Query) provides a powerful way to query collections, including generic collections. You can filter, sort, and perform various operations on collections using LINQ.


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

class LINQExample
{
static void Main()
{
// LINQ with List<T>
List numbers = new List { 1, 2, 3, 4, 5, 6 };
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
foreach (var num in evenNumbers)
{
Console.WriteLine(num);
}
}
}
Output:
2
4
6

9.6 Covariance and Contravariance

Covariance and contravariance allow for more flexibility in generic types when dealing with inheritance hierarchies. Covariance allows you to use a more derived type, while contravariance allows using a less derived type.


using System;
using System.Collections.Generic;

// Covariance example
class Animal { }
class Dog : Animal { }
class Program
{
static void Main()
{
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // Covariance
}
}
Output:
(No output — this is an example demonstrating covariance.)

9.7 Collection Best Practices

When working with collections, best practices include choosing the appropriate collection type for the task at hand (e.g., List for ordered data, HashSet for unique data) and using the correct data types to ensure type safety and efficiency.


// Always choose the right collection type based on the problem.
// Use List for ordered data, HashSet for unique data, Dictionary for key-value pairs.
// Be mindful of thread-safety when working with collections in multi-threaded environments.
Output:
(No output — this is a conceptual explanation.)

Chapter 10: Exception Handling

try, catch, finally Blocks

In exception handling, the try block is used to write code that may cause an exception. The catch block handles any exceptions thrown in the try block, while the finally block contains code that will always execute, regardless of whether an exception occurred or not.

        try {
            // Code that may cause an exception
            int[] numbers = {1, 2, 3};
            System.out.println(numbers[5]);  // IndexOutOfBoundsException
        } catch (ArrayIndexOutOfBoundsException e) {
            // Handling the exception
            System.out.println("Exception caught: " + e.getMessage());
        } finally {
            // This block will always execute
            System.out.println("This will always execute.");
        }
    

Explanation: - The try block attempts to access an invalid array index, causing an ArrayIndexOutOfBoundsException. - The catch block catches the exception and prints the message. - The finally block executes regardless of the exception, ensuring clean-up or final actions are always performed.

Catching Specific Exceptions

When catching exceptions, it's a good practice to catch specific exceptions rather than a generic Exception. This allows for more precise error handling.

        try {
            String input = "abc";
            int number = Integer.parseInt(input);  // NumberFormatException
        } catch (NumberFormatException e) {
            System.out.println("Invalid number format: " + e.getMessage());
        } catch (Exception e) {
            System.out.println("General exception: " + e.getMessage());
        }
    

Explanation: - The code attempts to parse a string that is not a valid number, which causes a NumberFormatException. - The specific NumberFormatException is caught first, handling this specific error. - The generic Exception block would catch other unexpected exceptions.

throw Keyword and Custom Exceptions

The throw keyword is used to explicitly throw exceptions. You can also create custom exceptions by extending the Exception class.

        class CustomException extends Exception {
            public CustomException(String message) {
                super(message);
            }
        }

        public class Main {
            public static void main(String[] args) {
                try {
                    throw new CustomException("This is a custom exception!");
                } catch (CustomException e) {
                    System.out.println("Caught custom exception: " + e.getMessage());
                }
            }
        }
    

Explanation: - A custom exception CustomException is created by extending the Exception class. - The throw keyword is used to manually throw an instance of CustomException. - The exception is then caught and handled in the catch block.

Exception Propagation

Exception propagation refers to how exceptions are passed from one method to another. If an exception is not caught in the method where it occurs, it propagates up to the calling method.

        public class Main {
            public static void main(String[] args) {
                try {
                    methodA();
                } catch (Exception e) {
                    System.out.println("Exception caught in main: " + e.getMessage());
                }
            }

            public static void methodA() throws Exception {
                methodB();
            }

            public static void methodB() throws Exception {
                throw new Exception("Exception from methodB");
            }
        }
    

Explanation: - The exception is thrown in methodB but not caught there. - The exception propagates to methodA, and then to the main method, where it is caught. - The throws keyword is used to declare that a method might throw an exception.

Using using for IDisposable

The using statement is used to ensure that objects implementing the IDisposable interface are disposed of properly. It is commonly used for managing resources such as file handles or database connections.

        using (StreamReader reader = new StreamReader("file.txt")) {
            String line = reader.ReadLine();
            Console.WriteLine(line);
        }  // Automatically calls Dispose() on reader when done
    

Explanation: - The using statement ensures that StreamReader is disposed of after it is no longer needed, even if an exception occurs. - This prevents resource leaks by automatically releasing resources at the end of the block.

Logging Exceptions

Logging exceptions is crucial for diagnosing and debugging. You can use logging libraries like log4j, SLF4J, or java.util.logging to log exceptions.

        try {
            // Simulating an error
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            // Log the exception
            Logger logger = Logger.getLogger(Main.class.getName());
            logger.log(Level.SEVERE, "Exception occurred: ", e);
        }
    

Explanation: - The code simulates a division by zero, causing an ArithmeticException. - The exception is caught and logged using a logger, which helps to record details about the error for future analysis.

Best Practices for Error Handling

Following best practices for error handling ensures that your application can recover gracefully from errors and maintain a smooth user experience.

  • Catch specific exceptions rather than generic ones.
  • Don't swallow exceptions; always log them or handle them appropriately.
  • Use custom exceptions to provide more context to errors.
  • Ensure that resources are properly disposed of using using or finally blocks.
  • Propagate exceptions when necessary, but make sure they are meaningful to the calling code.

Chapter 11: Delegates and Events

1. Understanding Delegates

A delegate is a type that represents references to methods with a particular parameter list and return type. It is like a pointer to a method that allows you to call methods indirectly, without knowing which method will be called at compile time. Delegates are often used for event handling and callback methods.

        // Delegate declaration
        public delegate void MyDelegate(string message);

        // Method matching the delegate signature
        public static void PrintMessage(string msg)
        {
            Console.WriteLine(msg);
        }

        // Usage of delegate
        MyDelegate del = PrintMessage;
        del("Hello, Delegate!"); // Output: Hello, Delegate!
    

2. Declaring and Using Delegates

To declare a delegate, you define the return type and parameter types. You can then instantiate the delegate and assign methods to it. The delegate can invoke multiple methods, one after the other, when called.

        // Declare a delegate type
        public delegate int AddDelegate(int a, int b);

        // Method matching the delegate
        public static int AddNumbers(int a, int b)
        {
            return a + b;
        }

        // Using the delegate
        AddDelegate add = AddNumbers;
        int result = add(5, 3); // Calls AddNumbers(5, 3)
        Console.WriteLine(result); // Output: 8
    

3. Multicast Delegates

A multicast delegate is a delegate that holds references to multiple methods. When invoked, it calls all the methods in its invocation list. All methods must match the delegate signature.

        // Declare a multicast delegate
        public delegate void MulticastDelegate(string message);

        // Methods matching the delegate signature
        public static void MethodOne(string msg)
        {
            Console.WriteLine("MethodOne: " + msg);
        }

        public static void MethodTwo(string msg)
        {
            Console.WriteLine("MethodTwo: " + msg);
        }

        // Using the multicast delegate
        MulticastDelegate del = MethodOne;
        del += MethodTwo; // Adding MethodTwo to the delegate

        del("Hello, Multicast!"); 
        // Output:
        // MethodOne: Hello, Multicast!
        // MethodTwo: Hello, Multicast!
    

4. Anonymous Methods

An anonymous method allows you to define a method inline, without having to declare a separate method. This is particularly useful for short-lived methods or when passing methods as arguments to other methods or delegates.

        // Declare a delegate
        public delegate void DisplayMessage(string message);

        // Anonymous method
        DisplayMessage display = delegate(string msg)
        {
            Console.WriteLine("Anonymous Method: " + msg);
        };

        display("Hello from Anonymous Method!"); // Output: Anonymous Method: Hello from Anonymous Method!
    

5. Events and Event Handlers

An event is a way to provide notifications to other classes or objects when something of interest occurs. An event handler is a method that responds to an event.

        // Declare a delegate type for the event
        public delegate void EventHandler();

        // Declare an event
        public event EventHandler MyEvent;

        // Method to invoke the event
        public static void TriggerEvent()
        {
            MyEvent?.Invoke(); // Calls all subscribed handlers
        }

        // Subscribe to the event
        MyEvent += () => Console.WriteLine("Event Triggered!");
        TriggerEvent(); // Output: Event Triggered!
    

6. Event Subscription and Unsubscription

You can subscribe to an event by using the += operator and unsubscribe by using the -= operator. Unsubscribing prevents an event from being triggered for that specific event handler.

        // Event declaration
        public delegate void SimpleEventHandler();

        public event SimpleEventHandler OnEventTriggered;

        // Event handler
        public static void EventHandlerMethod()
        {
            Console.WriteLine("The event has been triggered!");
        }

        // Subscribe and unsubscribe from event
        OnEventTriggered += EventHandlerMethod;
        OnEventTriggered(); // Output: The event has been triggered!

        OnEventTriggered -= EventHandlerMethod; // Unsubscribing
        OnEventTriggered(); // No output, as no handlers are subscribed
    

7. Built-in Event Patterns

C# provides built-in event patterns, including the use of the event keyword and the EventHandler delegate. The EventHandler delegate is a generic delegate that can be used for any event, simplifying event creation and handling.

        // Event using built-in EventHandler delegate
        public event EventHandler ButtonClicked;

        // Triggering the event
        public void OnButtonClick()
        {
            ButtonClicked?.Invoke(this, EventArgs.Empty); // Using EventHandler's built-in parameters
        }

        // Subscribe to the event
        ButtonClicked += (sender, e) => Console.WriteLine("Button clicked!");
        OnButtonClick(); // Output: Button clicked!
    

Chapter 12: Lambda Expressions and LINQ

Lambda Expression Basics

A Lambda Expression is an anonymous function that can contain expressions or statements. They are used to create delegates or expression tree types. Lambda expressions are useful for short-lived methods where a full method declaration isn't necessary.

Lambda expressions consist of the following components:

  • The parameter list.
  • The lambda operator (=>).
  • The body of the lambda expression.

Example:

        // A simple lambda expression that takes an integer and returns its square
        Func<int, int> square = x => x * x;
        Console.WriteLine(square(5)); // Output: 25
    

This example demonstrates a simple lambda expression that takes an integer as input and returns its square. The type of the lambda expression is a Func delegate, which is a predefined delegate type that returns a result (in this case, an integer).

Func, Action, Predicate Delegates

In C#, Func, Action, and Predicate are predefined delegate types used with lambda expressions:

  • Func: Represents a method that returns a value. It can have up to 16 input parameters.
  • Action: Represents a method that does not return a value (void). It can have up to 16 input parameters.
  • Predicate: Represents a method that returns a boolean value and takes one parameter.

Example of Func:

        // A Func delegate that adds two integers
        Func<int, int, int> add = (a, b) => a + b;
        Console.WriteLine(add(3, 4)); // Output: 7
    

Example of Action:

        // An Action delegate that prints a message
        Action<string> printMessage = message => Console.WriteLine(message);
        printMessage("Hello, World!"); // Output: Hello, World!
    

Example of Predicate:

        // A Predicate delegate that checks if a number is even
        Predicate<int> isEven = number => number % 2 == 0;
        Console.WriteLine(isEven(4)); // Output: True
    

LINQ Basics (Where, Select)

LINQ (Language Integrated Query) allows you to query collections in a declarative way using syntax integrated with C#. The most commonly used LINQ methods are:

  • Where: Filters elements based on a predicate.
  • Select: Projects each element of a collection to a new form.

Example of Where:

        // A LINQ query that filters numbers greater than 5 from an array
        int[] numbers = { 1, 2, 3, 6, 7, 8 };
        var filteredNumbers = numbers.Where(n => n > 5);
        foreach (var number in filteredNumbers)
        {
            Console.WriteLine(number); // Output: 6, 7, 8
        }
    

Example of Select:

        // A LINQ query that selects the square of each number
        var squaredNumbers = numbers.Select(n => n * n);
        foreach (var square in squaredNumbers)
        {
            Console.WriteLine(square); // Output: 1, 4, 9, 36, 49, 64
        }
    

LINQ Queries vs Method Syntax

LINQ can be written using either query syntax or method syntax. Both produce the same result, but the syntax is different.

Example of Query Syntax:

        // Query syntax to filter even numbers and select their squares
        var query = from n in numbers
                    where n % 2 == 0
                    select n * n;
        foreach (var square in query)
        {
            Console.WriteLine(square); // Output: 4, 36, 64
        }
    

Example of Method Syntax:

        // Method syntax to achieve the same result
        var methodQuery = numbers.Where(n => n % 2 == 0).Select(n => n * n);
        foreach (var square in methodQuery)
        {
            Console.WriteLine(square); // Output: 4, 36, 64
        }
    

Aggregations and Grouping

LINQ also supports aggregation and grouping operations:

  • GroupBy: Groups elements based on a key.
  • Count, Sum, Average, Max, Min: Aggregate functions.

Example of Grouping:

        // Grouping numbers by even and odd
        var groupedNumbers = numbers.GroupBy(n => n % 2 == 0 ? "Even" : "Odd");
        foreach (var group in groupedNumbers)
        {
            Console.WriteLine(group.Key);
            foreach (var number in group)
            {
                Console.WriteLine(number);
            }
        }
        // Output:
        // Even
        // 2
        // 6
        // 8
        // Odd
        // 1
        // 3
        // 7
    

Example of Aggregation:

        // Calculating the sum of all numbers
        int sum = numbers.Sum();
        Console.WriteLine(sum); // Output: 27
    

Joining Collections

LINQ supports joining collections based on a common key, similar to SQL joins.

Example of Join:

        // Two collections to join
        var products = new[]
        {
            new { ProductId = 1, Name = "Apple" },
            new { ProductId = 2, Name = "Banana" }
        };
        var categories = new[]
        {
            new { ProductId = 1, Category = "Fruit" },
            new { ProductId = 2, Category = "Fruit" }
        };

        var joinQuery = from p in products
                        join c in categories on p.ProductId equals c.ProductId
                        select new { p.Name, c.Category };

        foreach (var item in joinQuery)
        {
            Console.WriteLine($"{item.Name}: {item.Category}"); 
            // Output: Apple: Fruit
            //         Banana: Fruit
        }
    

Custom LINQ Extensions

You can extend LINQ with your own custom extension methods. These methods must be static and defined in a static class.

Example of Custom LINQ Extension:

        // Custom extension method for LINQ
        public static class MyExtensions
        {
            public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate)
            {
                foreach (var item in source)
                {
                    if (predicate(item))
                        yield return item;
                }
            }
        }

        // Using the custom extension
        var customFilteredNumbers = numbers.MyWhere(n => n > 5);
        foreach (var number in customFilteredNumbers)
        {
            Console.WriteLine(number); // Output: 6, 7, 8
        }
    

Chapter 13: Asynchronous Programming with async/await

Understanding async and await

The async keyword enables asynchronous programming by allowing a method to run asynchronously without blocking the main thread. The await keyword pauses the method execution until the asynchronous operation completes, making it possible to write asynchronous code in a way that looks synchronous.

Example:


// Using async and await
async function fetchData() {
    console.log("Fetching data...");
    await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate async operation
    console.log("Data fetched!");
}

fetchData(); // Calls the async function

    

In this example, the function fetchData is asynchronous. The await inside it waits for the promise to resolve before continuing. This means the "Fetching data..." message is logged first, and after a 2-second delay, "Data fetched!" is logged.

Task and Task<T>

A Task represents an asynchronous operation that can be awaited. Task<T> is a generic version that returns a result of type T after completion. Tasks allow you to represent operations that return a result.

Example:


// Using Task and Task
async function fetchNumber(): Promise {
    console.log("Fetching number...");
    await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate async operation
    return 42;
}

fetchNumber().then(result => console.log("Fetched number:", result));

    

In this example, fetchNumber is an asynchronous method that returns a Task<number>. The method returns a number after waiting for a simulated delay. The result is logged once the task completes.

Writing Asynchronous Methods

Asynchronous methods are written with the async keyword. Inside the method, you can use await to pause execution until the asynchronous operation finishes. This allows you to avoid using callbacks or events while still performing tasks asynchronously.

Example:


// Async method with await
async function loadData() {
    const data = await fetchDataFromServer(); // Waits for the promise to resolve
    console.log("Data received:", data);
}

// Simulate fetching data
function fetchDataFromServer() {
    return new Promise(resolve => setTimeout(() => resolve("Server Data"), 1500));
}

loadData(); // Calls the async method

    

The loadData method is asynchronous, and it waits for the fetchDataFromServer method to complete. This avoids blocking the program while waiting for the data.

Exception Handling in Async Code

When handling exceptions in asynchronous code, you can use try...catch blocks to catch errors that may occur during the asynchronous operation. This works similarly to how exceptions are handled in synchronous code.

Example:


// Async function with exception handling
async function fetchDataWithError() {
    try {
        const data = await fetchDataFromServer(); // Waits for data
        console.log("Data received:", data);
    } catch (error) {
        console.log("Error fetching data:", error);
    }
}

// Simulate a failing operation
function fetchDataFromServer() {
    return new Promise((_, reject) => setTimeout(() => reject("Server Error"), 1500));
}

fetchDataWithError(); // Calls the async function with error handling

    

In this example, if the fetchDataFromServer function rejects the promise, the error is caught in the catch block, and an error message is logged.

CancellationToken and Task Cancellation

Task cancellation is important in asynchronous programming to allow long-running tasks to be canceled if needed. The CancellationToken is used in .NET to cancel tasks. When the cancellation request is made, the task can check the token and gracefully cancel its operation.

Example:


// Task cancellation with CancellationToken
async function fetchDataWithCancellation(cancellationToken) {
    console.log("Fetching data...");
    for (let i = 0; i < 5; i++) {
        if (cancellationToken.isCancellationRequested) {
            console.log("Task was canceled.");
            return;
        }
        await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate work
        console.log(`Step ${i + 1} complete.`);
    }
    console.log("Data fetched!");
}

const cancellationToken = { isCancellationRequested: false };

// Simulate canceling the task after 3 seconds
setTimeout(() => {
    cancellationToken.isCancellationRequested = true;
}, 3000);

fetchDataWithCancellation(cancellationToken); // Calls the async function

    

Here, we simulate an asynchronous task that checks the cancellationToken for a cancel request. If the cancellation is triggered, the task exits early.

Parallel vs Asynchronous

Asynchronous programming allows tasks to run without blocking the main thread, and multiple tasks can run concurrently. Parallel programming refers to executing tasks simultaneously on multiple threads, often for CPU-bound tasks. While both improve performance, asynchronous programming is generally more efficient for I/O-bound operations.

Example of Asynchronous:


// Asynchronous tasks
async function asyncTask1() {
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log("Async Task 1 finished");
}

async function asyncTask2() {
    await new Promise(resolve => setTimeout(resolve, 500));
    console.log("Async Task 2 finished");
}

async function runAsyncTasks() {
    await asyncTask1();
    await asyncTask2();
}

runAsyncTasks(); // Calls async tasks

    

In this case, both tasks are asynchronous and will execute one after the other, but the second task will start after the first task finishes.

Best Practices in Async Code

Some best practices to follow when writing asynchronous code include:

  • Use async and await to make asynchronous code easier to understand and maintain.
  • Avoid blocking threads; prefer async methods over synchronous ones.
  • Handle exceptions with try...catch to prevent unhandled errors.
  • Use CancellationToken for canceling tasks when needed.
  • Consider performance implications and use asynchronous methods for I/O-bound tasks.

Chapter 14: Working with Files and Streams

1. File and Directory Classes

The .NET Framework provides the File and Directory classes in the System.IO namespace to perform operations on files and directories. These classes allow you to create, delete, move, and access files and directories.

        // Example: Checking if a file exists and creating it if it does not.
        using System;
        using System.IO;

        class Program
        {
            static void Main()
            {
                string filePath = "example.txt";

                // Check if file exists
                if (!File.Exists(filePath))
                {
                    // Create the file if it doesn't exist
                    File.Create(filePath).Dispose(); // Dispose immediately to close the file handle
                    Console.WriteLine("File created: " + filePath);
                }
                else
                {
                    Console.WriteLine("File already exists.");
                }
            }
        }
    

This code checks if the file example.txt exists. If not, it creates the file using File.Create and disposes of the file handle immediately after creation.

2. Reading and Writing Text Files

You can use the StreamReader and StreamWriter classes for reading from and writing to text files. These classes are part of the System.IO namespace.

        // Example: Writing to a text file using StreamWriter.
        using System;
        using System.IO;

        class Program
        {
            static void Main()
            {
                string filePath = "output.txt";

                // Create a StreamWriter to write to the file
                using (StreamWriter writer = new StreamWriter(filePath))
                {
                    writer.WriteLine("Hello, world!");
                    writer.WriteLine("This is a test file.");
                }

                Console.WriteLine("Text written to file.");
            }
        }
    

This code writes two lines of text to output.txt using StreamWriter. The using statement ensures that the file is closed automatically after writing.

        // Example: Reading from a text file using StreamReader.
        using System;
        using System.IO;

        class Program
        {
            static void Main()
            {
                string filePath = "output.txt";

                // Check if the file exists
                if (File.Exists(filePath))
                {
                    using (StreamReader reader = new StreamReader(filePath))
                    {
                        string content = reader.ReadToEnd();
                        Console.WriteLine("File Content: ");
                        Console.WriteLine(content);
                    }
                }
                else
                {
                    Console.WriteLine("File does not exist.");
                }
            }
        }
    

This code reads the content of output.txt using StreamReader and displays it in the console. The code checks if the file exists before attempting to read.

3. Binary Streams

FileStream is used for working with binary files. It allows you to read and write binary data such as images or serialized objects.

        // Example: Writing and reading a binary file using FileStream.
        using System;
        using System.IO;

        class Program
        {
            static void Main()
            {
                string filePath = "binaryData.dat";
                byte[] data = { 1, 2, 3, 4, 5 };

                // Writing binary data to a file
                using (FileStream fs = new FileStream(filePath, FileMode.Create))
                {
                    fs.Write(data, 0, data.Length);
                    Console.WriteLine("Binary data written to file.");
                }

                // Reading binary data from the file
                using (FileStream fs = new FileStream(filePath, FileMode.Open))
                {
                    byte[] buffer = new byte[data.Length];
                    fs.Read(buffer, 0, buffer.Length);
                    Console.WriteLine("Binary data read from file: " + string.Join(", ", buffer));
                }
            }
        }
    

In this code, we write a byte array to a file using FileStream and then read it back. The binary data is displayed as a comma-separated list.

4. Using StreamReader and StreamWriter

The StreamReader and StreamWriter classes are ideal for working with text files. We can specify encodings and handle various text formats.

        // Example: Using StreamReader with encoding.
        using System;
        using System.IO;
        using System.Text;

        class Program
        {
            static void Main()
            {
                string filePath = "encodedText.txt";

                // Writing text with UTF8 encoding
                using (StreamWriter writer = new StreamWriter(filePath, false, Encoding.UTF8))
                {
                    writer.WriteLine("This is an example with UTF8 encoding.");
                }

                // Reading text with UTF8 encoding
                using (StreamReader reader = new StreamReader(filePath, Encoding.UTF8))
                {
                    string content = reader.ReadToEnd();
                    Console.WriteLine("Content: ");
                    Console.WriteLine(content);
                }
            }
        }
    

This code demonstrates writing and reading a file with a specific encoding, namely UTF-8, using StreamWriter and StreamReader.

5. File Compression (GZip)

You can compress and decompress files using the GZipStream class. It allows you to read from and write to compressed files.

        // Example: Compressing and decompressing a file using GZip.
        using System;
        using System.IO;
        using System.IO.Compression;

        class Program
        {
            static void Main()
            {
                string originalFilePath = "sample.txt";
                string compressedFilePath = "sample.gz";

                // Compressing the file
                using (FileStream originalFileStream = new FileStream(originalFilePath, FileMode.Open))
                using (FileStream compressedFileStream = new FileStream(compressedFilePath, FileMode.Create))
                using (GZipStream compressionStream = new GZipStream(compressedFileStream, CompressionMode.Compress))
                {
                    originalFileStream.CopyTo(compressionStream);
                    Console.WriteLine("File compressed.");
                }

                // Decompressing the file
                using (FileStream compressedFileStream = new FileStream(compressedFilePath, FileMode.Open))
                using (FileStream decompressedFileStream = new FileStream("decompressed.txt", FileMode.Create))
                using (GZipStream decompressionStream = new GZipStream(compressedFileStream, CompressionMode.Decompress))
                {
                    decompressionStream.CopyTo(decompressedFileStream);
                    Console.WriteLine("File decompressed.");
                }
            }
        }
    

This example demonstrates compressing a file using GZipStream and decompressing it afterward. The original file is copied into the compression stream, and then it is decompressed to create a new file.

6. File I/O with async/await

Asynchronous file I/O operations can improve performance by allowing the application to continue other work while waiting for file operations to complete. The async and await keywords enable this.

        // Example: Asynchronous file reading and writing.
        using System;
        using System.IO;
        using System.Threading.Tasks;

        class Program
        {
            static async Task Main()
            {
                string filePath = "asyncFile.txt";

                // Asynchronously writing to a file
                await File.WriteAllTextAsync(filePath, "This is an async write operation.");
                Console.WriteLine("Async write completed.");

                // Asynchronously reading from the file
                string content = await File.ReadAllTextAsync(filePath);
                Console.WriteLine("Async read completed: ");
                Console.WriteLine(content);
            }
        }
    

This code demonstrates how to use asynchronous file operations with async and await. The program writes to and reads from a file asynchronously, allowing the application to continue processing other tasks while waiting for the file operations to complete.

7. Exception Handling in File Operations

When working with files, it is important to handle exceptions, such as file not found or access denied errors. Use try-catch blocks to manage errors during file operations.

        // Example: Handling exceptions during file operations.
        using System;
        using System.IO;

        class Program
        {
            static void Main()
            {
                string filePath = "nonExistentFile.txt";

                try
                {
                    // Attempting to read a non-existent file
                    string content = File.ReadAllText(filePath);
                    Console.WriteLine("File content: " + content);
                }
                catch (FileNotFoundException ex)
                {
                    Console.WriteLine("Error: " + ex.Message);
                }
                catch (UnauthorizedAccessException ex)
                {
                    Console.WriteLine("Error: Access denied.");
                }
            }
        }
    

This code demonstrates how to handle file-related exceptions using a try-catch block. It attempts to read a file that doesn't exist, and it catches both FileNotFoundException and UnauthorizedAccessException.

Chapter 15: Advanced OOP Techniques

1. Partial Classes and Partial Methods

Partial classes and methods allow you to split the implementation of a class across multiple files. This is particularly useful in large projects where multiple developers may need to work on the same class without interfering with each other's code.

        // File 1: PartialClassExample.cs
        public partial class MyClass
        {
            public void Greet()
            {
                Console.WriteLine("Hello from the first part of MyClass!");
            }
        }

        // File 2: PartialClassExample.cs
        public partial class MyClass
        {
            public void Farewell()
            {
                Console.WriteLine("Goodbye from the second part of MyClass!");
            }
        }

        // Usage
        MyClass obj = new MyClass();
        obj.Greet();
        obj.Farewell();
    

In this example, the class `MyClass` is split into two parts, each in a different file. Both parts contain different methods, but they are still part of the same class when compiled.

2. Extension Methods

Extension methods allow you to add new functionality to existing types without modifying their source code. This is especially useful for enhancing classes from libraries that you cannot modify.

        // Extension method for the string class
        public static class StringExtensions
        {
            public static bool IsPalindrome(this string str)
            {
                string reversed = new string(str.Reverse().ToArray());
                return str == reversed;
            }
        }

        // Usage
        string word = "madam";
        Console.WriteLine(word.IsPalindrome());  // Outputs: True
    

Here, we created an extension method `IsPalindrome` for the `string` class. This method checks if a given string is the same when reversed.

3. Operator Overloading

Operator overloading allows you to define how operators (like +, -, *, etc.) behave for your custom classes. This can be useful for mathematical or collection-based types.

        public class Complex
        {
            public int Real { get; set; }
            public int Imaginary { get; set; }

            // Overloading the + operator
            public static Complex operator +(Complex c1, Complex c2)
            {
                return new Complex
                {
                    Real = c1.Real + c2.Real,
                    Imaginary = c1.Imaginary + c2.Imaginary
                };
            }
        }

        // Usage
        Complex c1 = new Complex { Real = 1, Imaginary = 2 };
        Complex c2 = new Complex { Real = 3, Imaginary = 4 };
        Complex result = c1 + c2;
        Console.WriteLine($"Real: {result.Real}, Imaginary: {result.Imaginary}");
    

In this example, we overloaded the `+` operator to add two `Complex` numbers. The result is a new `Complex` object with summed real and imaginary parts.

4. Indexers and Custom Indexers

Indexers allow objects of a class to be accessed like arrays. You can define custom indexers to suit your class's needs, such as accessing elements by a key or index.

        public class MyCollection
        {
            private int[] data = new int[5];

            // Indexer to access elements by index
            public int this[int index]
            {
                get { return data[index]; }
                set { data[index] = value; }
            }
        }

        // Usage
        MyCollection collection = new MyCollection();
        collection[0] = 10;
        collection[1] = 20;
        Console.WriteLine(collection[0]);  // Outputs: 10
    

Here, we defined an indexer in the `MyCollection` class, allowing access to its internal array using an index. This makes the collection behave like an array.

5. Reflection and Metadata

Reflection allows you to inspect and interact with the metadata of types at runtime. This can be used to dynamically load types, call methods, or examine properties.

        using System;
        using System.Reflection;

        public class MyClass
        {
            public void Greet()
            {
                Console.WriteLine("Hello, World!");
            }
        }

        // Usage
        MyClass obj = new MyClass();
        MethodInfo method = typeof(MyClass).GetMethod("Greet");
        method.Invoke(obj, null);  // Outputs: Hello, World!
    

In this example, we used reflection to get the `Greet` method from the `MyClass` type and invoked it dynamically at runtime.

6. Attributes and Custom Attributes

Attributes provide a way to attach metadata to your code elements (classes, methods, properties, etc.). You can also define your own custom attributes.

        // Custom attribute definition
        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
        public class DocumentationAttribute : Attribute
        {
            public string Author { get; }
            public string Date { get; }

            public DocumentationAttribute(string author, string date)
            {
                Author = author;
                Date = date;
            }
        }

        // Applying the custom attribute
        [Documentation("John Doe", "2025-04-21")]
        public class MyClass
        {
            public void MyMethod()
            {
                Console.WriteLine("Method executed.");
            }
        }
    

In this example, we defined a custom `DocumentationAttribute` to add author and date information to classes or methods. We then applied this attribute to the `MyClass` class.

7. Dynamic Objects

Dynamic objects in C# are objects whose types are resolved at runtime. This allows for more flexible programming but sacrifices compile-time type checking.

        using System;
        using System.Dynamic;

        dynamic obj = new ExpandoObject();
        obj.Name = "John Doe";
        obj.Age = 30;

        Console.WriteLine($"Name: {obj.Name}, Age: {obj.Age}");
    

Here, we used the `ExpandoObject` to create a dynamic object where properties can be added at runtime. The properties `Name` and `Age` are assigned dynamically and accessed later.

Chapter 16: Working with Databases (ADO.NET & EF Core)

16.1 Introduction to Database Connectivity

In C#, databases are accessed using technologies like ADO.NET and Entity Framework Core (EF Core). ADO.NET is a lower-level data access framework, while EF Core is an Object-Relational Mapper (ORM) that simplifies database operations by mapping C# classes to database tables.


// ADO.NET is widely used for data access in enterprise applications.
// EF Core simplifies database interaction by allowing developers to work with data as C# objects.
Output:
(No output — this is an introduction.)

16.2 Using ADO.NET (SqlConnection, SqlCommand)

ADO.NET provides classes like SqlConnection to connect to SQL databases and SqlCommand to execute SQL queries or stored procedures. It’s a low-level approach for database interaction.


using System.Data.SqlClient;

class DatabaseExample
{
static void Main()
{
using (SqlConnection conn = new SqlConnection("YourConnectionStringHere"))
{
conn.Open();
SqlCommand cmd = new SqlCommand("SELECT * FROM Users", conn);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader[0].ToString());
}
}
}
}
Output:
Output rows from the 'Users' table.

16.3 Creating Models with Entity Framework Core

In EF Core, models are simple C# classes that represent tables in the database. EF Core automatically maps these models to database structures, simplifying data manipulation.


using Microsoft.EntityFrameworkCore;

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}

public class AppDbContext : DbContext
{
public DbSet Users { get; set; }
}
Output:
(No output — this demonstrates model creation.)

16.4 Querying with LINQ in EF Core

EF Core allows querying the database using LINQ (Language Integrated Query). LINQ enables you to write SQL-like queries in C# code, which is then translated to SQL when executed.


using (var context = new AppDbContext())
{
var users = from u in context.Users
where u.Name.Contains("John")
select u;

foreach (var user in users)
{
Console.WriteLine(user.Name);
}
}
Output:
Output names of users whose names contain "John".

16.5 Code-First vs Database-First

With Code-First, you create C# classes and generate the database schema based on them. With Database-First, you start with an existing database and generate models from it.


// Code-First approach: Define C# classes, then use migrations to create database.
// Database-First: Generate classes from an existing database using EF Core's scaffold command.
Output:
(No output — explanation of approach differences.)

16.6 Migrations and Seeding Data

Migrations allow you to evolve the database schema over time as the application’s data model changes. Seeding data populates the database with initial values.


// Create a migration: dotnet ef migrations add InitialCreate
// Apply the migration: dotnet ef database update
// Seeding data example: public void Seed(AppDbContext context) { if (!context.Users.Any()) { context.Users.Add(new User { Name = "Jane Doe", Email = "jane@example.com" }); context.SaveChanges(); } }
Output:
Seeds the Users table with an initial record.

16.7 CRUD Operations

CRUD stands for Create, Read, Update, Delete — the four basic operations for managing data in a database. EF Core allows you to perform these operations easily.


using (var context = new AppDbContext())
{
// CREATE
var newUser = new User { Name = "John Doe", Email = "john@example.com" };
context.Users.Add(newUser);
context.SaveChanges();

// READ
var user = context.Users.FirstOrDefault(u => u.Name == "John Doe");
Console.WriteLine(user.Name);

// UPDATE
user.Name = "John Smith";
context.SaveChanges();

// DELETE
context.Users.Remove(user);
context.SaveChanges();
}
Output:
Create, read, update, and delete operations on the Users table.

Chapter 17: Building APIs with ASP.NET Core

17.1 Introduction to ASP.NET Core

ASP.NET Core is an open-source framework developed by Microsoft for building web applications and APIs. It's lightweight, fast, and cross-platform, making it ideal for creating scalable and high-performance applications. ASP.NET Core supports building RESTful APIs, MVC applications, and real-time apps with SignalR.

  // To create an API, we first need to install ASP.NET Core.
  // Use Visual Studio or Visual Studio Code to create an ASP.NET Core project.
  // We will use .NET CLI commands like 'dotnet new webapi' to generate the project.

17.2 Creating Controllers and Routing

In ASP.NET Core, controllers handle the incoming HTTP requests and contain actions (methods) that perform the necessary operations. Routing is the process of mapping incoming requests to specific controller actions. You can define routes using attributes like `[Route]` and `[HttpGet]`.

  using Microsoft.AspNetCore.Mvc;
// Import necessary namespaces for controller and routing [Route("api/[controller]")]
public class ProductsController : ControllerBase
// Define the controller class with a route {
[HttpGet]
public IActionResult GetAllProducts()
// Define an action method to handle GET requests {
return Ok(new string[] { "Product 1", "Product 2", "Product 3" });
// Return a sample list of products }
}
Output:
A GET request to `http://localhost:5000/api/products` returns `["Product 1", "Product 2", "Product 3"]`.

17.3 Model Binding and Validation

Model binding allows ASP.NET Core to automatically map incoming request data (such as JSON or form data) to C# objects. Validation ensures the data conforms to specific rules before it's processed. You can use attributes like `[Required]` and `[Range]` to validate model properties.

  public class Product
// Define a simple model class {
[Required]
public string Name { get; set; }
// Name is required [Range(0, 1000)]
public decimal Price { get; set; }
// Price must be between 0 and 1000 } [HttpPost]
public IActionResult CreateProduct([FromBody] Product product)
// Accept product data in the request body {
if (!ModelState.IsValid)
// Check if model validation fails {
return BadRequest(ModelState);
// Return a bad request with validation errors }
return CreatedAtAction(nameof(CreateProduct), new { id = 1 }, product);
// Return a 201 Created status with the created product }
Output:
A POST request with invalid data, like missing `Name`, will return a 400 Bad Request.

17.4 Dependency Injection in Web APIs

Dependency Injection (DI) is a design pattern used in ASP.NET Core to manage the dependencies of objects. Services like logging, database access, and email can be injected into controllers, ensuring a clean separation of concerns and easier unit testing.

  public class ProductsController : ControllerBase
// Controller class {
private readonly IProductService _productService;
// Inject IProductService into the controller public ProductsController(IProductService productService)
// Constructor to inject the dependency {
_productService = productService;
} [HttpGet]
public IActionResult GetAllProducts()
{
var products = _productService.GetAll();
return Ok(products);
}
} // Register IProductService in Startup.cs public void ConfigureServices(IServiceCollection services)
{
services.AddScoped();
// Register service for DI }
Output:
The controller will now use `IProductService` for retrieving products, injected automatically via the constructor.

17.5 Middleware and Filters

Middleware components are executed in the request pipeline before a response is sent. You can use middleware to handle authentication, logging, and error handling. Filters are used for adding logic to actions, such as authorization checks or input validation.

  // Configure middleware in Startup.cs
  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware();
// Custom middleware for logging requests app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
} // Example of an action filter public class MyActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// Perform logic before action executes
} public void OnActionExecuted(ActionExecutedContext context)
{
// Perform logic after action executes
} }
Output:
Middleware will log each incoming request before routing it to the controller.

17.6 Consuming APIs (HttpClient)

In ASP.NET Core, `HttpClient` is used for sending HTTP requests and receiving responses from other APIs. It supports GET, POST, PUT, DELETE, and other HTTP methods for consuming RESTful APIs.

  public class ProductService
{
private readonly HttpClient _httpClient;
// Inject HttpClient public ProductService(HttpClient httpClient)
{
_httpClient = httpClient;
} public async Task> GetProductsAsync()
{
var response = await _httpClient.GetAsync("http://api.example.com/products");
var products = await response.Content.ReadAsAsync>();
return products;
} }
Output:
The `GetProductsAsync` method sends a GET request and returns a list of products from the external API.

17.7 Securing APIs with JWT

JSON Web Tokens (JWT) are commonly used for securing APIs. The client sends the token in the Authorization header, and the server validates it to ensure the request is authorized. JWTs can be used for authentication and authorization in web APIs.

  // In Startup.cs, configure JWT authentication
  public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.Authority = "https://example.com";
options.Audience = "myapi";
});
} [Authorize]
[HttpGet]
public IActionResult GetSecureData()
{
return Ok("This is secured data!");
}
Output:
Only authorized requests with a valid JWT will be able to access the `GetSecureData` endpoint.

Chapter 18: Unit Testing and Test-Driven Development

Introduction to Unit Testing

Unit testing is the process of testing individual units or components of software to ensure they work as expected. It focuses on small units of code, such as functions or methods, to ensure correctness in isolation. This is crucial for identifying bugs early in development and ensuring that changes don't introduce regressions.

<script>
// Example: Simple unit test for a function that adds two numbers
function add(a, b) {
return a + b;
}
document.write("Sum: " + add(2, 3) + "<br>");
// Expected output: 5
</script>

Using xUnit and NUnit

xUnit and NUnit are popular testing frameworks for C# that provide tools to write and execute unit tests. These frameworks allow for easy organization of test cases and provide various assertions to verify expected outcomes.

<script>
// Example: Test using xUnit (for .NET)
// This would be placed in a separate test file in a real .NET project.
using Xunit;
public class CalculatorTests
{ [Fact]
public void Add_ShouldReturnCorrectSum()
{ var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result);
// Assertion to check if the result is 5
}
}
document.write("Unit Test for Add function using xUnit.
");
</script>

Mocking with Moq

Moq is a popular mocking framework for .NET that allows you to create mock objects for unit testing. This is useful when your code depends on external services or interfaces that are difficult to test directly. Moq enables you to simulate these dependencies with predefined behavior to test your code in isolation.

<script>
// Example: Mocking a service with Moq in C#
// Mocking a repository for unit testing
using Moq;
using Xunit;
public class UserServiceTests
{ [Fact]
public void GetUserDetails_ShouldReturnCorrectDetails()
{ var mockRepo = new Mock();
mockRepo.Setup(repo => repo.GetUserById(1)).Returns(new User { Id = 1, Name = "John Doe" });
var userService = new UserService(mockRepo.Object);
var result = userService.GetUserDetails(1);
Assert.Equal("John Doe", result.Name);
// Checking if the mock returned the expected user
}
}
document.write("Unit Test with Moq Mocking Framework.
");
</script>

Writing Testable Code

Writing testable code involves structuring your code in a way that makes it easy to test. This includes focusing on separation of concerns, using dependency injection, and avoiding tightly coupled components. Testable code is modular, easy to mock, and follows good object-oriented design principles.

<script>
// Example: Writing testable code in C#
// Use of dependency injection to create testable classes
public class EmailService
{ private readonly IEmailSender _emailSender;
public EmailService(IEmailSender emailSender)
{ _emailSender = emailSender;
} public void SendEmail(string recipient, string message)
{ _emailSender.Send(recipient, message);
// Dependency injected EmailSender
}
}
document.write("Testable code example with dependency injection.
");
</script>

TDD Practices in C#

Test-Driven Development (TDD) is a practice where tests are written before the code. The typical TDD cycle is: write a failing test, write the minimum code to pass the test, then refactor. This cycle helps ensure that the code meets the requirements and behaves as expected from the start.

<script>
// Example: TDD in practice (simplified concept)
// First write the test, then write the code to pass the test
using Xunit;
public class MathServiceTests
{ [Fact]
public void Multiply_ShouldReturnCorrectProduct() {
var mathService = new MathService();
var result = mathService.Multiply(3, 4);
Assert.Equal(12, result);
// First we write a test expecting 12
}
} public class MathService
{ public int Multiply(int a, int b) {
return a * b;
// Then we implement the code to pass the test
}
} document.write("TDD: Test first, then code.
");
</script>

Integration Testing

Integration testing ensures that different parts of the application work together as expected. It involves testing the interaction between components, such as databases, external APIs, and services, to verify that they integrate seamlessly in real-world scenarios.

<script>
// Example: Integration testing (simplified)
// Testing a service that depends on an external API
public class ExternalApiServiceTests
{ [Fact]
public void GetUserData_ShouldReturnUserDetails()
{ var apiClient = new ApiClient(); // External dependency
var apiService = new ExternalApiService(apiClient);
var result = apiService.GetUserData(1);
Assert.NotNull(result); // Verifying that the result is not null
}
}
document.write("Integration testing example.
");
</script>

Code Coverage and Reporting

Code coverage refers to the percentage of code that is executed during testing. It helps identify parts of the code that are not adequately tested. Tools like Visual Studio and third-party frameworks can generate coverage reports to visualize and improve test coverage.

<script>
// Example: Code coverage tools in practice
// This would typically be run using a tool like Visual Studio's built-in coverage tools.
public class CalculatorTests
{ [Fact]
public void Add_ShouldReturnCorrectResult() {
var calculator = new Calculator();
var result = calculator.Add(5, 3);
Assert.Equal(8, result);
// This test would contribute to coverage
}
} document.write("Example of generating code coverage reports.
");
</script>

Chapter 19: Cross-Platform Development with .NET MAUI / Xamarin

1. Overview of .NET MAUI and Xamarin

.NET MAUI (Multi-platform App UI) and Xamarin are frameworks for building cross-platform mobile applications. Xamarin was the first iteration of the framework, allowing developers to write shared code for both Android and iOS. .NET MAUI extends Xamarin, enabling cross-platform development for Android, iOS, macOS, and Windows with a single codebase.

// Example: Basic setup for a MAUI app
using Microsoft.Maui.Controls;

public class MainPage : ContentPage
{
    public MainPage()
    {
        Content = new StackLayout
        {
            Children = {
                new Label {
                    Text = "Welcome to .NET MAUI!"
                }
            }
        };
    }
}

Output:
A simple "Welcome to .NET MAUI!" message displayed on the screen.

2. Shared Code and Platform-Specific Code

One of the key features of .NET MAUI and Xamarin is the ability to write shared code that runs across multiple platforms, while still allowing you to write platform-specific code for tasks that need to behave differently across devices (e.g., accessing device features like the camera or GPS).

// Shared code example: Cross-platform button click event
public class MainPage : ContentPage
{
    public MainPage()
    {
        var button = new Button
        {
            Text = "Click Me"
        };

        button.Clicked += OnButtonClicked;

        Content = new StackLayout
        {
            Children = { button }
        };
    }

    private void OnButtonClicked(object sender, EventArgs e)
    {
        // Platform-specific behavior can be added here
        Device.BeginInvokeOnMainThread(() =>
        {
            DisplayAlert("Button Clicked", "You clicked the button!", "OK");
        });
    }
}

Output:
A button labeled "Click Me". When clicked, a popup alert appears.

3. UI Design with XAML

XAML (Extensible Application Markup Language) is a declarative markup language used to design user interfaces in .NET MAUI and Xamarin. It allows developers to create UI components using XML-like syntax and define layouts, controls, and styles.



    
        

Output:
A centered label and button on the screen. Clicking the button triggers an event.

4. Handling Events and Data Binding

In .NET MAUI and Xamarin, events are used to handle user interactions, such as button clicks or text input. Data binding allows the UI to automatically update when data changes, creating a dynamic and responsive user interface.

// Binding data from a ViewModel to the UI
public class MainPage : ContentPage
{
    public MainPage()
    {
        var viewModel = new MainViewModel();
        BindingContext = viewModel;

        var label = new Label();
        label.SetBinding(Label.TextProperty, "WelcomeMessage");

        Content = new StackLayout
        {
            Children = { label }
        };
    }
}

// ViewModel
public class MainViewModel
{
    public string WelcomeMessage { get; set; } = "Hello from the ViewModel!";
}

Output:
The label automatically displays the "Hello from the ViewModel!" message.

5. Navigating Between Screens

Navigation between pages or screens in .NET MAUI and Xamarin is managed using the navigation stack. Developers can push and pop pages from the stack to allow users to move through the app's different views.

// Example of navigating to a new page
public class MainPage : ContentPage
{
    public MainPage()
    {
        var button = new Button { Text = "Go to Next Page" };
        button.Clicked += OnNavigateButtonClicked;

        Content = new StackLayout
        {
            Children = { button }
        };
    }

    private async void OnNavigateButtonClicked(object sender, EventArgs e)
    {
        // Navigate to another page
        await Navigation.PushAsync(new SecondPage());
    }
}

Output:
Clicking the button navigates to a second page.

6. Working with Device Features (Camera, GPS)

.NET MAUI and Xamarin provide access to a range of device features, such as the camera, GPS, sensors, etc., through plugins and APIs. You can use these features in your app to make it more interactive and context-aware.

// Accessing the camera on a device
using Xamarin.Essentials;

public class MainPage : ContentPage
{
    public MainPage()
    {
        var button = new Button { Text = "Take Photo" };
        button.Clicked += OnTakePhotoButtonClicked;

        Content = new StackLayout
        {
            Children = { button }
        };
    }

    private async void OnTakePhotoButtonClicked(object sender, EventArgs e)
    {
        // Take a photo using the camera
        var photo = await MediaPicker.CapturePhotoAsync();

        if (photo != null)
        {
            // Save the photo or display it
            var stream = await photo.OpenReadAsync();
            // Handle the photo stream
        }
    }
}

Output:
Clicking the button opens the camera to take a photo.

7. Deploying to Android and iOS

Once your app is complete, you can deploy it to Android and iOS devices. .NET MAUI and Xamarin support deploying through Visual Studio, either directly to devices or through simulators/emulators for testing.

// Deploying to Android or iOS through Visual Studio
// Select target platform (Android/iOS) in Visual Studio
// Click "Run" to deploy to a physical device or emulator

Output:
Your app is deployed to the selected Android or iOS device for testing and usage.

Chapter 20: Advanced .NET and C# Topics

Span<T> and Memory<T>

Span<T> and Memory<T> are new types introduced in .NET to handle slices of arrays and memory in a way that’s both safe and efficient. While Span<T> is a stack-only type, Memory<T> can be used on the heap and supports async operations.

// Using Span to create a slice of an array
int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> slice = numbers.AsSpan(1, 3);
// This creates a slice from the second element (index 1) for 3 elements
Console.WriteLine(string.Join(", ", slice)); // Output: 2, 3, 4

// Using Memory to create a heap-allocated memory slice
Memory<int> memory = new Memory<int>(numbers);
var sliceFromMemory = memory.Slice(1, 3);
Console.WriteLine(string.Join(", ", sliceFromMemory.Span)); // Output: 2, 3, 4

Unsafe Code and Pointers

Unsafe code in C# allows you to work with pointers, similar to languages like C and C++. It’s generally used when performance is crucial, but it’s important to handle it with caution as it bypasses some of C#’s safety features.

// Enabling unsafe code
unsafe {
int a = 5;
int* p = &a; // Pointer to the variable 'a'
Console.WriteLine(*p); // Output: 5
}

Source Generators

Source Generators are a new feature in C# that enables developers to generate code during the compilation process. This can help reduce boilerplate code and improve performance by generating code at compile time instead of runtime.

// Example of a source generator that automatically creates a ToString method for a class
// Note: This is a conceptual example, source generators must be implemented in a separate project
[Generator] public class ToStringGenerator : ISourceGenerator {
public void Initialize(GeneratorInitializationContext context) { }

public void Execute(GeneratorExecutionContext context) {
var classCode = "public override string ToString() { return \"Generated ToString\"; }";
context.AddSource("GeneratedToString", classCode);
}
}

Records and Immutable Data Structures

Records are reference types in C# that are used to define immutable objects. These are useful for scenarios where you want to create data objects that should not be modified after they are created.

// Defining a record
public record Person(string FirstName, string LastName);

// Creating an instance of a record
Person person = new Person("John", "Doe");
Console.WriteLine(person); // Output: Person { FirstName = John, LastName = Doe }

// Records are immutable, so modifying them creates a new instance
Person newPerson = person with { LastName = "Smith" };
Console.WriteLine(newPerson); // Output: Person { FirstName = John, LastName = Smith }

ValueTask and Performance Considerations

ValueTask is a structure introduced in C# that is used for improving performance, particularly in scenarios where a result is already available or a method is frequently awaited. It’s lighter than Task when the result is not always awaited.

// ValueTask example
public async ValueTask GetNumberAsync() {
return 42;
}

// Awaiting the ValueTask
ValueTask task = GetNumberAsync();
Console.WriteLine(await task); // Output: 42

Native Interop (P/Invoke)

Platform Invocation Services (P/Invoke) allow C# to call functions from native libraries (like DLLs). This is crucial for interacting with legacy code or native APIs.

// Importing a native library function (e.g., from kernel32.dll)
using System.Runtime.InteropServices;
class Program {
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetConsoleWindow();

static void Main() {
IntPtr consoleHandle = GetConsoleWindow();
Console.WriteLine(consoleHandle);
}
}

.NET 8+ Features and Future of C#

.NET 8 introduces several new features, including better performance, language enhancements, and tools for developing modern applications. The future of C# is expected to include more pattern matching, enhanced record types, and improved asynchronous programming features.

// Example of using C# 9 features: pattern matching
object obj = 42;
if (obj is int number) {
Console.WriteLine($"It's an integer: {number}"); // Output: It's an integer: 42
}

// Enhanced record types in C# 10
public record Person(string FirstName, string LastName) {
public string FullName => $"{FirstName} {LastName}";
}

Chapter 21: Software Architecture & Design Patterns

1. SOLID Principles in Practice

The SOLID principles are a set of five object-oriented design principles that help create more understandable, flexible, and maintainable software. They include:

  • S - Single Responsibility Principle
  • O - Open/Closed Principle
  • L - Liskov Substitution Principle
  • I - Interface Segregation Principle
  • D - Dependency Inversion Principle

public class Employee 
{
public string Name { get; set; }
public string Position { get; set; }

public Employee(string name, string position)
{
Name = name;
Position = position;
}
}

public class PayrollService
{
public void ProcessPayroll(Employee employee)
{
Console.WriteLine($"Processing payroll for {employee.Name}");
}
}

// Output: Processing payroll for Alice

2. Dependency Injection at Scale

Dependency Injection (DI) is a design pattern that allows you to achieve Inversion of Control (IoC) by passing dependencies to an object rather than creating them within. DI improves testability, scalability, and maintainability.

public interface IEmailService 
{
void SendEmail(string to, string subject, string body);
}

public class EmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
Console.WriteLine($"Sending email to {to}");
}
}

public class UserService
{
private readonly IEmailService _emailService;

public UserService(IEmailService emailService)
{
_emailService = emailService;
}

public void RegisterUser(string email)
{
Console.WriteLine("Registering user...");
_emailService.SendEmail(email, "Welcome!", "Thanks for registering.");
}
}

// Dependency Injection setup (e.g., using an IoC container like in .NET Core) // var userService = new UserService(new EmailService()); // userService.RegisterUser("test@example.com");

3. Repository and Unit of Work Pattern

The Repository Pattern helps in encapsulating data access logic, making it easier to manage and change data sources. The Unit of Work Pattern manages transactions by coordinating the writing of changes to the database, ensuring consistency.

public interface IProductRepository 
{
void Add(Product product);
Product Get(int id);
}

public class ProductRepository : IProductRepository
{
private List _products = new List();

public void Add(Product product)
{
_products.Add(product);
}

public Product Get(int id)
{
return _products.FirstOrDefault(p => p.Id == id);
}
}

public class UnitOfWork
{
private readonly IProductRepository _productRepo;

public UnitOfWork(IProductRepository productRepo)
{
_productRepo = productRepo;
}

public void Commit()
{
// Commit transaction here (e.g., save to database)
}
}

// Usage: // var uow = new UnitOfWork(new ProductRepository()); // uow.Commit();

4. Singleton, Factory, Strategy Patterns

- The Singleton Pattern ensures a class has only one instance and provides a global point of access to it. - The Factory Pattern defines an interface for creating an object, but allows subclasses to alter the type of objects that will be created. - The Strategy Pattern enables selecting an algorithm at runtime, encapsulating each algorithm into a strategy object.

public class Singleton 
{
private static Singleton _instance;

private Singleton() { }

public static Singleton Instance
{
get { return _instance ??= new Singleton(); }
}
}

public interface IOperationStrategy
{
int Execute(int a, int b);
}

public class AdditionStrategy : IOperationStrategy
{
public int Execute(int a, int b)
{
return a + b;
}
}

public class Calculator
{
private IOperationStrategy _strategy;

public void SetStrategy(IOperationStrategy strategy)
{
_strategy = strategy;
}

public int Calculate(int a, int b)
{
return _strategy.Execute(a, b);
}
}

// Usage: // Singleton singleton = Singleton.Instance; // Calculator calc = new Calculator(); // calc.SetStrategy(new AdditionStrategy()); // Console.WriteLine(calc.Calculate(5, 3)); // Output: 8

5. Adapter, Facade, and Mediator Patterns

- The Adapter Pattern allows incompatible interfaces to work together. - The Facade Pattern provides a simplified interface to a complex subsystem. - The Mediator Pattern defines an object that coordinates communication between different classes, avoiding direct communication.

public class LegacySystem 
{
public void LegacyMethod()
{
Console.WriteLine("Legacy method called.");
}
}

public class Adapter
{
private LegacySystem _legacySystem;

public Adapter(LegacySystem legacySystem)
{
_legacySystem = legacySystem;
}

public void NewMethod()
{
_legacySystem.LegacyMethod();
}
}

// Usage: // var adapter = new Adapter(new LegacySystem()); // adapter.NewMethod(); // Output: Legacy method called.

6. CQRS (Command Query Responsibility Segregation)

CQRS is a pattern that separates the command (write) and query (read) responsibilities, which allows for scalability and optimization of both actions.

public interface ICommand 
{
void Execute();
}

public class CreateOrderCommand : ICommand
{
public void Execute()
{
Console.WriteLine("Order created.");
}
}

public class OrderQuery
{
public string GetOrderDetails(int orderId)
{
return "Order details for ID: " + orderId;
}
}

// Usage: // ICommand command = new CreateOrderCommand(); // command.Execute(); // var query = new OrderQuery(); // Console.WriteLine(query.GetOrderDetails(123)); // Output: Order details for ID: 123

7. Clean Architecture in .NET Projects

Clean Architecture is a software design philosophy that separates concerns into different layers to create a more maintainable system. In .NET, it typically involves using layers like Presentation, Application, Domain, and Infrastructure.

public class ApplicationService 
{
public void Execute()
{
Console.WriteLine("Executing application service logic.");
}
}

public class Program
{
public static void Main(string[] args)
{
var service = new ApplicationService();
service.Execute();
}
}

// Output: Executing application service logic.

Chapter 22: Microservices with .NET

Introduction to Microservices Architecture

Microservices architecture involves breaking down an application into small, independent services that are loosely coupled, maintainable, and scalable. Each service focuses on a specific business functionality and can be developed, deployed, and scaled independently.

        // Microservice example: A simple order service
        public class OrderService
        {
            public string CreateOrder(int orderId)
            {
                return "Order " + orderId + " created successfully!";
            }
        }
        
        // Each microservice can interact with others, but they are independent and loosely coupled.
    

Creating Microservices with ASP.NET Core

ASP.NET Core provides a robust framework for building microservices. You can use RESTful APIs or gRPC to expose microservices to the outside world. In this example, we will create a simple microservice that handles orders.

        // OrderController in an ASP.NET Core microservice
        [ApiController]
        [Route("api/[controller]")]
        public class OrderController : ControllerBase
        {
            private readonly OrderService _orderService;

            public OrderController(OrderService orderService)
            {
                _orderService = orderService;
            }

            [HttpPost]
            public IActionResult CreateOrder(int orderId)
            {
                var result = _orderService.CreateOrder(orderId);
                return Ok(result);
            }
        }
        
        // To run this, create a new ASP.NET Core Web API project and define the OrderService.
    

Service Communication with gRPC and REST

Microservices often need to communicate with each other. This can be done using different protocols such as gRPC or REST. While REST is simple and widely used, gRPC is a high-performance, cross-platform framework that supports HTTP/2 for fast communication between services.

        // gRPC service definition (Proto file)
        syntax = "proto3";
        option csharp_namespace = "OrderService";

        service OrderService
        {
            rpc CreateOrder(OrderRequest) returns (OrderResponse);
        }

        message OrderRequest {
            int32 orderId = 1;
        }

        message OrderResponse {
            string message = 1;
        }

        // Implementing gRPC in ASP.NET Core
        public class OrderService : OrderService.OrderServiceBase
        {
            public override Task CreateOrder(OrderRequest request, ServerCallContext context)
            {
                return Task.FromResult(new OrderResponse
                {
                    Message = $"Order {request.OrderId} created successfully!"
                });
            }
        }
        
        // To use this, you would define the gRPC service in the .proto file, implement it in ASP.NET Core, and configure gRPC in the Startup class.
    

API Gateway (Ocelot/YARP)

An API Gateway is an entry point for all client requests to access microservices. It handles routing, load balancing, security, and aggregation of responses. Ocelot and YARP (Yet Another Reverse Proxy) are popular libraries for building API gateways in .NET.

        // Ocelot Configuration Example (ocelot.json)
        {
            "ReRoutes": [
                {
                    "DownstreamPathTemplate": "/api/order/{orderId}",
                    "UpstreamPathTemplate": "/order/{orderId}",
                    "DownstreamScheme": "http",
                    "DownstreamHostAndPorts": [
                        {
                            "Host": "localhost",
                            "Port": 5001
                        }
                    ]
                }
            ],
            "GlobalConfiguration": {
                "BaseUrl": "http://localhost:5000"
            }
        }

        // In Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOcelot(Configuration);
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseOcelot().Wait();
        }
        
        // This configuration allows the API Gateway to route requests to the appropriate downstream services.
    

Distributed Transactions and Eventual Consistency

Microservices often need to maintain data consistency across services. However, achieving strong consistency in a distributed system can be challenging. Eventual consistency is a principle where updates to a service will propagate across other services over time.

        // Implementing Eventual Consistency using an Event Bus
        public class OrderCreatedEvent
        {
            public int OrderId { get; set; }
            public string CustomerName { get; set; }
        }

        public class OrderService
        {
            private readonly IEventBus _eventBus;

            public OrderService(IEventBus eventBus)
            {
                _eventBus = eventBus;
            }

            public void CreateOrder(int orderId, string customerName)
            {
                // Create order logic here...
                
                // Publish event to notify other services
                _eventBus.Publish(new OrderCreatedEvent
                {
                    OrderId = orderId,
                    CustomerName = customerName
                });
            }
        }
        
        // Eventual consistency is achieved through asynchronous communication, where the services react to events published by other services.
    

Using Dapr and Service Bus

Dapr (Distributed Application Runtime) is a set of APIs that simplify building microservices. It provides solutions for state management, pub/sub messaging, and service invocation. The Service Bus (e.g., Azure Service Bus) is often used to manage asynchronous message communication between services.

        // Dapr Pub/Sub Example in .NET
        [Topic("order-topic")]
        public class OrderCreatedEvent
        {
            public int OrderId { get; set; }
            public string CustomerName { get; set; }
        }

        public class OrderService
        {
            private readonly IPublisher _publisher;

            public OrderService(IPublisher publisher)
            {
                _publisher = publisher;
            }

            public async Task CreateOrder(int orderId, string customerName)
            {
                var orderEvent = new OrderCreatedEvent
                {
                    OrderId = orderId,
                    CustomerName = customerName
                };
                
                // Publish to Dapr service bus
                await _publisher.PublishAsync("order-topic", orderEvent);
            }
        }
        
        // In Dapr, you would configure Pub/Sub to send messages across services.
    

Logging, Tracing, and Monitoring Microservices

To manage microservices at scale, it's important to have centralized logging, distributed tracing, and monitoring to track the performance and health of services. Tools like OpenTelemetry and Application Insights can be integrated with .NET to provide observability.

        // Configuring logging in ASP.NET Core
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddLogging(config =>
            {
                config.AddConsole();
                config.AddDebug();
            });
        }

        // Distributed Tracing using OpenTelemetry
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOpenTelemetryTracing(builder =>
                builder.AddAspNetCoreInstrumentation()
                       .AddHttpClientInstrumentation()
                       .AddConsoleExporter());
        }
        
        // Application Insights (using Azure)
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplicationInsightsTelemetry(Configuration["ApplicationInsights:InstrumentationKey"]);
        }

        // Monitoring and metrics can be integrated using tools like Prometheus, Grafana, and Azure Monitor.
    

Chapter 23: Security and Authentication

Introduction to Authentication and Authorization

Authentication and authorization are two key concepts in web security. Authentication is the process of verifying the identity of a user, while authorization determines whether an authenticated user has permission to access a particular resource or perform a specific action.

    // Authentication example (User login)
    public class AuthController : Controller
    {
        private readonly UserManager _userManager;
        private readonly SignInManager _signInManager;

        public AuthController(UserManager userManager, SignInManager signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }

        [HttpPost]
        public async Task Login(string username, string password)
        {
            var user = await _userManager.FindByNameAsync(username);
            if (user != null && await _userManager.CheckPasswordAsync(user, password))
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return RedirectToAction("Index", "Home");
            }
            return Unauthorized();
        }
    }
    

In this example, we authenticate the user by checking the username and password against the stored values using UserManager and SignInManager in ASP.NET Core Identity.

Implementing Identity in ASP.NET Core

ASP.NET Core Identity is a system for managing user information, authentication, and authorization. It provides functionalities such as registration, login, password recovery, and user management. To implement Identity in an ASP.NET Core application, you typically need to configure the Startup.cs file and create a custom ApplicationUser class.

    // Configuring Identity in Startup.cs
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentity()
            .AddEntityFrameworkStores()
            .AddDefaultTokenProviders();

        services.AddControllersWithViews();
    }

    // ApplicationUser class extending IdentityUser
    public class ApplicationUser : IdentityUser
    {
        public string FullName { get; set; }
    }
    

The code above configures ASP.NET Core Identity by registering the ApplicationUser class and the IdentityRole class, which will store user data and roles. It also enables token providers for features like password recovery.

Role-Based and Claims-Based Access

Role-based access control (RBAC) assigns users to roles (e.g., admin, user), and claims-based access control assigns claims (attributes or metadata) to users. Both methods are used to implement fine-grained authorization.

    // Role-based access
    public class AdminController : Controller
    {
        [Authorize(Roles = "Admin")]
        public IActionResult Dashboard()
        {
            return View();
        }
    }

    // Claims-based access
    public class ClaimsController : Controller
    {
        [Authorize(Policy = "CanAccess")]
        public IActionResult SpecialPage()
        {
            return View();
        }
    }
    

The AdminController example demonstrates role-based access using the Authorize attribute. The ClaimsController example demonstrates claims-based access where a user is authorized if they have a specific claim, like CanAccess.

Securing APIs with OAuth2 and JWT

OAuth2 is a framework for token-based authentication and authorization, commonly used in securing APIs. JWT (JSON Web Tokens) are often used as the token format in OAuth2 to represent claims securely.

    // JWT Bearer Token Authentication setup
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidIssuer = "your-issuer",
                    ValidAudience = "your-audience",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secret-key"))
                };
            });

        services.AddControllers();
    }

    // API Controller secured with JWT
    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public IActionResult GetValues()
        {
            return Ok(new string[] { "Value1", "Value2" });
        }
    }
    

This setup demonstrates how to secure an API using JWT Bearer tokens. The ConfigureServices method configures the JWT authentication, and the API controller uses the Authorize attribute to secure its actions.

Preventing CSRF, XSS, and SQL Injection

Security vulnerabilities such as Cross-Site Request Forgery (CSRF), Cross-Site Scripting (XSS), and SQL Injection are common in web applications. Here are best practices for preventing them:

  • CSRF: Use anti-forgery tokens in forms to protect against CSRF attacks.
  • XSS: Sanitize user input and output to prevent malicious scripts.
  • SQL Injection: Use parameterized queries or ORM (Object-Relational Mapping) to avoid raw SQL queries that are vulnerable to injection.
    // Preventing CSRF in ASP.NET Core (Automatic in forms)
    [ValidateAntiForgeryToken]
    public IActionResult SubmitForm(string input)
    {
        return Ok("Form submitted successfully.");
    }

    // Preventing XSS (Sanitizing input)
    var sanitizedInput = HttpUtility.HtmlEncode(userInput); // Encodes any HTML or script tags

    // Preventing SQL Injection (Using parameterized queries)
    var result = dbContext.Users.FromSqlRaw("SELECT * FROM Users WHERE UserName = {0}", userName);
    

In the examples above, we prevent CSRF by using the [ValidateAntiForgeryToken] attribute, XSS by encoding user input, and SQL Injection by using parameterized queries.

Using IdentityServer and OpenID Connect

IdentityServer is a framework that helps implement authentication and authorization using OpenID Connect and OAuth2 protocols. It is commonly used for single sign-on (SSO) and securing APIs.

    // Configuring IdentityServer in Startup.cs
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentityServer()
            .AddInMemoryClients(Config.GetClients()) // Clients setup
            .AddInMemoryIdentityResources(Config.GetIdentityResources()) // Identity resources setup
            .AddInMemoryApiScopes(Config.GetApiScopes()) // API Scopes setup
            .AddTestUsers(TestUsers.Users); // User setup
    }

    // OpenID Connect Authentication configuration
    services.AddAuthentication()
        .AddOpenIdConnect(options =>
        {
            options.Authority = "https://identityserver.com";
            options.ClientId = "client";
            options.ClientSecret = "client-secret";
            options.ResponseType = "code";
        });
    

This configuration demonstrates how to set up IdentityServer to manage clients and users. The OpenID Connect authentication is configured to authenticate users via IdentityServer.

Data Encryption and Secure Storage

Data encryption is crucial for securing sensitive information such as passwords, personal data, and API keys. ASP.NET Core provides various ways to encrypt data and store it securely.

    // Example of encrypting data using AES encryption
    public class EncryptionService
    {
        private readonly string _key = "your-encryption-key";

        public string EncryptData(string data)
        {
            using (var aesAlg = Aes.Create())
            {
                aesAlg.Key = Encoding.UTF8.GetBytes(_key);
                aesAlg.IV = new byte[16]; // Initialize with zeros for simplicity

                var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
                using (var msEncrypt = new MemoryStream())
                {
                    using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (var swEncrypt = new StreamWriter(csEncrypt))
                        {
                            swEncrypt.Write(data);
                        }
                    }
                    return Convert.ToBase64String(msEncrypt.ToArray());
                }
            }
        }
    }

    // Secure password storage using ASP.NET Core Identity
    var user = new ApplicationUser { UserName = "user@example.com", Email = "user@example.com" };
    var password = "UserPassword123!";
    var passwordHash = userManager.PasswordHasher.HashPassword(user, password);
    

The EncryptionService example shows how to use AES encryption to secure data. In addition, we show how passwords can be hashed and stored securely using ASP.NET Core Identity.

Chapter 24: Cloud-Native Development with Azure

1. Deploying .NET Apps to Azure App Service

Azure App Service is a fully managed platform for building, deploying, and scaling web apps. Deploying .NET applications to Azure App Service allows developers to host their web applications without managing infrastructure.

Example: Deploying a .NET Web Application

// Steps to deploy a .NET application to Azure App Service:
// 1. Create a Web App in Azure Portal
// 2. Publish your .NET app from Visual Studio
// 3. Select Azure as the target platform
// 4. Configure settings (e.g., resource group, app service plan) and deploy

// Example of a simple .NET Controller for a Web API
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

2. Azure Functions and Serverless C#

Azure Functions is a serverless compute service that allows you to run event-driven code without having to manage servers. You pay only for the compute time your code consumes.

Example: Azure Function in C#

// Define a simple HTTP-triggered Azure Function
public static class HelloFunction
{
    [FunctionName("HelloWorld")]
    public static async Task Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestMessage req,
        ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");
        return new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Hello, world!")
        };
    }
}

3. Azure Storage (Blobs, Tables, Queues)

Azure Storage provides cloud storage for data, applications, and workloads. It includes several services like Blobs, Tables, and Queues to store and manage different types of data.

Example: Azure Blob Storage

// Blob storage example for uploading a file to Azure
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference("mycontainer");
CloudBlockBlob blockBlob = container.GetBlockBlobReference("myfile.txt");

// Upload a file
await blockBlob.UploadFromFileAsync("localfile.txt");

Example: Azure Table Storage

// Azure Table Storage example for storing entities
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference("mytable");

DynamicTableEntity entity = new DynamicTableEntity("partitionKey", "rowKey")
{
    Properties = { { "Name", new EntityProperty("John Doe") } }
};

TableOperation insertOperation = TableOperation.InsertOrReplace(entity);
await table.ExecuteAsync(insertOperation);

Example: Azure Queue Storage

// Azure Queue Storage example for sending a message
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
CloudQueueClient queueClient = storageAccount.CreateCloudQueueClient();
CloudQueue queue = queueClient.GetQueueReference("myqueue");

// Add a message to the queue
CloudQueueMessage message = new CloudQueueMessage("This is a test message.");
await queue.AddMessageAsync(message);

4. Azure Key Vault and Secrets Management

Azure Key Vault is a cloud service for securely storing and managing sensitive information such as passwords, API keys, and certificates. It helps in protecting application secrets.

Example: Accessing Secrets from Azure Key Vault

// Setting up Azure Key Vault client
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(...));
var secret = await keyVaultClient.GetSecretAsync("https://mykeyvault.vault.azure.net/secrets/mysecret");

// Use the secret
string mySecret = secret.Value;
Console.WriteLine(mySecret);

5. Azure Cosmos DB and Table API

Azure Cosmos DB is a globally distributed, multi-model database service. The Table API in Cosmos DB provides a schema-less, NoSQL data model to store key-value pairs.

Example: Using Azure Cosmos DB Table API

// Create a CosmosClient instance
CosmosClient cosmosClient = new CosmosClient(connectionString);
Container container = cosmosClient.GetContainer("myDatabase", "myTable");

// Create an entity to insert
var item = new { PartitionKey = "partition1", RowKey = "row1", Name = "Alice" };

// Insert the entity
await container.CreateItemAsync(item);

6. Application Insights for Telemetry

Application Insights is an application performance management (APM) service that helps you monitor your live applications, providing real-time analytics and telemetry.

Example: Sending Telemetry Data

// Setting up Application Insights in your app
TelemetryClient telemetryClient = new TelemetryClient();
telemetryClient.TrackEvent("MyCustomEvent");

// Tracking an exception
try
{
    throw new Exception("Test Exception");
}
catch (Exception ex)
{
    telemetryClient.TrackException(ex);
}

7. CI/CD with GitHub Actions and Azure DevOps

CI/CD (Continuous Integration/Continuous Deployment) is a method to frequently deliver applications by automatically building, testing, and deploying code changes. GitHub Actions and Azure DevOps are two tools that allow you to automate the build and release pipelines.

Example: GitHub Actions Workflow for Azure Deployment

name: Deploy to Azure Web App

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Azure CLI
      uses: azure/setup-azure-cli@v1

    - name: Azure login
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: Deploy to Azure
      run: |
        az webapp up --name mywebapp --resource-group myresourcegroup

Example: Azure DevOps Pipeline for CI/CD

// Example YAML pipeline for Azure DevOps
trigger:
- main

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '5.x'
    installationPath: $(Agent.ToolsDirectory)/dotnet

- task: DotNetCoreCLI@2
  inputs:
    command: 'restore'
    projects: '**/*.csproj'

- task: DotNetCoreCLI@2
  inputs:
    command: 'build'
    projects: '**/*.csproj'

- task: DotNetCoreCLI@2
  inputs:
    command: 'publish'
    publishWebProjects: true
    arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)'

Chapter 25: Performance Optimization and Advanced Debugging

Memory Management and Garbage Collection

Memory management and garbage collection (GC) are crucial to optimize application performance and prevent memory-related issues. The .NET runtime automatically handles memory allocation and garbage collection, but understanding how it works can help improve performance.

        // Example: Basic memory allocation and garbage collection
        public class MemoryExample {
            public void AllocateMemory() {
                var largeObject = new byte[1000000];  // Allocates memory
                Console.WriteLine("Memory allocated.");
            }
        }

        public class Program {
            public static void Main() {
                MemoryExample example = new MemoryExample();
                example.AllocateMemory();
                
                // Force garbage collection (not recommended in production)
                GC.Collect();
                Console.WriteLine("Garbage collection triggered.");
            }
        }
    

Explanation: - Memory is allocated in the AllocateMemory method by creating a large byte array. - The GC.Collect() method manually triggers garbage collection, which is generally not needed but is shown here for demonstration. - In production, the .NET runtime handles garbage collection automatically to clean up unused objects.

Using Span<T> and Structs for Performance

Span<T> and structs can improve performance by reducing memory allocations and minimizing copying of data. Span<T> is a stack-only type that allows for efficient slicing of arrays and buffers.

        public class SpanExample {
            public void UseSpan() {
                int[] numbers = { 1, 2, 3, 4, 5 };
                Span span = numbers.AsSpan();  // Create a span from the array
                
                // Modify a portion of the array using the span
                span[0] = 10;
                
                Console.WriteLine("Updated value: " + numbers[0]);  // Output: 10
            }
        }

        public class Program {
            public static void Main() {
                SpanExample example = new SpanExample();
                example.UseSpan();
            }
        }
    

Explanation: - A Span<T> is created from an array, allowing efficient access to its elements. - The span allows in-place modifications to the underlying array, reducing overhead from copying data. - Using Span<T> can significantly improve performance when working with large arrays or buffers.

Benchmarking with BenchmarkDotNet

Benchmarking helps measure the performance of code by running tests in a controlled environment and capturing execution times. BenchmarkDotNet is a popular library for benchmarking in .NET.

        using BenchmarkDotNet.Attributes;
        using BenchmarkDotNet.Running;

        public class BenchmarkExample {
            [Benchmark]
            public void TestMethod() {
                int sum = 0;
                for (int i = 0; i < 100000; i++) {
                    sum += i;
                }
            }
        }

        class Program {
            static void Main(string[] args) {
                var summary = BenchmarkRunner.Run();  // Run the benchmark
            }
        }
    

Explanation: - The Benchmark attribute is used to mark methods for benchmarking. - BenchmarkRunner.Run<T> runs the benchmark and outputs results on execution time, memory usage, etc. - This helps in comparing the performance of different implementations and fine-tuning code for better performance.

Profiling Applications (dotMemory, dotTrace)

Profiling tools like dotMemory and dotTrace allow developers to analyze memory usage and performance in real-time, helping identify bottlenecks and memory leaks.

        // dotTrace example (Pseudo-code)
        // Start tracing the application
        dotTrace.StartTrace();
        
        // Code to profile
        var list = new List();
        for (int i = 0; i < 1000000; i++) {
            list.Add(i);
        }

        // Stop profiling and view results
        dotTrace.StopTrace();
    

Explanation: - Profiling with dotTrace captures performance data, allowing developers to see the time spent on methods, memory usage, and CPU utilization. - dotMemory provides insights into memory consumption, helping to spot unnecessary memory usage and potential leaks.

Handling Memory Leaks and Large Object Heap

Memory leaks occur when objects are not properly released, leading to excessive memory consumption. The Large Object Heap (LOH) is used to store objects larger than 85,000 bytes. Understanding LOH and managing memory leaks are important for performance optimization.

        // Example: Memory leak in a collection
        public class MemoryLeakExample {
            private List memoryLeaks = new List();

            public void CreateMemoryLeak() {
                // Simulate a memory leak by adding large objects to the list
                for (int i = 0; i < 1000; i++) {
                    memoryLeaks.Add(new byte[100000]);  // Allocating large arrays
                }
            }
        }

        public class Program {
            public static void Main() {
                MemoryLeakExample example = new MemoryLeakExample();
                example.CreateMemoryLeak();
                Console.WriteLine("Memory leak simulated.");
            }
        }
    

Explanation: - The code simulates a memory leak by continuously adding large objects to a list without properly disposing of them. - Over time, this can lead to memory exhaustion and performance degradation, especially if these objects are allocated on the Large Object Heap. - It is essential to properly dispose of large objects to avoid memory leaks.

Advanced Exception Diagnostics with Dump Files

Dump files provide a snapshot of an application's memory and state when an exception occurs. Analyzing dump files can help identify issues that are difficult to reproduce during normal debugging.

        // Example: Creating a dump file on unhandled exception
        public class Program {
            public static void Main() {
                try {
                    // Simulate an exception
                    throw new InvalidOperationException("Something went wrong!");
                } catch (Exception ex) {
                    // Create a dump file for debugging
                    var filePath = "exceptionDump.dmp";
                    using (var fs = new FileStream(filePath, FileMode.Create)) {
                        // Simulate writing the dump file (in practice, use specialized tools)
                        fs.Write(BitConverter.GetBytes(ex.Message.Length), 0, 4);
                    }
                    Console.WriteLine("Dump file created: " + filePath);
                }
            }
        }
    

Explanation: - When an exception occurs, a dump file can be generated for further inspection. - Tools like WinDbg or Visual Studio can be used to analyze dump files and gather insights into the application's state at the time of the exception.

Performance Tuning in High-Load Environments

In high-load environments, optimizing performance is crucial to maintain responsiveness and reliability. Techniques such as caching, load balancing, and asynchronous programming can significantly improve performance under heavy loads.

        // Example: Asynchronous programming for better responsiveness
        public class HighLoadExample {
            public async Task PerformTaskAsync() {
                var task1 = Task.Run(() => LongRunningOperation());
                var task2 = Task.Run(() => AnotherLongRunningOperation());
                
                await Task.WhenAll(task1, task2);  // Wait for both tasks to complete
                Console.WriteLine("All tasks completed.");
            }

            private void LongRunningOperation() {
                // Simulate a long-running operation
                Thread.Sleep(2000);
                Console.WriteLine("Long operation finished.");
            }

            private void AnotherLongRunningOperation() {
                // Simulate another long-running operation
                Thread.Sleep(3000);
                Console.WriteLine("Another long operation finished.");
            }
        }

        public class Program {
            public static void Main() {
                HighLoadExample example = new HighLoadExample();
                example.PerformTaskAsync().Wait();
            }
        }
    

Explanation: - Asynchronous programming with Task.Run() allows multiple operations to run concurrently, improving performance and responsiveness. - In high-load environments, this prevents the main thread from blocking and ensures that the system remains responsive even under heavy load.