Ensure that internal code is never exposed through InternalsVisibleTo - #170
Merged
Merged
Conversation
Reflectify is a source-only package, so its types are compiled directly into the consuming assembly. Until now those types were plain internal types, which means any assembly listed in the consumer's InternalsVisibleTo could see and use them. That makes Reflectify part of the consumer's internal API surface without the consumer asking for it. Mark every type with [Microsoft.CodeAnalysis.Embedded] when compiled into a consumer. Roslyn hides embedded types from all other assemblies, including friend assemblies. Inside Reflectify's own build the types stay public instead, which lets the spec project reference them normally and allowed the InternalsVisibleTo entry to be dropped. This ports dennisdoomen/pathy#54. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pathy is also a source-only package and declares the same Microsoft.CodeAnalysis.EmbeddedAttribute. A consumer using both compiles two copies into one assembly, which is a duplicate type error. Declaring the attribute as partial makes the two declarations merge into a single type. This needs no cooperation from the consumer, so the REFLECTIFY_EXCLUDE_EMBEDDED_ATTRIBUTE opt-out is no longer needed. Partial types cannot repeat a type-level attribute across parts, so [AttributeUsage] and [ExcludeFromCodeCoverage] had to go. Dropping [AttributeUsage] changes nothing, because AttributeTargets.All is already the default. Three analyzers ask for it back, so suppress them here. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
dennisdoomen
added a commit
to dennisdoomen/pathy
that referenced
this pull request
Aug 10, 2026
…s copy (#153) Pathy ships as source and compiles into the consuming assembly. It declares Microsoft.CodeAnalysis.EmbeddedAttribute so its own types can be marked [Embedded], which tells Roslyn to hide them from every other assembly, including InternalsVisibleTo friends. Reflectify is another source-only package by the same author and declares the same attribute for the same reason. A project that uses both packages therefore ended up with two declarations of one type in a single compilation, which failed to build with CS0101. Declaring the type as partial fixes this. Two partial declarations of the same type merge into one type instead of colliding, and Roslyn matches [Embedded] by name, so it does not care how many parts the type came from. Reflectify made the same change in dennisdoomen/reflectify#170. For the parts to merge they must have an identical shape, so the type-level attributes had to go. C# does not allow a non-AllowMultiple attribute to be repeated across parts of a partial type, so leaving [AttributeUsage] in place would fail with CS0579 and defeat the purpose. Dropping it costs nothing because AttributeTargets.All is already the default when an attribute carries no [AttributeUsage]. [ExcludeFromCodeCoverage] was removed for the same reason. Verified with a scratch consumer project that compiles Pathy's sources next to Reflectify's declaration: the library builds, a friend assembly can still reach the library's own internal types, and Pathy types remain hidden from it. Co-authored-by: Dennis Doomen <dennis.doomen@greenchoice.nl> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports dennisdoomen/pathy#54 to Reflectify.
The problem
Reflectify is a source-only package, so its types are compiled straight into the consuming assembly as plain
internaltypes. Anything the consumer lists inInternalsVisibleTocan therefore see and useReflectify.MemberKind,Reflectify.Reflector, and all the extension classes. Reflectify silently becomes part of the consumer's internal API surface, and consumers can start depending on it across assembly boundaries without ever asking for it.I proved this against
mainwith a throwaway two-project solution: aConsumerLibthat compiles Reflectify's sources and grantsInternalsVisibleTo("Friend"), plus aFriendproject that references it. Onmain,Friendcompiles fine while referencingReflectify.MemberKind.The fix
Every top-level type now gets a conditional declaration:
Roslyn hides types marked
[Microsoft.CodeAnalysis.Embedded]from every other assembly, friend assemblies included.EmbeddedAttribute.csdeclares the attribute itself and is picked up by the package automatically, since the.nuspecglobs*.cs.REFLECTIFY_COMPILEis only defined inReflectify.csproj, so inside our own build the types arepublicinstead. That letsReflectify.Specskeep its plainProjectReferenceand let me remove theInternalsVisibleToentry entirely — which is the whole point of the change. Making the types public is safe because the Reflectify assembly is never shipped; the.nuspecpublishes onlycontentFilesandReflectify.props, nolib/.Coexisting with Pathy
Pathy is a source-only package too, and it declares the same
Microsoft.CodeAnalysis.EmbeddedAttribute. A consumer using both — FluentAssertions already consumes Reflectify, so this is realistic — compiles two copies into one assembly and getsCS0101plus twoCS0579.The attribute is declared
partial, so the two declarations merge into a single type rather than colliding. Roslyn matches the attribute by name and doesn't care how many parts it came from. Nothing is required of the consumer, and Pathy only has to addpartialto its own copy for the pair to work together.The one constraint is that a type-level attribute cannot be repeated across parts, so
[AttributeUsage]and[ExcludeFromCodeCoverage]had to go. Dropping[AttributeUsage]is free:AttributeTargets.Allis already the default. Three analyzers (RCS1203, MA0010, CA1018) ask for it back, so they're suppressed on that declaration with the reason inline.Verification
Built a harness that compiles Reflectify's sources the way a consumer does, alongside a second file mimicking Pathy's attribute, plus a friend assembly:
mainReflectify.MemberKinddotnet testpasses: 187 on net6.0, 187 on net8.0, 182 on net472. (The netcoreapp3.0 run aborts on my machine because that runtime isn't installed — unrelated to this change.)Two other things worth knowing
[DebuggerNonUserCode]is not valid on an enum.MemberKindgets[Embedded]only. This one is easy to miss: the error (CS0592) never shows up in Reflectify's own build, where the enum is public and neither attribute is applied. It only appears when you compile the sources the way a consumer does.CA1852. Sealing the nested
OrderedPropertyCollectioninsideReflector. The analyzer only fires onceReflectoritself becomes externally visible.<NoWarn>1591;1573</NoWarn>inReflectify.csproj, since the now-public types would otherwise want XML doc comments. Same as Pathy does.