[Fixes #4246] Introducing CacheTagKey
Prevent string allocations in CacheTagHelper Fix hash collision by checking the serialized key with the actual content for DistributedCacheTagHelper
This commit is contained in:
parent
f22e234dab
commit
341430eae5
|
|
@ -0,0 +1,324 @@
|
|||
// 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.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// An instance of <see cref="CacheTagKey"/> represents the state of <see cref="CacheTagHelper"/>
|
||||
/// or <see cref="DistributedCacheTagHelper"/> keys.
|
||||
/// </summary>
|
||||
public class CacheTagKey : IEquatable<CacheTagKey>
|
||||
{
|
||||
private static readonly char[] AttributeSeparator = new[] { ',' };
|
||||
private static readonly Func<IRequestCookieCollection, string, string> CookieAcccessor = (c, key) => c[key];
|
||||
private static readonly Func<IHeaderDictionary, string, string> HeaderAccessor = (c, key) => c[key];
|
||||
private static readonly Func<IQueryCollection, string, string> QueryAccessor = (c, key) => c[key];
|
||||
private static readonly Func<RouteValueDictionary, string, string> RouteValueAccessor = (c, key) => c[key]?.ToString();
|
||||
|
||||
private const string CacheKeyTokenSeparator = "||";
|
||||
private const string VaryByName = "VaryBy";
|
||||
private const string VaryByHeaderName = "VaryByHeader";
|
||||
private const string VaryByQueryName = "VaryByQuery";
|
||||
private const string VaryByRouteName = "VaryByRoute";
|
||||
private const string VaryByCookieName = "VaryByCookie";
|
||||
private const string VaryByUserName = "VaryByUser";
|
||||
|
||||
private readonly string _key;
|
||||
private readonly string _prefix;
|
||||
private readonly string _varyBy;
|
||||
private readonly DateTimeOffset? _expiresOn;
|
||||
private readonly TimeSpan? _expiresAfter;
|
||||
private readonly TimeSpan? _expiresSliding;
|
||||
private readonly IList<KeyValuePair<string, string>> _headers;
|
||||
private readonly IList<KeyValuePair<string, string>> _queries;
|
||||
private readonly IList<KeyValuePair<string, string>> _routeValues;
|
||||
private readonly IList<KeyValuePair<string, string>> _cookies;
|
||||
private readonly bool _varyByUser;
|
||||
private readonly string _username;
|
||||
|
||||
private string _generatedKey;
|
||||
private int? _hashcode;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="CacheTagKey"/> for a specific <see cref="CacheTagHelper"/>.
|
||||
/// </summary>
|
||||
/// <param name="tagHelper">The <see cref="CacheTagHelper"/>.</param>
|
||||
/// <param name="context">The <see cref="TagHelperContext"/>.</param>
|
||||
/// <returns>A new <see cref="CacheTagKey"/>.</returns>
|
||||
public CacheTagKey(CacheTagHelper tagHelper, TagHelperContext context)
|
||||
: this(tagHelper)
|
||||
{
|
||||
_key = context.UniqueId;
|
||||
_prefix = nameof(CacheTagHelper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="CacheTagKey"/> for a specific <see cref="DistributedCacheTagHelper"/>.
|
||||
/// </summary>
|
||||
/// <param name="tagHelper">The <see cref="DistributedCacheTagHelper"/>.</param>
|
||||
/// <returns>A new <see cref="CacheTagKey"/>.</returns>
|
||||
public CacheTagKey(DistributedCacheTagHelper tagHelper)
|
||||
: this((CacheTagHelperBase)tagHelper)
|
||||
{
|
||||
_key = tagHelper.Name;
|
||||
_prefix = nameof(DistributedCacheTagHelper);
|
||||
}
|
||||
|
||||
private CacheTagKey(CacheTagHelperBase tagHelper)
|
||||
{
|
||||
var httpContext = tagHelper.ViewContext.HttpContext;
|
||||
var request = httpContext.Request;
|
||||
|
||||
_expiresAfter = tagHelper.ExpiresAfter;
|
||||
_expiresOn = tagHelper.ExpiresOn;
|
||||
_expiresSliding = tagHelper.ExpiresSliding;
|
||||
_varyBy = tagHelper.VaryBy;
|
||||
_cookies = ExtractCollection(tagHelper.VaryByCookie, request.Cookies, CookieAcccessor);
|
||||
_headers = ExtractCollection(tagHelper.VaryByHeader, request.Headers, HeaderAccessor);
|
||||
_queries = ExtractCollection(tagHelper.VaryByQuery, request.Query, QueryAccessor);
|
||||
_routeValues = ExtractCollection(tagHelper.VaryByRoute, tagHelper.ViewContext.RouteData.Values, RouteValueAccessor);
|
||||
_varyByUser = tagHelper.VaryByUser;
|
||||
|
||||
if (_varyByUser)
|
||||
{
|
||||
_username = httpContext.User?.Identity?.Name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="string"/> representation of the key.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="string"/> uniquely representing the key.</returns>
|
||||
public string GenerateKey()
|
||||
{
|
||||
// Caching as the key is immutable and it can be called multiple times during a request.
|
||||
if (_generatedKey != null)
|
||||
{
|
||||
return _generatedKey;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(_prefix);
|
||||
builder
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(_key);
|
||||
|
||||
if (!string.IsNullOrEmpty(_varyBy))
|
||||
{
|
||||
builder
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(VaryByName)
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(_varyBy);
|
||||
}
|
||||
|
||||
AddStringCollection(builder, VaryByCookieName, _cookies);
|
||||
AddStringCollection(builder, VaryByHeaderName, _headers);
|
||||
AddStringCollection(builder, VaryByQueryName, _queries);
|
||||
AddStringCollection(builder, VaryByRouteName, _routeValues);
|
||||
|
||||
if (_varyByUser)
|
||||
{
|
||||
builder
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(VaryByUserName)
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(_username);
|
||||
}
|
||||
|
||||
_generatedKey = builder.ToString();
|
||||
|
||||
return _generatedKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a hashed value of the key.
|
||||
/// </summary>
|
||||
/// <returns>A cryptographic hash of the key.</returns>
|
||||
public string GenerateHashedKey()
|
||||
{
|
||||
var key = GenerateKey();
|
||||
|
||||
// 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(key);
|
||||
var hashedBytes = sha.ComputeHash(contentBytes);
|
||||
return Convert.ToBase64String(hashedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as CacheTagKey;
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(CacheTagKey other)
|
||||
{
|
||||
return string.Equals(other._key, _key, StringComparison.Ordinal) &&
|
||||
other._expiresAfter == _expiresAfter &&
|
||||
other._expiresOn == _expiresOn &&
|
||||
other._expiresSliding == _expiresSliding &&
|
||||
string.Equals(other._varyBy, _varyBy, StringComparison.Ordinal) &&
|
||||
AreSame(_cookies, other._cookies) &&
|
||||
AreSame(_headers, other._headers) &&
|
||||
AreSame(_queries, other._queries) &&
|
||||
AreSame(_routeValues, other._routeValues) &&
|
||||
_varyByUser == other._varyByUser &&
|
||||
(!_varyByUser || string.Equals(other._username, _username, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// The hashcode is intentionally not using the computed
|
||||
// stringified key in order to prevent string allocations
|
||||
// in the common case where it's not explicitly required.
|
||||
|
||||
// Caching as the key is immutable and it can be called
|
||||
// multiple times during a request.
|
||||
if (_hashcode.HasValue)
|
||||
{
|
||||
return _hashcode.Value;
|
||||
}
|
||||
|
||||
var hashCodeCombiner = new HashCodeCombiner();
|
||||
|
||||
hashCodeCombiner.Add(_key, StringComparer.Ordinal);
|
||||
hashCodeCombiner.Add(_expiresAfter);
|
||||
hashCodeCombiner.Add(_expiresOn);
|
||||
hashCodeCombiner.Add(_expiresSliding);
|
||||
hashCodeCombiner.Add(_varyBy, StringComparer.Ordinal);
|
||||
hashCodeCombiner.Add(_username, StringComparer.Ordinal);
|
||||
|
||||
CombineCollectionHashCode(hashCodeCombiner, VaryByCookieName, _cookies);
|
||||
CombineCollectionHashCode(hashCodeCombiner, VaryByHeaderName, _headers);
|
||||
CombineCollectionHashCode(hashCodeCombiner, VaryByQueryName, _queries);
|
||||
CombineCollectionHashCode(hashCodeCombiner, VaryByRouteName, _routeValues);
|
||||
|
||||
_hashcode = hashCodeCombiner.CombinedHash;
|
||||
|
||||
return _hashcode.Value;
|
||||
}
|
||||
|
||||
private static IList<KeyValuePair<string, string>> ExtractCollection<TSourceCollection>(string keys, TSourceCollection collection, Func<TSourceCollection, string, string> accessor)
|
||||
{
|
||||
if (string.IsNullOrEmpty(keys))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var tokenizer = new StringTokenizer(keys, AttributeSeparator);
|
||||
|
||||
var result = new List<KeyValuePair<string, string>>();
|
||||
|
||||
foreach (var item in tokenizer)
|
||||
{
|
||||
var trimmedValue = item.Trim();
|
||||
|
||||
if (trimmedValue.Length != 0)
|
||||
{
|
||||
var value = accessor(collection, trimmedValue.Value);
|
||||
result.Add(new KeyValuePair<string, string>(trimmedValue.Value, value ?? string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddStringCollection(
|
||||
StringBuilder builder,
|
||||
string collectionName,
|
||||
IList<KeyValuePair<string, string>> values)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// keyName(param1=value1|param2=value2)
|
||||
builder
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(collectionName)
|
||||
.Append("(");
|
||||
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var item = values[i];
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
builder.Append(CacheKeyTokenSeparator);
|
||||
}
|
||||
|
||||
builder
|
||||
.Append(item.Key)
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(item.Value);
|
||||
}
|
||||
|
||||
builder.Append(")");
|
||||
}
|
||||
|
||||
private static void CombineCollectionHashCode(
|
||||
HashCodeCombiner hashCodeCombiner,
|
||||
string collectionName,
|
||||
IList<KeyValuePair<string, string>> values)
|
||||
{
|
||||
if (values != null)
|
||||
{
|
||||
hashCodeCombiner.Add(collectionName, StringComparer.Ordinal);
|
||||
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
var item = values[i];
|
||||
hashCodeCombiner.Add(item.Key);
|
||||
hashCodeCombiner.Add(item.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AreSame(IList<KeyValuePair<string, string>> values1, IList<KeyValuePair<string, string>> values2)
|
||||
{
|
||||
if (values1 == values2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (values1 == null || values2 == null || values1.Count != values2.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < values1.Count; i++)
|
||||
{
|
||||
if (!string.Equals(values1[i].Key, values2[i].Key, StringComparison.Ordinal) ||
|
||||
!string.Equals(values1[i].Value, values2[i].Value, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +1,81 @@
|
|||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements <see cref="IDistributedCacheTagHelperService"/> and ensure
|
||||
/// Implements <see cref="IDistributedCacheTagHelperService"/> and ensures
|
||||
/// multiple concurrent requests are gated.
|
||||
/// The entries are stored like this:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>Int32 representing the hashed cache key size.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>The UTF8 encoded hashed cache key.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>The UTF8 encoded cached content.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class DistributedCacheTagHelperService : IDistributedCacheTagHelperService
|
||||
{
|
||||
private readonly IDistributedCacheTagHelperStorage _storage;
|
||||
private readonly IDistributedCacheTagHelperFormatter _formatter;
|
||||
private readonly HtmlEncoder _htmlEncoder;
|
||||
private readonly ConcurrentDictionary<string, Task<IHtmlContent>> _workers;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ConcurrentDictionary<CacheTagKey, Task<IHtmlContent>> _workers;
|
||||
|
||||
public DistributedCacheTagHelperService(
|
||||
IDistributedCacheTagHelperStorage storage,
|
||||
IDistributedCacheTagHelperFormatter formatter,
|
||||
HtmlEncoder HtmlEncoder
|
||||
)
|
||||
HtmlEncoder HtmlEncoder,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (storage == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(storage));
|
||||
}
|
||||
|
||||
if (formatter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(formatter));
|
||||
}
|
||||
|
||||
if (HtmlEncoder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(HtmlEncoder));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
_formatter = formatter;
|
||||
_storage = storage;
|
||||
_htmlEncoder = HtmlEncoder;
|
||||
|
||||
_workers = new ConcurrentDictionary<string, Task<IHtmlContent>>();
|
||||
_logger = loggerFactory.CreateLogger<DistributedCacheTagHelperService>();
|
||||
_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;
|
||||
|
||||
|
|
@ -55,10 +92,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
|
|||
|
||||
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)
|
||||
{
|
||||
// The value is not cached, we need to render the tag helper output
|
||||
var processedContent = await output.GetChildContentAsync();
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
|
@ -72,28 +112,48 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
|
|||
Html = new HtmlString(stringBuilder.ToString())
|
||||
};
|
||||
|
||||
// Then cache the result
|
||||
value = await _formatter.SerializeAsync(formattingContext);
|
||||
|
||||
await _storage.SetAsync(key, value, options);
|
||||
var encodeValue = Encode(value, serializedKey);
|
||||
|
||||
await _storage.SetAsync(storageKey, encodeValue, options);
|
||||
|
||||
content = formattingContext.Html;
|
||||
}
|
||||
else
|
||||
{
|
||||
content = await _formatter.DeserializeAsync(value);
|
||||
|
||||
// If the deserialization fails, it can return null, for instance when the
|
||||
// value is not in the expected format.
|
||||
if (content == null)
|
||||
// The value was found in the storage, decode and ensure
|
||||
// there is no cache key hash collision
|
||||
byte[] decodedValue = Decode(value, serializedKey);
|
||||
|
||||
try
|
||||
{
|
||||
content = await output.GetChildContentAsync();
|
||||
if (decodedValue != null)
|
||||
{
|
||||
content = await _formatter.DeserializeAsync(decodedValue);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.DistributedFormatterDeserializationException(storageKey, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// If the deserialization fails the content is rendered
|
||||
if (content == null)
|
||||
{
|
||||
content = await output.GetChildContentAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify all other awaiters of the final content
|
||||
tcs.TrySetResult(content);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Notify all other awaiters to render the content
|
||||
tcs.TrySetResult(null);
|
||||
throw;
|
||||
}
|
||||
|
|
@ -112,5 +172,43 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
|
|||
|
||||
return content;
|
||||
}
|
||||
|
||||
private byte[] Encode(byte[] value, byte[] serializedKey)
|
||||
{
|
||||
using (var buffer = new MemoryStream())
|
||||
{
|
||||
var keyLength = BitConverter.GetBytes(serializedKey.Length);
|
||||
|
||||
buffer.Write(keyLength, 0, keyLength.Length);
|
||||
buffer.Write(serializedKey, 0, serializedKey.Length);
|
||||
buffer.Write(value, 0, value.Length);
|
||||
|
||||
return buffer.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Decode(byte[] value, byte[] expectedKey)
|
||||
{
|
||||
byte[] decoded = null;
|
||||
|
||||
using (var buffer = new MemoryStream(value))
|
||||
{
|
||||
var keyLengthBuffer = new byte[sizeof(int)];
|
||||
buffer.Read(keyLengthBuffer, 0, keyLengthBuffer.Length);
|
||||
|
||||
var keyLength = BitConverter.ToInt32(keyLengthBuffer, 0);
|
||||
var serializedKeyBuffer = new byte[keyLength];
|
||||
buffer.Read(serializedKeyBuffer, 0, serializedKeyBuffer.Length);
|
||||
|
||||
// Ensure we are reading the expected key before continuing
|
||||
if (serializedKeyBuffer.SequenceEqual(expectedKey))
|
||||
{
|
||||
decoded = new byte[value.Length - keyLengthBuffer.Length - serializedKeyBuffer.Length];
|
||||
buffer.Read(decoded, 0, decoded.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers.Cache
|
|||
/// <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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Text.Encodings.Web;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
|
@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
/// 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>
|
||||
|
|
@ -63,14 +65,15 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
|
||||
if (Enabled)
|
||||
{
|
||||
var key = GenerateKey(context);
|
||||
var cacheKey = new CacheTagKey(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();
|
||||
|
||||
|
|
@ -85,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
// The returned value is ignored, we only do this so that
|
||||
// the compiler doesn't complain about the returned task
|
||||
// not being awaited
|
||||
var localTcs = MemoryCache.Set(key, tcs.Task, options);
|
||||
var localTcs = MemoryCache.Set(cacheKey, tcs.Task, options);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -93,7 +96,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
// task so that the expiration options are are not impacted
|
||||
// by the time it took to compute it.
|
||||
|
||||
using (var entry = MemoryCache.CreateEntry(key))
|
||||
using (var entry = MemoryCache.CreateEntry(cacheKey))
|
||||
{
|
||||
// The result is processed inside an entry
|
||||
// such that the tokens are inherited.
|
||||
|
|
@ -167,16 +170,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -68,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
public string VaryBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of a HTTP request header to vary the cached result by.
|
||||
/// Gets or sets a comma-delimited set of HTTP request headers to vary the cached result by.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByHeaderAttributeName)]
|
||||
public string VaryByHeader { get; set; }
|
||||
|
|
@ -122,191 +118,5 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
[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(")");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -73,9 +73,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
|
||||
if (Enabled)
|
||||
{
|
||||
var key = GenerateKey(context);
|
||||
var cacheKey = new CacheTagKey(this);
|
||||
|
||||
content = await _distributedCacheService.ProcessContentAsync(output, key, GetDistributedCacheEntryOptions());
|
||||
content = await _distributedCacheService.ProcessContentAsync(output, cacheKey, GetDistributedCacheEntryOptions());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -109,15 +109,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
|
||||
return options;
|
||||
}
|
||||
|
||||
protected override string GetUniqueId(TagHelperContext context)
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
protected override string GetKeyPrefix(TagHelperContext context)
|
||||
{
|
||||
return CacheKeyPrefix;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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 Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers.Internal
|
||||
{
|
||||
internal static class MvcTagHelperLoggerExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, string, Exception> _distributedFormatterDeserializedFailed;
|
||||
|
||||
static MvcTagHelperLoggerExtensions()
|
||||
{
|
||||
_distributedFormatterDeserializedFailed = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
1,
|
||||
"Couldn't deserialize cached value for key {Key}.");
|
||||
}
|
||||
|
||||
public static void DistributedFormatterDeserializationException(this ILogger logger, string key, Exception exception)
|
||||
{
|
||||
_distributedFormatterDeserializedFailed(logger, key, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
|
@ -15,6 +11,7 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
|
|
@ -30,218 +27,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
{
|
||||
public class CacheTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateKey_ReturnsKeyBasedOnTagHelperUniqueId()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var tagHelperContext = GetTagHelperContext(id);
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
var expected = GetHashedBytes("CacheTagHelper||" + id);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Vary-By-Value")]
|
||||
[InlineData("Vary with spaces")]
|
||||
[InlineData(" Vary with more spaces ")]
|
||||
public void GenerateKey_UsesVaryByPropertyToGenerateKey(string varyBy)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryBy = varyBy
|
||||
};
|
||||
var expected = GetHashedBytes("CacheTagHelper||testid||VaryBy||" + varyBy);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cookie0", "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value)")]
|
||||
[InlineData("Cookie0,Cookie1",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData("Cookie0, Cookie1",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData(" Cookie0, , Cookie1 ",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData(",Cookie0,,Cookie1,",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
public void GenerateKey_UsesVaryByCookieName(string varyByCookie, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByCookie = varyByCookie
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.Headers["Cookie"] =
|
||||
"Cookie0=Cookie0Value;Cookie1=Cookie1Value";
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Accept-Language", "CacheTagHelper||testid||VaryByHeader(Accept-Language||en-us;charset=utf8)")]
|
||||
[InlineData("X-CustomHeader,Accept-Encoding, NotAvailable",
|
||||
"CacheTagHelper||testid||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
|
||||
[InlineData("X-CustomHeader, , Accept-Encoding, NotAvailable",
|
||||
"CacheTagHelper||testid||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
|
||||
public void GenerateKey_UsesVaryByHeader(string varyByHeader, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByHeader = varyByHeader
|
||||
};
|
||||
var headers = cacheTagHelper.ViewContext.HttpContext.Request.Headers;
|
||||
headers["Accept-Language"] = "en-us;charset=utf8";
|
||||
headers["Accept-Encoding"] = "utf8";
|
||||
headers["X-CustomHeader"] = "Header-Value";
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("category", "CacheTagHelper||testid||VaryByQuery(category||cats)")]
|
||||
[InlineData("Category,SortOrder,SortOption",
|
||||
"CacheTagHelper||testid||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
|
||||
[InlineData("Category, SortOrder, SortOption, ",
|
||||
"CacheTagHelper||testid||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
|
||||
public void GenerateKey_UsesVaryByQuery(string varyByQuery, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByQuery = varyByQuery
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.QueryString =
|
||||
new QueryString("?sortoption=Adorability&Category=cats&sortOrder=");
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("id", "CacheTagHelper||testid||VaryByRoute(id||4)")]
|
||||
[InlineData("Category,,Id,OptionRouteValue",
|
||||
"CacheTagHelper||testid||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
|
||||
[InlineData(" Category, , Id, OptionRouteValue, ",
|
||||
"CacheTagHelper||testid||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
|
||||
public void GenerateKey_UsesVaryByRoute(string varyByRoute, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByRoute = varyByRoute
|
||||
};
|
||||
cacheTagHelper.ViewContext.RouteData.Values["id"] = 4;
|
||||
cacheTagHelper.ViewContext.RouteData.Values["category"] = "MyCategory";
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_UsesVaryByUser_WhenUserIsNotAuthenticated()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "CacheTagHelper||testid||VaryByUser||";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_UsesVaryByUserAndAuthenticatedUserName()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "CacheTagHelper||testid||VaryByUser||test_name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true
|
||||
};
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "test_name") });
|
||||
cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_WithMultipleVaryByOptions_CreatesCombinedKey()
|
||||
{
|
||||
// Arrange
|
||||
var expected = GetHashedBytes("CacheTagHelper||testid||VaryBy||custom-value||" +
|
||||
"VaryByHeader(content-type||text/html)||VaryByUser||someuser");
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true,
|
||||
VaryByHeader = "content-type",
|
||||
VaryBy = "custom-value"
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.Headers["Content-Type"] = "text/html";
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "someuser") });
|
||||
cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_DoesNotCache_IfDisabled()
|
||||
{
|
||||
|
|
@ -284,21 +69,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
// Arrange
|
||||
var id = "unique-id";
|
||||
var childContent = "original-child-content";
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
var value = new Mock<ICacheEntry>();
|
||||
value.Setup(c => c.Value).Returns(new DefaultTagHelperContent().SetContent("ok"));
|
||||
value.Setup(c => c.ExpirationTokens).Returns(new List<IChangeToken>());
|
||||
cache.Setup(c => c.CreateEntry(
|
||||
/*key*/ It.IsAny<string>()))
|
||||
.Returns((object key) => value.Object);
|
||||
object cacheResult;
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), out cacheResult))
|
||||
.Returns(false);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var tagHelperContext = GetTagHelperContext(id);
|
||||
var tagHelperOutput = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
childContent: childContent);
|
||||
var cacheTagHelper = new CacheTagHelper(cache.Object, new HtmlTestEncoder())
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = true
|
||||
|
|
@ -312,11 +88,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
Assert.Empty(tagHelperOutput.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput.Content.GetContent());
|
||||
|
||||
// There are two calls to set (for the TCS and the processed value)
|
||||
cache.Verify(c => c.CreateEntry(
|
||||
/*key*/ It.IsAny<string>()),
|
||||
Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -686,12 +457,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
{
|
||||
ViewContext = GetViewContext(),
|
||||
};
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
|
||||
Task<IHtmlContent> cachedValue;
|
||||
var result = cache.TryGetValue(key, out cachedValue);
|
||||
var result = cache.TryGetValue(cacheTagKey, out cachedValue);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Equal("HtmlEncode[[some-content]]", tagHelperOutput.Content.GetContent());
|
||||
|
|
@ -699,7 +472,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
|
||||
// Act - 2
|
||||
tokenSource.Cancel();
|
||||
result = cache.TryGetValue(key, out cachedValue);
|
||||
result = cache.TryGetValue(cacheTagKey, out cachedValue);
|
||||
|
||||
// Assert - 2
|
||||
Assert.False(result);
|
||||
|
|
@ -910,26 +683,5 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||
});
|
||||
}
|
||||
|
||||
private static TagHelperOutput GetTagHelperOutput(
|
||||
Func<bool, HtmlEncoder, Task<TagHelperContent>> processAsync)
|
||||
{
|
||||
var attributes = new TagHelperAttributeList { { "attr", "value" } };
|
||||
|
||||
return new TagHelperOutput(
|
||||
"cache",
|
||||
attributes,
|
||||
getChildContentAsync: processAsync);
|
||||
}
|
||||
|
||||
private static string GetHashedBytes(string input)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(input);
|
||||
var hashedBytes = sha.ComputeHash(contentBytes);
|
||||
return Convert.ToBase64String(hashedBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,701 @@
|
|||
// 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.Generic;
|
||||
using System.IO;
|
||||
<<<<<<< HEAD
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
=======
|
||||
>>>>>>> [Fixes #4246] Introducing CacheTagKey
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Extensions.WebEncoders.Testing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
{
|
||||
public class CacheTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProcessAsync_DoesNotCache_IfDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var id = "unique-id";
|
||||
var childContent = "original-child-content";
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
var value = new Mock<ICacheEntry>();
|
||||
value.Setup(c => c.Value).Returns(new DefaultTagHelperContent().SetContent("ok"));
|
||||
cache.Setup(c => c.CreateEntry(
|
||||
/*key*/ It.IsAny<string>()))
|
||||
.Returns((object key) => value.Object)
|
||||
.Verifiable();
|
||||
object cacheResult;
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), out cacheResult))
|
||||
.Returns(false);
|
||||
var tagHelperContext = GetTagHelperContext(id);
|
||||
var tagHelperOutput = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
childContent: childContent);
|
||||
var cacheTagHelper = new CacheTagHelper(cache.Object, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
// Act
|
||||
await cacheTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(childContent, tagHelperOutput.Content.GetContent());
|
||||
cache.Verify(c => c.CreateEntry(
|
||||
/*key*/ It.IsAny<string>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ReturnsCachedValue_IfEnabled()
|
||||
{
|
||||
// Arrange
|
||||
var id = "unique-id";
|
||||
var childContent = "original-child-content";
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var tagHelperContext = GetTagHelperContext(id);
|
||||
var tagHelperOutput = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
childContent: childContent);
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
// Act
|
||||
await cacheTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(tagHelperOutput.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput.Content.GetContent());
|
||||
<<<<<<< HEAD
|
||||
|
||||
// There are two calls to set (for the TCS and the processed value)
|
||||
cache.Verify(c => c.CreateEntry(
|
||||
/*key*/ It.IsAny<string>()),
|
||||
Times.Exactly(2));
|
||||
=======
|
||||
>>>>>>> [Fixes #4246] Introducing CacheTagKey
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ReturnsCachedValue_IfVaryByParamIsUnchanged()
|
||||
{
|
||||
// Arrange - 1
|
||||
var id = "unique-id";
|
||||
var childContent = "original-child-content";
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var tagHelperContext1 = GetTagHelperContext(id);
|
||||
var tagHelperOutput1 = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
childContent: childContent);
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
VaryByQuery = "key1,key2",
|
||||
ViewContext = GetViewContext(),
|
||||
};
|
||||
cacheTagHelper1.ViewContext.HttpContext.Request.QueryString = new QueryString(
|
||||
"?key1=value1&key2=value2");
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput1.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput1.Content.GetContent());
|
||||
|
||||
// Arrange - 2
|
||||
var tagHelperContext2 = GetTagHelperContext(id);
|
||||
var tagHelperOutput2 = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
childContent: "different-content");
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
VaryByQuery = "key1,key2",
|
||||
ViewContext = GetViewContext(),
|
||||
};
|
||||
cacheTagHelper2.ViewContext.HttpContext.Request.QueryString = new QueryString(
|
||||
"?key1=value1&key2=value2");
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput2.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_RecalculatesValueIfCacheKeyChanges()
|
||||
{
|
||||
// Arrange - 1
|
||||
var id = "unique-id";
|
||||
var childContent1 = "original-child-content";
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var tagHelperContext1 = GetTagHelperContext(id);
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
tagHelperOutput1.PreContent.Append("<cache>");
|
||||
tagHelperOutput1.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
VaryByCookie = "cookie1,cookie2",
|
||||
ViewContext = GetViewContext(),
|
||||
};
|
||||
cacheTagHelper1.ViewContext.HttpContext.Request.Headers["Cookie"] = "cookie1=value1;cookie2=value2";
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput1.IsContentModified);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content.GetContent());
|
||||
|
||||
// Arrange - 2
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id);
|
||||
var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2);
|
||||
tagHelperOutput2.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput2.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
VaryByCookie = "cookie1,cookie2",
|
||||
ViewContext = GetViewContext(),
|
||||
};
|
||||
cacheTagHelper2.ViewContext.HttpContext.Request.Headers["Cookie"] = "cookie1=value1;cookie2=not-value2";
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheEntryOptions_SetsAbsoluteExpiration_IfExpiresOnIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expiresOn = DateTimeOffset.UtcNow.AddMinutes(4);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ExpiresOn = expiresOn
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheEntryOptions = cacheTagHelper.GetMemoryCacheEntryOptions();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expiresOn, cacheEntryOptions.AbsoluteExpiration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheEntryOptions_SetsAbsoluteExpiration_IfExpiresAfterIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expiresAfter = TimeSpan.FromSeconds(42);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ExpiresAfter = expiresAfter
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheEntryOptions = cacheTagHelper.GetMemoryCacheEntryOptions();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expiresAfter, cacheEntryOptions.AbsoluteExpirationRelativeToNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheEntryOptions_SetsSlidingExpiration_IfExpiresSlidingIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expiresSliding = TimeSpan.FromSeconds(37);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ExpiresSliding = expiresSliding
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheEntryOptions = cacheTagHelper.GetMemoryCacheEntryOptions();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expiresSliding, cacheEntryOptions.SlidingExpiration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheEntryOptions_SetsCachePreservationPriority()
|
||||
{
|
||||
// Arrange
|
||||
var priority = CacheItemPriority.High;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
Priority = priority
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheEntryOptions = cacheTagHelper.GetMemoryCacheEntryOptions();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(priority, cacheEntryOptions.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_UsesExpiresAfter_ToExpireCacheEntry()
|
||||
{
|
||||
// Arrange - 1
|
||||
var currentTime = new DateTimeOffset(2010, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var id = "unique-id";
|
||||
var childContent1 = "original-child-content";
|
||||
var clock = new Mock<ISystemClock>();
|
||||
clock.SetupGet(p => p.UtcNow)
|
||||
.Returns(() => currentTime);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions { Clock = clock.Object });
|
||||
var tagHelperContext1 = GetTagHelperContext(id);
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
tagHelperOutput1.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput1.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
ExpiresAfter = TimeSpan.FromMinutes(10)
|
||||
};
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput1.IsContentModified);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content.GetContent());
|
||||
|
||||
// Arrange - 2
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id);
|
||||
var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2);
|
||||
tagHelperOutput2.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput2.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
ExpiresAfter = TimeSpan.FromMinutes(10)
|
||||
};
|
||||
currentTime = currentTime.AddMinutes(11);
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_UsesExpiresOn_ToExpireCacheEntry()
|
||||
{
|
||||
// Arrange - 1
|
||||
var currentTime = new DateTimeOffset(2010, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var id = "unique-id";
|
||||
var childContent1 = "original-child-content";
|
||||
var clock = new Mock<ISystemClock>();
|
||||
clock.SetupGet(p => p.UtcNow)
|
||||
.Returns(() => currentTime);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions { Clock = clock.Object });
|
||||
var tagHelperContext1 = GetTagHelperContext(id);
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
tagHelperOutput1.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput1.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
ExpiresOn = currentTime.AddMinutes(5)
|
||||
};
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput1.IsContentModified);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content.GetContent());
|
||||
|
||||
// Arrange - 2
|
||||
currentTime = currentTime.AddMinutes(5).AddSeconds(2);
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id);
|
||||
var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2);
|
||||
tagHelperOutput2.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput2.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
ExpiresOn = currentTime.AddMinutes(5)
|
||||
};
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_UsesExpiresSliding_ToExpireCacheEntryWithSlidingExpiration()
|
||||
{
|
||||
// Arrange - 1
|
||||
var currentTime = new DateTimeOffset(2010, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var id = "unique-id";
|
||||
var childContent1 = "original-child-content";
|
||||
var clock = new Mock<ISystemClock>();
|
||||
clock.SetupGet(p => p.UtcNow)
|
||||
.Returns(() => currentTime);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions { Clock = clock.Object });
|
||||
var tagHelperContext1 = GetTagHelperContext(id);
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
tagHelperOutput1.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput1.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
ExpiresSliding = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput1.IsContentModified);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content.GetContent());
|
||||
|
||||
// Arrange - 2
|
||||
currentTime = currentTime.AddSeconds(35);
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id);
|
||||
var tagHelperOutput2 = GetTagHelperOutput(childContent: childContent2);
|
||||
tagHelperOutput2.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput2.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
ExpiresSliding = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_FlowsEntryLinkThatAllowsAddingTriggersToAddedEntry()
|
||||
{
|
||||
// Arrange
|
||||
var id = "some-id";
|
||||
var expectedContent = new DefaultTagHelperContent();
|
||||
expectedContent.SetContent("some-content");
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheEntryOptions = new MemoryCacheEntryOptions()
|
||||
.AddExpirationToken(new CancellationChangeToken(tokenSource.Token));
|
||||
var tagHelperContext = new TagHelperContext(
|
||||
allAttributes: new TagHelperAttributeList(),
|
||||
items: new Dictionary<object, object>(),
|
||||
uniqueId: id);
|
||||
var tagHelperOutput = new TagHelperOutput(
|
||||
"cache",
|
||||
new TagHelperAttributeList { { "attr", "value" } },
|
||||
getChildContentAsync: (useCachedResult, encoder) =>
|
||||
{
|
||||
TagHelperContent tagHelperContent;
|
||||
if (!cache.TryGetValue("key1", out tagHelperContent))
|
||||
{
|
||||
tagHelperContent = expectedContent;
|
||||
cache.Set("key1", tagHelperContent, cacheEntryOptions);
|
||||
}
|
||||
|
||||
return Task.FromResult(tagHelperContent);
|
||||
});
|
||||
tagHelperOutput.PreContent.SetContent("<cache>");
|
||||
tagHelperOutput.PostContent.SetContent("</cache>");
|
||||
var cacheTagHelper = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
};
|
||||
|
||||
var cacheTagKey = CacheTagKey.From(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
|
||||
Task<IHtmlContent> cachedValue;
|
||||
var result = cache.TryGetValue(cacheTagKey, out cachedValue);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Equal("HtmlEncode[[some-content]]", tagHelperOutput.Content.GetContent());
|
||||
Assert.True(result);
|
||||
|
||||
// Act - 2
|
||||
tokenSource.Cancel();
|
||||
result = cache.TryGetValue(cacheTagKey, out cachedValue);
|
||||
|
||||
// Assert - 2
|
||||
Assert.False(result);
|
||||
Assert.Null(cachedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ComputesValueOnce_WithConcurrentRequests()
|
||||
{
|
||||
// Arrange
|
||||
var id = "unique-id";
|
||||
var childContent = "some-content";
|
||||
var resetEvent1 = new ManualResetEvent(false);
|
||||
var resetEvent2 = new ManualResetEvent(false);
|
||||
var resetEvent3 = new ManualResetEvent(false);
|
||||
var calls = 0;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
var tagHelperContext1 = GetTagHelperContext(id + 1);
|
||||
var tagHelperContext2 = GetTagHelperContext(id + 2);
|
||||
|
||||
var tagHelperOutput1 = new TagHelperOutput(
|
||||
"cache",
|
||||
new TagHelperAttributeList(),
|
||||
getChildContentAsync: (useCachedResult, encoder) =>
|
||||
{
|
||||
calls++;
|
||||
resetEvent2.Set();
|
||||
|
||||
var tagHelperContent = new DefaultTagHelperContent();
|
||||
tagHelperContent.SetHtmlContent(childContent);
|
||||
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||
});
|
||||
|
||||
var tagHelperOutput2 = new TagHelperOutput(
|
||||
"cache",
|
||||
new TagHelperAttributeList(),
|
||||
getChildContentAsync: (useCachedResult, encoder) =>
|
||||
{
|
||||
calls++;
|
||||
resetEvent3.WaitOne(5000);
|
||||
|
||||
var tagHelperContent = new DefaultTagHelperContent();
|
||||
tagHelperContent.SetHtmlContent(childContent);
|
||||
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||
});
|
||||
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
var task1 = Task.Run(async () =>
|
||||
{
|
||||
resetEvent1.WaitOne(5000);
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
resetEvent3.Set();
|
||||
});
|
||||
|
||||
var task2 = Task.Run(async () =>
|
||||
{
|
||||
resetEvent2.WaitOne(5000);
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext1, tagHelperOutput2);
|
||||
});
|
||||
|
||||
resetEvent1.Set();
|
||||
await Task.WhenAll(task1, task2);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput1.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput1.Content.GetContent());
|
||||
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput2.Content.GetContent());
|
||||
|
||||
Assert.Equal(1, calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ExceptionInProcessing_DoesntBlockConcurrentRequests()
|
||||
{
|
||||
// Arrange
|
||||
var id = "unique-id";
|
||||
var childContent = "some-content";
|
||||
var resetEvent1 = new ManualResetEvent(false);
|
||||
var resetEvent2 = new ManualResetEvent(false);
|
||||
var resetEvent3 = new ManualResetEvent(false);
|
||||
var calls = 0;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
var tagHelperContext1 = GetTagHelperContext(id + 1);
|
||||
var tagHelperContext2 = GetTagHelperContext(id + 2);
|
||||
|
||||
var tagHelperOutput1 = new TagHelperOutput(
|
||||
"cache",
|
||||
new TagHelperAttributeList(),
|
||||
getChildContentAsync: (useCachedResult, encoder) =>
|
||||
{
|
||||
calls++;
|
||||
resetEvent2.Set();
|
||||
|
||||
throw new Exception();
|
||||
});
|
||||
|
||||
var tagHelperOutput2 = new TagHelperOutput(
|
||||
"cache",
|
||||
new TagHelperAttributeList(),
|
||||
getChildContentAsync: (useCachedResult, encoder) =>
|
||||
{
|
||||
calls++;
|
||||
resetEvent3.WaitOne(5000);
|
||||
|
||||
var tagHelperContent = new DefaultTagHelperContent();
|
||||
tagHelperContent.SetHtmlContent(childContent);
|
||||
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||
});
|
||||
|
||||
var cacheTagHelper1 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
var cacheTagHelper2 = new CacheTagHelper(cache, new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Enabled = true
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
var task1 = Task.Run(async () =>
|
||||
{
|
||||
resetEvent1.WaitOne(5000);
|
||||
await Assert.ThrowsAsync<Exception>(() => cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1));
|
||||
resetEvent3.Set();
|
||||
});
|
||||
|
||||
var task2 = Task.Run(async () =>
|
||||
{
|
||||
resetEvent2.WaitOne(5000);
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
});
|
||||
|
||||
resetEvent1.Set();
|
||||
await Task.WhenAll(task1, task2);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(tagHelperOutput1.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput1.PostContent.GetContent());
|
||||
Assert.False(tagHelperOutput1.IsContentModified);
|
||||
Assert.Empty(tagHelperOutput1.Content.GetContent());
|
||||
|
||||
Assert.Empty(tagHelperOutput2.PreContent.GetContent());
|
||||
Assert.Empty(tagHelperOutput2.PostContent.GetContent());
|
||||
Assert.True(tagHelperOutput2.IsContentModified);
|
||||
Assert.Equal(childContent, tagHelperOutput2.Content.GetContent());
|
||||
|
||||
Assert.Equal(2, calls);
|
||||
}
|
||||
|
||||
private static ViewContext GetViewContext()
|
||||
{
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
return new ViewContext(actionContext,
|
||||
Mock.Of<IView>(),
|
||||
new ViewDataDictionary(new EmptyModelMetadataProvider()),
|
||||
Mock.Of<ITempDataDictionary>(),
|
||||
TextWriter.Null,
|
||||
new HtmlHelperOptions());
|
||||
}
|
||||
|
||||
private static TagHelperContext GetTagHelperContext(string id = "testid")
|
||||
{
|
||||
return new TagHelperContext(
|
||||
allAttributes: new TagHelperAttributeList(),
|
||||
items: new Dictionary<object, object>(),
|
||||
uniqueId: id);
|
||||
}
|
||||
|
||||
private static TagHelperOutput GetTagHelperOutput(
|
||||
string tagName = "cache",
|
||||
TagHelperAttributeList attributes = null,
|
||||
string childContent = "some child content")
|
||||
{
|
||||
attributes = attributes ?? new TagHelperAttributeList { { "attr", "value" } };
|
||||
|
||||
return new TagHelperOutput(
|
||||
tagName,
|
||||
attributes,
|
||||
getChildContentAsync: (useCachedResult, encoder) =>
|
||||
{
|
||||
var tagHelperContent = new DefaultTagHelperContent();
|
||||
tagHelperContent.SetHtmlContent(childContent);
|
||||
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
// 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.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers.Cache;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.WebEncoders.Testing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
{
|
||||
public class CacheTagKeyTest
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateKey_ReturnsKeyBasedOnTagHelperUniqueId()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var tagHelperContext = GetTagHelperContext(id);
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
var expected = "CacheTagHelper||" + id;
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_ReturnsTrueOnSameKey()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var tagHelperContext1 = GetTagHelperContext(id);
|
||||
var cacheTagHelper1 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
var tagHelperContext2 = GetTagHelperContext(id);
|
||||
var cacheTagHelper2 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheTagKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
|
||||
var cacheTagKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(cacheTagKey1, cacheTagKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_ReturnsFalseOnDifferentKey()
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext1 = GetTagHelperContext("some-id");
|
||||
var cacheTagHelper1 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
var tagHelperContext2 = GetTagHelperContext("some-other-id");
|
||||
var cacheTagHelper2 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheTagKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
|
||||
var cacheTagKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(cacheTagKey1, cacheTagKey2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_IsSameForSimilarCacheTagHelper()
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext1 = GetTagHelperContext("some-id");
|
||||
var cacheTagHelper1 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
var tagHelperContext2 = GetTagHelperContext("some-id");
|
||||
var cacheTagHelper2 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
var cacheKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
|
||||
var cacheKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);
|
||||
|
||||
// Act
|
||||
var hashcode1 = cacheKey1.GetHashCode();
|
||||
var hashcode2 = cacheKey2.GetHashCode();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(hashcode1, hashcode2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_VariesByUniqueId()
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext1 = GetTagHelperContext("some-id");
|
||||
var cacheTagHelper1 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
var tagHelperContext2 = GetTagHelperContext("some-other-id");
|
||||
var cacheTagHelper2 = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext()
|
||||
};
|
||||
|
||||
var cacheKey1 = new CacheTagKey(cacheTagHelper1, tagHelperContext1);
|
||||
var cacheKey2 = new CacheTagKey(cacheTagHelper2, tagHelperContext2);
|
||||
|
||||
// Act
|
||||
var hashcode1 = cacheKey1.GetHashCode();
|
||||
var hashcode2 = cacheKey2.GetHashCode();
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(hashcode1, hashcode2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_ReturnsKeyBasedOnTagHelperName()
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Name = name
|
||||
};
|
||||
var expected = "DistributedCacheTagHelper||" + name;
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Vary-By-Value")]
|
||||
[InlineData("Vary with spaces")]
|
||||
[InlineData(" Vary with more spaces ")]
|
||||
public void GenerateKey_UsesVaryByPropertyToGenerateKey(string varyBy)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryBy = varyBy
|
||||
};
|
||||
var expected = "CacheTagHelper||testid||VaryBy||" + varyBy;
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cookie0", "CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value)")]
|
||||
[InlineData("Cookie0,Cookie1",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData("Cookie0, Cookie1",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData(" Cookie0, , Cookie1 ",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData(",Cookie0,,Cookie1,",
|
||||
"CacheTagHelper||testid||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
public void GenerateKey_UsesVaryByCookieName(string varyByCookie, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByCookie = varyByCookie
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.Headers["Cookie"] =
|
||||
"Cookie0=Cookie0Value;Cookie1=Cookie1Value";
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Accept-Language", "CacheTagHelper||testid||VaryByHeader(Accept-Language||en-us;charset=utf8)")]
|
||||
[InlineData("X-CustomHeader,Accept-Encoding, NotAvailable",
|
||||
"CacheTagHelper||testid||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
|
||||
[InlineData("X-CustomHeader, , Accept-Encoding, NotAvailable",
|
||||
"CacheTagHelper||testid||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
|
||||
public void GenerateKey_UsesVaryByHeader(string varyByHeader, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByHeader = varyByHeader
|
||||
};
|
||||
var headers = cacheTagHelper.ViewContext.HttpContext.Request.Headers;
|
||||
headers["Accept-Language"] = "en-us;charset=utf8";
|
||||
headers["Accept-Encoding"] = "utf8";
|
||||
headers["X-CustomHeader"] = "Header-Value";
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("category", "CacheTagHelper||testid||VaryByQuery(category||cats)")]
|
||||
[InlineData("Category,SortOrder,SortOption",
|
||||
"CacheTagHelper||testid||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
|
||||
[InlineData("Category, SortOrder, SortOption, ",
|
||||
"CacheTagHelper||testid||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
|
||||
public void GenerateKey_UsesVaryByQuery(string varyByQuery, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByQuery = varyByQuery
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.QueryString =
|
||||
new QueryString("?sortoption=Adorability&Category=cats&sortOrder=");
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("id", "CacheTagHelper||testid||VaryByRoute(id||4)")]
|
||||
[InlineData("Category,,Id,OptionRouteValue",
|
||||
"CacheTagHelper||testid||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
|
||||
[InlineData(" Category, , Id, OptionRouteValue, ",
|
||||
"CacheTagHelper||testid||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
|
||||
public void GenerateKey_UsesVaryByRoute(string varyByRoute, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByRoute = varyByRoute
|
||||
};
|
||||
cacheTagHelper.ViewContext.RouteData.Values["id"] = 4;
|
||||
cacheTagHelper.ViewContext.RouteData.Values["category"] = "MyCategory";
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_UsesVaryByUser_WhenUserIsNotAuthenticated()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "CacheTagHelper||testid||VaryByUser||";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true
|
||||
};
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_UsesVaryByUserAndAuthenticatedUserName()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "CacheTagHelper||testid||VaryByUser||test_name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true
|
||||
};
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "test_name") });
|
||||
cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_WithMultipleVaryByOptions_CreatesCombinedKey()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "CacheTagHelper||testid||VaryBy||custom-value||" +
|
||||
"VaryByHeader(content-type||text/html)||VaryByUser||someuser";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new CacheTagHelper(Mock.Of<IMemoryCache>(), new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true,
|
||||
VaryByHeader = "content-type",
|
||||
VaryBy = "custom-value"
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.Headers["Content-Type"] = "text/html";
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "someuser") });
|
||||
cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);
|
||||
|
||||
// Act
|
||||
var cacheTagKey = new CacheTagKey(cacheTagHelper, tagHelperContext);
|
||||
var key = cacheTagKey.GenerateKey();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
private static ViewContext GetViewContext()
|
||||
{
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
return new ViewContext(actionContext,
|
||||
Mock.Of<IView>(),
|
||||
new ViewDataDictionary(new EmptyModelMetadataProvider()),
|
||||
Mock.Of<ITempDataDictionary>(),
|
||||
TextWriter.Null,
|
||||
new HtmlHelperOptions());
|
||||
}
|
||||
|
||||
private static TagHelperContext GetTagHelperContext(string id = "testid")
|
||||
{
|
||||
return new TagHelperContext(
|
||||
allAttributes: new TagHelperAttributeList(),
|
||||
items: new Dictionary<object, object>(),
|
||||
uniqueId: id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -21,6 +19,7 @@ using Microsoft.AspNetCore.Routing;
|
|||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Microsoft.Extensions.WebEncoders.Testing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -29,253 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
{
|
||||
public class DistributedCacheTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateKey_ReturnsKeyBasedOnTagHelperName()
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
Name = name
|
||||
};
|
||||
var expected = GetHashedBytes("DistributedCacheTagHelper||" + name);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Vary-By-Value")]
|
||||
[InlineData("Vary with spaces")]
|
||||
[InlineData(" Vary with more spaces ")]
|
||||
public void GenerateKey_UsesVaryByPropertyToGenerateKey(string varyBy)
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryBy = varyBy,
|
||||
Name = name
|
||||
};
|
||||
var expected = GetHashedBytes("DistributedCacheTagHelper||some-name||VaryBy||" + varyBy);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Cookie0", "DistributedCacheTagHelper||some-name||VaryByCookie(Cookie0||Cookie0Value)")]
|
||||
[InlineData("Cookie0,Cookie1",
|
||||
"DistributedCacheTagHelper||some-name||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData("Cookie0, Cookie1",
|
||||
"DistributedCacheTagHelper||some-name||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData(" Cookie0, , Cookie1 ",
|
||||
"DistributedCacheTagHelper||some-name||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
[InlineData(",Cookie0,,Cookie1,",
|
||||
"DistributedCacheTagHelper||some-name||VaryByCookie(Cookie0||Cookie0Value||Cookie1||Cookie1Value)")]
|
||||
public void GenerateKey_UsesVaryByCookieName(string varyByCookie, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByCookie = varyByCookie,
|
||||
Name = name
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.Headers["Cookie"] =
|
||||
"Cookie0=Cookie0Value;Cookie1=Cookie1Value";
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Accept-Language", "DistributedCacheTagHelper||some-name||VaryByHeader(Accept-Language||en-us;charset=utf8)")]
|
||||
[InlineData("X-CustomHeader,Accept-Encoding, NotAvailable",
|
||||
"DistributedCacheTagHelper||some-name||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
|
||||
[InlineData("X-CustomHeader, , Accept-Encoding, NotAvailable",
|
||||
"DistributedCacheTagHelper||some-name||VaryByHeader(X-CustomHeader||Header-Value||Accept-Encoding||utf8||NotAvailable||)")]
|
||||
public void GenerateKey_UsesVaryByHeader(string varyByHeader, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByHeader = varyByHeader,
|
||||
Name = name
|
||||
};
|
||||
var headers = cacheTagHelper.ViewContext.HttpContext.Request.Headers;
|
||||
headers["Accept-Language"] = "en-us;charset=utf8";
|
||||
headers["Accept-Encoding"] = "utf8";
|
||||
headers["X-CustomHeader"] = "Header-Value";
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("category", "DistributedCacheTagHelper||some-name||VaryByQuery(category||cats)")]
|
||||
[InlineData("Category,SortOrder,SortOption",
|
||||
"DistributedCacheTagHelper||some-name||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
|
||||
[InlineData("Category, SortOrder, SortOption, ",
|
||||
"DistributedCacheTagHelper||some-name||VaryByQuery(Category||cats||SortOrder||||SortOption||Adorability)")]
|
||||
public void GenerateKey_UsesVaryByQuery(string varyByQuery, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByQuery = varyByQuery,
|
||||
Name = name
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.QueryString =
|
||||
new QueryString("?sortoption=Adorability&Category=cats&sortOrder=");
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("id", "DistributedCacheTagHelper||some-name||VaryByRoute(id||4)")]
|
||||
[InlineData("Category,,Id,OptionRouteValue",
|
||||
"DistributedCacheTagHelper||some-name||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
|
||||
[InlineData(" Category, , Id, OptionRouteValue, ",
|
||||
"DistributedCacheTagHelper||some-name||VaryByRoute(Category||MyCategory||Id||4||OptionRouteValue||)")]
|
||||
public void GenerateKey_UsesVaryByRoute(string varyByRoute, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByRoute = varyByRoute,
|
||||
Name = name
|
||||
};
|
||||
cacheTagHelper.ViewContext.RouteData.Values["id"] = 4;
|
||||
cacheTagHelper.ViewContext.RouteData.Values["category"] = "MyCategory";
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_UsesVaryByUser_WhenUserIsNotAuthenticated()
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var expected = "DistributedCacheTagHelper||some-name||VaryByUser||";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true,
|
||||
Name = name
|
||||
};
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_UsesVaryByUserAndAuthenticatedUserName()
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var expected = "DistributedCacheTagHelper||some-name||VaryByUser||test_name";
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true,
|
||||
Name = name
|
||||
};
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "test_name") });
|
||||
cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(GetHashedBytes(expected), key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateKey_WithMultipleVaryByOptions_CreatesCombinedKey()
|
||||
{
|
||||
// Arrange
|
||||
var name = "some-name";
|
||||
var expected = GetHashedBytes("DistributedCacheTagHelper||some-name||VaryBy||custom-value||" +
|
||||
"VaryByHeader(content-type||text/html)||VaryByUser||someuser");
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
Mock.Of<IDistributedCacheTagHelperService>(),
|
||||
new HtmlTestEncoder())
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByUser = true,
|
||||
VaryByHeader = "content-type",
|
||||
VaryBy = "custom-value",
|
||||
Name = name
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.Headers["Content-Type"] = "text/html";
|
||||
var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "someuser") });
|
||||
cacheTagHelper.ViewContext.HttpContext.User = new ClaimsPrincipal(identity);
|
||||
|
||||
// Act
|
||||
var key = cacheTagHelper.GenerateKey(tagHelperContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, key);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_DoesNotCache_IfDisabled()
|
||||
{
|
||||
|
|
@ -293,7 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage.Object,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder());
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance);
|
||||
var tagHelperOutput = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
childContent: childContent);
|
||||
|
|
@ -333,7 +87,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage.Object,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder());
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance);
|
||||
var tagHelperContext = GetTagHelperContext();
|
||||
var tagHelperOutput = GetTagHelperOutput(
|
||||
attributes: new TagHelperAttributeList(),
|
||||
|
|
@ -383,7 +138,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
formatter,
|
||||
new HtmlTestEncoder());
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance);
|
||||
var cacheTagHelper1 = new DistributedCacheTagHelper(
|
||||
service,
|
||||
new HtmlTestEncoder())
|
||||
|
|
@ -439,7 +195,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder());
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance);
|
||||
var tagHelperContext1 = GetTagHelperContext();
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
tagHelperOutput1.PreContent.Append("<cache>");
|
||||
|
|
@ -496,7 +253,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
service,
|
||||
|
|
@ -521,7 +279,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
service,
|
||||
|
|
@ -546,7 +305,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var cacheTagHelper = new DistributedCacheTagHelper(
|
||||
service,
|
||||
|
|
@ -575,7 +335,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var tagHelperContext1 = GetTagHelperContext();
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
|
|
@ -636,7 +397,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var tagHelperContext1 = GetTagHelperContext();
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
|
|
@ -697,7 +459,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
Mock.Of<IDistributedCacheTagHelperFormatter>(),
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var tagHelperContext1 = GetTagHelperContext();
|
||||
var tagHelperOutput1 = GetTagHelperOutput(childContent: childContent1);
|
||||
|
|
@ -759,7 +522,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
formatter,
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var tagHelperContext1 = GetTagHelperContext();
|
||||
var tagHelperContext2 = GetTagHelperContext();
|
||||
|
|
@ -852,7 +616,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
var service = new DistributedCacheTagHelperService(
|
||||
storage,
|
||||
formatter,
|
||||
new HtmlTestEncoder()
|
||||
new HtmlTestEncoder(),
|
||||
NullLoggerFactory.Instance
|
||||
);
|
||||
var tagHelperContext1 = GetTagHelperContext();
|
||||
var tagHelperContext2 = GetTagHelperContext();
|
||||
|
|
@ -985,16 +750,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
});
|
||||
}
|
||||
|
||||
private static string GetHashedBytes(string input)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var contentBytes = Encoding.UTF8.GetBytes(input);
|
||||
var hashedBytes = sha.ComputeHash(contentBytes);
|
||||
return Convert.ToBase64String(hashedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static IDistributedCacheTagHelperStorage GetStorage(MemoryCacheOptions options = null)
|
||||
{
|
||||
return new DistributedCacheTagHelperStorage(new MemoryDistributedCache(new MemoryCache(options ?? new MemoryCacheOptions())));
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -13,7 +13,8 @@
|
|||
"type": "build"
|
||||
},
|
||||
"Microsoft.AspNetCore.Testing": "1.0.0-*",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*"
|
||||
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
|
||||
"Microsoft.Extensions.Logging.Testing": "1.0.0-*"
|
||||
},
|
||||
"testRunner": "xunit",
|
||||
"frameworks": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue