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 appsOutput:
// It is also popular in enterprise backend development
// Example of a simple class declaration
public class Product
{
public string Name;
public double Price;
}
(No console output - this is just a class definition.)
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:Output:
// 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
A folder HelloWorldApp is created with basic C# code.
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 libraryOutput:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!"); // Output message
}
}
Hello, World!
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:Output:
// Navigate to project folder
// Run: dotnet build
// Then run: dotnet run
Build succeeded. Hello, World!
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:Output:
// MyApp/
// ├── Program.cs
// └── MyApp.csproj
Structure helps the compiler understand how to build your project.
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)Output:
{
Console.WriteLine("Starting program...");
}
Starting program...
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 commentOutput:
/*
This is a
multi-line comment
*/
namespace MyApp
{
class Utilities
{
// Helper methods go here
}
}
(No output — this demonstrates comments and structure.)
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;Output: Name: Alice, Age: 25, Student: True
// 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
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;Output:
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"
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;Output: 90, 19.99, True, A, London
float price = 19.99f;
bool isAvailable = true;
char grade = 'A';
string city = "London";
Console.WriteLine($"{score}, {price}, {isAvailable}, {grade}, {city}");
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;Output: y: 10, w: 9, s: 9
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}");
`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;Output: Pi: 3.14
// 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}");
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;Output: Age not provided.
if (age.HasValue)
{
Console.WriteLine($"Age: {age.Value}");
}
else
{
Console.WriteLine("Age not provided.");
}
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";Output: Name: John, Age: 30
// Inferred as string
var age = 30;
// Inferred as int
Console.WriteLine($"Name: {name}, Age: {age}");
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 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>
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>
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>
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 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>
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>
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.
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.
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.
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
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
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
Use control structures wisely to keep your code readable and maintainable. Some tips:
// 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!
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
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 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 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 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
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 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
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.
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
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
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
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
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
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
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!
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 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!
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 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 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 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!
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.
Both interfaces and abstract classes define a contract for other classes, but they differ in several key aspects:
// 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
.
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.
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 Listanimals = 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.
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.
There are two primary ways to inject dependencies in C#: constructor injection and method injection.
// 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.
.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.
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.Output:
// Collections are dynamic and provide more features for manipulating data.
(No output — This is a conceptual explanation.)
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;Output:
using System.Collections.Generic;
class CollectionExamples
{
static void Main()
{
// List example
Listnames = new List { "John", "Jane", "Paul" };
names.Add("Alice");
Console.WriteLine(names[0]); // John
// Dictionary example
DictionaryuserRoles = new Dictionary ();
userRoles.Add(1, "Admin");
Console.WriteLine(userRoles[1]); // Admin
// HashSet example
HashSetcountries = new HashSet { "USA", "Canada", "USA" };
Console.WriteLine(countries.Count); // 2 (duplicates are not allowed)
// Queue example
Queuequeue = new Queue ();
queue.Enqueue("First");
queue.Enqueue("Second");
Console.WriteLine(queue.Dequeue()); // First
// Stack example
Stackstack = new Stack ();
stack.Push("First");
stack.Push("Second");
Console.WriteLine(stack.Pop()); // Second
}
}
John Admin 2 First Second
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;Output:
using System.Collections.Generic;
class GenericCollections
{
static void Main()
{
// Generic List example
Listnumbers = new List { 1, 2, 3, 4, 5 };
numbers.Add(6);
Console.WriteLine(numbers[2]); // 3
// Generic Dictionary example
DictionaryageDictionary = new Dictionary ();
ageDictionary.Add("John", 30);
Console.WriteLine(ageDictionary["John"]); // 30
}
}
3 30
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;Output:
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!
}
}
10 Hello, 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;Output:
using System.Collections.Generic;
using System.Linq;
class LINQExample
{
static void Main()
{
// LINQ with List<T>
Listnumbers = 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);
}
}
}
2 4 6
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;Output:
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
}
}
(No output — this is an example demonstrating covariance.)
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.Output:
// 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.
(No output — this is a conceptual explanation.)
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.
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.
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 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.
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 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.
Following best practices for error handling ensures that your application can recover gracefully from errors and maintain a smooth user experience.
using
or finally
blocks.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!
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
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!
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!
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!
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
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!
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:
// 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).
In C#, Func, Action, and Predicate are predefined delegate types used with lambda expressions:
// A Func delegate that adds two integers Func<int, int, int> add = (a, b) => a + b; Console.WriteLine(add(3, 4)); // Output: 7
// An Action delegate that prints a message Action<string> printMessage = message => Console.WriteLine(message); printMessage("Hello, World!"); // Output: Hello, World!
// A Predicate delegate that checks if a number is even Predicate<int> isEven = number => number % 2 == 0; Console.WriteLine(isEven(4)); // Output: True
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:
// 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 }
// 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 can be written using either query syntax or method syntax. Both produce the same result, but the syntax is different.
// 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 }
// 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 }
LINQ also supports aggregation and grouping operations:
// 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
// Calculating the sum of all numbers int sum = numbers.Sum(); Console.WriteLine(sum); // Output: 27
LINQ supports joining collections based on a common key, similar to SQL joins.
// 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 }
You can extend LINQ with your own custom extension methods. These methods must be static and defined in a static class.
// 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 }
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.
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.
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.
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.
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.
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.
Some best practices to follow when writing asynchronous code include:
async
and await
to make asynchronous code easier to understand and maintain.try...catch
to prevent unhandled errors.CancellationToken
for canceling tasks when needed.
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.
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.
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.
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
.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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.Output:
// EF Core simplifies database interaction by allowing developers to work with data as C# objects.
(No output — this is an introduction.)
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;Output:
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 rows from the 'Users' table.
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;Output:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class AppDbContext : DbContext
{
public DbSetUsers { get; set; }
}
(No output — this demonstrates model creation.)
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())Output:
{
var users = from u in context.Users
where u.Name.Contains("John")
select u;
foreach (var user in users)
{
Console.WriteLine(user.Name);
}
}
Output names of users whose names contain "John".
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.Output:
// Database-First: Generate classes from an existing database using EF Core's scaffold command.
(No output — explanation of approach differences.)
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 InitialCreateOutput:
// 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(); } }
Seeds the Users table with an initial record.
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())Output:
{
// 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();
}
Create, read, update, and delete operations on the Users table.
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.
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;Output:
// 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 }
}
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 ProductOutput:
// 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 }
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 : ControllerBaseOutput:
// 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 }
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)Output:
{
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
} }
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 ProductServiceOutput:
{
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;
} }
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)Output:
{
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!");
}
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>
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>
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 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>
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 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 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>
.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.
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.
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.
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.
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.
.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.
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.
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 Spanto 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 Memoryto 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 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 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 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 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 ValueTaskGetNumberAsync() {
return 42;
}
// Awaiting the ValueTask
ValueTasktask = GetNumberAsync();
Console.WriteLine(await task); // Output: 42
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 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}";
}
The SOLID principles are a set of five object-oriented design principles that help create more understandable, flexible, and maintainable software. They include:
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
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");
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();
- 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
- 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.
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
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.
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.
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.
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 TaskCreateOrder(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.
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.
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.
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.
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.
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.
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 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
.
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.
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:
// 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.
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 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.
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.
// 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(); } }
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.
// 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!") }; } }
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.
// 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");
// 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);
// 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);
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.
// 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);
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.
// 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);
Application Insights is an application performance management (APM) service that helps you monitor your live applications, providing real-time analytics and telemetry.
// 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); }
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.
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 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)'
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.
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 }; Spanspan = 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 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 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.
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 ListmemoryLeaks = 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.
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.
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.