Sfoglia il codice sorgente

Serialize output caching entries to binary (#43672)

* Serialize output caching entries to binary

Fixes #43415

* Handle null values

* Fix formatting
Sébastien Ros 3 anni fa
parent
commit
1505f6f86f

+ 224 - 3
src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs

@@ -2,7 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 
 using System.Linq;
 using System.Linq;
-using System.Text.Json;
+using System.Text;
 using Microsoft.AspNetCore.OutputCaching.Serialization;
 using Microsoft.AspNetCore.OutputCaching.Serialization;
 
 
 namespace Microsoft.AspNetCore.OutputCaching;
 namespace Microsoft.AspNetCore.OutputCaching;
@@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.OutputCaching;
 /// </summary>
 /// </summary>
 internal static class OutputCacheEntryFormatter
 internal static class OutputCacheEntryFormatter
 {
 {
+    private const byte SerializationRevision = 1;
+
     public static async ValueTask<OutputCacheEntry?> GetAsync(string key, IOutputCacheStore store, CancellationToken cancellationToken)
     public static async ValueTask<OutputCacheEntry?> GetAsync(string key, IOutputCacheStore store, CancellationToken cancellationToken)
     {
     {
         ArgumentNullException.ThrowIfNull(key);
         ArgumentNullException.ThrowIfNull(key);
@@ -22,7 +24,7 @@ internal static class OutputCacheEntryFormatter
             return null;
             return null;
         }
         }
 
 
-        var formatter = JsonSerializer.Deserialize(content, FormatterEntrySerializerContext.Default.FormatterEntry);
+        var formatter = Deserialize(new MemoryStream(content));
 
 
         if (formatter == null)
         if (formatter == null)
         {
         {
@@ -74,8 +76,227 @@ internal static class OutputCacheEntryFormatter
 
 
         using var bufferStream = new MemoryStream();
         using var bufferStream = new MemoryStream();
 
 
-        JsonSerializer.Serialize(bufferStream, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry);
+        Serialize(bufferStream, formatterEntry);
 
 
         await store.SetAsync(key, bufferStream.ToArray(), value.Tags ?? Array.Empty<string>(), duration, cancellationToken);
         await store.SetAsync(key, bufferStream.ToArray(), value.Tags ?? Array.Empty<string>(), duration, cancellationToken);
     }
     }
+
+    // Format:
+    // Serialization revision:
+    //   7-bit encoded int
+    // Creation date:
+    //   Ticks: 7-bit encoded long
+    //   Offset.TotalMinutes: 7-bit encoded long
+    // Status code:
+    //   7-bit encoded int
+    // Headers:
+    //   Headers count: 7-bit encoded int
+    //   For each header:
+    //     key name byte length: 7-bit encoded int
+    //     UTF-8 encoded key name byte[]
+    //     Values count: 7-bit encoded int
+    //     For each header value:
+    //       data byte length: 7-bit encoded int
+    //       UTF-8 encoded byte[]
+    // Body:
+    //   Segments count: 7-bit encoded int
+    //   For each segment:
+    //     data byte length: 7-bit encoded int
+    //     data byte[]
+    // Tags:
+    //   Tags count: 7-bit encoded int
+    //   For each tag:
+    //     data byte length: 7-bit encoded int
+    //     UTF-8 encoded byte[]
+
+    private static void Serialize(Stream output, FormatterEntry entry)
+    {
+        using var writer = new BinaryWriter(output);
+
+        // Serialization revision:
+        //   7-bit encoded int
+        writer.Write7BitEncodedInt(SerializationRevision);
+
+        // Creation date:
+        //   Ticks: 7-bit encoded long
+        //   Offset.TotalMinutes: 7-bit encoded long
+
+        writer.Write7BitEncodedInt64(entry.Created.Ticks);
+        writer.Write7BitEncodedInt64((long)entry.Created.Offset.TotalMinutes);
+
+        // Status code:
+        //   7-bit encoded int
+        writer.Write7BitEncodedInt(entry.StatusCode);
+
+        // Headers:
+        //   Headers count: 7-bit encoded int
+
+        writer.Write7BitEncodedInt(entry.Headers.Count);
+
+        //   For each header:
+        //     key name byte length: 7-bit encoded int
+        //     UTF-8 encoded key name byte[]
+
+        foreach (var header in entry.Headers)
+        {
+            writer.Write(header.Key);
+
+            //     Values count: 7-bit encoded int
+
+            if (header.Value == null)
+            {
+                writer.Write7BitEncodedInt(0);
+                continue;
+            }
+            else
+            {
+                writer.Write7BitEncodedInt(header.Value.Length);
+            }
+
+            //     For each header value:
+            //       data byte length: 7-bit encoded int
+            //       UTF-8 encoded byte[]
+
+            foreach (var value in header.Value)
+            {
+                writer.Write(value ?? "");
+            }
+        }
+
+        // Body:
+        //   Segments count: 7-bit encoded int
+        //   For each segment:
+        //     data byte length: 7-bit encoded int
+        //     data byte[]
+
+        writer.Write7BitEncodedInt(entry.Body.Count);
+
+        foreach (var segment in entry.Body)
+        {
+            writer.Write7BitEncodedInt(segment.Length);
+            writer.Write(segment);
+        }
+
+        // Tags:
+        //   Tags count: 7-bit encoded int
+        //   For each tag:
+        //     data byte length: 7-bit encoded int
+        //     UTF-8 encoded byte[]
+
+        writer.Write7BitEncodedInt(entry.Tags.Length);
+
+        foreach (var tag in entry.Tags)
+        {
+            writer.Write(tag ?? "");
+        }
+    }
+
+    private static FormatterEntry? Deserialize(Stream content)
+    {
+        using var reader = new BinaryReader(content);
+
+        // Serialization revision:
+        //   7-bit encoded int
+
+        var revision = reader.Read7BitEncodedInt();
+
+        if (revision != SerializationRevision)
+        {
+            // In future versions, also support the previous revision format.
+
+            return null;
+        }
+
+        var result = new FormatterEntry();
+
+        // Creation date:
+        //   Ticks: 7-bit encoded long
+        //   Offset.TotalMinutes: 7-bit encoded long
+
+        var ticks = reader.Read7BitEncodedInt64();
+        var offsetMinutes = reader.Read7BitEncodedInt64();
+
+        result.Created = new DateTimeOffset(ticks, TimeSpan.FromMinutes(offsetMinutes));
+
+        // Status code:
+        //   7-bit encoded int
+
+        result.StatusCode = reader.Read7BitEncodedInt();
+
+        // Headers:
+        //   Headers count: 7-bit encoded int
+
+        var headersCount = reader.Read7BitEncodedInt();
+
+        //   For each header:
+        //     key name byte length: 7-bit encoded int
+        //     UTF-8 encoded key name byte[]
+        //     Values count: 7-bit encoded int
+
+        result.Headers = new Dictionary<string, string?[]>(headersCount);
+
+        for (var i = 0; i < headersCount; i++)
+        {
+            var key = reader.ReadString();
+
+            var valuesCount = reader.Read7BitEncodedInt();
+
+            //     For each header value:
+            //       data byte length: 7-bit encoded int
+            //       UTF-8 encoded byte[]
+
+            var values = new string[valuesCount];
+
+            for (var j = 0; j < valuesCount; j++)
+            {
+                values[j] = reader.ReadString();
+            }
+
+            result.Headers[key] = values;
+        }
+
+        // Body:
+        //   Segments count: 7-bit encoded int
+
+        var segmentsCount = reader.Read7BitEncodedInt();
+
+        //   For each segment:
+        //     data byte length: 7-bit encoded int
+        //     data byte[]
+
+        var segments = new List<byte[]>(segmentsCount);
+
+        for (var i = 0; i < segmentsCount; i++)
+        {
+            var segmentLength = reader.Read7BitEncodedInt();
+            var segment = reader.ReadBytes(segmentLength);
+
+            segments.Add(segment);
+        }
+
+        result.Body = segments;
+
+        // Tags:
+        //   Tags count: 7-bit encoded int
+
+        var tagsCount = reader.Read7BitEncodedInt();
+
+        //   For each tag:
+        //     data byte length: 7-bit encoded int
+        //     UTF-8 encoded byte[]
+
+        var tags = new string[tagsCount];
+
+        for (var i = 0; i < tagsCount; i++)
+        {
+            var tagLength = reader.Read7BitEncodedInt();
+            var tagData = reader.ReadBytes(tagLength);
+            var tag = Encoding.UTF8.GetString(tagData);
+
+            tags[i] = tag;
+        }
+
+        result.Tags = tags;
+        return result;
+    }
 }
 }

+ 0 - 12
src/Middleware/OutputCaching/src/Serialization/FormatterEntrySerializerContext.cs

@@ -1,12 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text.Json.Serialization;
-
-namespace Microsoft.AspNetCore.OutputCaching.Serialization;
-
-[JsonSourceGenerationOptions(WriteIndented = false)]
-[JsonSerializable(typeof(FormatterEntry))]
-internal partial class FormatterEntrySerializerContext : JsonSerializerContext
-{
-}

+ 58 - 3
src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs

@@ -2,12 +2,15 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
 using Microsoft.Net.Http.Headers;
 using Microsoft.Net.Http.Headers;
 
 
 namespace Microsoft.AspNetCore.OutputCaching.Tests;
 namespace Microsoft.AspNetCore.OutputCaching.Tests;
 
 
 public class OutputCacheEntryFormatterTests
 public class OutputCacheEntryFormatterTests
 {
 {
+    private static CachedResponseBody EmptyResponseBody = new(new List<byte[]>(), 0);
+
     [Fact]
     [Fact]
     public async Task StoreAndGet_StoresEmptyValues()
     public async Task StoreAndGet_StoresEmptyValues()
     {
     {
@@ -30,15 +33,18 @@ public class OutputCacheEntryFormatterTests
     [Fact]
     [Fact]
     public async Task StoreAndGet_StoresAllValues()
     public async Task StoreAndGet_StoresAllValues()
     {
     {
+        var bodySegment1 = "lorem"u8.ToArray();
+        var bodySegment2 = "こんにちは"u8.ToArray();
+
         var store = new TestOutputCache();
         var store = new TestOutputCache();
         var key = "abc";
         var key = "abc";
         var entry = new OutputCacheEntry()
         var entry = new OutputCacheEntry()
         {
         {
-            Body = new CachedResponseBody(new List<byte[]>() { "lorem"u8.ToArray(), "ipsum"u8.ToArray() }, 10),
+            Body = new CachedResponseBody(new List<byte[]>() { bodySegment1, bodySegment2 }, bodySegment1.Length + bodySegment2.Length),
             Created = DateTimeOffset.UtcNow,
             Created = DateTimeOffset.UtcNow,
-            Headers = new HeaderDictionary { [HeaderNames.Accept] = "text/plain", [HeaderNames.AcceptCharset] = "utf8" },
+            Headers = new HeaderDictionary { [HeaderNames.Accept] = new[] { "text/plain", "text/html" }, [HeaderNames.AcceptCharset] = "utf8" },
             StatusCode = StatusCodes.Status201Created,
             StatusCode = StatusCodes.Status201Created,
-            Tags = new[] { "tag1", "tag2" }
+            Tags = new[] { "tag", "タグ" }
         };
         };
 
 
         await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default);
         await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default);
@@ -48,6 +54,55 @@ public class OutputCacheEntryFormatterTests
         AssertEntriesAreSame(entry, result);
         AssertEntriesAreSame(entry, result);
     }
     }
 
 
+    [Fact]
+    public async Task StoreAndGet_StoresNullTags()
+    {
+        var store = new TestOutputCache();
+        var key = "abc";
+        var entry = new OutputCacheEntry()
+        {
+            Body = EmptyResponseBody,
+            Headers = new HeaderDictionary(),
+            Tags = new[] { null, null, "", "tag" }
+        };
+
+        await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default);
+
+        var result = await OutputCacheEntryFormatter.GetAsync(key, store, default);
+
+        Assert.Equal(4, result.Tags.Length);
+        Assert.Equal("", result.Tags[0]);
+        Assert.Equal("", result.Tags[1]);
+        Assert.Equal("", result.Tags[2]);
+        Assert.Equal("tag", result.Tags[3]);
+    }
+
+    [Fact]
+    public async Task StoreAndGet_StoresNullHeaders()
+    {
+        var store = new TestOutputCache();
+        var key = "abc";
+        var entry = new OutputCacheEntry()
+        {
+            Body = EmptyResponseBody,
+            Headers = new HeaderDictionary { [""] = "", [HeaderNames.Accept] = new[] { null, null, "", "text/html" }, [HeaderNames.AcceptCharset] = new string[] { null } },
+            Tags = Array.Empty<string>()
+        };
+
+        await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default);
+
+        var result = await OutputCacheEntryFormatter.GetAsync(key, store, default);
+
+        Assert.Equal(3, result.Headers.Count);
+        Assert.Equal("", result.Headers[""]);
+        Assert.Equal(4, result.Headers[HeaderNames.Accept].Count);
+        Assert.Equal("", result.Headers[HeaderNames.Accept][0]);
+        Assert.Equal("", result.Headers[HeaderNames.Accept][1]);
+        Assert.Equal("", result.Headers[HeaderNames.Accept][2]);
+        Assert.Equal("text/html", result.Headers[HeaderNames.Accept][3]);
+        Assert.Equal("", result.Headers[HeaderNames.AcceptCharset][0]);
+    }
+
     private static void AssertEntriesAreSame(OutputCacheEntry expected, OutputCacheEntry actual)
     private static void AssertEntriesAreSame(OutputCacheEntry expected, OutputCacheEntry actual)
     {
     {
         Assert.NotNull(expected);
         Assert.NotNull(expected);