Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Orleans.Core/Orleans.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<RootNamespace>Orleans</RootNamespace>
<OrleansBuildTimeCodeGen>true</OrleansBuildTimeCodeGen>
<PackageReadmeFile>README.md</PackageReadmeFile>
<EnableSingleFileAnalyzer Condition="'$(TargetFramework)' == 'net10.0'">true</EnableSingleFileAnalyzer>
</PropertyGroup>

<ItemGroup>
Expand Down
27 changes: 12 additions & 15 deletions src/Orleans.Core/Runtime/RuntimeVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,20 @@ namespace Orleans.Runtime
internal static class RuntimeVersion
{
/// <summary>
/// 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.
/// </summary>
public static string Current
public static string Current => GetVersion(typeof(RuntimeVersion).Assembly);

Comment thread
ReubenBond marked this conversation as resolved.
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<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
return string.IsNullOrWhiteSpace(productVersion)
? apiVersion
: productVersion + (IsAssemblyDebugBuild(assembly) ? " (Debug)." : " (Release).");
}

/// <summary>
Expand Down
34 changes: 31 additions & 3 deletions src/Orleans.Serialization/Hosting/ReferencedAssemblyProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;

Expand All @@ -23,8 +24,17 @@ public static class ReferencedAssemblyProvider
public static IEnumerable<Assembly> GetRelevantAssemblies()
{
var parts = new HashSet<Assembly>();
var entryAssembly = Assembly.GetEntryAssembly();

AddFromDependencyContext(parts);
if (entryAssembly is not null)
{
AddAssembly(parts, entryAssembly);
}

if (AssemblyFilesAvailable)
{
AddFromDependencyContext(parts, entryAssembly);
}
Comment thread
ReubenBond marked this conversation as resolved.

#if NETCOREAPP3_1_OR_GREATER
AddFromAssemblyLoadContext(parts);
Expand Down Expand Up @@ -60,8 +70,6 @@ public static void AddAssembly(HashSet<Assembly> parts, Assembly assembly)
return;
}

AddAssembly(parts, assembly);

// Add all referenced application parts.
foreach (var referencedAsm in GetApplicationPartAssemblies(assembly))
{
Expand Down Expand Up @@ -120,6 +128,7 @@ public static void AddFromAssemblyLoadContext(HashSet<Assembly> parts, Assembly?
/// <param name="assembly">
/// The assembly whose dependency context is inspected, or <see langword="null"/> to use the entry assembly.
/// </param>
[RequiresAssemblyFiles("Dependency-context discovery reads assembly files. Use " + nameof(GetRelevantAssemblies) + " for single-file-compatible discovery.")]
public static void AddFromDependencyContext(HashSet<Assembly> parts, Assembly? assembly = null)
{
assembly ??= Assembly.GetEntryAssembly();
Expand Down Expand Up @@ -193,6 +202,25 @@ public static void AddFromDependencyContext(HashSet<Assembly> 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<Assembly> GetApplicationPartAssemblies(Assembly assembly)
{
if (!assembly.IsDefined(typeof(ApplicationPartAttribute)))
Expand Down
1 change: 1 addition & 0 deletions src/Orleans.Serialization/Orleans.Serialization.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<OrleansBuildTimeCodeGen>true</OrleansBuildTimeCodeGen>
<IsOrleansFrameworkPart>false</IsOrleansFrameworkPart>
<PackageReadmeFile>README.md</PackageReadmeFile>
<EnableSingleFileAnalyzer Condition="'$(TargetFramework)' == 'net10.0'">true</EnableSingleFileAnalyzer>
</PropertyGroup>

<ItemGroup>
Expand Down
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
1 change: 1 addition & 0 deletions src/api/Orleans.Serialization/Orleans.Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3451,6 +3451,7 @@ public static void AddFromAssemblyLoadContext(System.Collections.Generic.HashSet

public static void AddFromAssemblyLoadContext(System.Collections.Generic.HashSet<System.Reflection.Assembly> 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<System.Reflection.Assembly> parts, System.Reflection.Assembly? assembly = null) { }

public static System.Collections.Generic.IEnumerable<System.Reflection.Assembly> GetRelevantAssemblies() { throw null; }
Expand Down
60 changes: 60 additions & 0 deletions test/Orleans.Core.Tests/RuntimeVersionTests.cs
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,19 @@
<Protobuf Include="protobuf-model.proto" GrpcServices="None" />
</ItemGroup>

<Target
Name="CopyNetStandardSerializationAsset"
AfterTargets="Build"
Condition="'$(TargetFramework)' == 'net10.0' and '$(DesignTimeBuild)' != 'true'">
<MSBuild
Projects="$(SourceRoot)src\Orleans.Serialization\Orleans.Serialization.csproj"
Targets="Build"
RemoveProperties="RuntimeIdentifier;SelfContained;TargetFramework"
Properties="Configuration=$(Configuration);TargetFramework=netstandard2.1" />
<Copy
SourceFiles="$(SourceRoot)src\Orleans.Serialization\bin\$(Configuration)\netstandard2.1\Orleans.Serialization.dll"
DestinationFolder="$(TargetDir)TestAssets"
SkipUnchangedFiles="true" />
</Target>

</Project>
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
}
Loading