C# Access Modifiers

Access modifiers control where a type or member can be used from. In C#, they are one of the main tools for encapsulation: keeping implementation details hidden while exposing a clear, safe public surface. Good access choices make code easier to change, test, and reuse because other code cannot accidentally depend on details that should remain private.

Overview: How Access Modifiers Work

An access modifier is a keyword such as public, private, protected, or internal placed before a class, method, property, field, constructor, or nested type. The compiler uses it to decide whether one piece of code is allowed to refer to another. If code tries to call a private method from the outside, for example, the program fails at compile time before it ever runs.

Access control is part of the type system. The compiled assembly stores metadata that describes each type and member, including its accessibility. The CLR respects that metadata when loading and verifying code, and reflection can inspect it. However, normal C# code should treat access modifiers as a design contract: public members are promises to other code; private members are implementation details.

The most common modifiers are:

Modifier Meaning Common use
public Accessible from any code that can reference the containing type or assembly. APIs, methods, and properties meant for callers.
private Accessible only inside the containing type. Fields, helper methods, cached data, validation details.
protected Accessible inside the containing type and derived types. Extension points for inheritance.
internal Accessible only within the same assembly or project output. Code shared inside a library but hidden from consumers.
protected internal Accessible from the same assembly, or from derived types in another assembly. Rare library inheritance scenarios.
private protected Accessible from derived types only when they are in the same assembly. Narrow inheritance hooks inside one assembly.

Top-level classes and other top-level types can usually be public or internal. If no modifier is written on a top-level class, it is internal. Class members default to private, which is why fields are often inaccessible unless exposed through properties or methods.

Syntax

access_modifier type_or_member_declaration
  • access_modifier is the visibility keyword, such as public, private, protected, or internal.
  • type_or_member_declaration is the class, method, property, field, constructor, or nested type being declared.
  • If you omit the modifier, C# applies a default based on where the declaration appears.
public class Customer
{
    private string name;

    public Customer(string name)
    {
        this.name = name;
    }

    public string GetName()
    {
        return name;
    }
}

Here the class and its constructor are public, so callers can create customers. The field is private, so callers cannot change name directly. GetName is the controlled public way to read it.

Examples

Public API with Private State

using System;

class Program
{
    static void Main()
    {
        TemperatureReading reading = new TemperatureReading(22.5);
        reading.Increase(1.25);

        Console.WriteLine(reading.Celsius);
        Console.WriteLine(reading.Fahrenheit);
    }
}

public class TemperatureReading
{
    private double celsius;

    public TemperatureReading(double celsius)
    {
        this.celsius = celsius;
    }

    public double Celsius
    {
        get { return celsius; }
    }

    public double Fahrenheit
    {
        get { return celsius * 9 / 5 + 32; }
    }

    public void Increase(double amount)
    {
        celsius += amount;
    }
}

Output:

23.75
74.75

The field celsius is private, so only TemperatureReading can change it directly. Callers use the public constructor, properties, and method. This protects the class from arbitrary outside writes while still providing useful behavior.

Protected Members and Inheritance

using System;

class Program
{
    static void Main()
    {
        SavingsAccount account = new SavingsAccount("A100", 500m, 0.03m);
        account.ApplyInterest();

        Console.WriteLine(account.AccountNumber);
        Console.WriteLine(account.GetBalance());
    }
}

public class Account
{
    public string AccountNumber { get; }
    protected decimal balance;

    public Account(string accountNumber, decimal openingBalance)
    {
        AccountNumber = accountNumber;
        balance = openingBalance;
    }

    public decimal GetBalance()
    {
        return balance;
    }
}

public class SavingsAccount : Account
{
    private readonly decimal interestRate;

    public SavingsAccount(string accountNumber, decimal openingBalance, decimal interestRate)
        : base(accountNumber, openingBalance)
    {
        this.interestRate = interestRate;
    }

    public void ApplyInterest()
    {
        balance += balance * interestRate;
    }
}

Output:

A100
515.00

balance is protected, so the derived SavingsAccount class can use it. Code in Main cannot access account.balance, because Main is outside the inheritance relationship. This is useful when a base class wants to give subclasses controlled access without making the member public to everyone.

Internal Classes for Project-Level Helpers

using System;

class Program
{
    static void Main()
    {
        string normalized = ProductCodeNormalizer.Normalize(" ab-120 ");
        Console.WriteLine(normalized);
    }
}

internal static class ProductCodeNormalizer
{
    public static string Normalize(string code)
    {
        return code.Trim().ToUpperInvariant();
    }
}

Output:

AB-120

ProductCodeNormalizer is internal, so any file in the same assembly can use it, but another project referencing the assembly cannot. This is common in class libraries: expose the main public types, and keep formatting, parsing, mapping, and persistence helpers internal.

A Realistic Encapsulated Class

using System;

class Program
{
    static void Main()
    {
        Wallet wallet = new Wallet("Mina");
        wallet.Deposit(50m);
        bool paid = wallet.TrySpend(18.75m);

        Console.WriteLine(wallet.Owner);
        Console.WriteLine(wallet.Balance);
        Console.WriteLine(paid);
    }
}

public class Wallet
{
    private decimal balance;

    public Wallet(string owner)
    {
        Owner = owner;
    }

    public string Owner { get; }

    public decimal Balance
    {
        get { return balance; }
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit must be positive.");
        }

        balance += amount;
    }

    public bool TrySpend(decimal amount)
    {
        if (amount <= 0 || amount > balance)
        {
            return false;
        }

        balance -= amount;
        return true;
    }
}

Output:

Mina
31.25
True

This class hides balance behind public behavior. Callers can read Balance, but they cannot set it to a negative number or skip validation. The public methods define the business rules, and the private field stores the implementation detail.

How It Works Step by Step

  1. The compiler reads each declaration and assigns an accessibility level, either from the keyword you wrote or from the C# default.
  2. When another line of code references that type or member, the compiler checks whether the reference is legal from that location.
  3. If the reference is illegal, compilation stops with an accessibility error, such as trying to access a private field from outside its class.
  4. If the reference is legal, the compiler emits metadata into the assembly that records the accessibility.
  5. At runtime, ordinary compiled C# code uses the already-checked member calls. The CLR also understands the metadata, which matters for loading, verification, dynamic code, and reflection.

The important point is that access modifiers are mostly a compile-time safety system for everyday C# development. They prevent accidental misuse before the program runs. They are not a security boundary for secrets: reflection, unsafe code, debuggers, and process access can sometimes inspect private data. Do not store passwords or keys in a field just because it is private.

Common Mistakes

Trying to Read a Private Field Directly

Wallet wallet = new Wallet("Mina");
Console.WriteLine(wallet.balance);

This does not compile because balance belongs to the private implementation of Wallet. Use the public property instead:

using System;

class Program
{
    static void Main()
    {
        Wallet wallet = new Wallet("Mina");
        wallet.Deposit(20m);
        Console.WriteLine(wallet.Balance);
    }
}

public class Wallet
{
    private decimal balance;

    public Wallet(string owner)
    {
        Owner = owner;
    }

    public string Owner { get; }
    public decimal Balance { get { return balance; } }

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

Output:

20

Making Fields Public Instead of Exposing Behavior

public class Player
{
    public int Health;
}

This compiles, but it lets any code set Health to invalid values such as -500. Prefer a private field with a public method or property that enforces rules:

using System;

class Program
{
    static void Main()
    {
        Player player = new Player();
        player.TakeDamage(35);
        Console.WriteLine(player.Health);
    }
}

public class Player
{
    private int health = 100;

    public int Health
    {
        get { return health; }
    }

    public void TakeDamage(int amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        health = Math.Max(0, health - amount);
    }
}

Output:

65

Confusing Internal with Private

internal does not mean private to one class. It means visible throughout the same assembly. If several projects reference your library, internal hides a type from those other projects, but every file inside the library can still use it. Choose private for implementation details inside one type, and internal for implementation details shared inside one assembly.

Best Practices

  • Start with the narrowest access that works. Widen access only when another part of the design genuinely needs it.
  • Keep fields private in normal application code. Expose state through properties or methods that preserve invariants.
  • Make classes internal by default in libraries unless they are part of the public API.
  • Use protected carefully. Every protected member becomes part of the inheritance contract and is harder to change later.
  • Prefer public methods that describe actions, such as Deposit or ApplyDiscount, instead of public setters that allow any value.
  • Avoid protected internal and private protected until you are writing library code with a clear assembly and inheritance design.
  • Remember that access modifiers manage code boundaries, not user permissions or data secrecy.

Practice Exercises

  1. Create a Book class with a public Title property and a private timesRead field. Add a public MarkRead method and a public read-only TimesRead property.
  2. Create a base class Vehicle with a protected speed field. Derive Car from it and add a public method that increases the speed without allowing it to exceed 120.
  3. Create an internal static class named EmailFormatter with a public static method that trims and lowercases an email address. Use it from Main in the same project.

Summary

  • Access modifiers define which code can see and use a type or member.
  • private protects implementation details inside one type.
  • public is for the API you want outside code to rely on.
  • protected supports inheritance, while internal supports assembly-level sharing.
  • Good access design preserves invariants, reduces accidental coupling, and makes future changes easier.