Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
26 changes: 13 additions & 13 deletions src/Orleans.Core/Runtime/RuntimeVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ 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.
Resolved
internal static string GetVersion(Assembly assembly)
{
get
if (assembly is null)
{
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;
throw new ArgumentNullException(nameof(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
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,21 @@ 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 NET5_0_OR_GREATER
if (AssemblyFilesAvailable)
{
AddFromDependencyContext(parts, entryAssembly);
}
Comment thread
ReubenBond marked this conversation as resolved.
Resolved
#else
AddFromDependencyContext(parts, entryAssembly);
#endif

#if NETCOREAPP3_1_OR_GREATER
AddFromAssemblyLoadContext(parts);
Expand Down Expand Up @@ -120,6 +134,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 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 +208,24 @@ public static void AddFromDependencyContext(HashSet<Assembly> parts, Assembly? a
}
}

#if NET5_0_OR_GREATER
#if NET9_0_OR_GREATER
[FeatureGuard(typeof(RequiresAssemblyFilesAttribute))]
#endif
[UnconditionalSuppressMessage(
"SingleFile",
"IL3000",
Justification = "Assembly.Location is used only as the documented availability check for bundled assembly files.")]
internal static bool AssemblyFilesAvailable
{
get
{
var assembly = Assembly.GetEntryAssembly() ?? typeof(ReferencedAssemblyProvider).Assembly;
return !assembly.IsDynamic && !string.IsNullOrEmpty(assembly.Location);
}
}
#endif

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,18 @@
<Protobuf Include="protobuf-model.proto" GrpcServices="None" />
</ItemGroup>

<Target
Name="CopyNetStandardSerializationAsset"
AfterTargets="Build"
Condition="'$(TargetFramework)' == 'net10.0'">
<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" />
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
#if NET10_0_OR_GREATER
using System.IO;
#endif
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
#if NET10_0_OR_GREATER
using System.Reflection.Metadata;
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 GetRelevantAssembliesIncludesGeneratedEntryAssemblyMetadata()
{
var result = ReferencedAssemblyProvider.GetRelevantAssemblies().ToHashSet();

Assert.Contains(typeof(ReferencedAssemblyProviderTests).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);
}

#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)));

Assert.Contains(
"System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute",
attributeNames);
}
#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