diff --git a/src/Orleans.Core/Orleans.Core.csproj b/src/Orleans.Core/Orleans.Core.csproj
index 58d7786ea85..c5d9cdce083 100644
--- a/src/Orleans.Core/Orleans.Core.csproj
+++ b/src/Orleans.Core/Orleans.Core.csproj
@@ -7,6 +7,7 @@
Orleans
true
README.md
+ true
diff --git a/src/Orleans.Core/Runtime/RuntimeVersion.cs b/src/Orleans.Core/Runtime/RuntimeVersion.cs
index d162763309c..5dea03f27c1 100644
--- a/src/Orleans.Core/Runtime/RuntimeVersion.cs
+++ b/src/Orleans.Core/Runtime/RuntimeVersion.cs
@@ -6,23 +6,20 @@ namespace Orleans.Runtime
internal static class RuntimeVersion
{
///
- /// The full version string of the Orleans runtime, eg: '2012.5.9.51607 Build:12345 Timestamp: 20120509-185359'
+ /// The informational version of the Orleans runtime and its build configuration,
+ /// or the assembly version when informational version metadata is unavailable.
///
- public static string Current
+ public static string Current => GetVersion(typeof(RuntimeVersion).Assembly);
+
+ internal static string GetVersion(Assembly assembly)
{
- get
- {
- Assembly thisProg = typeof(RuntimeVersion).Assembly;
- var ApiVersion = thisProg.GetName().Version!.ToString();
- if (string.IsNullOrWhiteSpace(thisProg.Location))
- {
- return ApiVersion;
- }
- FileVersionInfo progVersionInfo = FileVersionInfo.GetVersionInfo(thisProg.Location);
- bool isDebug = IsAssemblyDebugBuild(thisProg);
- string productVersion = progVersionInfo.ProductVersion + (isDebug ? " (Debug)." : " (Release)."); // progVersionInfo.IsDebug; does not work
- return string.IsNullOrEmpty(productVersion) ? ApiVersion : productVersion;
- }
+ ArgumentNullException.ThrowIfNull(assembly);
+
+ var apiVersion = assembly.GetName().Version!.ToString();
+ var productVersion = assembly.GetCustomAttribute()?.InformationalVersion;
+ return string.IsNullOrWhiteSpace(productVersion)
+ ? apiVersion
+ : productVersion + (IsAssemblyDebugBuild(assembly) ? " (Debug)." : " (Release).");
}
///
diff --git a/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs b/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs
index 344d75bd08e..033b9bd58f8 100644
--- a/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs
+++ b/src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs
@@ -2,6 +2,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
@@ -23,8 +24,17 @@ public static class ReferencedAssemblyProvider
public static IEnumerable GetRelevantAssemblies()
{
var parts = new HashSet();
+ var entryAssembly = Assembly.GetEntryAssembly();
- AddFromDependencyContext(parts);
+ if (entryAssembly is not null)
+ {
+ AddAssembly(parts, entryAssembly);
+ }
+
+ if (AssemblyFilesAvailable)
+ {
+ AddFromDependencyContext(parts, entryAssembly);
+ }
#if NETCOREAPP3_1_OR_GREATER
AddFromAssemblyLoadContext(parts);
@@ -60,8 +70,6 @@ public static void AddAssembly(HashSet parts, Assembly assembly)
return;
}
- AddAssembly(parts, assembly);
-
// Add all referenced application parts.
foreach (var referencedAsm in GetApplicationPartAssemblies(assembly))
{
@@ -120,6 +128,7 @@ public static void AddFromAssemblyLoadContext(HashSet parts, Assembly?
///
/// The assembly whose dependency context is inspected, or to use the entry assembly.
///
+ [RequiresAssemblyFiles("Dependency-context discovery reads assembly files. Use " + nameof(GetRelevantAssemblies) + " for single-file-compatible discovery.")]
public static void AddFromDependencyContext(HashSet parts, Assembly? assembly = null)
{
assembly ??= Assembly.GetEntryAssembly();
@@ -193,6 +202,25 @@ public static void AddFromDependencyContext(HashSet parts, Assembly? a
}
}
+#if NET9_0_OR_GREATER
+ [FeatureGuard(typeof(RequiresAssemblyFilesAttribute))]
+#endif
+ internal static bool AssemblyFilesAvailable => AreAssemblyFilesAvailable(Assembly.GetEntryAssembly());
+
+#if NET5_0_OR_GREATER
+ [UnconditionalSuppressMessage(
+ "SingleFile",
+ "IL3000",
+ Justification = "Assembly.Location is used only as the documented availability check for bundled assembly files.")]
+#endif
+ internal static bool AreAssemblyFilesAvailable(Assembly? entryAssembly)
+ {
+ var assembly = entryAssembly is null || entryAssembly.IsDynamic
+ ? typeof(ReferencedAssemblyProvider).Assembly
+ : entryAssembly;
+ return File.Exists(assembly.Location);
+ }
+
private static IEnumerable GetApplicationPartAssemblies(Assembly assembly)
{
if (!assembly.IsDefined(typeof(ApplicationPartAttribute)))
diff --git a/src/Orleans.Serialization/Orleans.Serialization.csproj b/src/Orleans.Serialization/Orleans.Serialization.csproj
index 43abd2aa35b..d59aa1ab2f8 100644
--- a/src/Orleans.Serialization/Orleans.Serialization.csproj
+++ b/src/Orleans.Serialization/Orleans.Serialization.csproj
@@ -8,6 +8,7 @@
true
false
README.md
+ true
diff --git a/src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs b/src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs
new file mode 100644
index 00000000000..e767b1f2b7d
--- /dev/null
+++ b/src/Orleans.Serialization/Properties/RequiresAssemblyFilesAttribute.cs
@@ -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
diff --git a/src/api/Orleans.Serialization/Orleans.Serialization.cs b/src/api/Orleans.Serialization/Orleans.Serialization.cs
index a1165ae43b1..2760519c249 100644
--- a/src/api/Orleans.Serialization/Orleans.Serialization.cs
+++ b/src/api/Orleans.Serialization/Orleans.Serialization.cs
@@ -3451,6 +3451,7 @@ public static void AddFromAssemblyLoadContext(System.Collections.Generic.HashSet
public static void AddFromAssemblyLoadContext(System.Collections.Generic.HashSet parts, System.Runtime.Loader.AssemblyLoadContext context) { }
+ [System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute("Dependency-context discovery reads assembly files. Use GetRelevantAssemblies for single-file-compatible discovery.")]
public static void AddFromDependencyContext(System.Collections.Generic.HashSet parts, System.Reflection.Assembly? assembly = null) { }
public static System.Collections.Generic.IEnumerable GetRelevantAssemblies() { throw null; }
diff --git a/test/Orleans.Core.Tests/RuntimeVersionTests.cs b/test/Orleans.Core.Tests/RuntimeVersionTests.cs
new file mode 100644
index 00000000000..c64634625e2
--- /dev/null
+++ b/test/Orleans.Core.Tests/RuntimeVersionTests.cs
@@ -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;
+ }
+}
diff --git a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj
index a210f5a211b..16942d7f27c 100644
--- a/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj
+++ b/test/Orleans.Serialization.UnitTests/Orleans.Serialization.UnitTests.csproj
@@ -40,4 +40,19 @@
+
+
+
+
+
diff --git a/test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs b/test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs
new file mode 100644
index 00000000000..e71cd1beda7
--- /dev/null
+++ b/test/Orleans.Serialization.UnitTests/ReferencedAssemblyProviderTests.cs
@@ -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();
+
+ 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();
+
+ 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
+}