C# Internal Keyword: Scope and Assembly Boundaries
c# internal keyword: Learn how the C# internal keyword limits visibility to a single assembly, and how to use it with InternalsVisibleTo for testing and modular design.
When you declare a class in C# without an access modifier, it becomes internal by default. The c# internal keyword restricts visibility to the current assembly, meaning any code in the same .dll or .exe can access the type, but external assemblies cannot. This is a fundamental tool for controlling the public API surface of your library.
What Does the internal Access Modifier Do?
The internal keyword is an access modifier that sets the accessibility of a type or member to the containing assembly. An assembly is the compiled output of your project, typically a .dll or .exe. When you mark a class as internal, only code within that same assembly can reference it. This is stricter than public, which allows access from any assembly, but less strict than private, which limits access to the containing type.
Consider a simple library that exposes a public facade but keeps its helper classes hidden:
public class OrderService { public void PlaceOrder(Order order) { var validator = new OrderValidator(); validator.Validate(order); } } internal class OrderValidator { public void Validate(Order order) { // validation logic } }
Here, OrderValidator is internal. External consumers of the library can call OrderService.PlaceOrder, but they cannot directly instantiate or call OrderValidator. This keeps the implementation details hidden while still allowing the internal code to work together.
How internal Defines Assembly Boundaries
The assembly is the boundary for internal access. When you compile a C# project, the compiler produces an assembly that contains all the types defined in that project. Any type marked as internal is visible to every other type in that assembly, regardless of namespace. This is different from protected, which is tied to inheritance, and private, which is tied to the containing type.
The assembly boundary is enforced at compile time. If you try to access an internal type from another assembly, the compiler will produce an error. This is a strong guarantee that prevents accidental coupling between separate modules.
For example, if you have two projects in a solution, Core and App, and Core defines an internal class, App cannot use it unless you explicitly opt in with InternalsVisibleTo. This makes the internal keyword a valuable tool for designing clean module boundaries.
Applying internal to Types and Members
You can apply internal to classes, structs, interfaces, enums, delegates, and members such as fields, properties, methods, and events. When applied to a member, the member is accessible only from within the same assembly, even if the containing type is public.
public class DataProcessor { internal int _cacheSize = 100; internal void ClearCache() { // ... } }
In this example, DataProcessor is public, but _cacheSize and ClearCache are internal. External code can create a DataProcessor instance, but cannot read or modify _cacheSize or call ClearCache. This allows you to expose a type while keeping certain operations internal to the assembly.
You can also combine internal with other modifiers, such as protected internal (which is actually protected internal in C#) to allow access from derived types in other assemblies as well. The exact combination depends on your design needs.
Comparing internal with Other Access Modifiers
| Modifier | Accessible from same class | Accessible from derived class | Accessible from same assembly | Accessible from other assemblies |
|---|---|---|---|---|
private | Yes | No | No | No |
protected | Yes | Yes | No | No (except derived) |
internal | Yes | Yes | Yes | No |
protected internal | Yes | Yes | Yes | No (except derived) |
public | Yes | Yes | Yes | Yes |
The table shows that internal gives you assembly-wide access without exposing anything to external callers. This is useful for shared infrastructure within a library, such as internal utilities, configuration helpers, or testable components.
Sharing internal Members with InternalsVisibleTo
Sometimes you need to expose internal types to a specific external assembly, typically for unit testing. The InternalsVisibleTo attribute allows you to grant another assembly access to your internal members. This is commonly used to let a test project call internal methods without making them public.
using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("MyProject.Tests")]
Place this attribute in your project file or in a source file. After adding it, the MyProject.Tests assembly can access all internal types and members of the current assembly. This keeps the production API clean while enabling white-box testing.
It is important to note that InternalsVisibleTo is a compile-time feature. It does not affect runtime security or reflection. Any code in the specified assembly can access internals, so you should only grant this to assemblies you trust, such as your own test project.
Common Misconceptions About internal
One common misconception is that internal is the same as private at the assembly level. It is not. private is limited to the containing type, while internal spans the entire assembly. Another misconception is that internal members are inaccessible from derived classes in other assemblies. That is true, but protected internal exists for that case.
Another mistake is using internal on a member of a public class and expecting it to be hidden from reflection. Reflection can still access internal members, but that is an advanced scenario. For normal compiled code, the compiler enforces the restriction.
Also, remember that the default accessibility for top-level types is internal. If you forget to add a modifier, your class becomes internal, which may surprise developers expecting public. Always be explicit about accessibility to avoid unintended behavior.
Using internal for Maintainable Assembly Design
The internal keyword is a key tool for designing maintainable assemblies. By keeping implementation details internal, you reduce the public API surface, which makes it easier to change internal behavior without breaking external consumers. This is especially important in libraries and frameworks where backward compatibility is critical.
When you mark a class as internal, you signal that it is not part of the contract. You can refactor, rename, or remove internal types freely without affecting users of the assembly. This reduces the risk of breaking changes and simplifies versioning.
In large codebases, internal types also help enforce architectural boundaries. For example, you can keep data access layers internal to a persistence assembly, ensuring that only the service layer can interact with them. This improves separation of concerns and makes the system easier to test and maintain.
The tradeoff is that internal types cannot be reused by other assemblies unless you explicitly expose them. If you anticipate reuse, consider making the type public or moving it to a shared assembly. The decision should be based on the intended audience and the stability of the API.
In practice, start with internal for any type that is not part of the public contract. When you later discover a genuine need to expose it, you can change the modifier with confidence, knowing that the internal boundary gave you room to evolve the design.