diff --git a/src/Common.Tests/NodeBuildResultTests.cs b/src/Common.Tests/NodeBuildResultTests.cs index 7a42eab..921b0d8 100644 --- a/src/Common.Tests/NodeBuildResultTests.cs +++ b/src/Common.Tests/NodeBuildResultTests.cs @@ -52,8 +52,8 @@ public void SortWorksConsistentlyAcrossJson() null ); - string serialized = JsonSerializer.Serialize(nodeBuildResult); - NodeBuildResult deserialized = JsonSerializer.Deserialize(serialized)!; + string serialized = JsonSerializer.Serialize(nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult); + NodeBuildResult deserialized = JsonSerializer.Deserialize(serialized, SourceGenerationContext.Default.NodeBuildResult)!; CollectionAssert.AreEqual(expected.Keys, deserialized.Outputs.Keys, "\n" + "Permutation: " + string.Join(", ", permutation) + "\n" + diff --git a/src/Common/Caching/CacheClient.cs b/src/Common/Caching/CacheClient.cs index ad3324f..24ccf09 100644 --- a/src/Common/Caching/CacheClient.cs +++ b/src/Common/Caching/CacheClient.cs @@ -5,6 +5,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using BuildXL.Cache.ContentStore.Hashing; @@ -21,7 +23,6 @@ using Microsoft.CopyOnWrite; using Microsoft.MSBuildCache.Fingerprinting; using Microsoft.MSBuildCache.Hashing; -using JsonSerializer = System.Text.Json.JsonSerializer; using Fingerprint = Microsoft.MSBuildCache.Fingerprinting.Fingerprint; using WeakFingerprint = BuildXL.Cache.MemoizationStore.Interfaces.Sessions.Fingerprint; @@ -276,7 +277,7 @@ public async Task AddNodeInternalAsync( Context context = new(RootContext); // compute the metadata content. - byte[] nodeBuildResultBytes = await SerializeAsync(nodeBuildResult, cancellationToken); + byte[] nodeBuildResultBytes = await SerializeAsync(nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult, cancellationToken); ContentHash nodeBuildResultHash = _hasher.GetContentHash(nodeBuildResultBytes)!; Tracer.Debug(context, $"Computed node metadata {nodeBuildResultHash.ToShortString()} to the cache for {nodeContext.Id}"); @@ -286,7 +287,7 @@ public async Task AddNodeInternalAsync( if (pathSet != null) { // Add the PathSet to the ContentStore - byte[] pathSetByteArray = await SerializeAsync(pathSet, cancellationToken); + byte[] pathSetByteArray = await SerializeAsync(pathSet, SourceGenerationContext.Default.PathSet, cancellationToken); ContentHash pathSetBytesHash = _hasher.GetContentHash(pathSetByteArray)!; Tracer.Debug(context, $"Computed PathSet {pathSetBytesHash.ToShortString()} to the cache for {nodeContext.Id}"); @@ -402,7 +403,7 @@ await AddNodeAsync( } // The first file is special: it is a serialized NodeBuildResult file. - NodeBuildResult? nodeBuildResult = await DeserializeAsync(context, nodeBuildResultStream, cancellationToken); + NodeBuildResult? nodeBuildResult = await DeserializeAsync(context, nodeBuildResultStream, SourceGenerationContext.Default.NodeBuildResult, cancellationToken); if (nodeBuildResult is null) { Tracer.Debug(context, $"Failed to deserialize NodeBuildResult for {cacheStrongFingerprint}"); @@ -505,7 +506,7 @@ async Task PlaceFilesAsync(CancellationToken ct) ContentHash pathSetHash = selector.ContentHash; byte[]? selectorStrongFingerprint = selector.Output; - PathSet? pathSet = await FetchAndDeserializeFromCacheAsync(context, pathSetHash, cancellationToken); + PathSet? pathSet = await FetchAndDeserializeFromCacheAsync(context, pathSetHash, SourceGenerationContext.Default.PathSet, cancellationToken); if (pathSet is null) { @@ -526,20 +527,20 @@ async Task PlaceFilesAsync(CancellationToken ct) return (null, null); } - private static async Task SerializeAsync(T data, CancellationToken cancellationToken) + private static async Task SerializeAsync(T data, JsonTypeInfo typeInfo, CancellationToken cancellationToken) where T : class { using (var memoryStream = new MemoryStream()) { - await JsonSerializer.SerializeAsync(memoryStream, data, SerializationHelper.SerializerOptions, cancellationToken); + await JsonSerializer.SerializeAsync(memoryStream, data, typeInfo, cancellationToken); return memoryStream.ToArray(); } } - private async Task DeserializeAsync(Context context, Stream stream, CancellationToken cancellationToken) + private async Task DeserializeAsync(Context context, Stream stream, JsonTypeInfo typeInfo, CancellationToken cancellationToken) where T : class { - T? data = await stream.DeserializeAsync(SerializationHelper.SerializerOptions, cancellationToken); + T? data = await stream.DeserializeAsync(typeInfo, cancellationToken); if (data is null) { Tracer.Debug(context, $"Content deserialized as null"); @@ -548,7 +549,7 @@ private static async Task SerializeAsync(T data, CancellationToken ca return data; } - protected async Task FetchAndDeserializeFromCacheAsync(Context context, ContentHash contentHash, CancellationToken cancellationToken) + protected async Task FetchAndDeserializeFromCacheAsync(Context context, ContentHash contentHash, JsonTypeInfo typeInfo, CancellationToken cancellationToken) where T : class { context = new(context); @@ -562,7 +563,7 @@ private static async Task SerializeAsync(T data, CancellationToken ca using (streamResult.Stream) { - return await DeserializeAsync(context, streamResult.Stream!, cancellationToken); + return await DeserializeAsync(context, streamResult.Stream!, typeInfo, cancellationToken); } } diff --git a/src/Common/MSBuildCachePluginBase.cs b/src/Common/MSBuildCachePluginBase.cs index 9332e7d..17adb49 100644 --- a/src/Common/MSBuildCachePluginBase.cs +++ b/src/Common/MSBuildCachePluginBase.cs @@ -882,7 +882,7 @@ private static async Task DumpBuildResultLogAsync( try { using FileStream fileStream = File.Create(filePath); - await JsonSerializer.SerializeAsync(fileStream, nodeBuildResult, SerializationHelper.SerializerOptions); + await JsonSerializer.SerializeAsync(fileStream, nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult); } catch (Exception ex) { diff --git a/src/Common/NodeBuildResult.cs b/src/Common/NodeBuildResult.cs index c0ea803..8895544 100644 --- a/src/Common/NodeBuildResult.cs +++ b/src/Common/NodeBuildResult.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text.Json; using System.Text.Json.Serialization; using BuildXL.Cache.ContentStore.Hashing; using Microsoft.Build.Execution; @@ -34,7 +33,6 @@ public NodeBuildResult( // Use a sorted dictionary so the JSON output is deterministically sorted and easier to compare build-to-build. // These paths are repo-relative. - [JsonConverter(typeof(SortedDictionaryConverter))] public SortedDictionary Outputs { get; } // Use a sorted dictionary so the JSON output is deterministically sorted and easier to compare build-to-build. @@ -76,48 +74,4 @@ public CacheResult ToCacheResult(PathNormalizer pathNormalizer) return CacheResult.IndicateCacheHit(targetResults); } - - private sealed class SortedDictionaryConverter : JsonConverter> - { - public override SortedDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var contentHashConverter = (JsonConverter)options.GetConverter(typeof(ContentHash)); - var outputs = new SortedDictionary(StringComparer.OrdinalIgnoreCase); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - - if (reader.TokenType != JsonTokenType.PropertyName) - { - throw new JsonException($"Unexpected token: {reader.TokenType}"); - } - - string propertyName = reader.GetString()!; - if (!reader.Read()) - { - throw new JsonException($"Property name '{propertyName}' does not have a value."); - } - - ContentHash? contentHash = contentHashConverter.Read(ref reader, typeof(ContentHash), options); - if (contentHash == null) - { - throw new JsonException($"Property value for '{propertyName}' could not be parsed."); - } - - outputs.Add(propertyName, contentHash.Value); - } - - return outputs; - } - - public override void Write(Utf8JsonWriter writer, SortedDictionary value, JsonSerializerOptions options) - { - var defaultConverter = (JsonConverter>) - options.GetConverter(typeof(SortedDictionary)); - defaultConverter.Write(writer, value, options); - } - } } \ No newline at end of file diff --git a/src/Common/SerializationHelper.cs b/src/Common/SerializationHelper.cs index 8bc9e5d..e11a443 100644 --- a/src/Common/SerializationHelper.cs +++ b/src/Common/SerializationHelper.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.IO; using System.Text; -using System.Threading.Tasks; -using System.Threading; -using System; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.MSBuildCache; @@ -14,14 +15,12 @@ internal static class SerializationHelper { public static JsonWriterOptions WriterOptions { get; } = new JsonWriterOptions { Indented = true }; - public static JsonSerializerOptions SerializerOptions { get; } = CreateJsonSerializerOptions(); - - internal static async Task DeserializeAsync(this Stream stream, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + internal static async Task DeserializeAsync(this Stream stream, JsonTypeInfo typeInfo, CancellationToken cancellationToken = default) where T : class { try { - return await JsonSerializer.DeserializeAsync(stream, options, cancellationToken); + return await JsonSerializer.DeserializeAsync(stream, typeInfo, cancellationToken); } catch (JsonException) { @@ -56,14 +55,4 @@ internal static class SerializationHelper throw new InvalidOperationException(message); } } - - private static JsonSerializerOptions CreateJsonSerializerOptions() - { - JsonSerializerOptions options = new() - { - WriteIndented = true, - }; - options.Converters.Add(new ContentHashJsonConverter()); - return options; - } } diff --git a/src/Common/SortedDictionaryConverter.cs b/src/Common/SortedDictionaryConverter.cs new file mode 100644 index 0000000..ac4ec29 --- /dev/null +++ b/src/Common/SortedDictionaryConverter.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using BuildXL.Cache.ContentStore.Hashing; +using System.Collections.Generic; + +namespace Microsoft.MSBuildCache; + +internal sealed class SortedDictionaryConverter : JsonConverter> +{ + public override SortedDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var contentHashConverter = (JsonConverter)options.GetConverter(typeof(ContentHash)); + var outputs = new SortedDictionary(StringComparer.OrdinalIgnoreCase); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException($"Unexpected token: {reader.TokenType}"); + } + + string propertyName = reader.GetString()!; + if (!reader.Read()) + { + throw new JsonException($"Property name '{propertyName}' does not have a value."); + } + + ContentHash? contentHash = contentHashConverter.Read(ref reader, typeof(ContentHash), options); + if (contentHash == null) + { + throw new JsonException($"Property value for '{propertyName}' could not be parsed."); + } + + outputs.Add(propertyName, contentHash.Value); + } + + return outputs; + } + + public override void Write(Utf8JsonWriter writer, SortedDictionary value, JsonSerializerOptions options) + { + var defaultConverter = (JsonConverter>)options.GetConverter(typeof(IDictionary)); + defaultConverter.Write(writer, value, options); + } +} diff --git a/src/Common/SourceGenerationContext.cs b/src/Common/SourceGenerationContext.cs new file mode 100644 index 0000000..6bfd3c6 --- /dev/null +++ b/src/Common/SourceGenerationContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using BuildXL.Cache.ContentStore.Hashing; +using Microsoft.MSBuildCache.Fingerprinting; + +namespace Microsoft.MSBuildCache; + +[JsonSourceGenerationOptions(WriteIndented = true, Converters = [typeof(ContentHashJsonConverter), typeof(SortedDictionaryConverter)])] +[JsonSerializable(typeof(NodeBuildResult))] +[JsonSerializable(typeof(PathSet))] +[JsonSerializable(typeof(IDictionary))] +internal partial class SourceGenerationContext : JsonSerializerContext +{ +}