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:
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:
// 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:
#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
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
// 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:
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:
// 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:
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:
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:
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:
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:
// 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:
// 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:
// 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
// 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
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
// ❌ 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
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
- Embrace Modern C#: Use the latest language features for more expressive code
- Follow Conventions: Consistent naming and code organization
- Handle Exceptions Properly: Use specific exception types and meaningful messages
- Leverage LINQ: For data querying and transformation
- Use Async/Await: For I/O-bound operations
- 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.