Skip to main content

C# Programming Language: Complete Guide for .NET Developers

C# (pronounced "C-sharp") is the primary programming language for .NET development. Created by Microsoft, it's a modern, type-safe, object-oriented language that combines the power of C++ with the simplicity of Visual Basic. This comprehensive guide will take you from C# basics to advanced features.

What is C#?

C# is a general-purpose, multi-paradigm programming language designed for the .NET platform. It supports:

  • Object-Oriented Programming (OOP)
  • Functional Programming
  • Component-Oriented Programming
  • Strong Type Safety
  • Garbage Collection
  • Cross-Platform Development

C# Language Philosophy

// C# aims to be simple, powerful, and expressive
public record Person(string Name, int Age)
{
public bool IsAdult => Age >= 18;
}

// Usage
var person = new Person("Alice", 25);
Console.WriteLine($"{person.Name} is {(person.IsAdult ? "an adult" : "a minor")}");

C# Syntax Fundamentals

Basic Structure

Every C# program has a basic structure:

Program.cs
using System;

namespace MyApplication
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}

// Modern C# 9+ top-level programs (simplified)
Console.WriteLine("Hello, World!");

Variables and Data Types

C# is strongly typed, meaning every variable has a specific type:

Variables and Types
// Value types
int age = 25;
double price = 19.99;
bool isActive = true;
char grade = 'A';
DateTime now = DateTime.Now;

// Reference types
string name = "Alice";
int[] numbers = { 1, 2, 3, 4, 5 };
List<string> names = new List<string> { "Alice", "Bob" };

// Type inference with var
var message = "Hello"; // Compiler infers string
var count = 42; // Compiler infers int
var items = new List<Product>(); // Compiler infers List<Product>

Null Safety (C# 8+)

C# 8 introduced nullable reference types for better null safety:

Null Safety
#nullable enable

// Non-nullable reference type
string name = "Alice"; // Cannot be null

// Nullable reference type
string? optionalName = null; // Can be null

// Null-conditional operators
int? length = optionalName?.Length; // Safe null access
string displayName = optionalName ?? "Unknown"; // Null coalescing

Object-Oriented Programming in C#

C# is fundamentally object-oriented. Let's explore the core OOP concepts:

Classes and Objects

Class Definition
public class BankAccount
{
// Fields
private decimal _balance;
private string _accountNumber;

// Properties
public string AccountNumber => _accountNumber;
public decimal Balance => _balance;
public string Owner { get; set; }

// Constructor
public BankAccount(string accountNumber, string owner)
{
_accountNumber = accountNumber;
Owner = owner;
_balance = 0;
}

// Methods
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");

_balance += amount;
}

public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive");

if (amount > _balance)
throw new InvalidOperationException("Insufficient funds");

_balance -= amount;
}

public override string ToString()
{
return $"Account: {AccountNumber}, Owner: {Owner}, Balance: ${Balance:F2}";
}
}

Inheritance and Polymorphism

Inheritance Example
// Base class
public abstract class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }

protected Vehicle(string make, string model, int year)
{
Make = make;
Model = model;
Year = year;
}

// Virtual method - can be overridden
public virtual void StartEngine()
{
Console.WriteLine("Engine starting...");
}

// Abstract method - must be implemented
public abstract double CalculateFuelEfficiency();
}

// Derived class
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }

public Car(string make, string model, int year, int doors)
: base(make, model, year)
{
NumberOfDoors = doors;
}

// Override virtual method
public override void StartEngine()
{
Console.WriteLine("Car engine purring to life...");
}

// Implement abstract method
public override double CalculateFuelEfficiency()
{
// Sample calculation
return 25.5; // MPG
}
}

Interfaces

Interfaces define contracts that classes must implement:

Interface Example
public interface IPaymentProcessor
{
Task<bool> ProcessPaymentAsync(decimal amount, string paymentMethod);
bool ValidatePaymentDetails(string details);
}

public interface INotificationService
{
Task SendNotificationAsync(string message, string recipient);
}

// Implementing multiple interfaces
public class PayPalProcessor : IPaymentProcessor, INotificationService
{
public async Task<bool> ProcessPaymentAsync(decimal amount, string paymentMethod)
{
// PayPal-specific implementation
Console.WriteLine($"Processing ${amount} via PayPal");
await Task.Delay(1000); // Simulate API call
return true;
}

public bool ValidatePaymentDetails(string details)
{
// Validation logic
return !string.IsNullOrEmpty(details) && details.Contains("@");
}

public async Task SendNotificationAsync(string message, string recipient)
{
Console.WriteLine($"Sending email to {recipient}: {message}");
await Task.Delay(500);
}
}

Modern C# Features

C# continues to evolve. Let's explore modern features that make code more expressive:

Records (C# 9+)

Records provide a concise way to create immutable data types:

Records
// Traditional class
public class PersonClass
{
public string Name { get; set; }
public int Age { get; set; }
}

// Record - immutable by default
public record Person(string Name, int Age);

// Record with additional members
public record Customer(string Name, int Age, string Email)
{
public bool IsAdult => Age >= 18;

public string GetDisplayName() => $"{Name} ({Age})";
}

// Usage
var customer = new Customer("Alice", 25, "alice@example.com");
var olderCustomer = customer with { Age = 26 }; // Create modified copy

Pattern Matching

C# offers powerful pattern matching capabilities:

Pattern Matching
public static string DescribeObject(object obj) => obj switch
{
int i when i > 0 => $"Positive integer: {i}",
int i when i < 0 => $"Negative integer: {i}",
int => "Zero",
string s when s.Length > 10 => $"Long string: {s[..10]}...",
string s => $"String: {s}",
Person { Age: >= 18 } p => $"Adult: {p.Name}",
Person p => $"Minor: {p.Name}",
null => "Null value",
_ => "Unknown type"
};

// Property patterns
public static decimal CalculateDiscount(Customer customer) => customer switch
{
{ Age: >= 65 } => 0.20m, // Senior discount
{ Age: >= 18, Email: not null } => 0.10m, // Adult with email
_ => 0.05m // Default discount
};

Async/Await Programming

C# makes asynchronous programming straightforward:

Async/Await Example
public class DataService
{
private readonly HttpClient _httpClient;

public DataService(HttpClient httpClient)
{
_httpClient = httpClient;
}

// Async method returning a value
public async Task<List<User>> GetUsersAsync()
{
try
{
var response = await _httpClient.GetStringAsync("/api/users");
var users = JsonSerializer.Deserialize<List<User>>(response);
return users ?? new List<User>();
}
catch (HttpRequestException ex)
{
// Log error
throw new ServiceException("Failed to retrieve users", ex);
}
}

// Async method without return value
public async Task UpdateUserAsync(User user)
{
var json = JsonSerializer.Serialize(user);
var content = new StringContent(json, Encoding.UTF8, "application/json");

var response = await _httpClient.PutAsync($"/api/users/{user.Id}", content);
response.EnsureSuccessStatusCode();
}

// Running multiple async operations concurrently
public async Task<(List<User> users, List<Order> orders)> GetUserDataAsync(int userId)
{
var usersTask = GetUsersAsync();
var ordersTask = GetUserOrdersAsync(userId);

// Wait for both to complete
await Task.WhenAll(usersTask, ordersTask);

return (await usersTask, await ordersTask);
}
}

LINQ (Language Integrated Query)

LINQ allows you to query collections using a SQL-like syntax:

LINQ Examples
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var customers = GetCustomers();

// Method syntax
var evenNumbers = numbers
.Where(n => n % 2 == 0)
.Select(n => n * 2)
.ToList();

var topCustomers = customers
.Where(c => c.Orders.Count > 5)
.OrderByDescending(c => c.TotalSpent)
.Take(10)
.Select(c => new { c.Name, c.TotalSpent });

// Query syntax
var youngCustomersWithOrders =
from customer in customers
where customer.Age < 30
where customer.Orders.Any()
orderby customer.Name
select new
{
customer.Name,
customer.Age,
OrderCount = customer.Orders.Count
};

// Complex queries
var customerOrderSummary = customers
.SelectMany(c => c.Orders, (customer, order) => new { customer, order })
.GroupBy(x => x.customer.Name)
.Select(g => new
{
CustomerName = g.Key,
TotalOrders = g.Count(),
TotalAmount = g.Sum(x => x.order.Amount),
AverageOrderValue = g.Average(x => x.order.Amount)
});

Exception Handling

Proper exception handling is crucial for robust applications:

Exception Handling
public class FileProcessor
{
public async Task<string> ProcessFileAsync(string filePath)
{
try
{
// Multiple operations that can throw different exceptions
var content = await File.ReadAllTextAsync(filePath);
var processedContent = ProcessContent(content);

return processedContent;
}
catch (FileNotFoundException)
{
// Specific exception handling
throw new ProcessingException($"File not found: {filePath}");
}
catch (UnauthorizedAccessException)
{
// Another specific exception
throw new ProcessingException($"Access denied to file: {filePath}");
}
catch (Exception ex)
{
// Generic exception handling
throw new ProcessingException($"Error processing file: {filePath}", ex);
}
}

private string ProcessContent(string content)
{
if (string.IsNullOrWhiteSpace(content))
throw new ArgumentException("Content cannot be empty");

return content.ToUpperInvariant();
}
}

// Custom exception
public class ProcessingException : Exception
{
public ProcessingException(string message) : base(message) { }
public ProcessingException(string message, Exception innerException)
: base(message, innerException) { }
}

Generics

Generics provide type safety and performance benefits:

Generics Example
// Generic class
public class Repository<T> where T : class, IEntity
{
private readonly List<T> _items = new();

public void Add(T item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));

_items.Add(item);
}

public T? GetById(int id)
{
return _items.FirstOrDefault(item => item.Id == id);
}

public IEnumerable<T> GetAll()
{
return _items.AsReadOnly();
}

public void Remove(T item)
{
_items.Remove(item);
}
}

// Generic interface
public interface IEntity
{
int Id { get; set; }
}

// Generic method
public static class CollectionExtensions
{
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
{
return source.Where(item => item != null)!;
}

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
{
action(item);
}
}
}

Delegates and Events

Delegates provide type-safe function pointers, and events enable the observer pattern:

Delegates and Events
// Delegate declaration
public delegate bool ValidationRule<T>(T item);

// Event-driven class
public class OrderProcessor
{
// Event declaration
public event EventHandler<OrderEventArgs>? OrderProcessed;
public event EventHandler<OrderEventArgs>? OrderFailed;

private readonly List<ValidationRule<Order>> _validationRules = new();

public void AddValidationRule(ValidationRule<Order> rule)
{
_validationRules.Add(rule);
}

public async Task ProcessOrderAsync(Order order)
{
try
{
// Run all validation rules
var isValid = _validationRules.All(rule => rule(order));

if (!isValid)
{
OnOrderFailed(new OrderEventArgs(order, "Validation failed"));
return;
}

// Process order
await ProcessOrderInternalAsync(order);

// Raise event
OnOrderProcessed(new OrderEventArgs(order, "Order processed successfully"));
}
catch (Exception ex)
{
OnOrderFailed(new OrderEventArgs(order, ex.Message));
}
}

protected virtual void OnOrderProcessed(OrderEventArgs args)
{
OrderProcessed?.Invoke(this, args);
}

protected virtual void OnOrderFailed(OrderEventArgs args)
{
OrderFailed?.Invoke(this, args);
}

private async Task ProcessOrderInternalAsync(Order order)
{
// Implementation details
await Task.Delay(1000);
}
}

public class OrderEventArgs : EventArgs
{
public Order Order { get; }
public string Message { get; }

public OrderEventArgs(Order order, string message)
{
Order = order;
Message = message;
}
}

Functional Programming Features

C# supports functional programming paradigms:

Functional Programming
// Higher-order functions
public static class FunctionalExtensions
{
public static Func<T, TResult> Memoize<T, TResult>(Func<T, TResult> func)
where T : notnull
{
var cache = new Dictionary<T, TResult>();
return input =>
{
if (cache.TryGetValue(input, out var cached))
return cached;

var result = func(input);
cache[input] = result;
return result;
};
}

public static Func<T2, TResult> Curry<T1, T2, TResult>(
Func<T1, T2, TResult> func, T1 arg1)
{
return arg2 => func(arg1, arg2);
}
}

// Immutable data structures
public record CustomerData(string Name, List<OrderData> Orders)
{
public CustomerData AddOrder(OrderData order)
{
return this with { Orders = Orders.Append(order).ToList() };
}

public CustomerData UpdateName(string newName)
{
return this with { Name = newName };
}
}

// Functional approach to data processing
public static class DataProcessor
{
public static IEnumerable<TResult> ProcessPipeline<T, TResult>(
IEnumerable<T> source,
params Func<IEnumerable<T>, IEnumerable<T>>[] transformations)
{
var result = source;
foreach (var transformation in transformations)
{
result = transformation(result);
}
return result.Cast<TResult>();
}
}

// Usage
var customers = GetCustomers();
var result = DataProcessor.ProcessPipeline<Customer, string>(
customers,
c => c.Where(customer => customer.IsActive),
c => c.OrderBy(customer => customer.Name),
c => c.Take(10),
c => c.Select(customer => customer.Name)
);

C# Best Practices

Naming Conventions

Naming Conventions
// PascalCase for public members
public class CustomerService
{
public string CustomerName { get; set; }
public void ProcessOrder() { }
}

// camelCase for private fields and variables
private readonly ILogger _logger;
private int _orderCount;

// Constants in PascalCase
public const int MaxRetryAttempts = 3;
private const string DefaultConnectionString = "...";

// Method parameters in camelCase
public void CreateOrder(string customerName, decimal totalAmount) { }

Code Organization

Well-Organized Code
namespace MyApp.Services
{
using System;
using System.Threading.Tasks;
using MyApp.Models;
using MyApp.Interfaces;

/// <summary>
/// Handles customer-related business operations
/// </summary>
public class CustomerService : ICustomerService
{
private readonly ICustomerRepository _repository;
private readonly ILogger<CustomerService> _logger;

public CustomerService(
ICustomerRepository repository,
ILogger<CustomerService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<Customer?> GetCustomerAsync(int customerId)
{
if (customerId <= 0)
throw new ArgumentException("Customer ID must be positive", nameof(customerId));

try
{
var customer = await _repository.GetByIdAsync(customerId);
_logger.LogInformation("Retrieved customer {CustomerId}", customerId);
return customer;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving customer {CustomerId}", customerId);
throw;
}
}
}
}

C# Performance Tips

Memory Efficiency

Memory Optimization
// ❌ Inefficient string concatenation
public string BuildReport(List<Customer> customers)
{
string report = "";
foreach (var customer in customers)
{
report += $"{customer.Name}: {customer.Orders.Count} orders\n";
}
return report;
}

// ✅ Efficient string building
public string BuildReport(List<Customer> customers)
{
var sb = new StringBuilder(customers.Count * 50); // Pre-size
foreach (var customer in customers)
{
sb.AppendLine($"{customer.Name}: {customer.Orders.Count} orders");
}
return sb.ToString();
}

// ✅ Even better with LINQ
public string BuildReport(List<Customer> customers)
{
return string.Join("\n", customers.Select(c =>
$"{c.Name}: {c.Orders.Count} orders"));
}

Span<T> for High-Performance Scenarios

Using Span<T>
public static class StringParser
{
// Traditional approach - allocates substrings
public static string[] SplitTraditional(string input, char separator)
{
return input.Split(separator);
}

// Span approach - no allocations for parsing
public static void ParseCsv(ReadOnlySpan<char> input, List<string> results)
{
while (!input.IsEmpty)
{
var commaIndex = input.IndexOf(',');
if (commaIndex == -1)
{
results.Add(input.ToString());
break;
}

results.Add(input[..commaIndex].ToString());
input = input[(commaIndex + 1)..];
}
}
}

Summary

C# is a powerful, modern programming language that offers:

  • Strong Type Safety: Catch errors at compile time
  • Object-Oriented Design: Encapsulation, inheritance, polymorphism
  • Modern Features: Records, pattern matching, nullable reference types
  • Async Programming: First-class support for asynchronous operations
  • LINQ: Powerful query capabilities
  • Performance: Excellent runtime performance
  • Ecosystem: Rich library ecosystem and tooling

Key Takeaways

  1. Embrace Modern C#: Use the latest language features for more expressive code
  2. Follow Conventions: Consistent naming and code organization
  3. Handle Exceptions Properly: Use specific exception types and meaningful messages
  4. Leverage LINQ: For data querying and transformation
  5. Use Async/Await: For I/O-bound operations
  6. Consider Performance: Be mindful of allocations and choose appropriate data structures

Next Steps

Now that you understand C# fundamentals, it's time to explore the rich .NET ecosystem:

Continue to Tutorial 4: Essential .NET Libraries and Types to discover the powerful libraries that come with .NET.


Building a C# application? Get expert help from PBX Digital. Contact us at mp@pbxdigital.net for C# and .NET development services.