C# Access Modifiers: public, private, protected, internal và cách chọn đúng

Access Modifier trả lời một câu hỏi rất đơn giản:

Code ở đâu được phép truy cập member hoặc type này?

Ví dụ:

public class User
{
    private string _passwordHash = "";
}

Ở đây:

  • User là public → code bên ngoài có thể nhìn thấy type này.
  • _passwordHash là private → chỉ code bên trong User được truy cập trực tiếp.

Mental model:

Access Modifier
→ đặt boundary
→ ai được nhìn thấy?
→ ai được gọi?
→ ai được thay đổi?

Access modifier là một phần quan trọng của encapsulation.


1. Vì sao cần Access Modifier?

Nếu tất cả mọi thứ đều public:

public class BankAccount
{
    public decimal Balance;
}

Caller có thể:

account.Balance = -1000000;

Business rule bị phá vỡ.

Thiết kế tốt hơn:

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

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException();

        if (amount > Balance)
            throw new InvalidOperationException();

        Balance -= amount;
    }
}

Bây giờ caller không được:

// account.Balance = -100;

Mà phải:

account.Withdraw(100);

Mental model:

Access Modifier
→ không chỉ để hide code

mà còn để
→ bảo vệ invariant
→ giảm API surface
→ kiểm soát cách state thay đổi

2. public

public là mức accessibility rộng nhất.

public class UserService
{
    public void CreateUser()
    {
    }
}

Code khác có thể:

var service = new UserService();
service.CreateUser();

Mental model:

public
→ expose ra bên ngoài

Thường dùng cho:

  • API của library
  • Controller
  • DTO
  • public service contract
  • method thực sự muốn consumer gọi

3. private

private nghĩa là:

Chỉ code bên trong chính type đó được truy cập.

public class User
{
    private string _passwordHash = "";

    private void ValidatePassword()
    {
    }
}

Bên ngoài:

var user = new User();

// user._passwordHash // lỗi
// user.ValidatePassword(); // lỗi

Mental model:

private
→ implementation detail của chính type

Một mindset tốt:

Nếu không có lý do expose ra ngoài thì giữ private.


4. protected

protected liên quan tới inheritance.

public class Animal
{
    protected string Name = "";
}

Derived class:

public class Dog : Animal
{
    public void PrintName()
    {
        Console.WriteLine(Name);
    }
}

Dog truy cập được Name vì Dog là derived type của Animal.

Nhưng code bên ngoài:

var dog = new Dog();
// dog.Name // lỗi

Mental model:

protected
→ class hiện tại
+
derived classes

5. private vs protected

public class Animal
{
    private int _age;
    protected string Name = "";
}

public class Dog : Animal
{
    public void Test()
    {
        // _age = 10; // không được
        Name = "Lucky"; // được
    }
}
private
→ chỉ Animal

protected
→ Animal + derived types

6. internal

internal có nghĩa:

Chỉ code nằm trong cùng assembly mới truy cập được.

internal class TokenGenerator
{
}

Nếu cùng project/assembly thì dùng được. Project khác reference assembly này thì không truy cập được.

Mental model:

internal
→ public bên trong assembly
→ private đối với assembly bên ngoài

7. Assembly là gì?

Hiểu đơn giản:

C# project
↓
compile
↓
.dll / .exe
↓
assembly

Ví dụ solution:

MyApp.Api
MyApp.Application
MyApp.Domain
MyApp.Infrastructure

Thông thường mỗi project compile thành một assembly riêng.


8. Khi nào internal hữu ích?

Ví dụ:

public interface IEmailSender
{
    Task SendAsync(string email);
}

Implementation:

internal sealed class SendGridEmailSender : IEmailSender
{
    public Task SendAsync(string email)
    {
        return Task.CompletedTask;
    }
}

Consumer chỉ cần biết IEmailSender, không cần biết implementation cụ thể.


9. protected internal

protected internal void Log()
{
}

Mental model:

protected OR internal

Nghĩa là access được nếu:

cùng assembly
OR
derived type

10. private protected

private protected void Log()
{
}

Mental model:

protected AND internal

Nghĩa là caller phải:

  1. là derived type
  2. và nằm trong cùng assembly

11. So sánh protected internal và private protected

protected internal
→ protected OR internal

private protected
→ protected AND internal
Modifier Cùng assembly Derived khác assembly
internal ✅ ❌
protected nếu derived ✅
protected internal ✅ ✅
private protected chỉ derived ❌

12. Bảng đầy đủ

Modifier Chính type Same assembly Derived khác assembly Code ngoài
private ✅ ❌ ❌ ❌
protected ✅ nếu derived ✅ ❌
internal ✅ ✅ ❌ ❌
protected internal ✅ ✅ ✅ ❌
private protected ✅ nếu derived ❌ ❌
public ✅ ✅ ✅ ✅

13. Default Access Modifier

Top-level class:

class User
{
}

mặc định là:

internal class User
{
}

Member trong class:

public class User
{
    string Name;
}

Name mặc định là private.

Mental model:

top-level type
→ internal mặc định

class member
→ private mặc định

14. Accessibility của Member bị giới hạn bởi Type chứa nó

internal class UserService
{
    public void Run()
    {
    }
}

Run() là public, nhưng UserService là internal.

Project khác không thể truy cập UserService để gọi method đó.

Mental model:

Member accessibility
không thể vượt qua boundary
của containing type

15. Access Modifier trên Property

public string Name { get; private set; }

Nghĩa là:

get
→ public

set
→ private

Bên ngoài đọc được nhưng không set trực tiếp được.


16. Ví dụ Domain Entity

public class Order
{
    public Guid Id { get; }

    public OrderStatus Status { get; private set; }

    public Order(Guid id)
    {
        Id = id;
        Status = OrderStatus.Pending;
    }

    public void Pay()
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException();

        Status = OrderStatus.Paid;
    }
}

Bên ngoài không được:

// order.Status = OrderStatus.Paid;

Mà phải:

order.Pay();

Đây chính là encapsulation.


17. Access Modifier trên Constructor

Constructor cũng có access modifier.

public User()
{
}

mọi nơi có thể tạo object.

Nhưng:

private User()
{
}

caller bên ngoài không thể new User().

Private constructor thường dùng trong:

  • factory
  • singleton
  • controlled creation

18. file Access Modifier

C# hiện đại còn hỗ trợ:

file class Helper
{
}

file nghĩa là type chỉ accessible trong cùng source file.

Mental model:

file
→ visibility chỉ trong source file

Thường hữu ích cho helper implementation hoặc generated code.


19. Access Modifier và Encapsulation

Đừng học modifier như các keyword rời rạc.

Hãy nhìn theo boundary:

private
→ boundary của type

protected
→ boundary của inheritance hierarchy

internal
→ boundary của assembly

public
→ boundary rộng nhất

file
→ boundary của source file

20. Principle: Least Privilege

Một rule thiết kế hữu ích:

Chỉ expose mức accessibility rộng đến mức thực sự cần thiết.

Mindset:

Nếu chỉ dùng trong type
→ private

Nếu chỉ phục vụ inheritance
→ protected / private protected

Nếu chỉ dùng trong assembly
→ internal

Nếu thực sự là public contract
→ public

21. Ví dụ Backend thực tế

public class CouponService
{
    private readonly ICouponRepository _repository;

    public CouponService(ICouponRepository repository)
    {
        _repository = repository;
    }

    public async Task RedeemAsync(Guid couponId)
    {
        await ValidateAsync(couponId);
        await _repository.RedeemAsync(couponId);
    }

    private Task ValidateAsync(Guid couponId)
    {
        return Task.CompletedTask;
    }
}

Ở đây:

CouponService
→ public type

RedeemAsync
→ public behavior

_repository
→ private implementation detail

ValidateAsync
→ private internal behavior

22. Sai lầm thường gặp

Tất cả đều public

Không nên nếu caller không thực sự cần access.

Nghĩ internal = private

Sai:

private
→ cùng type

internal
→ cả assembly

Nhầm protected internal

Đúng là:

protected OR internal

Nhầm private protected

Đúng là:

protected AND internal

23. Câu hỏi Interview + Đáp án

1. private và internal khác gì?

private chỉ accessible trong containing type. internal accessible từ code trong cùng assembly.

2. protected là gì?

Cho phép containing type và derived types truy cập member.

3. protected internal nghĩa là gì?

Accessible nếu caller ở cùng assembly hoặc là derived type.

4. private protected nghĩa là gì?

Accessible cho derived type nhưng chỉ khi derived type ở cùng assembly.

5. Top-level class không có modifier thì là gì?

Mặc định internal.

6. Member trong class không có modifier thì sao?

Mặc định private.

7. public method trong internal class có public toàn hệ thống không?

Không. Containing type là internal, nên code ngoài assembly không thể truy cập class để gọi method đó.

8. public get; private set; nghĩa là gì?

Code bên ngoài đọc được property nhưng chỉ class chứa property mới dùng setter.

9. Khi nào nên dùng internal?

Khi type/member cần dùng trong assembly nhưng không muốn trở thành public API cho assembly khác.

10. Vì sao không nên để mọi thứ public?

Vì tăng API surface, coupling, khó thay đổi implementation và dễ phá encapsulation.


24. Key Takeaways

private
→ only this type

protected
→ this type + derived types

internal
→ same assembly

protected internal
→ derived OR same assembly

private protected
→ derived AND same assembly

public
→ accessible rộng nhất

file
→ same source file

Access modifier không chỉ là “ai thấy code”, mà là công cụ thiết kế boundary và bảo vệ state của hệ thống.

Nếu một member không cần public thì đừng làm nó public.