Can public members inside an internal nested module be accessed outside the assembly in F#
0 reputation · 31 Jan 2020, 05:57 UTC
0 reputation · 31 Jan 2020, 05:57 UTC
Designing an F# library with the goal of avoiding accidental public access requires clear understanding of how access modifiers compose for modules and nested types. Top-level modules and types are public by default, so internal is used to restrict them to the assembly. Nested modules and types default to private, visible only within the enclosing module or type unless explicitly marked internal or public.
A documented nuance is that a nested module marked internal is invisible to external assemblies, while its members follow their own access modifiers. The internal keyword applied to a module does not change the visibility of its nested types; each nested type still follows its own modifier, which can lead to unexpected public exposure if not carefully annotated. The interaction between a private nested module that contains public members and runtime visibility is also unclear from official documentation, particularly regarding reflection-based access.
Which visibility rules apply to public members declared inside an internal nested module when the module itself is not visible outside the assembly? Does the module's internal modifier restrict access to those members for external consumers, or do the members remain reachable through metadata? How should library authors annotate nested modules and their members to guarantee no accidental public surface area?
No. Public members inside an internal nested module cannot be accessed outside the assembly. In F#, the visibility of a member is constrained by the visibility of its containing parent.
When a module is marked as internal, it is mapped to the .NET internal access modifier. This creates a hard boundary at the assembly level. Even if a function or type inside that module is explicitly marked public, the external consumer cannot "reach" the member because the path to it—the module itself—is invisible to any code outside the current assembly.
public in the compiled IL (Intermediate Language) metadata, the containing module's internal status prevents the .NET runtime from resolving the reference via standard strongly-typed calls.System.Reflection), as reflection bypasses standard language access modifiers.To ensure a nested structure does not accidentally expose functionality to external consumers, follow these annotation patterns:
internal. This is the most effective way to hide all contained members from external assemblies.internal. This prevents the members from becoming public even if the containing module is later changed to public.private for nested modules that should only be visible within the enclosing module or type.To verify this behavior in your environment (assuming F# 6.0+ and .NET 6+), you can use the following scoped test:
// Project A (Library)
module Library.InternalNamespace =
module internal NestedModule =
let public SecretValue = "Hidden"
In Project B (referencing Project A), attempting to call Library.InternalNamespace.NestedModule.SecretValue will result in a compiler error stating that the module NestedModule is not accessible.
Diagnostic Detail: Are you attempting to access these members via a specific interop layer (such as C# or a dynamic language), or strictly through F# project references?
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 31 Jan 2020, 16:47 UTC
One practical follow-up: an internal nested module is not a hard boundary against friend assemblies. If you declare InternalsVisibleTo, those friend assemblies can reference the internal module and its public members directly, so your effective surface area depends on that configuration.
To audit the result, compare Assembly.GetExportedTypes() with Assembly.GetTypes() in a scratch tool. The internal nested module should be missing from exported types but present in the full list. That gap is where metadata-only exposure lives, and it is the fastest way to confirm that external consumers cannot compile against paths you intended to hide.