Example 1
abstract class Shape
{
  public abstract int GetArea();
}

class Square : Shape
{
  private int _side;

  public Square(int n) => _side = n;

  // GetArea method is required to avoid a compile-time error.
  public override int GetArea() => _side * _side;

  static void Main()
  {
    var sq = new Square(12);
    Console.WriteLine($"Area of the square = {sq.GetArea()}");
  }
}
// Output: Area of the square = 144
Example 2
// Abstract class
abstract class BaseClass
{
  protected int _x = 100;
  protected int _y = 150;

  // Abstract method
  public abstract void AbstractMethod();

  // Abstract properties
  public abstract int X { get; }
  public abstract int Y { get; }
}

class DerivedClass : BaseClass
{
  public override void AbstractMethod()
  {
    _x++;
    _y++;
  }

  public override int X   // overriding property
  {
    get
    {
      return _x + 10;
    }
  }

  public override int Y   // overriding property
  {
    get
    {
      return _y + 10;
    }
  }

  static void Main()
  {
    var o = new DerivedClass();
    o.AbstractMethod();
    Console.WriteLine($"x = {o.X}, y = {o.Y}");
  }
}
// Output: x = 111, y = 161
Example 3
public abstract class Shape
{
  public string Color { get; set; }

  // Constructor of the abstract class
  protected Shape(string color)
  {
    Color = color;
    Console.WriteLine($"Created a shape with color {color}.");
  }

  // Abstract method that must be implemented by derived classes
  public abstract double CalculateArea();
}

public class Square : Shape
{
  public double Side { get; set; }

  // Constructor of the derived class calling the base class constructor
  public Square(string color, double side) : base(color)
  {
    Side = side;
  }

  public override double CalculateArea()
  {
    return Side * Side;
  }
}

public class Program
{
  public static void Main(string[] args)
  {
    Square square = new Square("red", 5);
    Console.WriteLine($"Area of the square: {square.CalculateArea()}");            
  }
}
Definition

A record in C# is a class or struct that provides special syntax and behavior for working with data models. The record modifier instructs the compiler to synthesize members that are useful for types whose primary role is storing data. These members include an overload of ToString() and members that support value equality.

When to use

Consider using a record in place of a class or struct in the following scenarios:

  • You want to define a data model that depends on value equality.
  • You want to define a type for which objects are immutable.
Record Types

Declaration:

 // Value type: Stores all values
public record Point(int X, int Y)
{
  public double Slope() => (double)Y / (double)X;
}

public static void Main()
{
  Point pt = new Point(1, 1);
  var pt2 = pt with { Y = 10 };
  double slope = pt.Slope();

  Console.WriteLine($"The two points are {pt} and {pt2}"); // The two points are Point { X = 1, Y = 1 } and Point { X = 1, Y = 10 }

  Console.WriteLine($"The slope of {pt} is {slope}"); //The slope of Point { X = 1, Y = 1 } is 1
}
Declaration
interface IEquatable<T>
{
  bool Equals(T obj);
}

public class Car : IEquatable<Car>
{
  public string? Make { get; set; }
  public string? Model { get; set; }
  public string? Year { get; set; }

  // Implementation of IEquatable<T> interface
  public bool Equals(Car? car)
  {
    return (this.Make, this.Model, this.Year) ==
      (car?.Make, car?.Model, car?.Year);
  }
}
Declaration
// Declare the generic class.
public class GenericList<T>
{
  public void Add(T item) { }
}

public class ExampleClass { }

class TestGenericList
{
  static void Main()
  {
    // Create a list of type int.
    GenericList<int> list1 = new();
    list1.Add(1);

    // Create a list of type string.
    GenericList<string> list2 = new();
    list2.Add("");

    // Create a list of type ExampleClass.
    GenericList<ExampleClass> list3 = new();
    list3.Add(new ExampleClass());
  }
}
Declaration
var v = new { Amount = 108, Message = "Hello" };

// Rest the mouse pointer over v.Amount and v.Message in the following
// statement to verify that their inferred types are int and string.
Console.WriteLine(v.Amount + v.Message);
Principles

C# is an object-oriented programming language. The four basic principles of object-oriented programming are:

  • Encapsulation: Hiding the internal state and functionality of an object and only allowing access through a public set of functions.
  • Inheritance: Ability to create new abstractions based on existing abstractions.
  • Polymorphism: Ability to implement inherited properties or methods in different ways across multiple abstractions.
  • Abstraction: Modeling the relevant attributes and interactions of entities as classes to define an abstract representation of a system.
Encapsulation

Definition

Encapsulation is sometimes referred to as the first pillar or principle of object-oriented programming. A class or struct can specify how accessible each of its members is to code outside of the class or struct. Members not intended for consumers outside of the class or assembly are hidden to limit the potential for coding errors or malicious exploits.

Members

The following list includes all the various kinds of members that can be declared in a class, struct, or record.

  • Fields
  • Constants
  • Properties
  • Methods
  • Constructors
  • Events
  • Finalizers
  • Indexers
  • Operators
  • Nested Types

Accessibility

Some methods and properties are meant to be called or accessed from code outside a class or struct, known as client code. Other methods and properties might be only for use in the class or struct itself. It’s important to limit the accessibility of your code so that only the intended client code can reach it. You specify how accessible your types and their members are to client code by using the following access modifiers:

  • public: Access is not restricted.
  • protected: Access is limited to the containing class or types derived from the containing class.
  • internal: Access is limited to the current assembly.
  • protected internal: Access is limited to the current assembly or types derived from the containing class.
  • private: Access is limited to the containing type.
  • private protected: Access is limited to the containing class or types derived from the containing class within the current assembly.
// Assembly1.cs  
public class OtherClass
{  
  internal static int intM = 0;  
}

// Assembly2.cs  
using System;
					
class ParentClass
{
  public int a = 10; // No access restrictions.
  protected int b = 20; // Access from the ParentClass and any other class that inherits this one.
  internal int c = 30; // Access only on the same assembly.
  private int d = 40; // Access only on the same class.

  public int GetB()
  {
    return b; // OK
  }

  public int GetD()
  {
    return d; // OK
  }
}

class ChildClass : ParentClass
{
  public void ChangeB(int newB)
  {
    b = newB; // OK
  }
}

class Program {
  static void Main()
  {
    var parent = new ParentClass();
    var child = new ChildClass();
    // var other = new OtherClass(); OK
	
    // public
    parent.a = 20; // OK

    // protected
    // parent.b ERROR
    // child.b ERROR
    
    // internal
    parent.c = 20; // OK
    child.c = 20; // OK
    // other.intM = 20; ERROR

    // private
    // parent.d ERROR
    // child.d ERROR
  }
}
Inheritance

Classes (but not structs) support the concept of inheritance. A class that derives from another class, called the base class, automatically contains all the public, protected, and internal members of the base class except its constructors and finalizers.

// Base class
public class Animal
{
  public virtual string Name { get; set; }

  public virtual void Eat() // virtual keyword allows to be overridden in a derived class.
  {
    Console.WriteLine("Animal is eating.");
  }
}

// Derived class
public class Dog : Animal
{
  private string _name;

  public string Breed { get; set; }
  public override string Name
  {
    get
    {
      return _name;
    }
    set
    {
      if (!string.IsNullOrEmpty(value))
      {
        _name = value;
      }
      else
      {
        _name = "Unknown";
      }
    }
  }

  public void Bark()
  {
    Console.WriteLine("Dog is barking.");
  }

  public override void Eat() // Because the Eat method is virtual, in a derived class it's possible to use override method to change the body.
  {
    Console.WriteLine("Dog is eating."); // Overriding the Eat method
  }
}

Inheritance from abstract class

abstract class Shape
{
  public abstract int GetArea();
}

class Square : Shape
{
  private int _side;

  public Square(int n) => _side = n;

  // GetArea method is required to avoid a compile-time error.
  public override int GetArea() => _side * _side;

  static void Main()
  {
    var sq = new Square(12);
    Console.WriteLine($"Area of the square = {sq.GetArea()}");
  }
}
// Output: Area of the square = 144
Polymorphism

Polymorphism (meaning “many forms”) allows objects of different classes to be treated as objects of a common type. This is achieved through inheritance and method overriding.

public class Shape
{
  // A few example members
  public int X { get; private set; }
  public int Y { get; private set; }
  public int Height { get; set; }
  public int Width { get; set; }

  // Virtual method
  public virtual void Draw()
  {
    Console.WriteLine("Performing base class drawing tasks");
  }
}

public class Circle : Shape
{
  public override void Draw()
  {
    // Code to draw a circle...
    Console.WriteLine("Drawing a circle");
    base.Draw();
  }
}

public class Rectangle : Shape
{
  public override void Draw()
  {
    // Code to draw a rectangle...
    Console.WriteLine("Drawing a rectangle");
    base.Draw();
  }
}

public class Triangle : Shape
{
  public override void Draw()
  {
    // Code to draw a triangle...
    Console.WriteLine("Drawing a triangle");
    base.Draw();
  }
}

class Program {
  static void Main()
  {
    var shapes = new List<Shape>
    {
      new Rectangle(),
      new Triangle(),
      new Circle()
    };

    foreach (var shape in shapes)
    {
      shape.Draw();
    }
    /*
      Output:
      Drawing a rectangle
      Performing base class drawing tasks
      Drawing a triangle
      Performing base class drawing tasks
      Drawing a circle
      Performing base class drawing tasks
    */
  }
}

Hide base class members with new members

public class BaseClass
{
  public void DoWork() { WorkField++; }
  public int WorkField;
  public int WorkProperty
  {
    get { return 0; }
  }
}

public class DerivedClass : BaseClass
{
  public new void DoWork() { WorkField++; }
  public new int WorkField;
  public new int WorkProperty
  {
    get { return 0; }
  }
}

class Program {
  static void Main()
  {
    DerivedClass B = new DerivedClass();
    B.DoWork();  // Calls the new method.

    BaseClass A = (BaseClass)B;
    A.DoWork();  // Calls the old method.
  }
}

Prevent derived classes from overriding virtual members

public class A
{
  public virtual void DoWork() { }
}

public class B : A
{
  public override void DoWork() { }
}

public class C : B
{
  public sealed override void DoWork() { } // sealed keyword is used to prevent to override it on derived class.
}

public class D : C
{
  public new void DoWork() { } // it's possible to use the new keyword to declare a new method called DoWork for D class.
}

Access base class virtual members from derived classes

public class Base
{
  public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
  public override void DoWork()
  {
    //Perform Derived's work here
    //...
    // Call DoWork on base class
    base.DoWork();
  }
}
Abstraction

Abstraction focuses on presenting only essential details to the user while hiding the complex implementation details. It involves defining abstract classes or interfaces that specify a contract (methods that must be implemented by derived classes) without providing concrete implementations.

public abstract class Shape
{
  public abstract int GetArea();
}

public class Square : Shape
{
  private int _side;

  public Square(int n) => _side = n;

  // GetArea method is required to avoid a compile-time error.
  public override int GetArea() => _side * _side;

  static void Main()
  {
    var sq = new Square(12);
    Console.WriteLine($"Area of the square = {sq.GetArea()}");
  }
}
// Output: Area of the square = 144
SOLID

Definition

SOLID is a set of five design principles that aim to make object-oriented software easier to understand, maintain, and extend. These principles were popularized by Robert C. Martin (Uncle Bob) in his 2000 paper “Design Principles and Design Patterns” and the acronym SOLID was coined a few years later.

The five SOLID principles are:

  • Single Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

I’ll be using the same example from C# Clean Code: SOLID Principles – Dev.to, so as the initial code we have the following:

public class OrderItem
{
  public string Name { get; set; }
  public decimal Price { get; set; }
  public int Quantity { get; set; }
}

public class Order 
{
  public int Id { get; set; }
  public List<OrderItem> Items { get; set; }
  public string CustomerType { get; set; } // 'Regular', 'Premium'

  public Order()
  {
    Items = new List<OrderItem>();
  }

  // Calculate order total
  public decimal GetTotal()
  {
    decimal total = 0;
    foreach (var item in Items)
    {
      total += item.Price * item.Quantity;
    }

    // Apply discount based on customer type
    if (CustomerType == "Premium")
    {
      total *= 0.9m; // 10% discount for premium customers
    }

    return total;
  }

  // Print order receipt
  public void PrintReceipt()
  {
    Console.WriteLine($"Order ID: {Id}");

    foreach (var item in Items)
    {
      Console.WriteLine($"{item.Name} - {item.Quantity} x {item.Price} = {item.Quantity * item.Price}");
    }
    
    Console.WriteLine($"Total: {GetTotal()}");
    }
}
Single Responsibility Principle (SRP)

Definition

The Single Responsibility Principle states that a class should have only one reason to change.

Use Case

Order class violates the SRP because it has two responsibilities, Order calculation and receipt printing. To fix it, we can separate the responsibilities in two different classes.

Solution

public class OrderItem { ... }

public class Order // This class only handles order calculation now.
{
  public int Id { get; set; }
  public List<OrderItem> Items { get; set; }
  public string CustomerType { get; set; } // 'Regular', 'Premium'

  public Order()
  {
    Items = new List<OrderItem>();
  }

  // Calculate order total
  public decimal GetTotal()
  {
    decimal total = 0;
    foreach (var item in Items)
    {
      total += item.Price * item.Quantity;
    }

    // Apply discount based on customer type
    if (CustomerType == "Premium")
    {
      total *= 0.9m; // 10% discount for premium customers
    }

    return total;
  }
}

public class ReceiptPrinter // This class only handles receipt printing now.
{
  // Print order receipt
  public void PrintReceipt()
  {
    Console.WriteLine($"Order ID: {Id}");

    foreach (var item in Items)
    {
      Console.WriteLine($"{item.Name} - {item.Quantity} x {item.Price} = {item.Quantity * item.Price}");
    }
    
    Console.WriteLine($"Total: {GetTotal()}");
  }
}
Open-Closed Principle (OCP)

Definition

The Open-Closed Principle suggests that software entities should be open for extension but closed for modification.

Use Case

The GetTotal method in our Order class has hardcoded discount logic for premium customers, which violates OCP. To fix it, we can move the discount logic outside the Order class, and pass it as parameter. This way, the discount functionality will be extendable without chaging the Order class code.

Solution

public class OrderItem { ... }

public interface IDiscount // We declare IDiscount interface, and include a ApplyDiscount method, so we can have multiple classes implementing it for various kinds of discounts.
{
  decimal ApplyDiscount(decimal total);
}

public class NoDiscount : IDiscount // This first class will handle no discount scenario.
{
  public decimal ApplyDiscount(decimal total)
  {
    return total;
  }
}

public class PremiumDiscount : IDiscount // This second class will handle the discount for premium customers.
{
  public decimal ApplyDiscount(decimal total)
  {
    return total * 0.9m; // 10% discount for premium customers
  }
}

public class Order // This class only handles order calculation now.
{
  public int Id { get; set; }
  public List<OrderItem> Items { get; set; }
  public IDiscount Discount { get; set; } // Customer Type logic was removed, and instead IDiscount was added, so the discount can be handled from outside.

  public Order(IDiscount discount) // IDiscount interface is included in the constructor, so any kind of discount can passed from outside and we don't need to worry about how it's applied.
  {
    Items = new List<OrderItem>();
    Discount = discount;
  }

  // Calculate order total
  public decimal GetTotal()
  {
    decimal total = 0;
    foreach (var item in Items)
    {
      total += item.Price * item.Quantity;
    }

    return Discount.ApplyDiscount(total); // Condition to apply discount for premium customer was removed and instead the ApplyDiscount method is called from the Discount instance.
  }
}

public class ReceiptPrinter { ... }
Liskov Substitution Principle (LSP)

Definition

The Liskov Substitution Principle states that subclasses should be substitutable for their base classes without altering the correctness of the program.

Use Case

By introducing the IDiscount interface, we’ve already ensured that any class implementing IDiscount (like PremiumDiscount, NoDiscount, or BirthdayDiscount) can replace each other without breaking the functionality.

Solution

public class OrderItem { ... }

public interface IDiscount
{
  decimal ApplyDiscount(decimal total);
}

public class NoDiscount : IDiscount { ... }

public class PremiumDiscount : IDiscount { ... }

public class BirthdayDiscount : IDiscount
{
  public decimal ApplyDiscount(decimal total)
  {
    return total * 0.95m; // 5% birthday discount
  }
}

public class ReceiptPrinter { ... }

var birthdayOrder = new Order(new BirthdayDiscount()); // Any discount class can now be used in place of another, following LSP
Console.WriteLine(birthdayOrder.GetTotal());
Interface Segregation Principle (ISP)

Definition

The Interface Segregation Principle suggests that clients should not be forced to depend on interfaces they don’t use.

Use Case

Instead of creating one large interface (e.g., IOrderManager), it’s better to break it into smaller, more focused interfaces. Let’s apply ISP by splitting responsibilities into smaller interfaces for orders and receipt printing.

Solution

public class OrderItem { ... }

public interface IDiscount { ... }

public class NoDiscount : IDiscount { ... }

public class PremiumDiscount : IDiscount { ... }

public class BirthdayDiscount : IDiscount { ... }

public interface IOrder // Adding this new interface, with "GetTotal" as the only required method to be implemented .
{
  decimal GetTotal();
}

public interface IReceiptPrinter // Adding this new interface, with "PrintReceipt" as the only required method to be implemented .
{
  void PrintReceipt(Order order);
}

public class Order : IOrder { ... } // Order class is only implementing required methods from IOrder interface.

public class ReceiptPrinter : IReceiptPrinter { ... } // ReceiptPrinter class is only implementing required methods from IReceiptPrinter interface.
Dependency Inversion Principle (DIP)

Definition

The Dependency Inversion Principle states that high-level modules should depend on abstractions, not on concrete implementations.

Use Case

Instead of creating one large interface (e.g., IOrderManager), it’s better to break it into smaller, more focused interfaces. Let’s apply ISP by splitting responsibilities into smaller interfaces for orders and receipt printing.

Solution

public class OrderItem { ... }

public interface IDiscount { ... }

public class NoDiscount : IDiscount { ... }

public class PremiumDiscount : IDiscount { ... }

public class BirthdayDiscount : IDiscount { ... }

public interface IOrder { ... }

public interface IReceiptPrinter { ... }

public class Order : IOrder  // Order class depends on IOrder interface, this way it ensures this class is flexible and it can easily use different implementations without tightly coupled to any one class.
{
  public int Id { get; set; }
  public List<OrderItem> Items { get; set; }
  private readonly IDiscount _discount; // We can change the access modifier to private and set it to readonly, so this parameter is only passed in the constructor.

  public Order(IDiscount discount) // The discount will depend on an abstraction for discounts, IDiscount, instead of concrete implementations like PremiumDiscount or NoDiscount.
  {
    Items = new List<OrderItem>();
     _discount = discount;
  }

  public decimal GetTotal()
  {
    decimal total = 0;
    foreach (var item in Items)
    {
      total += item.Price * item.Quantity;
    }

    return _discount.ApplyDiscount(total); 
  }
}

public class ReceiptPrinter : IReceiptPrinter { ... } // ReceiptPrinter class depends on IReceiptPrinter interface, this way it ensures this class is flexible and it can easily use different implementations without tightly coupled to any one class.
Conclusion

By applying the SOLID principles to a simple Customer Order System example, we refactored the code to become more maintainable, scalable, and flexible. Each principle brings a unique benefit:

  • Single Responsibility Principle: Makes each class focused on a single task, improving clarity and maintainability.
  • Open-Closed Principle: Allows extending functionality without modifying existing code, reducing the risk of introducing bugs.
  • Liskov Substitution Principle: Ensures that subclasses can be used in place of their base classes, preserving correctness.
  • Interface Segregation Principle: Promotes the use of smaller, more focused interfaces, reducing unnecessary dependencies.
  • Dependency Inversion Principle: Encourages classes to depend on abstractions rather than concrete implementations, improving flexibility.
SOLID

Definition

SOLID is a set of five design principles that aim to make object-oriented software easier to understand, maintain, and extend. These principles were popularized by Robert C. Martin (Uncle Bob) in his 2000 paper “Design Principles and Design Patterns” and the acronym SOLID was coined a few years later.

The five SOLID principles are:

  • Single Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

I’ll be using the same example from C# Clean Code: SOLID Principles – Dev.to, so as the initial code we have the following:

Error Handling
try
{   
  // try code block - code that may generate an exception
}
catch (Exception ex)
{   
  // catch code block - code to handle an exception
}
finally
{   
  // finally code block - code to clean up resources
}
Throwing Exceptions
throw new FormatException("FormatException: Calculations in process XYZ have been cancelled due to invalid data format.");