Explore object oriented programming with classes and objects

In this tutorial, you'll build a console application and see the basic object-oriented features that are part of the C# language.

Prerequisites

Create your application

Using a terminal window, create a directory named classes. You'll build your application there. Change to that directory and type dotnet new console in the console window. This command creates your application. Open Program.cs. It should look like this:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

In this tutorial, you're going to create new types that represent a bank account. Typically developers define each class in a different text file. That makes it easier to manage as a program grows in size. Create a new file named BankAccount.cs in the Classes directory.

This file will contain the definition of a bank account. Object Oriented programming organizes code by creating types in the form of classes. These classes contain the code that represents a specific entity. The BankAccount class represents a bank account. The code implements specific operations through methods and properties. In this tutorial, the bank account supports this behavior:

  1. It has a 10-digit number that uniquely identifies the bank account.
  2. It has a string that stores the name or names of the owners.
  3. The balance can be retrieved.
  4. It accepts deposits.
  5. It accepts withdrawals.
  6. The initial balance must be positive.
  7. Withdrawals can't result in a negative balance.

Define the bank account type

You can start by creating the basics of a class that defines that behavior. Create a new file using the File:New command. Name it BankAccount.cs. Add the following code to your BankAccount.cs file:

namespace Classes;

public class BankAccount
{
    public string Number { get; }
    public string Owner { get; set; }
    public decimal Balance { get; }

    public void MakeDeposit(decimal amount, DateTime date, string note)
    {
    }

    public void MakeWithdrawal(decimal amount, DateTime date, string note)
    {
    }
}

Before going on, let's take a look at what you've built. The namespace declaration provides a way to logically organize your code. This tutorial is relatively small, so you'll put all the code in one namespace.

public class BankAccount defines the class, or type, you're creating. Everything inside the { and } that follows the class declaration defines the state and behavior of the class. There are five members of the BankAccount class. The first three are properties. Properties are data elements and can have code that enforces validation or other rules. The last two are methods. Methods are blocks of code that perform a single function. Reading the names of each of the members should provide enough information for you or another developer to understand what the class does.

Open a new account

The first feature to implement is to open a bank account. When a customer opens an account, they must supply an initial balance, and information about the owner or owners of that account.

Creating a new object of the BankAccount type means defining a constructor that assigns those values. A constructor is a member that has the same name as the class. It's used to initialize objects of that class type. Add the following constructor to the BankAccount type. Place the following code above the declaration of MakeDeposit:

public BankAccount(string name, decimal initialBalance)
{
    this.Owner = name;
    this.Balance = initialBalance;
}

The preceding code identifies the properties of the object being constructed by including the this qualifier. That qualifier is usually optional and omitted. You could also have written:

public BankAccount(string name, decimal initialBalance)
{
    Owner = name;
    Balance = initialBalance;
}

The this qualifier is only required when a local variable or parameter has the same name as that field or property. The this qualifier is omitted throughout the remainder of this article unless it's necessary.

Constructors are called when you create an object using new. Replace the line Console.WriteLine("Hello World!"); in Program.cs with the following code (replace <name> with your name):

using Classes;

var account = new BankAccount("<name>", 1000);
Console.WriteLine($"Account {account.Number} was created for {account.Owner} with {account.Balance} initial balance.");

Let's run what you've built so far. If you're using Visual Studio, Select Start without debugging from the Debug menu. If you're using a command line, type dotnet run in the directory where you've created your project.

Did you notice that the account number is blank? It's time to fix that. The account number should be assigned when the object is constructed. But it shouldn't be the responsibility of the caller to create it. The BankAccount class code should know how to assign new account numbers. A simple way is to start with a 10-digit number. Increment it when each new account is created. Finally, store the current account number when an object is constructed.

Add a member declaration to the BankAccount class. Place the following line of code after the opening brace { at the beginning of the BankAccount class:

private static int s_accountNumberSeed = 1234567890;

The accountNumberSeed is a data member. It's private, which means it can only be accessed by code inside the BankAccount class. It's a way of separating the public responsibilities (like having an account number) from the private implementation (how account numbers are generated). It's also static, which means it's shared by all of the BankAccount objects. The value of a non-static variable is unique to each instance of the BankAccount object. The accountNumberSeed is a private static field and thus has the s_ prefix as per C# naming conventions. The s denoting static and _ denoting private field. Add the following two lines to the constructor to assign the account number. Place them after the line that says this.Balance = initialBalance:

Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;

Type dotnet run to see the results.

Create deposits and withdrawals

Your bank account class needs to accept deposits and withdrawals to work correctly. Let's implement deposits and withdrawals by creating a journal of every transaction for the account. Tracking every transaction has a few advantages over simply updating the balance on each transaction. The history can be used to audit all transactions and manage daily balances. Computing the balance from the history of all transactions when needed ensures any errors in a single transaction that are fixed will be correctly reflected in the balance on the next computation.

Let's start by creating a new type to represent a transaction. The transaction is a simple type that doesn't have any responsibilities. It needs a few properties. Create a new file named Transaction.cs. Add the following code to it:

namespace Classes;

public class Transaction
{
    public decimal Amount { get; }
    public DateTime Date { get; }
    public string Notes { get; }

    public Transaction(decimal amount, DateTime date, string note)
    {
        Amount = amount;
        Date = date;
        Notes = note;
    }
}

Now, let's add a List<T> of Transaction objects to the BankAccount class. Add the following declaration after the constructor in your BankAccount.cs file:

private List<Transaction> _allTransactions = new List<Transaction>();

Now, let's correctly compute the Balance. The current balance can be found by summing the values of all transactions. As the code is currently, you can only get the initial balance of the account, so you'll have to update the Balance property. Replace the line public decimal Balance { get; } in BankAccount.cs with the following code:

public decimal Balance
{
    get
    {
        decimal balance = 0;
        foreach (var item in _allTransactions)
        {
            balance += item.Amount;
        }

        return balance;
    }
}

This example shows an important aspect of properties. You're now computing the balance when another programmer asks for the value. Your computation enumerates all transactions, and provides the sum as the current balance.

Next, implement the MakeDeposit and MakeWithdrawal methods. These methods will enforce the final two rules: the initial balance must be positive, and any withdrawal must not create a negative balance.

These rules introduce the concept of exceptions. The standard way of indicating that a method can't complete its work successfully is to throw an exception. The type of exception and the message associated with it describe the error. Here, the MakeDeposit method throws an exception if the amount of the deposit isn't greater than 0. The MakeWithdrawal method throws an exception if the withdrawal amount isn't greater than 0, or if applying the withdrawal results in a negative balance. Add the following code after the declaration of the _allTransactions list:

public void MakeDeposit(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of deposit must be positive");
    }
    var deposit = new Transaction(amount, date, note);
    _allTransactions.Add(deposit);
}

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    if (Balance - amount < 0)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

The throw statement throws an exception. Execution of the current block ends, and control transfers to the first matching catch block found in the call stack. You'll add a catch block to test this code a little later on.

The constructor should get one change so that it adds an initial transaction, rather than updating the balance directly. Since you already wrote the MakeDeposit method, call it from your constructor. The finished constructor should look like this:

public BankAccount(string name, decimal initialBalance)
{
    Number = s_accountNumberSeed.ToString();
    s_accountNumberSeed++;

    Owner = name;
    MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

DateTime.Now is a property that returns the current date and time. Test this code by adding a few deposits and withdrawals in your Main method, following the code that creates a new BankAccount:

account.MakeWithdrawal(500, DateTime.Now, "Rent payment");
Console.WriteLine(account.Balance);
account.MakeDeposit(100, DateTime.Now, "Friend paid me back");
Console.WriteLine(account.Balance);

Next, test that you're catching error conditions by trying to create an account with a negative balance. Add the following code after the preceding code you just added:

// Test that the initial balances must be positive.
BankAccount invalidAccount;
try
{
    invalidAccount = new BankAccount("invalid", -55);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine("Exception caught creating account with negative balance");
    Console.WriteLine(e.ToString());
    return;
}

You use the try-catch statement to mark a block of code that may throw exceptions and to catch those errors that you expect. You can use the same technique to test the code that throws an exception for a negative balance. Add the following code before the declaration of invalidAccount in your Main method:

// Test for a negative balance.
try
{
    account.MakeWithdrawal(750, DateTime.Now, "Attempt to overdraw");
}
catch (InvalidOperationException e)
{
    Console.WriteLine("Exception caught trying to overdraw");
    Console.WriteLine(e.ToString());
}

Save the file and type dotnet run to try it.

Challenge - log all transactions

To finish this tutorial, you can write the GetAccountHistory method that creates a string for the transaction history. Add this method to the BankAccount type:

public string GetAccountHistory()
{
    var report = new System.Text.StringBuilder();

    decimal balance = 0;
    report.AppendLine("Date\t\tAmount\tBalance\tNote");
    foreach (var item in _allTransactions)
    {
        balance += item.Amount;
        report.AppendLine($"{item.Date.ToShortDateString()}\t{item.Amount}\t{balance}\t{item.Notes}");
    }

    return report.ToString();
}

The history uses the StringBuilder class to format a string that contains one line for each transaction. You've seen the string formatting code earlier in these tutorials. One new character is \t. That inserts a tab to format the output.

Add this line to test it in Program.cs:

Console.WriteLine(account.GetAccountHistory());

Run your program to see the results.

Next steps

If you got stuck, you can see the source for this tutorial in our GitHub repo.

You can continue with the object oriented programming tutorial.

You can learn more about these concepts in these articles: