Back to Blog
C#

C# Public Keyword: Scope and Access Control

Understand the C# public keyword, its role in access control, interactions with other modifiers, and practical implications for API design.

access modifiersC# scopeencapsulationmember visibilitypublic members
A stylized C# code editor showing a public access modifier applied to a method, with a visible key or lock metaphor.

The C# public keyword is the most permissive access modifier. A public type is accessible from other code in the same assembly or in a different assembly that references it. A public member is accessible from any code that can access its containing type. Using public in the right places is central to designing clean, maintainable APIs.

What the public Keyword Actually Allows

When you apply public to a class, method, property, field, or other type member, you remove the declaration's own access restrictions. A member is still bounded by the accessibility of its containing type. Consider a simple public method:

public class Calculator { public int Add(int a, int b) { return a + b; } }

Any code that can reference the Calculator class can call Add. This includes code outside the assembly, such as a separate application or library. If the method lacked an access modifier, its default accessibility would be private: only code inside the Calculator class could call it. Top-level types default to internal, so a class with no access modifier is visible only within its own assembly.

Accessibility Levels Compared

C# provides several access modifiers. The public keyword sits at the most permissive end. The other modifiers are:

  • private – accessible only within the containing type.
  • protected – accessible within the containing type and derived types.
  • internal – accessible within the same assembly.
  • protected internal – accessible from the same assembly or by derived types in another assembly.
  • private protected – accessible within the containing type or derived types, but only in the same assembly.

Use public intentionally. It is not a default; to expose a member beyond the assembly, you must explicitly mark both the containing type and the member public. For example, another project can access a public class only if the relevant members are also public. A public class with no public members of its own exposes no declared functionality to external callers.

Interaction with Other Modifiers

The public keyword can combine with static, readonly, const, virtual, abstract, override, and async, among others. The access modifier is independent of these behaviors. For example, a public static method is callable without an instance from any code that can access its containing type:

public static class MathHelper { public static int Square(int value) { return value * value; } }

A public const field is a compile-time constant that is directly embedded in referencing code. Changing its value requires recompiling all consumers. A public static readonly field is also accessible from anywhere but its value is evaluated at runtime, providing more flexibility.

public class Settings { public static readonly string DefaultConnectionString = string.Empty; public const int MaxRetryCount = 3; }

Here, MaxRetryCount is a public const, so any code referencing it copies the value 3 at compile time. DefaultConnectionString is a public static readonly field initialized at declaration, and its value is fetched at runtime.

Public Members and API Design

Once a member is public, it becomes part of your API surface. Changing it later can break consumers. Adding a new public method is usually a non-breaking change, but removing one, changing its signature, or altering its behavior is breaking. This is why exposing public is a commitment.

A common mistake is marking internal helpers as public without considering the consequences. For example, a public method that is only used by tests encourages other teams to use it, creating unintended dependencies.

public class OrderProcessor { public decimal CalculateTax(Order order) { return 0m; // Placeholder for internal calculation } }

If CalculateTax was meant for internal business logic, making it public allows any consumer to call it, potentially with invalid orders. Better to keep it private or internal unless you intend it for external use.

Default Accessibility and Common Pitfalls

The default accessibility for class members is private. For top-level types, the default is internal. Many developers forget this and wonder why a class they wrote is not visible to a referencing project. The fix is to explicitly mark the type and its desired members as public.

Another pitfall is marking a class as public but leaving its members without access modifiers. The result is a type that external code can instantiate but, in this example, cannot read a value or call a method, because all declared members are private.

public class TemperatureSensor { double _celsius; double GetFahrenheit() => _celsius * 9 / 5 + 32; }

Even though TemperatureSensor is public, _celsius and GetFahrenheit are private. External code can create an instance but cannot read the temperature. If the intention is to expose a temperature value, the property or method must be public.

Deciding Where to Use Public

There is no automatic rule that all code inside a public class must be public. Keep fields and implementation details private. Make methods public only when the caller is outside the class. If the caller is in the same assembly but not necessarily the same class, internal is a better fit.

The choice also depends on library vs application code. In a library, every public member is part of your contract. In an application, the boundaries matter less because there is usually only one assembly, but using public still affects testability and future refactoring. A public class in an application is exposed to integration tests and other internal projects, so its surface still needs scrutiny.

Public and Inheritance

Public members are inherited by derived classes. If a base class has a public method, a derived instance can call it. This is part of the contract for derived classes. For example:

public class Animal { public void Eat() { } } public class Dog : Animal { }

A Dog instance can call Eat because it is inherited from Animal. Overriding a public method with public override preserves its public visibility. Changing the access level in an override is not allowed; you cannot make an overridden method private or internal.

Public vs Internal for Cross-Assembly Usage

Under normal cross-assembly usage, public is how you expose types to other assemblies. internal types are invisible outside the assembly, except to friend assemblies named through InternalsVisibleTo. This is useful for unit tests, which often need access to internals. However, do not fall back on InternalsVisibleTo when a type should simply be public.

For most shared code, prefer internal over public unless external consumption is a requirement. This keeps your API surface small and reduces accidental coupling. For example, a helper class used by several classes within the same library should be internal, not public, even if the library is intended for external use.

Maintainability Implications of Over-Exposing

Each public member adds maintenance burden. Documentation generators, XML documentation warnings, and versioning policies apply to public members. Tools like API analyzers can flag missing XML comments on public members, and API compatibility checks help detect changes to public APIs. Minimizing unnecessary public surface keeps the codebase easier to evolve.

Consider refactoring a public method that was only used internally. Change its access to private or internal to see if any external callers break. In a well-structured solution, the compiler will reveal unintended dependencies, allowing you to tighten the API. This is a practical way to apply the principle of least privilege without abstract theorizing.

C# Public Keyword: Practical Usage and Code Examples | RYUSLOG DEV