22th May 2024

The ASP.NET Core OOP Mastery: Your Guide to Object-Oriented Development

Oops-Dotnet-Guide-VS-Online-img

Why Object-Oriented Programming (OOP) is Crucial for ASP.NET Core Development

Object-oriented programming (OOP) is an essential paradigm for modern software development. By adopting object-oriented principles, developers can structure their code to be modular and reusable, promoting greater cohesion and less coupling between components, resulting in a more flexible and maintainable architecture.

ASP.NET Core benefits greatly from OOP because it allows developers to:
  • Encapsulate Data and Behaviour: By using classes, you can encapsulate data and methods, which helps in organizing and protecting the internal state of objects.
  • Reuse Code Efficiently: Inheritance enables the creation of new classes based on existing ones, promoting code reuse, and reducing redundancy.
  • Enhance Flexibility and Scalability: Polymorphism allows objects to be treated as instances of their parent class, enabling flexibility in how objects interact and are manipulated.
  • Create Maintainable Code: Abstraction helps in managing complexity by allowing developers to focus on high-level operations while hiding implementation details.

What You'll Learn in This Blog

In this blog, we will explore the four fundamental principles of OOP—Abstraction, Encapsulation, Inheritance, and Polymorphism—and demonstrate how to implement each in an ASP.NET Core application. By the end of this blog, you will understand:

  • The core concepts of OOP and why they are important.
  • How to apply OOP principles to build a simple ASP.NET Core application.
  • Practical examples of each OOP principle using C# in an ASP.NET Core context.

What is Object-Oriented Programming?

Object-oriented programming (OOP) is a programming style that uses "objects" to represent data and methods. It emerged in the 1960s and became popular in the 1990.It became the foundation for several programming languages, including C#, Java, C++, Python, Lua, and PHP. OOP introduces a way of writing computer programs where real-world entities are abstracted into software constructs called “objects.”

Understanding Objects and Classes
  • Objects: Instances of classes that encapsulate data and behaviour.
  • Classes: Blueprints that define the structure and behaviour of objects.

Think of a class as a recipe, and an object as the dish you prepare from that recipe. For example, in a library system, a Book class might include properties like “Title”, “Author” and “ReleaseDate” and methods like “Borrow” and “Return”.

Here’s a simple Book class in C#:

                                
                                    
  public class Book
  {
      public Guid Id { get; set; }
      public string Title { get; set; }
      public string Author { get; set; }
      public DateTime ReleaseDate { get; set; }
  }
  
  
                                
                            

This class defines a Book with properties such as Title, Author, Genre, and ReleaseDate.

Benefits of OOP
  • Modularity: Breaks down a program into manageable, self-contained objects.
  • Reusability: Classes can be reused across different programs.
  • Scalability: Easy to add new features or modify existing ones.
  • Maintainability: Encapsulation protects data and makes the code easier to manage.
  • Collaboration: Multiple developers can work on different parts of the application simultaneously.

By adopting OOP, developers can create more organized and scalable software, which is especially useful in ASP.NET Core applications.

The core principles of Object-Oriented Programming

Opps-VS-Online-img

Object-Oriented Programming (OOP) relies on four core principles: Abstraction, Encapsulation, Inheritance, and Polymorphism. Let's explore each with a simple C# example in .NET:

1. Abstraction

Abstraction focuses on the essential details of an object while hiding unnecessary complexity. Imagine a Car class. We care about its ability to drive, brake, and maybe its color. But the internal combustion engine details are irrelevant for most interactions.

                                
                                    
  public class Car
  {
    public string Color { get; set; }
  
    public void Drive()
    {
      Console.WriteLine("Car is driving!");
    }
  
    public void Brake()
    {
      Console.WriteLine("Car is braking!");
    }
  }
  
                                
                            

In this example, the Car class abstracts away the engine details, focusing on functionalities like driving and braking.

2. Encapsulation

Encapsulation bundles data (attributes) and related operations (methods) within a class. It controls access to this internal data using access modifiers (public, private, protected). This ensures data integrity and prevents unauthorized modifications.

encapsulation

Continuing with the Car class:

                                
                                    
  public class Car
{
  private int _speed; // Private attribute (encapsulated)

  public string Color { get; set; }  

  public void Drive(int speed) // Public method to modify private speed
  {
    _speed = speed;
    Console.WriteLine($"Car is driving at {_speed} mph!");
  }

  public void Brake()
  {
    Console.WriteLine("Car is braking!");
  }
}
 
                                
                            

Here, the _speed attribute is private, ensuring it can only be modified through the public Drive method. This enforces data integrity and prevents unintended changes.

3. Inheritance

Inheritance allows creating new classes (subclasses) based on existing classes (base classes). The subclass inherits properties and methods from the base class, promoting code reusability.

Imagine a class SportsCar inheriting from the Car class:

                                
                                    
  public class SportsCar : Car  // Inherits from Car
  {
    public void Boost()
    {
      Console.WriteLine("Sports car is using boost!");
    }
  }
  
  
                                
                            

The SportsCar inherits functionalities like driving and braking from Car. It also adds its own method Boost. This promotes code reuse and reduces redundancy.

4. Polymorphism

Polymorphism allows objects of different classes to respond differently to the same method call. This enables flexible and dynamic behaviour.

For example, consider a method MakeSound defined in both Car and Truck classes:

                                
                                    
  public class Car : Vehicle // Assuming Vehicle is a base class
  {
    public override void MakeSound() // Override from Vehicle
    {
      Console.WriteLine("Car Honking!");
    }
  }

  public class Truck : Vehicle
  {
    public override void MakeSound()
    {
      Console.WriteLine("Truck Honking!");
    }
  }
  
                                
                            

Here, the MakeSound method is overridden in both Car and Truck. When calling MakeSound on an object of either class, the appropriate version executes based on the object's type. This demonstrates polymorphic behaviour.

Understanding Object-Oriented Programming in ASP.NET Core

In the previous section we have discussed the core principles of OOPs in brief, now we'll build a simple ASP.NET Core application to manage bank accounts and demonstrate how these OOP principles can be implemented.

Prerequisites

To follow along with this sample application, you need to have installed a recent version of .NET, the latest version is .NET 8 and Visual Studio 2022 (latest version recommended).

Setting Up the Application

We'll create a Web API for managing bank accounts.

Create a New ASP.NET Core Project
  • Open Visual Studio.
  • Click on Create a new project.
  • Select ASP.NET Core Web API and click Next.
  • Name your project BankAccountManager and choose a suitable location. Click Create.
  • Select .NET 8 as the framework and click Create.
Project Structure
  • Once the project is created, you will see a default folder structure.
  • Add a new folder named Models to hold your entity classes.
Implementing Abstraction

In our application, a bank account represents a real-world object with associated properties and actions. We'll define a BankAccount class as our main entity.

Create the BankAccount Class
  • Right-click the Models folder, select Add > Class.
  • Name the class BankAccount and click Add.
                                
                                    
  namespace BankAccountManager.Models
  {
      public class BankAccount
      {
          public Guid Id { get; set; }
          public string? AccountHolder { get; set; }
          public decimal Balance { get; set; }
          public DateTime CreatedDate { get; set; }

          public BankAccount()
          {
              Id = Guid.NewGuid();
              CreatedDate = DateTime.UtcNow;
          }
      }
  }
  
                                
                            

In this class, we abstract the bank account entity, which includes properties like account holder, balance, and created date.

Implementing Encapsulation

Encapsulation means hiding the internal details of a class and providing controlled access through access modifiers. C# offers several access modifiers:

  • public: Accessible from anywhere.
  • private: Accessible only within the class.
  • protected: Accessible within the class and its derived classes.
  • internal: Accessible within the same assembly.
  • protected internal: Accessible within the same assembly and in derived classes.
  • To demonstrate encapsulation, we'll create a Bank class to manage bank accounts. In the Models folder, add a new class named Bank.
                                
                                    
  namespace BankAccountManager.Models
  {
      public class Bank
      {
          private List<BankAccount> accounts = new List<BankAccount>();

          public void AddAccount(BankAccount account)
          {
              accounts.Add(account);
          }

          public void RemoveAccount(Guid accountId)
          {
              var account = accounts.FirstOrDefault(a => a.Id == accountId);
              if (account != null)
              {
                  accounts.Remove(account);
              }
          }

          public IEnumerable<BankAccount> GetAccounts()
          {
              return accounts;
          }
      }
  }
  
                                
                            

In this example, accounts is a private list that stores the bank accounts. The AddAccount, RemoveAccount, and GetAccounts methods provide controlled access to this list, encapsulating the data and behaviour within the Bank class.

Implementing Inheritance

Inheritance allows a class to inherit properties and methods from another class. We’ll create a new SavingsAccount class that inherits from BankAccount and adds additional properties specific to savings accounts.

In the Models folder, add a new class named SavingsAccount.

                                
                                    
  namespace BankAccountManager.Models
  {
      public class SavingsAccount : BankAccount
      {
          public double InterestRate { get; set; }
      }
  }
  
                                
                            

The SavingsAccount class inherits from BankAccount, gaining its properties while adding an additional InterestRate property.

Implementing Polymorphism

Polymorphism allows methods to be implemented in different ways. We'll demonstrate this by adding a method to calculate interest in the BankAccount and SavingsAccount classes.

Update the BankAccount and SavingsAccount Classes

                                
                                    
  namespace BankAccountManager.Models
  {
      public class BankAccount
      {
          public Guid Id { get; set; }
          public string? AccountHolder { get; set; }
          public decimal Balance { get; set; }
          public DateTime CreatedDate { get; set; }

          public BankAccount()
          {
              Id = Guid.NewGuid();
              CreatedDate = DateTime.UtcNow;
          }

          public virtual void CalculateInterest()
          {
              // Base interest calculation (if any)
          }
      }

      public class SavingsAccount : BankAccount
      {
          public double InterestRate { get; set; }

          public override void CalculateInterest()
          {
              Balance += Balance * (decimal)InterestRate;
          }
      }
  }
  
                                
                            

The CalculateInterest method in the SavingsAccount class overrides the base method in BankAccount, demonstrating polymorphism.

Create the BankController Class
  • Right-click the Controllers folder, select Add > Controller.
  • Select API Controller - Empty and click Add.
  • Name the controller BankController.
                                
                                    
  using BankAccountManager.Models;
  using Microsoft.AspNetCore.Mvc;
  using System;

  namespace BankAccountManager.Controllers
  {
      [ApiController]
      [Route("api/[controller]")]
      public class BankController : ControllerBase
      {
          private readonly Bank _bank;

          public BankController(Bank bank)
          {
              _bank = bank;
          }

          [HttpGet("accounts")]
          public IActionResult GetAccounts()
          {
              return Ok(_bank.GetAccounts());
          }

          [HttpPost("accounts")]
          public IActionResult AddAccount([FromBody] BankAccount account)
          {
              _bank.AddAccount(account);
              return CreatedAtAction(nameof(GetAccounts), new { id = account.Id }, account);
          }

          [HttpDelete("accounts/{id}")]
          public IActionResult RemoveAccount(Guid id)
          {
              _bank.RemoveAccount(id);
              return NoContent();
          }
      }
  }
  
                                
                            
Update Program.cs
                                
                                    
  using BankAccountManager.Models;


  var builder = WebApplication.CreateBuilder(args);
  builder.Services.AddSingleton<Bank>();
  // Add services to the container.

  builder.Services.AddControllers();
  // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
  builder.Services.AddEndpointsApiExplorer();
  builder.Services.AddSwaggerGen();

  var app = builder.Build();

  // Configure the HTTP request pipeline.
  if (app.Environment.IsDevelopment())
  {
      app.UseSwagger();
      app.UseSwaggerUI();
  }

  app.UseHttpsRedirection();

  app.UseAuthorization();

  app.MapControllers();

  app.Run();
  
                                
                            
Running the Application
  • Press F5 to run the application.
  • Open your browser and navigate to https://localhost:port/swagger/index.html to access the Swagger UI.
Testing the API with Swagger UI
  • In Swagger UI, find the POST /api/bank/accounts endpoint.
  • Click on the endpoint to expand it, then click Try it out.

Enter the following sample data in the request body:

                                
                                    
    {
      "accountHolder": "John Doe",
      "balance": 1000.00
    }
  
                                
                            

Click Execute to send the request.

You should receive a 201 Created response with the created account details.

Dive Deeper: Go beyond the basics

Interfaces

Interfaces define a contract that classes must implement, providing a common set of methods for different classes to implement in their own way.

                                
                                    
  public interface ITransaction
  {
      void Process();
  }
  
                                
                            

In the example above, any class implementing the ITransaction interface must provide an implementation for the Process method, ensuring that different types of transactions can be handled uniformly.

Methods and Properties

Methods and properties define the behaviour and characteristics of classes. Methods perform actions, and properties provide access to internal data.

                                
                                    
  public class BankAccount
  {
      public Guid Id { get; set; }
      public decimal Balance { get; private set; }

      public void Deposit(decimal amount)
      {
          Balance += amount;
      }

      public void Withdraw(decimal amount)
      {
          if (amount <= Balance)
          {
              Balance -= amount;
          }
      }
  }
  
                                
                            

In this example, the Deposit and Withdraw methods allow for actions to be performed on the Balance property, encapsulating the logic to ensure the balance is correctly modified and never goes negative.

Constructors and Destructors

Constructors initialize objects when they are created, while destructors release resources when an object is destroyed.

                                
                                    
  public class BankAccount
  {
      public Guid Id { get; set; }
      public decimal Balance { get; private set; }

      public BankAccount(decimal initialBalance)
      {
          Id = Guid.NewGuid();
          Balance = initialBalance;
      }

      ~BankAccount()
      {
          // Cleanup code here
      }
  }
  
                                
                            

In the example, the constructor initializes the BankAccount with an initial balance, while the destructor contains logic to clean up resources when the account is no longer needed.

Static Classes

Static classes contain static members that can be accessed without creating an instance of the class. This is useful for utility or helper functions that are shared across the application.

                                
                                    
  public static class InterestCalculator
  {
      public static decimal CalculateInterest(decimal principal, decimal rate, int time)
      {
          return principal * rate * time;
      }
  }
  
                                
                            

The InterestCalculator class provides a static method CalculateInterest that can be used anywhere in the application without needing to create an instance of the class.

Namespaces

Namespaces organize and group related classes into a hierarchy, helping to avoid name conflicts and to modularize the code, making it more manageable and readable.

                                
                                    
  namespace BankAccountManager.Models
  {
      public class BankAccount
      {
          public Guid Id { get; set; }
          public decimal Balance { get; private set; }
      }
  }
  
                                
                            

In this example, the BankAccount class is placed within the BankAccountManager.Models namespace, which helps organize the code and prevents naming conflicts with classes from other libraries or parts of the project.

Object-Oriented Design Patterns

ASP.NET Core frequently uses object-oriented design patterns such as MVC (Model-View-Controller) to separate business logic, presentation, and user interaction, promoting organized and maintainable code.

Example:

In the MVC pattern, the Controllers handle user input, the Models represent the data, and the Views display the data. This separation of concerns makes the application easier to manage and scale.

Let's develop your ideas into reality