C#: Complete Developer Learning Guide – Part 1 (Fundamentals & Object-Oriented Programming)
Build a strong C# foundation with clear explanations, practical examples, modern syntax, and object-oriented programming concepts. Written for learners preparing to build real applications with the .NET platform using C# 14.
Introduction
C# is a modern, strongly typed, object-oriented programming language developed by Microsoft. It is widely used to build web applications, Web APIs, desktop software, cloud services, background workers, games, and enterprise systems.
For a .NET developer, C# is more than a language used to write controller methods. It is the foundation for expressing business rules, modelling domain objects, managing data, handling errors, working asynchronously, and designing maintainable application code.
This two-part guide is designed to take you from the language fundamentals to the concepts used in professional .NET development. Part 1 establishes the foundation: syntax, data types, control flow, methods, collections, and object-oriented programming. Part 2 will build on that foundation and cover the language and runtime capabilities commonly used in medium-to-advanced .NET applications.
C# and the .NET Platform
What is C#?
C# (pronounced “C sharp”) is a general-purpose, strongly typed language designed for developer productivity, safety, and performance. It supports object-oriented programming, functional-style techniques, generic programming, asynchronous programming, and many other programming models.
What is .NET?
.NET is a development platform that includes a runtime, libraries, SDK tools, and application frameworks. C# is one of the primary languages used to build applications on .NET. The language and the platform are related, but they are not the same thing.
| Term | Meaning |
|---|---|
| C# | The programming language used to write application code. |
| .NET SDK | Development tools used to create, build, test, and publish .NET applications. |
| .NET Runtime | Executes managed .NET application code and provides runtime services. |
| Base Class Library (BCL) | Reusable types for strings, collections, files, dates, networking, and more. |
| ASP.NET Core | A framework for building web applications and HTTP APIs on .NET. |
How C# Code Runs
C# Source Code (.cs)
|
v
C# Compiler
|
v
Intermediate Language (IL) + Metadata
|
v
.NET Runtime (CLR)
|
v
JIT Compilation / Execution
|
v
Application Runs
The C# compiler translates source code into Intermediate Language (IL) and metadata. The .NET runtime loads and executes the application. Just-in-time (JIT) compilation converts IL into native machine code as needed. Some deployment models, such as Native AOT, use a different compilation approach.
Where is C# Used?
- ASP.NET Core MVC applications and RESTful Web APIs.
- Enterprise applications and business systems.
- Cloud services, microservices, and background workers.
- Desktop applications using Windows Forms or WPF.
- Cross-platform applications using .NET MAUI.
- Games and interactive applications using supported engines and libraries.
Set Up Your C# Development Environment
You can write C# in Visual Studio, Visual Studio Code, or another editor that supports the .NET SDK. For a complete IDE experience on Windows, Visual Studio is a common choice.
Install the .NET 10 SDK
Install the .NET 10 SDK from the official .NET 10 download page. The SDK includes the compiler, CLI, runtime components needed for development, and project templates.
Verify the Installation
dotnet --version
dotnet --list-sdks
dotnet --list-runtimes
The first command displays the selected SDK version. The other commands list installed SDKs and runtimes.
Create a Console Project
dotnet new console -n CSharpBasics
cd CSharpBasics
dotnet run
dotnet new console creates a console application. dotnet run builds and executes the project.
Useful .NET CLI Commands
| Command | Purpose |
|---|---|
dotnet new console | Create a console application. |
dotnet build | Compile the project. |
dotnet run | Build and run the project. |
dotnet clean | Remove previous build output. |
dotnet restore | Restore NuGet dependencies. |
dotnet test | Run tests in the solution or project. |
dotnet publish -c Release | Produce deployment-ready output. |
Your First C# Program
A new console project created with modern .NET templates may use top-level statements. This means you can write executable statements without declaring a Program class and a Main method yourself.
Example: Top-Level Statements
Console.WriteLine("Hello, C#!");
Console.WriteLine("Welcome to Skills Tutorials.");
Traditional Program Entry Point
The same basic program can be written with an explicit class and entry-point method:
using System;
namespace CSharpBasics;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, C#!");
}
}
Main is the conventional entry point in this form. The args parameter contains command-line arguments. Top-level statements are a compiler-supported alternative that keeps small programs concise.
C# Program Structure and Syntax
Namespaces
A namespace organizes related types and helps prevent naming conflicts. A project can contain many namespaces and types.
namespace Company.Sales;
public class Order
{
public int Id { get; set; }
}
The semicolon after the namespace name uses a file-scoped namespace declaration, introduced in C# 10. A block-scoped namespace is also valid.
Using Directives
A using directive lets you refer to types in a namespace without writing the full namespace name every time.
using System;
using System.Collections.Generic;
Console.WriteLine("C#");
List<string> names = new();
Statements, Expressions and Blocks
- Statement: An instruction that performs an action, such as declaring a variable or calling a method.
- Expression: Code that produces a value, such as
price * quantity. - Block: A group of statements enclosed in curly braces
{ }.
Case Sensitivity
C# is case-sensitive. customerName and CustomerName are different identifiers.
Comments, Naming and Coding Conventions
Comments explain intent or provide context. Good names and clear code are usually more useful than comments that simply repeat what the code already says.
// Single-line comment
/*
Multi-line comment
*/
/// <summary>
/// Calculates the total order amount.
/// </summary>
public decimal CalculateTotal(decimal price, int quantity)
{
return price * quantity;
}
Common Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Class / Record / Struct | PascalCase | EmployeeService |
| Method | PascalCase | CalculateSalary() |
| Public property | PascalCase | FirstName |
| Local variable | camelCase | totalAmount |
| Method parameter | camelCase | employeeId |
| Private field | _camelCase | _connectionString |
| Constant | PascalCase commonly used | MaxRetryCount |
Follow the conventions already used by your team or repository. Consistency is more important than inventing a personal style.
Variables and Constants
A variable is a named storage location whose value can change while the program runs. A constant represents a compile-time constant value that cannot be changed after it is declared.
Declaring Variables
int age = 30;
string firstName = "Jordan";
decimal salary = 65000.50m;
bool isActive = true;
age = 31; // A variable can be reassigned.
Using var
The var keyword asks the compiler to infer the variable's type from its initialization expression. It does not make the variable dynamically typed.
var employeeName = "Jordan"; // string
var employeeAge = 30; // int
var monthlySalary = 65000m; // decimal
Use var when the type is clear from the right-hand side or when the type name is lengthy. Use an explicit type when it improves readability.
Constants and Readonly Fields
const int MaxLoginAttempts = 5;
public class TaxCalculator
{
private readonly decimal _taxRate;
public TaxCalculator(decimal taxRate)
{
_taxRate = taxRate;
}
}
const | readonly |
|---|---|
| Value must be known at compile time. | Value can be assigned at declaration or in an instance constructor. |
| Implicitly static. | Can be an instance or static field. |
| Used for true compile-time constants. | Used for values fixed after construction. |
Data Types in C#
C# is statically typed: each variable has a type known to the compiler. The type determines the values the variable can hold and the operations available to it.
Common Built-in Types
| Type | Purpose | Example |
|---|---|---|
int | Whole numbers, 32-bit signed integer. | int count = 10; |
long | Larger whole numbers, 64-bit signed integer. | long population = 8000000L; |
short | 16-bit signed integer. | short level = 3; |
byte | 8-bit unsigned integer. | byte code = 255; |
float | 32-bit floating-point number. | float rate = 2.5f; |
double | 64-bit floating-point number. | double distance = 12.75; |
decimal | High-precision base-10 decimal arithmetic. | decimal price = 99.95m; |
bool | Boolean true/false value. | bool isEnabled = true; |
char | Single UTF-16 code unit. | char grade = 'A'; |
string | Sequence of UTF-16 characters. | string city = "Delhi"; |
object | Base type of all C# types. | object value = 10; |
Choosing Numeric Types
Use int for most ordinary whole-number values, long when the range may exceed int, and decimal for financial calculations where decimal precision is important. double is common for scientific and general floating-point calculations.
String and Character Literals
char initial = 'S';
string language = "C#";
string path = @"C:\Projects\CSharp";
string message = $"Learning {language}";
A char literal uses single quotes. A string literal uses double quotes. The @ prefix creates a verbatim string literal, useful for paths and text containing backslashes.
Value Types and Reference Types
C# types are classified as value types or reference types. This distinction affects assignment, parameter passing, and how values are stored and managed.
Value Types
A value-type variable contains its value. Assigning it to another variable copies that value. Common value types include numeric types, bool, char, struct, and enum.
int first = 10;
int second = first;
second = 50;
Console.WriteLine(first); // 10
Console.WriteLine(second); // 50
Reference Types
A reference-type variable holds a reference to an object. Assigning the variable copies the reference, so both variables can refer to the same object.
var first = new List<string> { "C#", "SQL" };
var second = first;
second.Add("ASP.NET Core");
Console.WriteLine(first.Count); // 3
| Value types | Reference types |
|---|---|
| Assignment copies the value. | Assignment copies the reference. |
Examples: int, bool, struct, enum. | Examples: class, string, arrays, delegates, interfaces. |
| Cannot be null unless made nullable. | Can be null unless nullable reference analysis indicates otherwise. |
Type Conversion and Parsing
Type conversion changes a value from one type to another. C# supports implicit conversions when the conversion is safe under the language rules and explicit conversions when you request a conversion that may lose information.
Implicit Conversion
int number = 100;
long largeNumber = number; // Safe widening conversion.
Explicit Conversion (Casting)
double amount = 125.75;
int wholeAmount = (int)amount; // Fractional part is removed.
Convert and Parse
string text = "250";
int value1 = int.Parse(text);
int value2 = Convert.ToInt32(text);
Console.WriteLine(value1 + value2);
TryParse for User Input
TryParse is usually preferable when invalid input is an expected possibility because it reports success or failure instead of throwing a format exception.
Console.Write("Enter your age: ");
string? input = Console.ReadLine();
if (int.TryParse(input, out int age))
{
Console.WriteLine($"Your age is {age}.");
}
else
{
Console.WriteLine("Please enter a valid whole number.");
}
Operators in C#
Operators perform operations on values and variables.
Arithmetic Operators
| Operator | Meaning | Example |
|---|---|---|
+ | Addition | 10 + 5 gives 15 |
- | Subtraction | 10 - 5 gives 5 |
* | Multiplication | 10 * 5 gives 50 |
/ | Division | 10 / 5 gives 2 |
% | Remainder | 10 % 3 gives 1 |
Comparison and Logical Operators
| Category | Operators | Purpose |
|---|---|---|
| Comparison | ==, !=, >, <, >=, <= | Compare values. |
| Logical AND | && | True when both conditions are true. |
| Logical OR | || | True when at least one condition is true. |
| Logical NOT | ! | Negates a Boolean value. |
Assignment and Increment Operators
int score = 10;
score += 5; // score = score + 5
score -= 2;
score *= 2;
score++; // Increase by one
score--;
Conditional (Ternary) Operator
int age = 20;
string category = age >= 18 ? "Adult" : "Minor";
Null-Coalescing Operators
string? suppliedName = null;
string displayName = suppliedName ?? "Guest";
suppliedName ??= "Guest";
?? returns the right-hand value when the left side is null. ??= assigns the fallback only when the variable is null.
Strings and String Interpolation
A string represents text and is immutable: operations that appear to modify a string produce a new string rather than changing the original instance.
Common String Operations
string fullName = " Jordan Lee ";
string cleaned = fullName.Trim();
string upper = cleaned.ToUpperInvariant();
string lower = cleaned.ToLowerInvariant();
bool contains = cleaned.Contains("Jordan");
string[] parts = cleaned.Split(' ');
Console.WriteLine(cleaned.Length);
Console.WriteLine(cleaned.Replace("Jordan", "Taylor"));
String Interpolation
String interpolation embeds expressions inside a string using the $ prefix.
string product = "Keyboard";
decimal price = 1499.50m;
string message = $"The {product} costs {price:C}.";
Console.WriteLine(message);
String Concatenation vs StringBuilder
For a small number of values, interpolation or concatenation is clear and appropriate. When repeatedly building a large string in a loop, StringBuilder can avoid creating many intermediate strings.
using System.Text;
var builder = new StringBuilder();
foreach (string item in new[] { "C#", ".NET", "SQL" })
{
builder.AppendLine(item);
}
string result = builder.ToString();
Decision-Making Statements
Conditional statements allow a program to choose which code to execute based on a condition.
if, else if and else
int marks = 78;
if (marks >= 90)
{
Console.WriteLine("Grade A");
}
else if (marks >= 70)
{
Console.WriteLine("Grade B");
}
else if (marks >= 50)
{
Console.WriteLine("Grade C");
}
else
{
Console.WriteLine("Needs improvement");
}
switch Statement
string role = "Admin";
switch (role)
{
case "Admin":
Console.WriteLine("Full access");
break;
case "Manager":
Console.WriteLine("Team access");
break;
case "User":
Console.WriteLine("Standard access");
break;
default:
Console.WriteLine("Unknown role");
break;
}
Switch Expression
A switch expression returns a value and is useful when mapping an input to an output.
int score = 82;
string grade = score switch
{
>= 90 => "A",
>= 75 => "B",
>= 60 => "C",
>= 40 => "D",
_ => "F"
};
The discard pattern _ acts as the fallback arm.
Loops and Iteration
Loops repeat a block of code. Choose the loop form that makes the stopping condition and iteration intent easiest to understand.
for Loop
for (int i = 1; i <= 5; i++)
{
Console.WriteLine(i);
}
while Loop
int count = 1;
while (count <= 5)
{
Console.WriteLine(count);
count++;
}
do-while Loop
int option;
do
{
Console.WriteLine("Enter 1 to continue, 0 to exit:");
int.TryParse(Console.ReadLine(), out option);
}
while (option != 0);
foreach Loop
string[] languages = { "C#", "Java", "Python" };
foreach (string language in languages)
{
Console.WriteLine(language);
}
break and continue
breakexits the nearest loop or switch statement.continueskips the rest of the current loop iteration and proceeds to the next iteration.
Methods and Parameters
A method is a named block of code that performs a task. Methods help divide a program into smaller, reusable units.
Method Declaration
public static decimal CalculateTotal(decimal price, int quantity)
{
return price * quantity;
}
decimal total = CalculateTotal(250m, 3);
Console.WriteLine(total);
The method has a return type, name, parameters, and a body. The return statement sends a result to the caller. A method with no return value uses void.
Optional and Named Arguments
static void PrintMessage(string message, int repeatCount = 1)
{
for (int i = 0; i < repeatCount; i++)
{
Console.WriteLine(message);
}
}
PrintMessage("Welcome");
PrintMessage(message: "Hello", repeatCount: 3);
Method Overloading
Overloading allows methods to share a name when their parameter lists differ. Return type alone cannot distinguish overloaded methods.
static int Add(int first, int second)
=> first + second;
static decimal Add(decimal first, decimal second)
=> first + second;
Value Parameters and ref / out / in
static void Increase(int value)
{
value++;
}
static void IncreaseByReference(ref int value)
{
value++;
}
static bool TryGetAge(string input, out int age)
{
return int.TryParse(input, out age);
}
static void PrintValue(in int value)
{
Console.WriteLine(value);
}
- Default parameter: A copy of the argument value is passed for ordinary value parameters. For reference-type parameters, the reference itself is copied.
- ref: Passes a variable by reference; the called method can read and modify the caller's variable.
- out: Passes a variable by reference that the method must assign before it returns normally.
- in: Passes a variable by readonly reference, commonly used for larger structs when appropriate.
Expression-Bodied Methods
static bool IsAdult(int age) => age >= 18;
Use expression-bodied syntax for short members where it improves readability. Do not compress complex logic into a single line.
Arrays and Multidimensional Arrays
An array stores a fixed number of elements of the same type. Array indexes are zero-based.
One-Dimensional Array
int[] numbers = { 10, 20, 30, 40 };
Console.WriteLine(numbers[0]); // 10
Console.WriteLine(numbers.Length); // 4
numbers[1] = 25;
Array Creation
string[] names = new string[3];
names[0] = "Asha";
names[1] = "Ravi";
names[2] = "Mina";
Multidimensional Array
int[,] matrix =
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};
Console.WriteLine(matrix[1, 2]); // 6
Jagged Array
A jagged array is an array whose elements are themselves arrays. Each inner array can have a different length.
int[][] groups =
{
new[] { 1, 2 },
new[] { 3, 4, 5 },
new[] { 6 }
};
Use arrays when the number of elements is fixed or array semantics suit the task. Use collections such as List<T> when the number of elements needs to grow or shrink.
Collections: List, Dictionary, HashSet and Queue
Collections store and organize groups of values. The generic collections in System.Collections.Generic provide type safety and are used throughout .NET applications.
List<T>
A List<T> is a resizable, ordered collection that supports index-based access.
var employees = new List<string>
{
"Asha",
"Ravi"
};
employees.Add("Mina");
employees.Remove("Ravi");
foreach (string employee in employees)
{
Console.WriteLine(employee);
}
Dictionary<TKey, TValue>
A dictionary stores key-value pairs. Keys must be unique according to the dictionary's equality comparer.
var employeeById = new Dictionary<int, string>
{
[101] = "Asha",
[102] = "Ravi"
};
if (employeeById.TryGetValue(101, out string? name))
{
Console.WriteLine(name);
}
HashSet<T>
A HashSet<T> stores unique elements and is useful for membership checks and set operations.
var skills = new HashSet<string>
{
"C#", ".NET", "SQL", "C#"
};
Console.WriteLine(skills.Count); // 3
Queue<T> and Stack<T>
var queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
string next = queue.Dequeue();
var stack = new Stack<string>();
stack.Push("First");
stack.Push("Second");
string top = stack.Pop();
| Collection | Typical use |
|---|---|
List<T> | Ordered, resizable sequence of items. |
Dictionary<TKey,TValue> | Fast key-based lookup. |
HashSet<T> | Unique values and membership checks. |
Queue<T> | First-in, first-out processing. |
Stack<T> | Last-in, first-out processing. |
Nullable Types and Null Handling
null represents the absence of an object reference or value. Null handling is important because attempting to access a member through a null reference can cause a NullReferenceException.
Nullable Value Types
int? optionalAge = null;
if (optionalAge.HasValue)
{
Console.WriteLine(optionalAge.Value);
}
int age = optionalAge ?? 0;
A nullable value type such as int? can contain either a value of the underlying type or null.
Nullable Reference Types
Nullable reference types provide compiler analysis that helps identify possible null dereferences. Enable nullable analysis in a project and express intent with annotations such as string and string?.
#nullable enable
string title = "C# Developer";
string? optionalTitle = null;
Console.WriteLine(title.Length);
if (optionalTitle is not null)
{
Console.WriteLine(optionalTitle.Length);
}
The nullable annotations do not create a different runtime string type. They help the compiler analyze whether a reference may be null.
Null-Conditional Operator
string? customerName = null;
int? length = customerName?.Length;
C# 14: Null-Conditional Assignment
C# 14 allows ?. and ?[] on the left side of assignment and compound assignment. The right-hand expression is evaluated only when the receiver is not null.
Customer? customer = FindCustomer();
customer?.LastOrderDate = DateTimeOffset.UtcNow;
This can replace a simple null-checking block. Use it when it makes the intent clearer; explicit if statements remain useful when several operations must be performed together.
Exception Handling
An exception represents an error or unexpected condition that interrupts the normal flow of execution. Exception handling lets code respond to failures or allow them to propagate to a layer that can handle them appropriately.
try, catch and finally
try
{
int result = int.Parse("not-a-number");
Console.WriteLine(result);
}
catch (FormatException ex)
{
Console.WriteLine($"Invalid number: {ex.Message}");
}
finally
{
Console.WriteLine("Operation finished.");
}
trycontains code that may throw an exception.catchhandles a matching exception type.finallyruns when control leaves the try/catch construct, commonly for cleanup.
Throwing an Exception
static void ValidateQuantity(int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(quantity),
"Quantity must be greater than zero.");
}
}
Preserve the Original Stack Trace
try
{
ProcessOrder();
}
catch
{
LogFailure();
throw; // Rethrows the current exception.
}
Use throw; to rethrow the current exception while preserving its stack trace. Avoid catching exceptions only to ignore them. Catch an exception when you can handle it, add useful context, or perform required cleanup.
TryParse. Exceptions are for exceptional failure paths, not routine control flow.Introduction to Object-Oriented Programming
Object-Oriented Programming (OOP) is a way of organizing software around objects that combine data and behavior. It helps developers model business concepts, separate responsibilities, and build code that can evolve as requirements change.
The Four Common OOP Principles
| Principle | Meaning | Typical C# mechanism |
|---|---|---|
| Encapsulation | Protect internal state and expose controlled operations. | Classes, access modifiers, properties, methods. |
| Inheritance | Create a specialized type based on an existing type. | :, virtual, override. |
| Polymorphism | Use a common contract while allowing different implementations. | Virtual methods, abstract classes, interfaces. |
| Abstraction | Expose essential capabilities while hiding implementation details. | Interfaces and abstract classes. |
These principles are related and often work together. They are design tools, not rules that require every class to use inheritance or every property to have elaborate validation.
Classes and Objects
A class is a blueprint that defines the data and behavior of a type. An object is an instance of that class created at runtime.
Define a Class
public class Employee
{
public int Id;
public string Name = string.Empty;
public void DisplayDetails()
{
Console.WriteLine($"Employee: {Id} - {Name}");
}
}
Create and Use an Object
var employee = new Employee();
employee.Id = 101;
employee.Name = "Asha";
employee.DisplayDetails();
The new operator creates an instance and returns a reference to it. The object exposes the public members defined by its class.
Class Members
- Fields: Store data inside an object or type.
- Properties: Provide controlled access to data.
- Methods: Define behavior or operations.
- Constructors: Initialize a new instance.
- Events: Allow an object to notify subscribers that something happened.
Fields, Properties and Encapsulation
Fields
A field is a variable declared directly inside a class or struct. Fields are commonly private implementation details.
Properties
A property exposes a value through get and/or set accessors. Properties can validate, normalize, or restrict updates while keeping a simple member-access syntax.
public class Employee
{
private decimal _salary;
public string Name { get; set; } = string.Empty;
public decimal Salary
{
get => _salary;
private set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_salary = value;
}
}
public void UpdateSalary(decimal amount)
{
Salary = amount;
}
}
Auto-Implemented Properties
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; private set; }
}
The compiler creates the backing storage for an auto-implemented property. A private setter allows the class to change the value while preventing ordinary callers from setting it directly.
Encapsulation in Practice
Encapsulation means keeping an object's state protected and providing intentional operations for changing it. For example, a bank account should not expose a public balance field that any caller can set to an arbitrary amount.
public class BankAccount
{
public string AccountNumber { get; }
public decimal Balance { get; private set; }
public BankAccount(string accountNumber, decimal openingBalance)
{
if (openingBalance < 0)
throw new ArgumentOutOfRangeException(nameof(openingBalance));
AccountNumber = accountNumber;
Balance = openingBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
Balance += amount;
}
public bool Withdraw(decimal amount)
{
if (amount <= 0 || amount > Balance)
return false;
Balance -= amount;
return true;
}
}
Callers can deposit or withdraw through defined operations, but cannot directly assign an arbitrary balance. This keeps the object's rules close to the data they protect.
C# 14: The field Keyword in Properties
C# 14 introduces the contextual field keyword. It lets an accessor use the compiler-generated backing field without declaring a separate field manually.
public class Customer
{
public string Name
{
get;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name is required.")
: value.Trim();
}
}
This is useful when an auto-property needs a small amount of accessor logic. It does not replace careful domain validation or make every property require custom accessors. When a field with the name field already exists in scope, use an unambiguous identifier such as @field where necessary.
Constructors and Object Initialization
A constructor runs when an object is created. It is commonly used to establish the initial state required for that object to be valid.
Parameterless and Parameterized Constructors
public class Employee
{
public int Id { get; }
public string Name { get; private set; }
public Employee()
{
Name = "Unknown";
}
public Employee(int id, string name)
{
Id = id;
Name = name;
}
}
Constructor Overloading and this
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
public Product(string name)
: this(name, 0m)
{
}
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
this(...) calls another constructor in the same type. Constructor chaining can reduce duplicated initialization logic.
Object Initializers
var product = new Product
{
Name = "Monitor",
Price = 12500m
};
An object initializer assigns accessible fields or properties as part of object creation. The example assumes a type with suitable settable properties.
Primary Constructors
Modern C# supports primary constructors on classes and structs. They place constructor parameters in the type declaration. This syntax is useful for concise types, but understand that primary constructor parameters are not automatically public properties.
public class OrderService(IOrderRepository repository)
{
public void Process(int orderId)
{
repository.Save(orderId);
}
}
This example uses a primary constructor parameter as a value available throughout the class body. Dependency injection in ASP.NET Core commonly uses constructor parameters in this style or a traditional constructor.
Access Modifiers
Access modifiers control where a type or member can be accessed. They are a central part of encapsulation and API design.
| Modifier | Accessibility |
|---|---|
public | Accessible wherever the containing type is accessible. |
private | Accessible only within the containing type (with defined nested-type rules). |
protected | Accessible within the containing type and derived types. |
internal | Accessible within the same assembly. |
protected internal | Accessible within the same assembly or from a derived type. |
private protected | Accessible within the containing type or derived types in the same assembly. |
file | Restricts a top-level type to the current source file. |
public class Employee
{
public int Id { get; set; }
private decimal _salary;
protected string Department = "General";
internal string InternalCode = "EMP";
}
Choose the narrowest accessibility that supports the intended use. A smaller public surface is easier to understand, test, and maintain.
Static Members and Static Classes
A static member belongs to the type itself rather than to a particular object instance. A static class cannot be instantiated and can contain only static members.
public static class TaxRules
{
public const decimal StandardRate = 0.18m;
public static decimal CalculateTax(decimal amount)
{
return amount * StandardRate;
}
}
decimal tax = TaxRules.CalculateTax(1000m);
Use static members for behavior or data that genuinely belongs to the type and does not depend on instance state. Avoid turning every helper or service into a static class when doing so makes dependencies and testing harder.
Inheritance
Inheritance lets a class derive from another class. The derived class inherits accessible members from the base class and can add or specialize behavior.
Base and Derived Classes
public class Person
{
public string Name { get; set; } = string.Empty;
public void Introduce()
{
Console.WriteLine($"Hello, I am {Name}.");
}
}
public class Employee : Person
{
public int EmployeeId { get; set; }
}
var employee = new Employee
{
Name = "Asha",
EmployeeId = 101
};
employee.Introduce();
base Keyword
The base keyword accesses a base-class member or invokes a base-class constructor.
public class Person
{
public string Name { get; }
public Person(string name)
{
Name = name;
}
}
public class Employee : Person
{
public int EmployeeId { get; }
public Employee(string name, int employeeId)
: base(name)
{
EmployeeId = employeeId;
}
}
Important Inheritance Rules
- A class can directly inherit from one base class.
- A class can implement multiple interfaces.
- Constructors are not inherited.
- Use inheritance when there is a genuine “is-a” relationship.
- Prefer composition when one object simply needs to use another object's capability.
Polymorphism
Polymorphism means that code can work with a common base type or interface while the actual object provides the appropriate implementation.
Method Overriding
A base-class method marked virtual can be overridden by a derived class. The derived implementation uses override.
public class Notification
{
public virtual void Send()
{
Console.WriteLine("Sending a notification.");
}
}
public class EmailNotification : Notification
{
public override void Send()
{
Console.WriteLine("Sending an email.");
}
}
public class SmsNotification : Notification
{
public override void Send()
{
Console.WriteLine("Sending an SMS.");
}
}
Runtime Polymorphism
List<Notification> notifications =
[
new EmailNotification(),
new SmsNotification()
];
foreach (Notification notification in notifications)
{
notification.Send();
}
The variable is typed as Notification, but the overridden method that runs depends on the actual object at runtime.
Overloading vs Overriding
| Overloading | Overriding |
|---|---|
| Same method name with different parameter lists. | Derived class replaces virtual or abstract base behavior. |
| Resolved by compile-time overload resolution. | Virtual dispatch selects the implementation at runtime. |
| Can occur in the same class. | Requires an inheritance relationship. |
Abstraction and Abstract Classes
Abstraction focuses on what an object can do rather than exposing every detail of how it does it. An abstract class provides a shared base type and can define both implemented and abstract members.
public abstract class Payment
{
public decimal Amount { get; }
protected Payment(decimal amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
Amount = amount;
}
public abstract void Process();
public void PrintReceipt()
{
Console.WriteLine($"Payment amount: {Amount:C}");
}
}
public class CardPayment : Payment
{
public CardPayment(decimal amount) : base(amount) { }
public override void Process()
{
Console.WriteLine("Processing card payment.");
}
}
An abstract class cannot be instantiated directly. A non-abstract derived class must implement inherited abstract members unless it is also declared abstract.
When to Use an Abstract Class
- Related types share common state or implementation.
- You need a protected constructor or shared helper methods.
- You want to define a common base with some required behavior.
Interfaces
An interface defines a contract: the members a type promises to provide. A class or struct can implement multiple interfaces, which makes interfaces useful for flexible application design and dependency inversion.
Define and Implement an Interface
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
public class CardPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Card payment: {amount:C}");
}
}
public class BankPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Bank transfer: {amount:C}");
}
}
Program Against the Interface
public class CheckoutService
{
private readonly IPaymentProcessor _paymentProcessor;
public CheckoutService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public void Checkout(decimal amount)
{
_paymentProcessor.ProcessPayment(amount);
}
}
CheckoutService depends on the contract rather than a specific payment implementation. This makes it easier to substitute implementations and test the service with a test double.
Interface vs Abstract Class
| Interface | Abstract class |
|---|---|
| Defines a capability or contract. | Defines a shared base abstraction. |
| A type can implement multiple interfaces. | A class can have only one direct base class. |
| Does not provide per-instance fields. | Can contain instance fields and constructors. |
| Can include default implementations for members in supported cases. | Can include abstract and implemented members. |
Composition, Structs, Enums and Records
Composition
Composition builds a type by combining objects that provide the capabilities it needs. It often models a “has-a” relationship and can be more flexible than inheritance.
public class Engine
{
public void Start()
{
Console.WriteLine("Engine started.");
}
}
public class Car
{
private readonly Engine _engine;
public Car(Engine engine)
{
_engine = engine;
}
public void Start()
{
_engine.Start();
Console.WriteLine("Car is ready.");
}
}
A car has an engine; it is not an engine. The Car class delegates engine behavior to an Engine object.
Structs
A struct is a value type. Structs are often suitable for small values that represent a single concept, such as coordinates or measurements. They can have methods, properties, constructors, and implement interfaces.
public struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y)
{
X = x;
Y = y;
}
}
var point = new Point(10, 20);
Enums
An enum defines a set of named constants backed by an integral type.
public enum OrderStatus
{
Pending,
Confirmed,
Shipped,
Delivered,
Cancelled
}
OrderStatus status = OrderStatus.Confirmed;
Enums make code more readable than unexplained numeric values. If an enum value is persisted or exchanged externally, consider assigning explicit numeric values and planning for future compatibility.
Records
Records are types designed for data-centric models and provide value-based equality by default. A record class is a reference type; a record struct is a value type.
public record EmployeeDto(int Id, string Name);
var first = new EmployeeDto(101, "Asha");
var second = new EmployeeDto(101, "Asha");
Console.WriteLine(first == second); // True
Records are useful for DTOs, immutable-style data models, and values where equality should be based on the data. They do not automatically make every referenced object deeply immutable.
Putting the Fundamentals Together
The following example combines classes, properties, constructors, encapsulation, interfaces, collections, and basic validation in a small in-memory employee directory. It is intentionally simple and does not use a database or ASP.NET Core so that the language concepts remain visible.
Define the Contract
public interface IEmployeeDirectory
{
void Add(Employee employee);
Employee? FindById(int id);
IReadOnlyCollection<Employee> GetAll();
}
Define the Employee Model
public class Employee
{
public int Id { get; }
public string Name { get; }
public string Department { get; }
public Employee(int id, string name, string department)
{
if (id <= 0)
throw new ArgumentOutOfRangeException(nameof(id));
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name is required.", nameof(name));
Id = id;
Name = name.Trim();
Department = department.Trim();
}
}
Implement the Directory
public class InMemoryEmployeeDirectory : IEmployeeDirectory
{
private readonly Dictionary<int, Employee> _employees = new();
public void Add(Employee employee)
{
ArgumentNullException.ThrowIfNull(employee);
if (!_employees.TryAdd(employee.Id, employee))
{
throw new InvalidOperationException(
$"Employee {employee.Id} already exists.");
}
}
public Employee? FindById(int id)
{
return _employees.GetValueOrDefault(id);
}
public IReadOnlyCollection<Employee> GetAll()
{
return _employees.Values.ToArray();
}
}
Use the Directory
IEmployeeDirectory directory = new InMemoryEmployeeDirectory();
directory.Add(new Employee(101, "Asha", "Engineering"));
directory.Add(new Employee(102, "Ravi", "Finance"));
Employee? employee = directory.FindById(101);
if (employee is not null)
{
Console.WriteLine($"{employee.Id}: {employee.Name}");
}
foreach (Employee item in directory.GetAll())
{
Console.WriteLine($"{item.Name} - {item.Department}");
}
What This Example Demonstrates
- Abstraction: The application uses
IEmployeeDirectoryas a contract. - Encapsulation: The dictionary is private and callers use methods to interact with it.
- Object construction: The constructor validates required employee data.
- Collections: A dictionary provides lookup by employee ID.
- Null handling: The lookup result is nullable and checked before use.
- Separation of concerns: The employee model and directory have distinct responsibilities.
Practice Exercises and Mini Project
Practice Exercises
- Variables: Declare a product name, quantity, and unit price. Calculate and display the total.
- Conditions: Read a number and display whether it is positive, negative, or zero.
- Loops: Print the multiplication table for a number entered by the user.
- Methods: Create a method that accepts an array of integers and returns the largest value.
- Collections: Store employee names in a list and search for a name without considering letter case.
- Classes: Create a
Bookclass with an ID, title, author, and a method that displays its details. - Encapsulation: Create a bank account that prevents negative deposits and withdrawals larger than the balance.
- Inheritance: Create a base
Vehicleclass and derivedCarandBikeclasses. - Polymorphism: Create a base
Shapetype with an abstract area calculation and implement it for a circle and rectangle. - Interfaces: Define an
ILoggerinterface and implement console and file-based loggers.
Mini Project: Console-Based Employee Management
Build a small console application that manages employee records in memory. Focus on clean C# code and the OOP concepts from this part.
Functional Requirements
- Add an employee with ID, name, department, and salary.
- List all employees.
- Find an employee by ID.
- Update an employee's department or salary.
- Remove an employee by ID.
- Validate user input and handle invalid operations gracefully.
Suggested Class Design
Employee
- Id
- Name
- Department
- Salary
IEmployeeService
- AddEmployee()
- GetAllEmployees()
- GetEmployeeById()
- UpdateEmployee()
- DeleteEmployee()
EmployeeService
- Implements IEmployeeService
- Uses List<Employee> or Dictionary<int, Employee>
Completion Checklist
- Use properties instead of exposing public data fields.
- Use constructors to establish valid initial state.
- Keep collection storage private.
- Use an interface for the service contract.
- Validate IDs, names, and numeric input.
- Separate menu/input code from employee-management logic.
Part 1 Knowledge Check
1. What is the difference between C# and .NET?
C# is a programming language. .NET is the platform that provides the runtime, libraries, SDK tools, and frameworks used to build and execute applications.
2. What is the difference between a value type and a reference type?
Value-type assignment copies the value. Reference-type assignment copies a reference to an object, so multiple variables can refer to the same object.
3. What is the difference between const and readonly?
const requires a compile-time constant. A readonly field can be assigned at declaration or in a constructor and then cannot be reassigned.
4. Why is TryParse useful?
It attempts a conversion and reports success or failure without using an exception for ordinary invalid input.
5. What is the difference between a class and an object?
A class defines a type and its members. An object is an instance of that type.
6. What is encapsulation?
Encapsulation protects internal state and exposes controlled operations through a type's public members.
7. What is inheritance?
Inheritance allows a derived class to reuse and specialize accessible members from a base class.
8. What is polymorphism?
Polymorphism allows code to work through a common base type or interface while different runtime types provide different behavior.
9. What is the difference between an abstract class and an interface?
An abstract class can provide shared state, constructors, and implementation. An interface primarily defines a contract and can be implemented alongside other interfaces.
10. Why is composition often preferred over inheritance?
Composition combines objects through their capabilities and can avoid rigid inheritance hierarchies when there is no genuine “is-a” relationship.
11. What does nullable reference type analysis do?
It helps the compiler identify code paths where a reference may be null, reducing potential null-reference errors.
12. What is new in C# 14 for properties?
C# 14 introduces the contextual field keyword, which lets property accessors use compiler-generated backing storage while adding limited custom logic.
Official Learning Resources
Use Microsoft's official documentation as the authoritative reference for language behavior, version-specific features, SDK installation, and platform support.
- C# Documentation – Microsoft Learn
- A Tour of C#
- C# Fundamentals
- C# Type System
- Object-Oriented Programming in C#
- What's New in C# 14
- C# Language Reference
- What's New in .NET 10
- Download .NET 10
What’s Next? – C# Developer Learning Guide, Part 2
Part 1 has established the language foundation and the core OOP principles. Part 2 will move into the C# features and design techniques used more heavily in professional .NET development.
The advanced guide will cover generics and constraints, delegates and events, lambda expressions, LINQ, extension methods and C# 14 extension members, pattern matching, advanced records, iterators, exception design, file and stream handling, asynchronous programming with async and await, tasks and cancellation, memory and performance concepts, reflection, attributes, expression trees, dependency injection, and practical application-level patterns.
The aim is to connect these language features to real ASP.NET Core development: building maintainable services, working with data, handling concurrency, integrating external systems, and writing testable code.