Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Serialize output caching entries to binary
Fixes #43415
  • Loading branch information
sebastienros committed Sep 1, 2022
commit bd265c48ecf0c7e5f2907eee3ceeb6e07a3cd996
250 changes: 247 additions & 3 deletions src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Text.Json;
using System.Text;
using Microsoft.AspNetCore.OutputCaching.Serialization;

namespace Microsoft.AspNetCore.OutputCaching;
Expand All @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.OutputCaching;
/// </summary>
internal static class OutputCacheEntryFormatter
{
private const byte SerializationRevision = 1;

public static async ValueTask<OutputCacheEntry?> GetAsync(string key, IOutputCacheStore store, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(key);
Expand All @@ -22,7 +24,7 @@ internal static class OutputCacheEntryFormatter
return null;
}

var formatter = JsonSerializer.Deserialize(content, FormatterEntrySerializerContext.Default.FormatterEntry);
var formatter = Deserialize(new MemoryStream(content));

if (formatter == null)
{
Expand Down Expand Up @@ -74,8 +76,250 @@ public static async ValueTask StoreAsync(string key, OutputCacheEntry value, Tim

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

// 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)
{
WriteUTF8String(writer, header.Key);

if (header.Value == null)
{
continue;
}

// Values count: 7-bit encoded int

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)
{
if (value == null)
{
continue;
}

WriteUTF8String(writer, 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)
{
if (tag == null)
{
continue;
}

WriteUTF8String(writer, 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?[]>();

for (var i = 0; i < headersCount; i++)
{
var key = ReadUTF8String(reader);

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] = ReadUTF8String(reader);
}

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;
}

private static void WriteUTF8String(BinaryWriter writer, string value)
{
var data = Encoding.UTF8.GetBytes(value);
writer.Write7BitEncodedInt(data.Length);
writer.Write(data);
}

private static string ReadUTF8String(BinaryReader reader)
{
var dataLength = reader.Read7BitEncodedInt();
var data = reader.ReadBytes(dataLength);
return Encoding.UTF8.GetString(data);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ public async Task StoreAndGet_StoresEmptyValues()
[Fact]
public async Task StoreAndGet_StoresAllValues()
{
var bodySegment1 = "lorem"u8.ToArray();
var bodySegment2 = "こんにちは"u8.ToArray();

var store = new TestOutputCache();
var key = "abc";
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,
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,
Tags = new[] { "tag1", "tag2" }
Tags = new[] { "tag", "タグ" }
};

await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default);
Expand Down