Introduction to C# & .NET Setup
What is C#?
C# (pronounced "C-sharp") is a modern, object-oriented, and type-safe programming language developed by Microsoft. It is designed to be simple, powerful, and versatile. While it shares roots with C and C++, it incorporates modern features that make it more productive and less prone to errors.
C# is the primary language for the .NET ecosystem, meaning it can be used to build almost anything:
- Enterprise Software: Robust backend systems for banks and corporations.
- Web Development: Using ASP.NET Core to build high-performance APIs and websites.
- Game Development: The industry standard for the Unity Engine.
- Cloud Services: Deep integration with Microsoft Azure for scalable cloud apps.
- Mobile Apps: Cross-platform development via .NET MAUI.
The .NET Ecosystem: How it Works
To understand C#, you must understand .NET. C# is the language, but .NET is the runtime and library that executes the code.
C# does not compile directly into machine code (binary). Instead, it follows a two-step process:
- C# Source Code -> Compiled into CIL (Common Intermediate Language).
- CIL -> Translated by the CLR (Common Language Runtime) using JIT (Just-In-Time) Compilation into machine code that your specific CPU understands.
This is why C# is cross-platform; the CIL is the same regardless of whether you are on Windows, Linux, or macOS.
Setting Up Your Environment
To start developing, you need two main components: the SDK and an IDE.
1. The .NET SDK (Software Development Kit)
The SDK contains the compilers, libraries, and tools (like the dotnet command) needed to build applications. Download the latest version from dotnet.microsoft.com.
dotnet --version
# Expected Output: 8.0.x (or the latest stable version)
2. Choosing Your Editor (IDE)
- Visual Studio 2022: The "Full Power" IDE. Best for large enterprise projects and Windows desktop apps.
- VS Code: A lightweight, fast editor. Preferred by many professionals for web development and cross-platform work (requires the "C# Dev Kit" extension).
- JetBrains Rider: A premium, high-performance cross-platform IDE.
Anatomy of Your First Program
Let's look at a standard C# program and break down exactly what each line is doing.
using System; class Program { static void Main() { Console.WriteLine("Hello, World!"); Console.WriteLine("Welcome to C# Mastery!"); } }
Code Breakdown:
using System;: This tells the program to use the System namespace, which contains fundamental classes likeConsole.class Program: C# is an Object-Oriented language. Every piece of code must live inside a class.static void Main(): This is the Entry Point. When you run your app, the .NET runtime looks for this exact method to start execution.
Interacting with the User
Professional apps aren't just one-way; they interact with users. We use Console.ReadLine() to capture input.
using System; class Program { static void Main() { Console.Write("Enter your name: "); string userName = Console.ReadLine(); Console.WriteLine("Hello, " + userName + "! Welcome to the course."); } }
Objective: Create a "User Profile Generator".
Build a console application that asks the user for the following four pieces of information:
- Full Name
- Age
- Country of Residence
- Favorite Programming Language
Once the input is collected, the program should print a professional summary: "User Profile: [Name], [Age] years old, from [Country], specializing in [Language]."
Variables & Data Types
Understanding Variables
In programming, a variable is a named storage location in the computer's memory. Think of it as a labeled box where you can store a value, and then retrieve or change that value later in your program.
C# is a Strongly Typed language. This means that every variable must have a defined type. Once a variable is declared as an integer, you cannot store a string in it. This prevents a huge category of bugs that occur in "loosely typed" languages like JavaScript.
Primitive Data Types
C# provides a variety of built-in types to handle different kinds of data. Choosing the right type is critical for performance and memory efficiency.
| Type | Storage Size | Description | Example |
|---|---|---|---|
int |
4 bytes | Whole numbers (Integers) | int age = 25; |
double |
8 bytes | Decimal numbers (Floating point) | double price = 19.99; |
decimal |
16 bytes | High-precision decimals (Financial) | decimal tax = 15.50m; |
bool |
1 byte | True or False values | bool isComplete = true; |
char |
2 bytes | Single character (Unicode) | char grade = 'A'; |
string |
Variable | Sequence of characters (Text) | string name = "Alice"; |
Never use double or float for financial calculations. They use binary floating-point math which can cause tiny rounding errors (e.g., 0.1 + 0.2 = 0.30000000000000004). Always use decimal for money to ensure absolute precision.
The var Keyword (Implicit Typing)
C# allows you to use the var keyword. This tells the compiler to infer (guess) the type based on the value assigned to it.
var message = "Hello World"; // Compiler knows this is a string var count = 10; // Compiler knows this is an int
Professional Tip: Use var when the type is obvious (e.g., var list = new List<string>();). Avoid using var if it makes the code harder for other developers to read.
Type Casting
Sometimes you need to convert a value from one type to another. This is called Casting.
1. Implicit Casting (Automatic)
Happens when you move a smaller type to a larger type. No data loss occurs.
int myInt = 9; double myDouble = myInt; // Automatic: int becomes double
2. Explicit Casting (Manual)
Required when moving a larger type to a smaller type. This can lead to data loss (truncation).
double myDouble = 9.78; int myInt = (int)myDouble; // Manual: 9.78 becomes 9 (decimals are dropped)
Constants and Read-Only Fields
If a value should never change during the execution of the program, use the const keyword. This improves performance and prevents accidental bugs.
const double Pi = 3.14159; const string CompanyName = "TechCorp Inc"; // Pi = 3.15; // ❌ This will cause a compile-time error
Professional Naming Conventions
In the professional world, following the Microsoft C# Coding Conventions is mandatory for teamwork.
- camelCase: Used for local variables and method parameters.
Example:int userAge = 25; - PascalCase: Used for Classes, Methods, and Constants.
Example:public class UserProfile { } - Avoid: Single letter names like
a,b,x. Use descriptive names liketotalInvoiceAmount.
Scenario: You are building a simple billing system for a client.
Requirements: Create a program that does the following:
- Declare a
const decimalfor a Tax Rate (e.g., 0.15 for 15%). - Ask the user to input the Product Name (string).
- Ask the user to input the Price of the product (decimal).
- Ask the user for the Quantity (int).
- Calculate the Subtotal (Price × Quantity).
- Calculate the Tax Amount (Subtotal × Tax Rate).
- Calculate the Final Total (Subtotal + Tax Amount).
- Print a professional receipt including all the above values.
Hint: Use decimal.Parse(Console.ReadLine()) to convert user input to a decimal.
Operators & Expressions
What are Operators & Expressions?
An expression is a combination of operators, variables, and values that are evaluated to produce a single result. An operator is a symbol that tells the compiler to perform a specific mathematical or logical manipulation.
Every expression in C# results in a value. For example, 5 + 2 is an expression that evaluates to the integer 7. In professional software, we aim for "clean" expressions—avoiding overly complex one-liners that are hard for other developers to read.
1. Arithmetic Operators
Used to perform standard mathematical operations.
| Operator | Name | Description | Example (int a=10, b=3) |
|---|---|---|---|
+ |
Addition | Adds two values | a + b // 13 |
- |
Subtraction | Subtracts one value from another | a - b // 7 |
* |
Multiplication | Multiplies two values | a * b // 30 |
/ |
Division | Divides one value by another | a / b // 3 |
% |
Modulus | Returns the division remainder | a % b // 1 |
In C#, if you divide two integers, the result is always an integer. Any remainder is simply discarded (truncated).
Example: int result = 5 / 2; will give you 2, not 2.5. To get a decimal result, at least one number must be a double or decimal: double result = 5.0 / 2; // 2.5.
2. Comparison (Relational) Operators
These operators compare two values and always return a bool (true or false). They are the foundation of logic in programming.
==Equal to!=Not equal to>Greater than<Less than>=Greater than or equal to<=Less than or equal to
3. Logical Operators
Logical operators are used to combine multiple comparison expressions.
- AND (
&&): Returns true only if both statements are true. - OR (
||): Returns true if at least one statement is true. - NOT (
!): Reverses the result (true becomes false, false becomes true).
C# uses "short-circuiting." In an && operation, if the first condition is false, C# doesn't even check the second condition because the whole thing is already guaranteed to be false. This is used by professionals to prevent crashes (e.g., checking if an object is not null before accessing its properties).
4. Assignment & Compound Operators
Instead of writing x = x + 5, professionals use compound assignments for brevity and clarity.
int score = 100; score += 10; // Same as: score = score + 10; (110) score -= 5; // Same as: score = score - 5; (105) score *= 2; // Same as: score = score * 2; (210) score /= 10; // Same as: score = score / 10; (21)
5. Professional "Syntactic Sugar"
Modern C# provides shorthand operators that make code cleaner and more readable.
The Ternary Operator (?:)
A compact way to write a simple If-Else statement.
int age = 20; string status = (age >= 18) ? "Adult" : "Minor"; // If age >= 18 is true, result is "Adult". Otherwise, "Minor".
The Null-Coalescing Operator (??)
Used to provide a default value if a variable is null.
string input = null; string displayName = input ?? "Guest User"; // If input is null, displayName becomes "Guest User".
Scenario: You are coding the checkout logic for an online store.
Requirements: Build a program that calculates the final price based on these rules:
- Ask user for the Order Total (decimal).
- Ask user if they have a Discount Coupon (bool/string).
- Rule 1: If the Order Total is over $100, they get a 10% discount.
- Rule 2: If they have a coupon AND the order is over $50, they get an additional $5 off.
- Rule 3: If the final total is under $20, add a $5 shipping fee. If it's $20 or more, shipping is free.
- Final Output: Display the Original Price, Total Discounts applied, and the Final Amount to pay.
Challenge: Try using a Ternary operator to determine the shipping fee message (e.g., "Shipping: Free" or "Shipping: $5").
Control Flow: Decision Making
What is Control Flow?
By default, code executes line-by-line from top to bottom. Control Flow allows us to break this linear path, skipping sections of code or executing specific blocks based on whether a condition is true or false.
In the industry, we track "Cyclomatic Complexity"—a measure of how many independent paths there are through your code. The more nested if statements you have, the higher the complexity, and the harder the code is to test and maintain. Professional code aims for low complexity.
1. Conditional Branching (If, Else If, Else)
The if statement is the most basic form of decision-making. It evaluates a boolean expression and executes a block of code if the result is true.
int userAge = 20; if (userAge >= 21) { Console.WriteLine("Access Granted: Full Access."); } else if (userAge >= 18) { Console.WriteLine("Access Granted: Limited Access."); } else { Console.WriteLine("Access Denied: Underage."); }
Professional Tip: Always use curly braces { } even for single-line statements. It prevents bugs during future code edits.
2. The Switch Statement
When you have a single variable that could be one of many distinct values, a switch statement is cleaner and more performant than multiple else if blocks.
string role = "Admin"; switch (role) { case "Admin": Console.WriteLine("Full System Access."); break; case "Editor": Console.WriteLine("Content Management Access."); break; case "Guest": Console.WriteLine("Read-Only Access."); break; default: Console.WriteLine("Unknown Role: Access Denied."); break; }
The break keyword is mandatory. Without it, the code would "fall through" to the next case, which usually causes a compiler error in C#.
3. Modern C#: Switch Expressions (C# 8.0+)
Professional developers rarely use the old switch statement for simple assignments. They use Switch Expressions, which are more concise and return a value directly.
int dayNumber = 3; string dayName = dayNumber switch { 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 => "Thursday", 5 => "Friday", _ => "Weekend" // The underscore (_) is the default case }; Console.WriteLine($"Today is {dayName}");
4. Professional Pattern: The Guard Clause
Beginners often write "nested" code that drifts to the right (Arrow Code). Professionals use Guard Clauses to handle errors early and "flatten" the code.
if (user != null) {
if (user.IsActive) {
if (user.HasPermission) {
// Do the actual work here...
}
}
}
✅ The "Professional" Way (Guard Clauses)
if (user == null) return;
if (!user.IsActive) return;
if (!user.HasPermission) return;
// Do the actual work here... (The code stays flat!)
Scenario: You are building a security module for a corporate building.
Requirements: Create a program that asks for the user's Employee ID and Security Clearance Level (1 to 5).
- Requirement 1: If the Employee ID is empty, print "Error: ID Required" and stop. (Use a Guard Clause).
- Requirement 2: Use a Switch Expression to determine the Access Level based on the Clearance Level:
- 1 -> "Public Area Only"
- 2 -> "Office Area"
- 3 -> "Server Room"
- 4 -> "Executive Suite"
- 5 -> "Top Secret Vault"
- Any other number -> "Invalid Clearance Level"
- Requirement 3: If the Clearance Level is 5 AND the Employee ID is "ADMIN-001", print "Welcome, Director." Otherwise, print the assigned Access Level.
Loops: Mastering Iteration
What is Iteration?
Iteration is the process of executing a block of code repeatedly until a specific condition is met. In professional software, loops are used for processing data sets, polling for status changes, or repeating user prompts until valid input is received.
When you write a loop, you are consuming CPU cycles. A single loop through a list of $n$ items has a time complexity of O(n). However, if you put a loop inside another loop (nested loops), the complexity becomes O(n²). Professionals always strive to minimize nested loops to prevent "performance bottlenecks" as data grows.
1. The for Loop
The for loop is best used when you know exactly how many times the code should run. It is a "counter-driven" loop.
// Structure: (Initialization; Condition; Iterator) for (int i = 0; i < 5; i++) { Console.WriteLine($"Processing item number: {i}"); }
Breakdown:
1. int i = 0: Starts the counter.
2. i < 5: The loop runs as long as this is true.
3. i++: Increases the counter by 1 after each loop.
2. The while and do-while Loops
These are "condition-driven" loops. They are used when you don't know how many times the loop will run, but you know when it should stop.
The while Loop (Check then Act)
Checks the condition before executing the block. If the condition is false at the start, the code inside never runs.
bool isRunning = true; while (isRunning) { Console.WriteLine("System is scanning..."); // Logic to eventually set isRunning = false; }
The do-while Loop (Act then Check)
Executes the block once first, then checks the condition. This guarantees the code runs at least once.
string input; do { Console.WriteLine("Please enter 'exit' to stop:"); input = Console.ReadLine(); } while (input != "exit");
3. The foreach Loop (Industry Standard)
The foreach loop is the most common loop in professional C# development. It is used to iterate through collections (Arrays, Lists, Dictionaries) without needing an index counter.
string[] employees = { "Alice", "Bob", "Charlie" }; foreach (var name in employees) { Console.WriteLine($"Sending email to: {name}"); }
You cannot modify a collection while iterating through it with a foreach loop. If you try to add or remove an item from the list inside a foreach, C# will throw a InvalidOperationException. To modify a list while looping, use a standard for loop and iterate backwards.
4. Loop Control: break and continue
Professionals use these keywords to optimize loops and handle edge cases.
- break: Immediately terminates the loop and jumps to the code following the loop.
- continue: Skips the current iteration and jumps immediately to the next cycle of the loop.
for (int i = 1; i <= 10; i++) { if (i == 3) continue; // Skip 3 if (i == 7) break; // Stop entirely at 7 Console.WriteLine(i); // Output: 1, 2, 4, 5, 6 }
Scenario: You are processing a list of server response times (in milliseconds). You need to analyze the data and filter out "noise".
Requirements:
- Create an array of integers representing response times:
{ 120, 450, 80, -10, 1100, 90, 300, -5, 150 }. - Use a
foreachloop to iterate through the times. - Rule 1 (Data Cleaning): If a response time is negative, it's an error. Use
continueto skip it. - Rule 2 (Critical Failure): If a response time exceeds 1000ms, the server is considered "Crashed". Use
breakto stop processing immediately. - Rule 3 (Analysis): Calculate the total sum of all valid response times processed.
- Final Output: Print the total sum and the number of valid servers processed before the crash (or end of list).
Methods & Parameters
What Are Methods?
Methods are reusable blocks of code that perform a specific task. Instead of writing the same logic multiple times, we place that logic inside a method and call it whenever needed.
Methods help make programs:
- Cleaner and easier to read
- Reusable and modular
- Easier to debug and maintain
- More organized in large applications
void SayHello() { Console.WriteLine("Hello World"); }
Once defined, the method can be called anywhere in the program:
SayHello(); SayHello(); SayHello();
Defining Methods
A method definition usually contains:
- Access modifier (optional)
- Return type
- Method name
- Parameters
- Method body
static int Add(int x, int y) => x + y; void Greet(string name, string title = "Dr.") { Console.WriteLine($"Hello {title} {name}"); }
Understanding Return Types
The return type tells C# what kind of value the method sends back.
int→ returns a whole numberdouble→ returns decimal valuesstring→ returns textbool→ returns true or falsevoid→ returns nothing
int Square(int number) { return number * number; }
Calling the method:
int result = Square(5);
Console.WriteLine(result);
Method Parameters
Parameters allow methods to receive data from outside. They act like variables that belong to the method.
void DisplayUser(string username, int age) { Console.WriteLine(username); Console.WriteLine(age); }
Passing arguments into the method:
DisplayUser("Alex", 22);
Default Parameters
Default parameters provide fallback values when an argument is not supplied. This makes methods more flexible.
void Welcome(string name = "Guest") { Console.WriteLine($"Welcome {name}"); }
Welcome();
Welcome("Emma");
Expression-Bodied Methods
Small methods can be shortened using the lambda-style arrow syntax. This creates cleaner and shorter code.
int Multiply(int a, int b) => a * b;
Method Overloading
C# allows multiple methods with the same name as long as their parameters are different.
int Add(int a, int b) { return a + b; } double Add(double a, double b) { return a + b; }
The compiler automatically selects the correct version.
Pass By Value
By default, C# passes arguments by value. This means the method receives a copy of the data.
void ChangeNumber(int num) { num = 100; } int value = 10; ChangeNumber(value); Console.WriteLine(value);
Output:
10
The original variable remains unchanged because only a copy was modified.
Using ref Parameters
The ref keyword passes variables by reference,
allowing the method to directly modify the original variable.
void Increase(ref int number) { number++; } int score = 5; Increase(ref score); Console.WriteLine(score);
Using out Parameters
The out keyword is used when a method needs to return
multiple values.
void GetValues(out int x, out int y) { x = 10; y = 20; } GetValues(out int a, out int b); Console.WriteLine(a); Console.WriteLine(b);
Local Methods
Methods can also exist inside another method. These are called local methods.
void MainMethod() { void PrintMessage() { Console.WriteLine("Inside Local Method"); } PrintMessage(); }
Best Practices
- Keep methods short and focused
- Use meaningful method names
- Avoid methods that do too many tasks
- Reuse methods whenever possible
- Use parameters instead of hardcoded values
Common Mistakes
- Forgetting the return statement
- Using the wrong return type
- Passing incorrect argument types
- Creating methods with too many parameters
- Not understanding value vs reference passing
Mini Practice
Try creating the following methods yourself:
- A method that calculates area of a rectangle
- A method that checks if a number is even
- A method that prints a full name
- A method with a default parameter
- A method using
reforout
Collections & LINQ
What Are Collections?
Collections are data structures used to store and manage groups of objects. Instead of creating multiple individual variables, collections allow us to organize related data efficiently.
C# provides several built-in collection types that are powerful, flexible, and easy to use.
- Arrays
- Lists
- Dictionaries
- Queues
- Stacks
- HashSets
Why Use Collections?
Collections help developers work with large amounts of data dynamically. They make programs cleaner and more scalable.
- Store multiple values together
- Add or remove data dynamically
- Search and sort efficiently
- Loop through items easily
- Organize related information
Working with Lists
A List<T> is one of the most commonly used collections in C#.
Unlike arrays, lists can grow and shrink dynamically.
var numbers = new List<int> { 10, 20, 30, 40 }; numbers.Add(50); numbers.RemoveAt(0);
After removing the first element, the list becomes:
20, 30, 40, 50
Adding Items to a List
Use the Add() method to insert new items into a list.
var fruits = new List<string>(); fruits.Add("Apple"); fruits.Add("Banana"); fruits.Add("Orange");
Accessing List Elements
List items are accessed using indexes.
Indexes start from 0.
Console.WriteLine(fruits[0]); Console.WriteLine(fruits[1]);
Looping Through Lists
Lists are commonly used with loops to process every item.
foreach (var item in fruits) { Console.WriteLine(item); }
Useful List Methods
| Method | Description |
|---|---|
| Add() | Adds a new item |
| Remove() | Removes a specific item |
| RemoveAt() | Removes item by index |
| Contains() | Checks if item exists |
| Clear() | Removes all items |
| Count | Returns total number of items |
Using Dictionaries
Dictionaries store data as key-value pairs. They are useful when each item has a unique identifier.
var students = new Dictionary<int, string>(); students.Add(1, "John"); students.Add(2, "Emma"); Console.WriteLine(students[1]);
Understanding LINQ
LINQ stands for Language Integrated Query. It allows developers to query collections using simple and readable syntax.
LINQ works with:
- Lists
- Arrays
- Dictionaries
- Databases
- XML data
Introduction to LINQ
LINQ (Language Integrated Query) allows you to query collections like SQL.
var highScores = numbers
.Where(n => n > 25)
.OrderByDescending(n => n);
This query:
- Filters numbers greater than 25
- Sorts them in descending order
The Where() Method
Where() filters data based on a condition.
var evenNumbers = numbers.Where(n => n % 2 == 0);
Only even numbers will be selected.
The Select() Method
Select() transforms data into another form.
var squared = numbers.Select(n => n * n);
This creates a new collection with squared values.
Sorting Data with LINQ
LINQ provides easy methods for sorting collections.
var ascending = numbers.OrderBy(n => n); var descending = numbers.OrderByDescending(n => n);
Finding Data
LINQ can quickly search for matching values.
var firstLargeNumber = numbers.First(n => n > 25);
The first number greater than 25 will be returned.
Counting Items
The Count() method returns the total number of matching items.
int total = numbers.Count(); int evenCount = numbers.Count(n => n % 2 == 0);
Combining LINQ Methods
Multiple LINQ methods can be chained together for powerful queries.
var result = numbers
.Where(n => n > 10)
.OrderBy(n => n)
.Select(n => n * 2);
Deferred Execution
Most LINQ queries use deferred execution. This means the query runs only when the data is actually used.
var query = numbers.Where(n => n > 20);
Console.WriteLine(query.Count());
Common LINQ Methods
| Method | Purpose |
|---|---|
| Where() | Filter data |
| Select() | Transform data |
| OrderBy() | Sort ascending |
| OrderByDescending() | Sort descending |
| First() | Get first matching item |
| Count() | Count items |
| Any() | Check if data exists |
Best Practices
- Use collections instead of many separate variables
- Choose the correct collection type for the task
- Keep LINQ queries readable
- Avoid overly complex chained queries
- Use meaningful variable names
Common Mistakes
- Using the wrong collection type
- Forgetting indexes start at 0
- Modifying collections during iteration
- Writing unreadable LINQ chains
- Ignoring null values in queries
Mini Practice
Try creating the following:
- A list of student names
- A dictionary of product prices
- A LINQ query that finds even numbers
- A query that sorts numbers descending
- A query using both
Where()andSelect()
Strings & Formatting
What Are Strings?
A string is a sequence of characters used to store text. Strings are one of the most commonly used data types in C#.
Examples of strings:
- Names
- Email addresses
- Messages
- User input
- File paths
string name = "John"; string message = "Welcome to C#";
Creating Strings
Strings are written inside double quotation marks.
string city = "London"; string language = "C#";
String Manipulation
C# provides many built-in methods for modifying and working with strings.
string text = " C# Programming "; Console.WriteLine(text.Trim().ToUpper()); Console.WriteLine(text.Contains("C#"));
This example:
- Removes extra spaces using
Trim() - Converts text to uppercase using
ToUpper() - Checks if text contains a value using
Contains()
Common String Methods
| Method | Description |
|---|---|
| ToUpper() | Converts text to uppercase |
| ToLower() | Converts text to lowercase |
| Trim() | Removes extra spaces |
| Contains() | Checks for matching text |
| Replace() | Replaces text |
| Substring() | Extracts part of a string |
| StartsWith() | Checks starting characters |
| EndsWith() | Checks ending characters |
Converting Case
Strings can easily be converted between uppercase and lowercase.
string word = "Programming"; Console.WriteLine(word.ToUpper()); Console.WriteLine(word.ToLower());
Replacing Text
The Replace() method changes parts of a string.
string sentence = "I like Java"; sentence = sentence.Replace("Java", "C#"); Console.WriteLine(sentence);
Extracting Text
The Substring() method extracts a portion of a string.
string value = "Programming"; Console.WriteLine(value.Substring(0, 4));
Output:
Prog
Escaping Characters
Escape characters allow special characters inside strings.
Console.WriteLine("She said \"Hello\""); Console.WriteLine("C:\\Projects\\App");
Verbatim Strings
Verbatim strings use the @ symbol to preserve formatting.
string path = @" C:\Projects\MyApp\data.txt"; Console.WriteLine(path);
String Concatenation
Concatenation means joining strings together.
string firstName = "John"; string lastName = "Doe"; string fullName = firstName + " " + lastName; Console.WriteLine(fullName);
String Interpolation
String interpolation is a cleaner way to insert variables into strings.
Console.WriteLine($"Date: {DateTime.Now:d}"); Console.WriteLine($"Price: {19.99:C}"); // Currency
The $ symbol allows variables and expressions inside curly braces.
Formatting Values
C# supports formatting numbers, dates, and currency values.
double price = 99.95; Console.WriteLine($"{price:C}"); Console.WriteLine($"{price:N2}"); Console.WriteLine($"{price:P}");
| Format | Meaning |
|---|---|
| C | Currency |
| N2 | Number with 2 decimals |
| P | Percentage |
| d | Short date format |
Working with String Length
The Length property returns the number of characters.
string username = "Admin"; Console.WriteLine(username.Length);
Splitting Strings
The Split() method separates text into multiple parts.
string data = "HTML,CSS,JavaScript"; var skills = data.Split(','); foreach (var skill in skills) { Console.WriteLine(skill); }
Comparing Strings
Strings can be compared using comparison operators or helper methods.
string a = "C#"; string b = "c#"; Console.WriteLine(a == b); Console.WriteLine(a.Equals(b, StringComparison.OrdinalIgnoreCase));
String Immutability
Strings in C# are immutable. This means their contents cannot be changed after creation.
Methods like Replace() or ToUpper()
create new strings instead of modifying the original one.
string textValue = "hello"; string upper = textValue.ToUpper(); Console.WriteLine(textValue); Console.WriteLine(upper);
Best Practices
- Use meaningful variable names
- Prefer string interpolation over concatenation
- Trim user input when necessary
- Use formatting for readable output
- Keep string operations simple and readable
Common Mistakes
- Forgetting escape characters
- Using invalid indexes in Substring()
- Ignoring whitespace in user input
- Comparing strings with incorrect casing
- Confusing immutable behavior
Mini Practice
Try creating programs that:
- Convert text to uppercase
- Replace words in a sentence
- Display formatted prices
- Split a CSV string into values
- Create a formatted welcome message using interpolation
Object-Oriented Programming
Welcome, class. Today we deep-dive into the foundational architecture of scalable software design: Object-Oriented Programming (OOP). As software systems grow in complexity, linear, procedural code inevitably collapses under its own weight. OOP provides us with a framework to manage this complexity by partitioning our programs into self-contained, reusable modules modeled after real-world entities.
The Conceptual Paradigm: Blueprints vs. Artifacts
Before looking at the syntax, you must master the fundamental distinction between a Class and an Object:
- The Class (The Blueprint): This is an abstract data type. It defines the structure, capabilities, and rules of engagement, but it consumes no operational memory for data storage. It is merely a design.
- The Object (The Instance): This is the concrete artifact generated from that blueprint. It lives dynamically within your system's RAM, maintaining its own unique state.
Consider the architectural example below written in C#. Here, we define a baseline archetype (Person) and a specialized extension of that archetype (Developer).
public class Person { public string Name { get; set; } public virtual void Work() => Console.WriteLine("Working..."); } public class Developer : Person { public override void Work() => Console.WriteLine("Coding..."); }
Anatomy of the Mechanics: Inheritance & Polymorphism
Let us break down the underlying mechanics of this code slice. Notice the use of the : operator in Developer : Person. This establishes an IS-A relationship (Inheritance). A Developer is a Person, meaning it automatically inherits the Name property without explicit redeclaration.
However, the real magic happens within the execution behavior, driven by two critical keywords:
virtual: Declared in the base class, this signals to the compiler that this method's behavior is fluid and can be intercepted by derived child classes.override: Declared in the subclass, this actively intercepts the runtime execution path, substituting the base behavior with a specialized implementation.
Advanced Execution: Dynamic Binding in Action
To fully appreciate why this matters to a software architect, look at how the runtime engine processes these objects when grouped together. Because of polymorphism, a collection typed to the base class can hold any derived class instance seamlessly.
List<Person> team = new List<Person>() { new Person { Name = "Professor Smith" }, new Developer { Name = "Grace Hopper" } }; foreach (var member in team) { Console.Write(member.Name + " execution path: "); member.Work(); }
When this loop runs, the output demonstrates Dynamic Binding (or late binding). The system determines which method to call at runtime based on the actual object type, not the variable type:
Professor Smith execution path: Working... Grace Hopper execution path: Coding...
Professor's Lab Task: Expanding the Ecosystem
To reinforce today's lecture, you must complete a practical coding task. Your goal is to introduce a third tier to our organizational hierarchy that introduces unique tracking and alters behavior appropriately.
Your Instructions:
- Create a new class named
Managerthat inherits directly fromPerson. - Add an exclusive property to the
Managerclass:TeamSize(an integer type). - Override the
Work()method so that it outputs:"Managing a team of [TeamSize] people..."instead of the generic response.
public class Manager : Person { // 1. Task: Define your TeamSize property here // 2. Task: Override the Work() method using string interpolation }
Verify your solution by creating an instance of your Manager, assigning it a TeamSize of 5, adding it to the team list created above, and confirming that the output shifts dynamically at runtime.
Exceptions & File I/O
Handling Errors
try { int x = int.Parse(Console.ReadLine()); } catch (FormatException ex) { Console.WriteLine("Invalid number format."); } finally { Console.WriteLine("Done."); }
Reading/Writing Files
File.WriteAllText("log.txt", "Hello C#"); string content = File.ReadAllText("log.txt");
The .NET Ecosystem
NuGet & Libraries
Use dotnet add package to install community libraries like Newtonsoft.Json.
Asynchronous Programming
public async Task<string> GetDataAsync() { var client = new HttpClient(); return await client.GetStringAsync("https://api.example.com"); }
Capstone Project: Task Manager
Build a console-based Task Manager that uses everything we've learned: Lists, LINQ, OOP, and File I/O.
// Project requirements: // 1. Add/Remove tasks // 2. Mark tasks as complete // 3. Save tasks to tasks.json // 4. Filter tasks using LINQ
Asynchronous Programming with async & await
Asynchronous programming is fundamental to scalable .NET backends and responsive UI applications. By leveraging the Task-based Asynchronous Pattern (TAP), async, and await, your threads remain unblocked during I/O-bound database queries, HTTP network calls, and file operations.
1. The Task-based Asynchronous Pattern (TAP)
In synchronous execution, a thread waits idle while network packets travel across the wire or a disk head seeks sectors. In asynchronous C#, the thread is yielded back to the thread pool to process other web requests while the I/O completion port awaits completion:
using System.Net.Http.Json; public class WeatherService { private readonly HttpClient _http = new(); // Task<T> represents an ongoing operation that will yield a T in the future public async Task<WeatherForecast?> FetchForecastAsync(string city, CancellationToken ct = default) { string url = $"https://api.weather.com/v1/{Uri.EscapeDataString(city)}"; // Thread is NOT blocked during GetFromJsonAsync! return await _http.GetFromJsonAsync<WeatherForecast>(url, ct); } } public record WeatherForecast(string City, double TemperatureC, string Summary);
2. CancellationTokens & Resource Management
Every asynchronous API in production .NET should accept a CancellationToken. When a user navigates away from a page or closes an HTTP connection, the token triggers cancellation to immediately stop wasteful backend work.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Timeout after 5s try { var service = new WeatherService(); var forecast = await service.FetchForecastAsync("London", cts.Token); Console.WriteLine($"Forecast: {forecast?.Summary} at {forecast?.TemperatureC}°C"); } catch (OperationCanceledException) { Console.WriteLine("Request timed out or was aborted by the client."); } catch (HttpRequestException ex) { Console.WriteLine($"Network error: {ex.Message}"); }
- Avoid async void: Never write
async voidexcept for UI event handlers. Exceptions thrown inside anasync voidmethod cannot be caught by callers and will immediately crash the process. Always returnTaskorTask<T>. - Do not use .Result or .Wait(): Calling
task.Resultortask.Wait()synchronously blocks the calling thread, risking thread pool starvation and deadlocks in ASP.NET SynchronizationContexts. Always useawait.
3. Concurrency with Task.WhenAll & ValueTask
When you have multiple independent I/O tasks, run them concurrently instead of sequentially with Task.WhenAll:
public async Task<DashboardData> LoadDashboardAsync(int userId, CancellationToken ct) { // Initiate both queries concurrently without awaiting yet Task<User> userTask = _userRepo.GetByIdAsync(userId, ct); Task<List<Order>> ordersTask = _orderRepo.GetRecentOrdersAsync(userId, ct); // Wait for both to complete simultaneously await Task.WhenAll(userTask, ordersTask); return new DashboardData(userTask.Result, ordersTask.Result); }
- When to use ValueTask<T>: Use
ValueTask<T>when a method frequently completes synchronously (for instance, reading from an in-memory cache). It avoids heap allocations forTaskobjects when results are already available. - ConfigureAwait(false): In general class libraries that do not depend on UI contexts, appending
.ConfigureAwait(false)prevents capturing synchronization context and improves throughput.
What is the primary danger of using `async void` instead of `async Task` in C#?
Because `async void` methods do not return a Task, the caller cannot await them or catch exceptions thrown inside them. Any unhandled exception is posted directly to the synchronization context and terminates the application.
What happens when calling `.Result` on a running Task instead of awaiting it?
`.Result` blocks the calling thread synchronously. This ties up thread pool threads and, if run in an environment with a SynchronizationContext (like ASP.NET classic or WPF/WinForms), frequently causes circular deadlocks.
Which utility method allows executing multiple independent Tasks concurrently and waiting for all of them to finish?
`Task.WhenAll()` returns a Task that completes only when all supplied tasks have completed, enabling parallel asynchronous execution of multiple I/O operations.
Delegates, Lambdas & Events
Delegates are type-safe function pointers that form the bedrock of event-driven architectures, LINQ expressions, and callback pipelines in .NET. Master custom delegates, standard library delegates (Action, Func, Predicate), and thread-safe event publishing.
1. What is a Delegate?
A delegate is a reference type that defines a strict method signature (return type and parameters). Any method matching that signature can be assigned to the delegate:
// Custom delegate declaration public delegate int MathOperation(int a, int b); public class Calculator { public static int Add(int x, int y) => x + y; public static int Multiply(int x, int y) => x * y; } public class Program { public static void Main() { MathOperation op = Calculator.Add; Console.WriteLine(op(4, 5)); // 9 op = Calculator.Multiply; Console.WriteLine(op(4, 5)); // 20 } }
2. Built-in Delegates: Action, Func, and Predicate
In modern C#, you rarely need to declare custom delegate types. The .NET BCL provides generic delegates that cover virtually every scenario:
Action<T1, T2...>: Represents a method returningvoidwith up to 16 parameters.Func<T1, T2..., TResult>: Represents a method returningTResultwith up to 16 parameters.Predicate<T>: Equivalent toFunc<T, bool>, used for filtering criteria.
// Action: returns void Action<string> logger = message => Console.WriteLine($"[LOG {DateTime.UtcNow:s}] {message}"); logger("Application booted"); // Func: last generic type argument is always the return type Func<string, int, bool> isLongerThan = (text, threshold) => text.Length > threshold; bool check = isLongerThan("Enterprise .NET", 10); // true // Predicate: filtering lists Predicate<int> isEven = n => n % 2 == 0; List<int> numbers = [1, 2, 3, 4, 5, 6]; List<int> evens = numbers.FindAll(isEven); // [2, 4, 6]
3. The Event Pattern & Memory Leaks
Events encapsulate delegates using the publish-subscribe pattern, ensuring that external classes can only subscribe (+=) or unsubscribe (-=), preventing external callers from clearing or triggering the delegate directly.
// Custom event arguments inheriting from EventArgs public class OrderEventArgs : EventArgs { public required string OrderId { get; init; } public decimal TotalAmount { get; init; } } public class OrderProcessor { // Standard .NET event declaration public event EventHandler<OrderEventArgs>? OrderCompleted; public void ProcessOrder(string id, decimal amount) { Console.WriteLine($"Processing order {id} for ${amount:F2}..."); // Thread-safe invocation using null-conditional operator OrderCompleted?.Invoke(this, new OrderEventArgs { OrderId = id, TotalAmount = amount }); } } // Subscriber public class InvoiceService { public void OnOrderCompleted(object? sender, OrderEventArgs e) { Console.WriteLine($"Generating invoice for order {e.OrderId}: ${e.TotalAmount:F2}"); } }
- Event Subscription Memory Leaks: If a long-lived publisher holds an event subscription to a short-lived subscriber without unsubscribing (
publisher.Event -= subscriber.Handler), the garbage collector cannot collect the subscriber. Always unsubscribe or implementIDisposable. - Calling delegate without null check: In multithreaded apps, if the last subscriber unregisters between your null check and invocation, a
NullReferenceExceptionwill occur. Always useEventName?.Invoke(this, args);.
In the delegate `Func
In the generic `Func` delegate family, the last generic type parameter is always the return type of the method. The preceding types represent the parameter types.
Why should you always invoke events using `MyEvent?.Invoke(this, e)` instead of `MyEvent(this, e)`?
If an event has no registered subscribers, its internal multicast delegate field is `null`. Using `?.Invoke()` safely short-circuits and prevents a `NullReferenceException`.
How does an event subscription cause a memory leak if not unsubscribed?
A delegate instance stores a reference to its target object (the subscriber). As long as the publisher remains alive, that reference prevents the GC from freeing the subscriber.
Generics, Constraints & Interface Design
Generics provide type safety, code reusability, and high runtime performance by eliminating boxing/unboxing overhead. Paired with robust interface design and generic constraints, they form the cornerstone of enterprise software architecture and Dependency Injection.
1. Generic Classes & Methods
Generics decouple algorithms and data structures from specific concrete data types. In C#, generics are reified at runtime by the CLR, generating specialized native code for value types without boxing overhead:
// Generic Result container pattern public class Result<T> { public bool IsSuccess { get; } public T? Value { get; } public string? Error { get; } private Result(T value) { IsSuccess = true; Value = value; } private Result(string error) { IsSuccess = false; Error = error; } public static Result<T> Ok(T value) => new(value); public static Result<T> Fail(string error) => new(error); } // Generic Utility Method public static class EnumerableExtensions { public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> source, int count) { var rng = new Random(); return source.OrderBy(_ => rng.Next()).Take(count); } }
2. Generic Constraints (where T : ...)
Generic constraints inform the compiler what capabilities the type argument must possess. Without constraints, you can only invoke methods defined on object:
public interface IEntity { Guid Id { get; } DateTime CreatedAtUtc { get; } } // Repository constrained to reference types implementing IEntity with a parameterless constructor public class Repository<T> where T : class, IEntity, new() { private readonly Dictionary<Guid, T> _storage = new(); public void Add(T entity) { _storage[entity.Id] = entity; } public T? GetById(Guid id) => _storage.GetValueOrDefault(id); public T CreateDefault() { // Allowed because of 'new()' constraint! return new T(); } }
- Common Generic Constraints:
where T : class: Must be a reference type.where T : struct: Must be a non-nullable value type.where T : notnull: Must be non-nullable (C# 8+).where T : new(): Must have a public parameterless constructor.where T : unmanaged: Must be a blittable struct with no reference fields.
3. Interface Segregation & DI Contracts
Follow the Interface Segregation Principle (ISP): prefer small, focused interfaces over monolithic ones. This simplifies unit test mocking and dependency injection registration:
public interface IReadOnlyRepository<T> where T : class { Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default); Task<IReadOnlyList<T>> ListAllAsync(CancellationToken ct = default); } public interface IWriteRepository<T> where T : class { Task AddAsync(T entity, CancellationToken ct = default); Task DeleteAsync(Guid id, CancellationToken ct = default); } // Combined interface when both capabilities are required public interface IRepository<T> : IReadOnlyRepository<T>, IWriteRepository<T> where T : class { }
What is the performance benefit of C# generics over non-generic collections like ArrayList?
When value types (like int, struct) are stored in generic collections (List
What does the constraint `where T : new()` enable you to do inside a generic class?
The `new()` constraint specifies that any type argument supplied for T must have a public parameterless constructor, allowing the compiler to permit expressions like `new T()`.
Which generic variance keyword allows an interface to return derived instances when a base type is expected (covariance)?
The `out` keyword marks a generic type parameter as covariant, meaning the type parameter can only be used as an output/return type (e.g., `IEnumerable
Entity Framework Core & Relational Databases
Entity Framework Core (EF Core) is the modern object-database mapper for .NET. It lets you query and persist domain models to relational databases (PostgreSQL, SQL Server, SQLite) using strongly-typed LINQ queries, automatic change tracking, and code-first database migrations.
1. Modeling Entities & DbContext
An entity is a domain model mapped to a database table. The DbContext coordinates querying, relationship configuration, and saving changes:
using Microsoft.EntityFrameworkCore; // Domain Entities public class Customer { public int Id { get; set; } public required string FullName { get; set; } public required string Email { get; set; } // Navigation property: One-to-Many public List<Order> Orders { get; set; } = new(); } public class Order { public int Id { get; set; } public DateTime OrderDate { get; set; } = DateTime.UtcNow; public decimal TotalAmount { get; set; } // Foreign key and navigation back to Customer public int CustomerId { get; set; } public Customer Customer { get; set; } = null!; } // Application DbContext public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {} public DbSet<Customer> Customers => Set<Customer>(); public DbSet<Order> Orders => Set<Order>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Customer>(entity => { entity.HasKey(c => c.Id); entity.HasIndex(c => c.Email).IsUnique(); entity.Property(c => c.FullName).HasMaxLength(150).IsRequired(); }); } }
2. Efficient LINQ Queries & AsNoTracking
EF Core translates LINQ expressions directly into optimized SQL queries executed by the database server:
public async Task<List<CustomerDto>> GetTopSpendersAsync(AppDbContext db, CancellationToken ct) { // AsNoTracking(): Skips change-tracking snapshots; 2-3x faster for read-only queries! return await db.Customers .AsNoTracking() .Where(c => c.Orders.Any()) .Select(c => new CustomerDto( c.Id, c.FullName, c.Email, c.Orders.Sum(o => o.TotalAmount) )) .OrderByDescending(dto => dto.TotalSpent) .Take(10) .ToListAsync(ct); } public record CustomerDto(int Id, string FullName, string Email, decimal TotalSpent);
- The N+1 Query Problem: Never access navigation properties in a loop without eager loading (
.Include(c => c.Orders)) or projecting with.Select(). Otherwise, EF executes 1 query for the parent and N separate queries for each child. - Client-Side Evaluation: Avoid invoking arbitrary C# methods inside
Where()orSelect()clauses that the EF provider cannot translate into SQL. This forces EF to pull entire tables into memory before filtering.
3. Transactions & Unit of Work (SaveChangesAsync)
SaveChangesAsync collects all dirty, added, and deleted entities and executes them inside an implicit database transaction:
public async Task<Order> CreateOrderAsync(AppDbContext db, int customerId, decimal amount, CancellationToken ct) { var customer = await db.Customers.FindAsync([customerId], ct) ?? throw new KeyNotFoundException($"Customer {customerId} not found"); var order = new Order { CustomerId = customerId, TotalAmount = amount, OrderDate = DateTime.UtcNow }; db.Orders.Add(order); // Executes inside an atomic transaction await db.SaveChangesAsync(ct); return order; }
Why should you include `.AsNoTracking()` on read-only queries in EF Core?
When EF Core queries entities, it normally attaches them to the change tracker and saves a state snapshot. For read-only operations, `.AsNoTracking()` bypasses this tracking overhead, dramatically cutting memory allocations and query execution time.
How do you prevent the N+1 query problem when loading related child entities?
Eager loading with `.Include()` instructs EF Core to generate SQL JOINs to fetch parent and child rows in a single query, eliminating separate roundtrips for each individual child record.
What is the role of EF Core Migrations in a production project?
EF Core Migrations provide a structured, version-controlled mechanism to apply incremental schema updates (creating tables, modifying columns, adding indexes) to database instances across development, staging, and production.
Unit Testing & Mocking with xUnit & Moq
High-quality software demands automated tests that verify correctness, protect against regressions, and document design behavior. Learn test-driven development (TDD) using xUnit (the de facto standard in .NET), parameterized testing with [Theory], and test doubles with Moq.
1. Test Anatomy: Arrange, Act, Assert
xUnit identifies test methods using attributes. Every clean unit test follows the AAA pattern:
using Xunit; public class BankAccountTests { [Fact] // [Fact] denotes a test that is always true under invariant conditions public void Deposit_ValidAmount_IncreasesBalance() { // 1. Arrange: setup initial state and dependencies var account = new BankAccount(initialBalance: 100m); // 2. Act: invoke the method under test account.Deposit(50m); // 3. Assert: verify expected outcome Assert.Equal(150m, account.Balance); } [Fact] public void Withdraw_AmountGreaterThanBalance_ThrowsInvalidOperationException() { // Arrange var account = new BankAccount(initialBalance: 50m); // Act & Assert var ex = Assert.Throws<InvalidOperationException>(() => account.Withdraw(100m)); Assert.Contains("Insufficient funds", ex.Message); } }
2. Parameterized Tests with [Theory] & [InlineData]
Instead of duplicating test methods for different test cases, use [Theory] to run the same test logic against a matrix of inputs:
public class EmailValidatorTests { [Theory] [InlineData("[email protected]", true)] [InlineData("[email protected]", true)] [InlineData("missing-at-sign.com", false)] [InlineData("@missing-local.com", false)] [InlineData("", false)] [InlineData(null, false)] public void IsValidEmail_EvaluatesCorrectly(string? input, bool expected) { var validator = new EmailValidator(); bool result = validator.IsValid(input); Assert.Equal(expected, result); } }
3. Mocking Dependencies with Moq
Unit tests must test logic in isolation without talking to real databases or sending real emails. We substitute interfaces with mock objects:
using Moq; using Xunit; public class OrderServiceTests { [Fact] public async Task PlaceOrderAsync_ValidOrder_SendsNotificationAndSaves() { // Arrange: Mock dependencies var mockRepo = new Mock<IOrderRepository>(); var mockNotifier = new Mock<INotificationService>(); var service = new OrderService(mockRepo.Object, mockNotifier.Object); var order = new Order { Id = 101, CustomerId = 1, TotalAmount = 99.99m }; // Act await service.PlaceOrderAsync(order); // Assert: Verify mock interactions mockRepo.Verify(r => r.SaveAsync(order, It.IsAny<CancellationToken>()), Times.Once); mockNotifier.Verify(n => n.NotifyCustomerAsync(order.CustomerId, It.IsAny<string>()), Times.Once); } }
- FluentAssertions: For expressive assertions that read like natural language, install
FluentAssertions. ReplaceAssert.Equal(5, list.Count);withlist.Should().HaveCount(5).And.Contain(x);. - Fast and Deterministic: Unit tests should execute in milliseconds and never touch disk, network, or external clocks. Use
TimeProvider(introduced in .NET 8) to mock time without callingDateTime.UtcNow.
In xUnit, what is the difference between `[Fact]` and `[Theory]` attributes?
`[Fact]` marks an invariant unit test requiring no parameters. `[Theory]` marks a parameterized data-driven test that xUnit runs repeatedly against each supplied dataset (from `[InlineData]`, `[MemberData]`, or `[ClassData]`).
What does `mock.Verify(x => x.SaveAsync(...), Times.Once)` check in a unit test?
Moq's .Verify() method asserts behavioral expectations on mocked interfaces, confirming that the code under test interacted with the dependency exactly the expected number of times.
Which phase of the Arrange-Act-Assert (AAA) pattern executes the method being evaluated?
The "Act" phase is where you execute the specific method or business logic under test, capturing any returned values or thrown exceptions for subsequent assertion.
Final Project — Production ASP.NET Core Web API
In this capstone project, you will build a production-grade RESTful Web API using ASP.NET Core Minimal APIs, Entity Framework Core, structured validation, dependency injection, and OpenAPI documentation.
1. Project Architecture & Minimal API Endpoints
Modern ASP.NET Core uses Minimal APIs to achieve industry-leading throughput with concise, readable syntax:
using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); // Register Services in Dependency Injection Container builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<AppDbContext>(opts => opts.UseSqlite(builder.Configuration.GetConnectionString("Default") ?? "Data Source=store.db")); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Route Group: /api/products var products = app.MapGroup("/api/products").WithTags("Products"); products.MapGet("/", async (AppDbContext db, CancellationToken ct) => Results.Ok(await db.Products.AsNoTracking().ToListAsync(ct))); products.MapGet("/{id:int}", async (int id, AppDbContext db, CancellationToken ct) => await db.Products.FindAsync([id], ct) is Product p ? Results.Ok(p) : Results.NotFound(new { Message = $"Product {id} not found" })); products.MapPost("/", async (CreateProductDto dto, AppDbContext db, CancellationToken ct) => { if (string.IsNullOrWhiteSpace(dto.Name) || dto.Price <= 0) return Results.BadRequest(new { Error = "Invalid name or price." }); var product = new Product { Name = dto.Name, Price = dto.Price, Stock = dto.Stock }; db.Products.Add(product); await db.SaveChangesAsync(ct); return Results.Created($"/api/products/{product.Id}", product); }); products.MapDelete("/{id:int}", async (int id, AppDbContext db, CancellationToken ct) => { var product = await db.Products.FindAsync([id], ct); if (product is null) return Results.NotFound(); db.Products.Remove(product); await db.SaveChangesAsync(ct); return Results.NoContent(); }); app.Run(); public record CreateProductDto(string Name, decimal Price, int Stock); public class Product { public int Id { get; set; } public required string Name { get; set; } public decimal Price { get; set; } public int Stock { get; set; } }
2. Global Error Handling Middleware
Never let unhandled exceptions leak stack traces to public API consumers. Use custom exception handling or app.UseExceptionHandler() with ProblemDetails:
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
var problem = new
{
Status = 500,
Title = "An unexpected server error occurred.",
TraceId = context.TraceIdentifier
};
await context.Response.WriteAsJsonAsync(problem);
});
});
- DTOs (Data Transfer Objects): Never accept or return EF Core entity classes directly in API endpoints. Exposing entities leaks internal schema details and makes your API vulnerable to mass-assignment over-posting attacks.
- CancellationToken Propagation: Always accept
CancellationToken ctin your endpoint delegates and pass it to async EF Core methods. If the client drops the connection, EF aborts the SQL query immediately.
To finalize your C# & .NET Certification, implement these extensions:
- Add a
PUT /api/products/{id}endpoint with concurrency token checks. - Introduce an authentication endpoint using JWT Bearer tokens (
app.UseAuthentication()andapp.UseAuthorization()). - Write xUnit integration tests using
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryto verify endpoints return correct HTTP status codes.
Why should you use DTOs instead of binding directly to EF Core entity classes in API endpoints?
Accepting EF entities directly allows malicious callers to inject fields (like IsAdmin = true or Balance = 999999) that they should not modify. DTOs define strict input contracts and prevent over-posting vulnerabilities.
What is the standard HTTP status code returned upon successfully creating a resource via POST?
HTTP 201 Created is the standard REST status code for a successful resource creation, typically accompanied by a `Location` header pointing to the URI of the newly created resource.
What does `Results.NoContent()` translate to in HTTP response terms?
HTTP 204 No Content indicates that the server successfully processed the request (such as a DELETE or PUT) and there is no entity-body to return to the client.
Project 1: Enterprise Inventory Manager (LINQ & File I/O)
Build a robust C# Inventory Management application using LINQ queries, JSON serialization, and async tasks.
Project Overview
Master LINQ filtering, aggregation, file storage, and object-oriented C# design.
Full Code Implementation
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.IO;
public class Product {
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
public class InventoryManager {
private List _products = new List();
public void AddProduct(Product p) => _products.Add(p);
public void DisplayLowStock(int threshold) {
var lowStock = _products.Where(p => p.Stock < threshold).OrderBy(p => p.Stock);
foreach (var p in lowStock) {
Console.WriteLine($"{p.Name} (Category: {p.Category}) - Stock: {p.Stock}");
}
}
}
class Program {
static void Main() {
var manager = new InventoryManager();
manager.AddProduct(new Product { Id = 1, Name = "Laptop", Category = "Tech", Price = 999.99m, Stock = 5 });
manager.DisplayLowStock(10);
}
}
Write a LINQ query that calculates total inventory valuation (SUM(Price * Stock)) grouped by Category!