1. Introduction
Access modifiers are like fences and locks in your house: they decide who can walk into which room, and who can only peek through the keyhole. Just a reminder, in C# you get these:
| Modifier | Who can see |
|---|---|
|
Everyone |
|
Only inside the current class |
|
Current class and its kids (inheritors) |
|
The whole project (assembly) |
|
The whole project + inheritors |
|
Only inheritors in this same project |
The point of encapsulation is to hide the class's implementation details, so nobody can mess up your object with some evil line like obj.Field = 99999; if you don't want them to.
2. Classic Access Level Mistakes
Making Everything public
The most common mistake is declaring all fields and methods in a class as public. The logic is like: "What if I need it? Let everyone use my stuff wherever they want!" This seems handy until someone assigns your object an invalid state, or suddenly starts using your internals in a way that makes changing code scary.
Example of a mistake:
public class BankAccount
{
public string Owner;
public decimal Balance;
}
Now anyone can do this:
var acc = new BankAccount();
acc.Owner = "";
acc.Balance = -1000000M; // Suddenly, a million in debt!
What's wrong?
You broke all the rules of safety and common sense. Even if your bank only stores leaves or Monopoly money, a negative balance "just because" is weird.
Turning a class into a "structure without structure"
Another classic mistake is making not just fields public, but also internal properties and methods that should never be available from outside.
public class Vault
{
public string DoorPIN = "1234";
public void OpenDoor()
{
// Some magic
}
}
So what now? Well, now anyone can find out the PIN and open your vault. Even if it's a "test vault," in a month someone will forget about this hole and something bad will happen.
The right way:
The class interface should be minimal and protected. Stuff others need — make it public. Stuff that's just for you — private. If something's for inheritors — protected.
3. Encapsulation Mistakes
In .NET, there's a strict consensus: no class data (state) should be stored in public fields! Everything should be closed off or at least wrapped in properties. Direct field access is classic bad style and a source of sneaky bugs.
Why fields should be private
Fields are the core of an object's internal state. If you give public access to them, you lose control over when and what values get written there. Your object becomes vulnerable to invalid values, and if you change their format, all public usages will break.
Instead:
Declare fields as private, and only expose the properties or methods you really need:
public class BankAccount
{
private decimal balance;
public decimal Balance
{
get { return balance; }
private set
{
if (value < 0)
throw new ArgumentException("Balance can't be negative");
balance = value;
}
}
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Can't deposit zero or less!");
Balance += amount;
}
}
Big mistake: public set for data that shouldn't change
Sometimes you only need a get, but devs write public { get; set; } "by default" because it's faster. As a result, anyone can do:
account.Balance = 99999999; // Why not?
Better like this:
If a property should only be set inside the class, make set private:
public decimal Balance { get; private set; }
or, if the value should only be set when creating the object (C# 14):
public decimal Balance { get; init; }
4. When protected is a trap too
Wide open doors for all inheritors
protected is a good way to pass functionality to inheritors, but some data shouldn't even be available to them! For example, if your kids (class inheritors) shouldn't mess with critical fields directly.
Example:
public class SecureVault
{
protected string secretCode = "1234";
}
Now any inheritor can do this:
public class HackerVault : SecureVault
{
public void Hack()
{
secretCode = "0000"; // Changed the secret easily!
}
}
Tip:
Use protected only for stuff that really needs to be available to inheritors. Keep everything else private, and write protected methods for the stuff they should be able to do.
5. Mistakes with internal and mixed-up assemblies
The internal modifier seems tempting: "let everything be available inside the project." But as soon as your project is more than one file or you hook up libraries, conflicts pop up. Students often accidentally make something public inside the assembly, even though the class should be totally hidden.
Example:
internal class Logger
{
internal void Write(string msg) { /* ... */ }
}
Now anyone can call Logger.Write from any file in the project. And if a year later someone plugs your DLL into another project? All your "internal kitchen" will be visible if you left something public.
Advice:
Use internal for technical classes, but try to keep sensitive stuff private.
6. Practice: Banking App
Let's keep going with our banking app example to see how easy it is to mess up and what to do about it.
Mistake Example #1: Public fields
public class BankAccount
{
public decimal Balance;
public string Owner;
}
Problem: anyone can break this.
Fixing it with properties
public class BankAccount
{
private decimal _balance;
private string _owner;
public string Owner => _owner; // Read-only
public decimal Balance
{
get => _balance;
private set
{
if (value < 0) throw new ArgumentException("Balance can't be negative");
_balance = value;
}
}
public BankAccount(string owner, decimal initialBalance)
{
if (string.IsNullOrWhiteSpace(owner))
throw new ArgumentException("Owner name is required");
_owner = owner;
Balance = initialBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive");
Balance += amount;
}
}
Now if you try this:
var acc = new BankAccount("Lena", 1000m);
acc.Balance = -700m; // Error! set is private.
Or even this:
acc.Owner = "Hacker"; // Compile error, no set
— it won't work. Only through the constructor or methods.
Mistake: "Pretty" properties
A lot of people write:
public int Age { get; set; }
Even though age should only change through a method like IncreaseAge (say, on January 1st), and not directly.
7. Visual Reminder: How Not To and How To
graph TD
A[Public Fields] -->|Any code| D[Uncontrolled state]
B[Private Fields + Public Methods] -->|Well-defined| E[Controlled state]
F[Public Setters] -->|Anyone can change| D
G[Private Setters] -->|Only class| E
| Approach | State protected? | Can validate? | Easy to extend? |
|---|---|---|---|
| Public fields | ❌ | ❌ | ❌ |
| Properties (get/set public) | ❌ | Partially | Yes, but not safe |
| Private fields + property with set private | ✅ | ✅ | ✅ |
8. Style Tips for Working with Encapsulation
- Only expose what your class user really needs.
- Always validate data coming into your object.
- Don't go overboard: if a property shouldn't be changed from outside — make set private.
- For custom logic — always use methods, not direct access.
- Even if you're tempted — don't make public fields "just for testing."
- Use properties instead of direct fields even for public reading — it's easy to tweak them later.
GO TO FULL VERSION