Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Implementing CacheTagKey
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros committed Mar 25, 2016
1 parent 476d2eb commit d122854
Show file tree
Hide file tree
Showing 6 changed files with 493 additions and 227 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
Expand All @@ -22,7 +23,7 @@ public class DistributedCacheTagHelperService : IDistributedCacheTagHelperServic
private readonly IDistributedCacheTagHelperStorage _storage;
private readonly IDistributedCacheTagHelperFormatter _formatter;
private readonly HtmlEncoder _htmlEncoder;
private readonly ConcurrentDictionary<string, Task<IHtmlContent>> _workers;
private readonly ConcurrentDictionary<CacheTagKey, Task<IHtmlContent>> _workers;

public DistributedCacheTagHelperService(
IDistributedCacheTagHelperStorage storage,
Expand All @@ -34,11 +35,11 @@ HtmlEncoder HtmlEncoder
_storage = storage;
_htmlEncoder = HtmlEncoder;

_workers = new ConcurrentDictionary<string, Task<IHtmlContent>>();
_workers = new ConcurrentDictionary<CacheTagKey, Task<IHtmlContent>>();
}

/// <inheritdoc />
public async Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output, string key, DistributedCacheEntryOptions options)
public async Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output, CacheTagKey key, DistributedCacheEntryOptions options)
{
IHtmlContent content = null;

Expand All @@ -55,8 +56,10 @@ public async Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output, stri

try
{
var value = await _storage.GetAsync(key);

var serializedKey = Encoding.UTF8.GetBytes(key.GenerateKey());
var storageKey = key.GenerateHashedKey();
var value = await _storage.GetAsync(storageKey);

if (value == null)
{
var processedContent = await output.GetChildContentAsync();
Expand All @@ -74,20 +77,62 @@ public async Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output, stri

value = await _formatter.SerializeAsync(formattingContext);

await _storage.SetAsync(key, value, options);

using (var buffer = new MemoryStream())
{
// The stored content is
// - Length of the serialized cache key in bytes
// - Cache Key
// - Content

var keyLength = BitConverter.GetBytes(serializedKey.Length);

await buffer.WriteAsync(keyLength, 0, keyLength.Length);
await buffer.WriteAsync(serializedKey, 0, serializedKey.Length);
await buffer.WriteAsync(value, 0, value.Length);

await _storage.SetAsync(storageKey, buffer.ToArray(), options);
}

content = formattingContext.Html;
}
else
{
content = await _formatter.DeserializeAsync(value);
// Extract the length of the serialized key
byte[] contentBuffer = null;
using (var buffer = new MemoryStream(value))
{
var keyLengthBuffer = new byte[sizeof(int)];
await buffer.ReadAsync(keyLengthBuffer, 0, keyLengthBuffer.Length);

var keyLength = BitConverter.ToInt32(keyLengthBuffer, 0);
var serializedKeyBuffer = new byte[keyLength];
await buffer.ReadAsync(serializedKeyBuffer, 0, serializedKeyBuffer.Length);

// Ensure we are reading the expected key before continuing
if (serializedKeyBuffer == serializedKey)
{
contentBuffer = new byte[value.Length - keyLengthBuffer.Length - serializedKeyBuffer.Length];
await buffer.ReadAsync(contentBuffer, 0, contentBuffer.Length);
}
}

// If the deserialization fails, it can return null, for instance when the
// value is not in the expected format.
if (content == null)
try
{
content = await output.GetChildContentAsync();
if (contentBuffer != null)
{
content = await _formatter.DeserializeAsync(contentBuffer);
}
}
finally
{
// If the deserialization fails, it can return null, for instance when the
// value is not in the expected format, or the keys have collisions.
if (content == null)
{
content = await output.GetChildContentAsync();
}
}
}

tcs.TrySetResult(content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ public interface IDistributedCacheTagHelperService
/// <param name="key">The key in the storage.</param>
/// <param name="options">The <see cref="DistributedCacheEntryOptions"/>.</param>
/// <returns>A cached or new content for the cache tag helper.</returns>
Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output, string key, DistributedCacheEntryOptions options);
Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output, CacheTagKey key, DistributedCacheEntryOptions options);
}
}
18 changes: 5 additions & 13 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class CacheTagHelper : CacheTagHelperBase
/// Prefix used by <see cref="CacheTagHelper"/> instances when creating entries in <see cref="MemoryCache"/>.
/// </summary>
public static readonly string CacheKeyPrefix = nameof(CacheTagHelper);

private const string CachePriorityAttributeName = "priority";

/// <summary>
Expand Down Expand Up @@ -63,14 +64,15 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu

if (Enabled)
{
var key = GenerateKey(context);
var cacheKey = CacheTagKey.From(this, context);

MemoryCacheEntryOptions options;

while (content == null)
{
Task<IHtmlContent> result = null;

if (!MemoryCache.TryGetValue(key, out result))
if (!MemoryCache.TryGetValue(cacheKey, out result))
{
var tokenSource = new CancellationTokenSource();

Expand All @@ -82,7 +84,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu

var tcs = new TaskCompletionSource<IHtmlContent>();

// The returned value is ignored, we only do this so that
MemoryCache.Set(cacheKey, tcs.Task, options);
// the compiler doesn't complain about the returned task
// not being awaited
var localTcs = MemoryCache.Set(key, tcs.Task, options);
Expand Down Expand Up @@ -167,16 +169,6 @@ internal MemoryCacheEntryOptions GetMemoryCacheEntryOptions()
return options;
}

protected override string GetUniqueId(TagHelperContext context)
{
return context.UniqueId;
}

protected override string GetKeyPrefix(TagHelperContext context)
{
return CacheKeyPrefix;
}

private async Task<IHtmlContent> ProcessContentAsync(TagHelperOutput output)
{
var content = await output.GetChildContentAsync();
Expand Down
190 changes: 0 additions & 190 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/CacheTagHelperBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
Expand Down Expand Up @@ -122,191 +118,5 @@ public override int Order
[HtmlAttributeName(EnabledAttributeName)]
public bool Enabled { get; set; } = true;

// Internal for unit testing
protected internal string GenerateKey(TagHelperContext context)
{
var builder = new StringBuilder(GetKeyPrefix(context));
builder
.Append(CacheKeyTokenSeparator)
.Append(GetUniqueId(context));

var request = ViewContext.HttpContext.Request;

if (!string.IsNullOrEmpty(VaryBy))
{
builder
.Append(CacheKeyTokenSeparator)
.Append(nameof(VaryBy))
.Append(CacheKeyTokenSeparator)
.Append(VaryBy);
}

AddStringCollectionKey(builder, nameof(VaryByCookie), VaryByCookie, request.Cookies, (c, key) => c[key]);
AddStringCollectionKey(builder, nameof(VaryByHeader), VaryByHeader, request.Headers, (c, key) => c[key]);
AddStringCollectionKey(builder, nameof(VaryByQuery), VaryByQuery, request.Query, (c, key) => c[key]);
AddVaryByRouteKey(builder);

if (VaryByUser)
{
builder
.Append(CacheKeyTokenSeparator)
.Append(nameof(VaryByUser))
.Append(CacheKeyTokenSeparator)
.Append(ViewContext.HttpContext.User?.Identity?.Name);
}

// The key is typically too long to be useful, so we use a cryptographic hash
// as the actual key (better randomization and key distribution, so small vary
// values will generate dramatically different keys).
using (var sha = SHA256.Create())
{
var contentBytes = Encoding.UTF8.GetBytes(builder.ToString());
var hashedBytes = sha.ComputeHash(contentBytes);
return Convert.ToBase64String(hashedBytes);
}
}

protected abstract string GetUniqueId(TagHelperContext context);

protected abstract string GetKeyPrefix(TagHelperContext context);

protected static void AddStringCollectionKey(
StringBuilder builder,
string keyName,
string value,
IDictionary<string, StringValues> sourceCollection)
{
if (string.IsNullOrEmpty(value))
{
return;
}

// keyName(param1=value1|param2=value2)
builder
.Append(CacheKeyTokenSeparator)
.Append(keyName)
.Append("(");

var values = Tokenize(value);

// Perf: Avoid allocating enumerator
for (var i = 0; i < values.Count; i++)
{
var item = values[i];
builder
.Append(item)
.Append(CacheKeyTokenSeparator)
.Append(sourceCollection[item])
.Append(CacheKeyTokenSeparator);
}

if (values.Count > 0)
{
// Remove the trailing separator
builder.Length -= CacheKeyTokenSeparator.Length;
}

builder.Append(")");
}

protected static void AddStringCollectionKey<TSourceCollection>(
StringBuilder builder,
string keyName,
string value,
TSourceCollection sourceCollection,
Func<TSourceCollection, string, StringValues> accessor)
{
if (string.IsNullOrEmpty(value))
{
return;
}

// keyName(param1=value1|param2=value2)
builder
.Append(CacheKeyTokenSeparator)
.Append(keyName)
.Append("(");

var values = Tokenize(value);

// Perf: Avoid allocating enumerator
for (var i = 0; i < values.Count; i++)
{
var item = values[i];

builder
.Append(item)
.Append(CacheKeyTokenSeparator)
.Append(accessor(sourceCollection, item))
.Append(CacheKeyTokenSeparator);
}

if (values.Count > 0)
{
// Remove the trailing separator
builder.Length -= CacheKeyTokenSeparator.Length;
}

builder.Append(")");
}

protected static IList<string> Tokenize(string value)
{
var values = value.Split(AttributeSeparator, StringSplitOptions.RemoveEmptyEntries);
if (values.Length == 0)
{
return values;
}

var trimmedValues = new List<string>();

for (var i = 0; i < values.Length; i++)
{
var trimmedValue = values[i].Trim();

if (trimmedValue.Length > 0)
{
trimmedValues.Add(trimmedValue);
}
}

return trimmedValues;
}

protected void AddVaryByRouteKey(StringBuilder builder)
{
var tokenFound = false;

if (string.IsNullOrEmpty(VaryByRoute))
{
return;
}

builder
.Append(CacheKeyTokenSeparator)
.Append(nameof(VaryByRoute))
.Append("(");

var varyByRoutes = Tokenize(VaryByRoute);
for (var i = 0; i < varyByRoutes.Count; i++)
{
var route = varyByRoutes[i];
tokenFound = true;

builder
.Append(route)
.Append(CacheKeyTokenSeparator)
.Append(ViewContext.RouteData.Values[route])
.Append(CacheKeyTokenSeparator);
}

if (tokenFound)
{
builder.Length -= CacheKeyTokenSeparator.Length;
}

builder.Append(")");
}

}
}
Loading

0 comments on commit d122854

Please sign in to comment.