Overview
Proper use of access modifiers is essential for creating maintainable, secure APIs. They define the contract between your code and its consumers, preventing misuse and enabling safe refactoring.
Core Concepts
Private - The Default Choice
public class BankAccount
{
// Private fields - implementation details
private decimal _balance;
private readonly List<Transaction> _transactions = new();
private const decimal MinimumBalance = 100m;
// Private methods - internal helpers
private bool IsValidAmount(decimal amount) => amount > 0;
private void LogTransaction(string type, decimal amount)
{
_transactions.Add(new Transaction(type, amount, DateTime.UtcNow));
}
// Public interface - what consumers use
public decimal Balance => _balance; // Read-only exposure
public bool Withdraw(decimal amount)
{
if (!IsValidAmount(amount)) return false;
if (_balance - amount < MinimumBalance) return false;
_balance -= amount;
LogTransaction("Withdrawal", amount);
return true;
}
}
Protected - For Inheritance
public abstract class Document
{
// Protected - derived classes can access and override
protected virtual string FileExtension => ".doc";
// Protected method - shared implementation for derived classes
protected void ValidateContent(string content)
{
if (string.IsNullOrWhiteSpace(content))
throw new ArgumentException("Content cannot be empty");
}
// Protected property - derived classes can set, others cannot
protected string Author { get; set; }
// Public method using protected members
public void Save(string content)
{
ValidateContent(content);
SaveToFile($"document{FileExtension}", content);
}
private void SaveToFile(string filename, string content) { }
}
public class PdfDocument : Document
{
protected override string FileExtension => ".pdf";
public PdfDocument(string author)
{
Author = author; // Can access protected setter
}
public void SaveWithWatermark(string content)
{
ValidateContent(content); // Can call protected method
// Add watermark logic
Save(content);
}
}
Internal - Assembly Scope
// Internal class - only visible within assembly
internal class InternalHelper
{
internal static void HelperMethod() { }
}
// Public class with internal members
public class ApiClient
{
// Public - part of the API
public async Task<Response> GetAsync(string url) { }
// Internal - used by other classes in library, not exposed to consumers
internal HttpClient HttpClient { get; }
internal void ConfigureDefaults()
{
// Setup logic used by factory classes
}
// Internal constructor - prevent external instantiation
internal ApiClient(HttpClient httpClient)
{
HttpClient = httpClient;
}
}
// Factory in same assembly can access internal
public static class ApiClientFactory
{
public static ApiClient Create()
{
var client = new ApiClient(new HttpClient()); // Uses internal constructor
client.ConfigureDefaults(); // Calls internal method
return client;
}
}
Protected Internal vs Private Protected
public class BaseClass
{
// Protected Internal: Same assembly OR derived class (anywhere)
protected internal void ProtectedInternalMethod() { }
// Private Protected: Same assembly AND derived class
private protected void PrivateProtectedMethod() { }
}
// Same assembly, derived class - both accessible
public class DerivedSameAssembly : BaseClass
{
public void Test()
{
ProtectedInternalMethod(); // OK
PrivateProtectedMethod(); // OK
}
}
// Same assembly, not derived - only protected internal accessible
public class OtherClassSameAssembly
{
public void Test(BaseClass obj)
{
obj.ProtectedInternalMethod(); // OK (internal part)
// obj.PrivateProtectedMethod(); // Error: not derived
}
}
// Different assembly, derived class
public class DerivedOtherAssembly : BaseClass
{
public void Test()
{
ProtectedInternalMethod(); // OK (protected part)
// PrivateProtectedMethod(); // Error: different assembly
}
}
Default Access Levels
// Classes default to internal
class DefaultClass { } // Same as: internal class DefaultClass
// Class members default to private
public class Example
{
int _field; // private
void Method() { } // private
string Property { get; set; } // private
}
// Interface members are always public
public interface IExample
{
void Method(); // Always public, no modifier needed
}
// Struct members default to private
public struct Point
{
int x; // private
int y; // private
}
// Enum members are always public
public enum Status
{
Active, // public
Inactive // public
}
Accessor Modifiers
public class Product
{
// Asymmetric accessor visibility
public string Name { get; private set; } // Get is public, set is private
public decimal Price { get; internal set; } // Set is internal
// Init-only with restricted setter
public string Id { get; private init; }
// Protected setter
public int Stock { get; protected set; }
public Product(string id, string name)
{
Id = id;
Name = name;
}
public void UpdateName(string name)
{
Name = name; // Can set within class
}
}
public class SpecialProduct : Product
{
public SpecialProduct(string id, string name) : base(id, name) { }
public void AddStock(int amount)
{
Stock += amount; // Can access protected setter
}
}
Interview Tips
Tip 1: Start with the most restrictive access level (private) and only increase visibility when needed. This is the principle of least privilege.
Tip 2: Know that internal is useful for assembly-level encapsulation - keeping implementation details hidden from library consumers while sharing between classes.
Tip 3: Protected internal is OR (either condition), private protected is AND (both conditions). This is a common interview question.
Common Interview Questions
-
What is the default access modifier for class members?
- Private. Class members (fields, methods, properties) are private by default. Classes themselves default to internal.
-
What's the difference between protected and internal?
- Protected: Accessible in derived classes regardless of assembly. Internal: Accessible anywhere in the same assembly regardless of inheritance. Protected is about inheritance; internal is about assembly boundaries.
-
When would you use private protected vs protected internal?
- Private protected: When you want only derived classes within your assembly to access (stricter). Protected internal: When you want either derived classes anywhere OR any code in the same assembly (looser). Use private protected for library internals; protected internal when building extensible frameworks.
-
Can you make a class member more accessible than its containing class?
- No. A public member in an internal class is effectively internal. The containing type's accessibility is the ceiling for its members.
-
What's the purpose of internal access modifier?
- Internal restricts access to the same assembly. Use it for: implementation details shared between classes in a library, factory patterns, helper classes, and testing (with InternalsVisibleTo attribute).
-
How do you expose internal members for unit testing?
- Use
[assembly: InternalsVisibleTo("TestProjectName")]in AssemblyInfo.cs or project file. This allows the test assembly to access internal members without making them public.
- Use
-
What's the difference in default access between classes and structs?
- Both default to internal for type access and private for member access. However, struct fields are commonly public for simple data containers. The key difference is classes can inherit (protected makes sense) while structs cannot.