File: FileUtilities.MetadataReader.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.NET.Build.Tasks\Microsoft.NET.Build.Tasks.csproj (Microsoft.NET.Build.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
//  Use MetadataReader version of GetAssemblyVersion for:
//  - netcoreapp version of Microsoft.NET.Build.Extensions.Tasks
//  - All versions of Microsoft.NET.Build.Tasks
 
//  We don't use it for the .NET Framework version of Microsoft.NET.Build.Extensions in order to
//  avoid loading the System.Reflection.Metadata assembly in vanilla .NET Framework build scenarios
 
//  We do use the MetadataReader version for the SDK tasks in order to correctly read the assembly
//  versions of cross-gened assemblies.  See https://github.com/dotnet/sdk/issues/1502
#if NETCOREAPP || !EXTENSIONS
 
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
 
namespace Microsoft.NET.Build.Tasks
{
    static partial class FileUtilities
    {
        private static Dictionary<string, (DateTime LastKnownWriteTimeUtc, Version? Version)> s_versionCache = new(StringComparer.OrdinalIgnoreCase /* Not strictly correct on *nix. Fix? */);
 
        private static Version? GetAssemblyVersion(string sourcePath)
        {
            DateTime lastWriteTimeUtc = File.GetLastWriteTimeUtc(sourcePath);
 
            if (s_versionCache.TryGetValue(sourcePath, out var cacheEntry)
                && lastWriteTimeUtc == cacheEntry.LastKnownWriteTimeUtc)
            {
                return cacheEntry.Version;
            }
 
            Version? version = GetAssemblyVersionFromFile(sourcePath);
 
            s_versionCache[sourcePath] = (lastWriteTimeUtc, version);
 
            // When introducing this cache, we decided that the cached
            // data was small and likely to be basically finite for
            // any given MSBuild process lifetime, so we did not implement
            // a cache lifetime. If you're here because those assumptions
            // don't hold, there's no reason the cache must be static
            // and monotonically growing.
 
            return version;
 
            static Version? GetAssemblyVersionFromFile(string sourcePath)
            {
                using (var assemblyStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read))
                {
                    Version? result = null;
                    try
                    {
                        using (PEReader peReader = new(assemblyStream, PEStreamOptions.LeaveOpen))
                        {
                            if (peReader.HasMetadata)
                            {
                                MetadataReader reader = peReader.GetMetadataReader();
                                if (reader.IsAssembly)
                                {
                                    result = reader.GetAssemblyDefinition().Version;
                                }
                            }
                        }
                    }
                    catch (BadImageFormatException)
                    {
                        // not a PE
                    }
 
                    return result;
                }
            }
        }
    }
}
 
#endif