-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: support single-file metadata discovery #11054
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ReubenBond
merged 14 commits into
dotnet:main
from
ReubenBond:rb-fix-support-single-file-metadata
Sep 4, 2026
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
e90208c
fix: support single-file metadata discovery
ReubenBond 5556532
fix: annotate netstandard dependency discovery
ReubenBond edcc79b
docs: update runtime version description
ReubenBond eafe43c
fix: preserve dynamic entry discovery
ReubenBond 10e7b05
refactor: keep discovery guidance rename-safe
ReubenBond a793244
test: optimize netstandard metadata fixture
ReubenBond 66aeb36
test: assert entry assembly discovery
ReubenBond 436216f
refactor: use standard null guard
ReubenBond c963106
fix: gate netstandard dependency discovery
ReubenBond d420da8
test: avoid entry assembly assumption
ReubenBond 6c35c0b
test: encode metadata tokens little-endian
ReubenBond bd21dab
fix: verify assembly files exist
ReubenBond 36e0d82
test: isolate memory-loaded assembly probe
ReubenBond b4d6f58
refactor: remove redundant assembly recursion
ReubenBond File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
23 changes: 23 additions & 0 deletions
23
src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| #if NETSTANDARD2_1 | ||
| namespace System.Diagnostics.CodeAnalysis; | ||
|
|
||
| [AttributeUsage( | ||
| AttributeTargets.Constructor | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property, | ||
| Inherited = false, | ||
| AllowMultiple = false)] | ||
| internal sealed class RequiresAssemblyFilesAttribute : Attribute | ||
| { | ||
| public RequiresAssemblyFilesAttribute() | ||
| { | ||
| } | ||
|
|
||
| public RequiresAssemblyFilesAttribute(string message) | ||
| { | ||
| Message = message; | ||
| } | ||
|
|
||
| public string? Message { get; } | ||
|
|
||
| public string? Url { get; set; } | ||
| } | ||
| #endif |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| using System.Diagnostics; | ||
| using System.Reflection; | ||
| using System.Reflection.Emit; | ||
| using Orleans.Runtime; | ||
| using Xunit; | ||
|
|
||
| namespace NonSilo.Tests; | ||
|
|
||
| [TestCategory("BVT")] | ||
| [TestSuite("BVT")] | ||
| [TestProvider("None")] | ||
| [TestArea("Runtime")] | ||
| public sealed class RuntimeVersionTests | ||
| { | ||
| [Fact] | ||
| public void GetVersionUsesInformationalVersionMetadataForFilelessAssembly() | ||
| { | ||
| var assembly = CreateAssembly( | ||
| new Version(1, 2, 3, 4), | ||
| informationalVersion: "9.8.7+metadata", | ||
| isDebug: true); | ||
|
|
||
| var result = RuntimeVersion.GetVersion(assembly); | ||
|
|
||
| Assert.Equal("9.8.7+metadata (Debug).", result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetVersionUsesAssemblyVersionWhenInformationalVersionIsUnavailable() | ||
| { | ||
| var assembly = CreateAssembly(new Version(1, 2, 3, 4)); | ||
|
|
||
| var result = RuntimeVersion.GetVersion(assembly); | ||
|
|
||
| Assert.Equal("1.2.3.4", result); | ||
| } | ||
|
|
||
| private static Assembly CreateAssembly( | ||
| Version version, | ||
| string? informationalVersion = null, | ||
| bool isDebug = false) | ||
| { | ||
| var name = new AssemblyName($"RuntimeVersionTests_{Guid.NewGuid():N}") { Version = version }; | ||
| var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); | ||
| if (informationalVersion is not null) | ||
| { | ||
| var constructor = typeof(AssemblyInformationalVersionAttribute).GetConstructor([typeof(string)])!; | ||
| assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [informationalVersion])); | ||
| } | ||
|
|
||
| if (isDebug) | ||
| { | ||
| var constructor = typeof(DebuggableAttribute).GetConstructor([typeof(DebuggableAttribute.DebuggingModes)])!; | ||
| var modes = DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations; | ||
| assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [modes])); | ||
| } | ||
|
|
||
| return assembly; | ||
| } | ||
| } |
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
158 changes: 158 additions & 0 deletions
158
test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| using System; | ||
| #if NET10_0_OR_GREATER | ||
| using System.Buffers.Binary; | ||
| #endif | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Reflection.Emit; | ||
| #if NET10_0_OR_GREATER | ||
| using System.Reflection.Metadata; | ||
| using System.Reflection.Metadata.Ecma335; | ||
| using System.Reflection.PortableExecutable; | ||
| #endif | ||
| using Orleans.Serialization.Internal; | ||
|
|
||
| namespace Orleans.Serialization.UnitTests; | ||
|
|
||
| [Trait("Category", "BVT")] | ||
| [TestSuite("BVT")] | ||
| [TestProvider("None")] | ||
| [TestArea("Serialization")] | ||
| public sealed class ReferencedAssemblyProviderTests | ||
| { | ||
| [Fact] | ||
| public void AddAssemblyDiscoversGeneratedApplicationPartsWithoutAssemblyFiles() | ||
| { | ||
| var referencedAssembly = typeof(ReferencedAssemblyProviderTests).Assembly; | ||
| var assembly = CreateApplicationPartAssembly(referencedAssembly.GetName().Name!); | ||
| var result = new HashSet<Assembly>(); | ||
|
|
||
| ReferencedAssemblyProvider.AddAssembly(result, assembly); | ||
|
|
||
| Assert.True(assembly.IsDynamic); | ||
| Assert.Contains(assembly, result); | ||
| Assert.Contains(referencedAssembly, result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetRelevantAssembliesIncludesLoadedGeneratedAssembly() | ||
| { | ||
| var assembly = typeof(ReferencedAssemblyProviderTests).Assembly; | ||
| var result = ReferencedAssemblyProvider.GetRelevantAssemblies().ToHashSet(); | ||
|
|
||
| Assert.True(assembly.IsDefined(typeof(ApplicationPartAttribute))); | ||
| Assert.Contains(assembly, result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void DependencyContextDiscoveryExposesAssemblyFileRequirement() | ||
| { | ||
| var method = typeof(ReferencedAssemblyProvider).GetMethod( | ||
| nameof(ReferencedAssemblyProvider.AddFromDependencyContext)); | ||
|
|
||
| var attribute = method!.GetCustomAttribute<RequiresAssemblyFilesAttribute>(); | ||
|
|
||
| Assert.NotNull(attribute); | ||
| Assert.Contains(nameof(ReferencedAssemblyProvider.GetRelevantAssemblies), attribute!.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AssemblyFileAvailabilityUsesProviderAssemblyForDynamicEntryAssembly() | ||
| { | ||
| var dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly( | ||
| new AssemblyName($"DynamicEntryAssembly_{Guid.NewGuid():N}"), | ||
| AssemblyBuilderAccess.Run); | ||
|
|
||
| Assert.True(ReferencedAssemblyProvider.AreAssemblyFilesAvailable(null)); | ||
| Assert.Equal( | ||
| ReferencedAssemblyProvider.AreAssemblyFilesAvailable(null), | ||
| ReferencedAssemblyProvider.AreAssemblyFilesAvailable(dynamicAssembly)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void AssemblyFileAvailabilityIsFalseForMemoryLoadedAssembly() | ||
| { | ||
| var assembly = Assembly.Load(File.ReadAllBytes(typeof(Enumerable).Assembly.Location)); | ||
|
|
||
| Assert.Empty(assembly.Location); | ||
| Assert.False(ReferencedAssemblyProvider.AreAssemblyFilesAvailable(assembly)); | ||
| } | ||
|
|
||
| #if NET10_0_OR_GREATER | ||
| [Fact] | ||
| public void NetStandardAssetDependencyContextDiscoveryExposesAssemblyFileRequirement() | ||
| { | ||
| var assemblyPath = Path.Combine( | ||
| AppContext.BaseDirectory, | ||
| "TestAssets", | ||
| "Orleans.Serialization.dll"); | ||
| using var stream = File.OpenRead(assemblyPath); | ||
| using var peReader = new PEReader(stream); | ||
| var metadata = peReader.GetMetadataReader(); | ||
| var providerType = metadata.TypeDefinitions | ||
| .Select(metadata.GetTypeDefinition) | ||
| .Single(type => | ||
| metadata.GetString(type.Namespace) == "Orleans.Serialization.Internal" | ||
| && metadata.GetString(type.Name) == nameof(ReferencedAssemblyProvider)); | ||
| var method = providerType.GetMethods() | ||
| .Select(metadata.GetMethodDefinition) | ||
| .Single(method => metadata.GetString(method.Name) == nameof(ReferencedAssemblyProvider.AddFromDependencyContext)); | ||
| var attributeNames = method.GetCustomAttributes() | ||
| .Select(handle => GetAttributeTypeName(metadata, metadata.GetCustomAttribute(handle))); | ||
| var assemblyFilesAvailableGetter = providerType.GetMethods() | ||
| .Single(handle => metadata.GetString(metadata.GetMethodDefinition(handle).Name) == "get_AssemblyFilesAvailable"); | ||
| var getRelevantAssemblies = providerType.GetMethods() | ||
| .Select(handle => (Handle: handle, Definition: metadata.GetMethodDefinition(handle))) | ||
| .Single(method => metadata.GetString(method.Definition.Name) == nameof(ReferencedAssemblyProvider.GetRelevantAssemblies)); | ||
| var methodBody = peReader.GetMethodBody(getRelevantAssemblies.Definition.RelativeVirtualAddress); | ||
| var expectedCall = new byte[5]; | ||
| expectedCall[0] = 0x28; | ||
| BinaryPrimitives.WriteInt32LittleEndian( | ||
| expectedCall.AsSpan(1), | ||
| MetadataTokens.GetToken(assemblyFilesAvailableGetter)); | ||
|
|
||
| Assert.Contains( | ||
| "System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute", | ||
| attributeNames); | ||
| Assert.True(methodBody.GetILBytes().AsSpan().IndexOf(expectedCall) >= 0); | ||
| } | ||
| #endif | ||
|
|
||
| private static Assembly CreateApplicationPartAssembly(string referencedAssemblyName) | ||
| { | ||
| var name = new AssemblyName($"ReferencedAssemblyProviderTests_{Guid.NewGuid():N}"); | ||
| var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); | ||
| var constructor = typeof(ApplicationPartAttribute).GetConstructor([typeof(string)])!; | ||
| assembly.SetCustomAttribute(new CustomAttributeBuilder(constructor, [referencedAssemblyName])); | ||
| assembly.DefineDynamicModule("Main"); | ||
| return assembly; | ||
| } | ||
|
|
||
| #if NET10_0_OR_GREATER | ||
| private static string GetAttributeTypeName(MetadataReader metadata, CustomAttribute attribute) | ||
| { | ||
| var typeHandle = attribute.Constructor.Kind switch | ||
| { | ||
| HandleKind.MemberReference => metadata.GetMemberReference((MemberReferenceHandle)attribute.Constructor).Parent, | ||
| HandleKind.MethodDefinition => metadata.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType(), | ||
| _ => throw new InvalidOperationException($"Unsupported attribute constructor handle: {attribute.Constructor.Kind}."), | ||
| }; | ||
|
|
||
| return typeHandle.Kind switch | ||
| { | ||
| HandleKind.TypeDefinition => GetTypeName(metadata, metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle)), | ||
| HandleKind.TypeReference => GetTypeName(metadata, metadata.GetTypeReference((TypeReferenceHandle)typeHandle)), | ||
| _ => throw new InvalidOperationException($"Unsupported attribute type handle: {typeHandle.Kind}."), | ||
| }; | ||
| } | ||
|
|
||
| private static string GetTypeName(MetadataReader metadata, TypeDefinition type) | ||
| => $"{metadata.GetString(type.Namespace)}.{metadata.GetString(type.Name)}"; | ||
|
|
||
| private static string GetTypeName(MetadataReader metadata, TypeReference type) | ||
| => $"{metadata.GetString(type.Namespace)}.{metadata.GetString(type.Name)}"; | ||
| #endif | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.