File: MetadataCache.cs
Web Access
Project: ..\..\..\src\RazorSdk\Tool\Microsoft.NET.Sdk.Razor.Tool.csproj (rzc)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Diagnostics;
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis;
 
namespace Microsoft.NET.Sdk.Razor.Tool
{
    internal class MetadataCache
    {
        // Store 1000 entries -- arbitrary number
        private const int CacheSize = 1000;
        private readonly ConcurrentLruCache<string, MetadataCacheEntry> _metadataCache =
            new(CacheSize, StringComparer.OrdinalIgnoreCase);
 
        // For testing purposes only.
        internal ConcurrentLruCache<string, MetadataCacheEntry> Cache => _metadataCache;
 
        internal Metadata GetMetadata(string fullPath)
        {
            var timestamp = GetFileTimeStamp(fullPath);
 
            // Check if we have an entry in the dictionary.
            if (_metadataCache.TryGetValue(fullPath, out var entry))
            {
                if (timestamp.HasValue && timestamp.Value == entry.Timestamp)
                {
                    // The file has not changed since we cached it. Return the cached entry.
                    return entry.Metadata;
                }
                else
                {
                    // The file has changed recently. Remove the cache entry.
                    _metadataCache.Remove(fullPath);
                }
            }
 
            Metadata metadata;
            using (var fileStream = File.OpenRead(fullPath))
            {
                metadata = AssemblyMetadata.CreateFromStream(fileStream, PEStreamOptions.PrefetchMetadata);
            }
 
            _metadataCache.GetOrAdd(fullPath, new MetadataCacheEntry(timestamp.Value, metadata));
 
            return metadata;
        }
 
        private static DateTime? GetFileTimeStamp(string fullPath)
        {
            try
            {
                Debug.Assert(Path.IsPathRooted(fullPath));
 
                return File.GetLastWriteTimeUtc(fullPath);
            }
            catch (Exception e)
            {
                // There are several exceptions that can occur here: NotSupportedException or PathTooLongException
                // for a bad path, UnauthorizedAccessException for access denied, etc. Rather than listing them all,
                // just catch all exceptions and log.
                ServerLogger.LogException(e, $"Error getting timestamp of file {fullPath}.");
 
                return null;
            }
        }
 
        internal struct MetadataCacheEntry
        {
            public MetadataCacheEntry(DateTime timestamp, Metadata metadata)
            {
                Debug.Assert(timestamp.Kind == DateTimeKind.Utc);
 
                Timestamp = timestamp;
                Metadata = metadata;
            }
 
            public DateTime Timestamp { get; }
 
            public Metadata Metadata { get; }
        }
    }
}