parent
eb1eca9e1a
commit
80ada8d01b
|
|
@ -0,0 +1,266 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Cache.Memory;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="TagHelper"/> implementation targeting <cache> elements.
|
||||
/// </summary>
|
||||
public class CacheTagHelper : TagHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 VaryByAttributeName = "vary-by";
|
||||
private const string VaryByHeaderAttributeName = "vary-by-header";
|
||||
private const string VaryByQueryAttributeName = "vary-by-query";
|
||||
private const string VaryByRouteAttributeName = "vary-by-route";
|
||||
private const string VaryByCookieAttributeName = "vary-by-cookie";
|
||||
private const string VaryByUserAttributeName = "vary-by-user";
|
||||
private const string ExpiresOnAttributeName = "expires-on";
|
||||
private const string ExpiresAfterAttributeName = "expires-after";
|
||||
private const string ExpiresSlidingAttributeName = "expires-sliding";
|
||||
private const string CachePriorityAttributeName = "priority";
|
||||
private const string CacheKeyTokenSeparator = "||";
|
||||
private static readonly char[] AttributeSeparator = new[] { ',' };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="IMemoryCache"/> instance used to cache entries.
|
||||
/// </summary>
|
||||
[Activate]
|
||||
protected internal IMemoryCache MemoryCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ViewContext"/> for the current executing View.
|
||||
/// </summary>
|
||||
[Activate]
|
||||
protected internal ViewContext ViewContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a <see cref="string" /> to vary the cached result by.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByAttributeName)]
|
||||
public string VaryBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of a HTTP request header to vary the cached result by.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByHeaderAttributeName)]
|
||||
public string VaryByHeader { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma-delimited set of query parameters to vary the cached result by.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByQueryAttributeName)]
|
||||
public string VaryByQuery { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma-delimited set of route data parameters to vary the cached result by.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByRouteAttributeName)]
|
||||
public string VaryByRoute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma-delimited set of cookie names to vary the cached result by.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByCookieAttributeName)]
|
||||
public string VaryByCookie { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that determines if the cached result is to be varied by the Identity for the logged in
|
||||
/// <see cref="HttpContext.User"/>.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(VaryByUserAttributeName)]
|
||||
public bool VaryByUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exact <see cref="DateTimeOffset"/> the cache entry should be evicted.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(ExpiresOnAttributeName)]
|
||||
public DateTimeOffset? ExpiresOn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration, from the time the cache entry was added, when it should be evicted.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(ExpiresAfterAttributeName)]
|
||||
public TimeSpan? ExpiresAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration from last access that the cache entry should be evicted.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(ExpiresSlidingAttributeName)]
|
||||
public TimeSpan? ExpiresSliding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="CachePreservationPriority"/> policy for the cache entry.
|
||||
/// </summary>
|
||||
[HtmlAttributeName(CachePriorityAttributeName)]
|
||||
public CachePreservationPriority? Priority { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var key = GenerateKey(context);
|
||||
string result;
|
||||
if (!MemoryCache.TryGetValue(key, out result))
|
||||
{
|
||||
result = await context.GetChildContentAsync();
|
||||
MemoryCache.Set(key, cacheSetContext =>
|
||||
{
|
||||
UpdateCacheContext(cacheSetContext);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
// Clear the contents of the "cache" element since we don't want to render it.
|
||||
output.SuppressOutput();
|
||||
|
||||
output.Content = result;
|
||||
}
|
||||
|
||||
// Internal for unit testing
|
||||
internal string GenerateKey(TagHelperContext context)
|
||||
{
|
||||
var builder = new StringBuilder(CacheKeyPrefix);
|
||||
builder.Append(CacheKeyTokenSeparator)
|
||||
.Append(context.UniqueId);
|
||||
|
||||
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);
|
||||
AddStringCollectionKey(builder, nameof(VaryByHeader), VaryByHeader, request.Headers);
|
||||
AddStringCollectionKey(builder, nameof(VaryByQuery), VaryByQuery, request.Query);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for unit testing
|
||||
internal void UpdateCacheContext(ICacheSetContext cacheSetContext)
|
||||
{
|
||||
if (ExpiresOn != null)
|
||||
{
|
||||
cacheSetContext.SetAbsoluteExpiration(ExpiresOn.Value);
|
||||
}
|
||||
|
||||
if (ExpiresAfter != null)
|
||||
{
|
||||
cacheSetContext.SetAbsoluteExpiration(ExpiresAfter.Value);
|
||||
}
|
||||
|
||||
if (ExpiresSliding != null)
|
||||
{
|
||||
cacheSetContext.SetSlidingExpiration(ExpiresSliding.Value);
|
||||
}
|
||||
|
||||
if (Priority != null)
|
||||
{
|
||||
cacheSetContext.SetPriority(Priority.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddStringCollectionKey(StringBuilder builder,
|
||||
string keyName,
|
||||
string value,
|
||||
IReadableStringCollection sourceCollection)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
// keyName(param1=value1|param2=value2)
|
||||
builder.Append(CacheKeyTokenSeparator)
|
||||
.Append(keyName)
|
||||
.Append("(");
|
||||
|
||||
var tokenFound = false;
|
||||
foreach (var item in Tokenize(value))
|
||||
{
|
||||
tokenFound = true;
|
||||
|
||||
builder.Append(item)
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(sourceCollection[item])
|
||||
.Append(CacheKeyTokenSeparator);
|
||||
}
|
||||
|
||||
if (tokenFound)
|
||||
{
|
||||
// Remove the trailing separator
|
||||
builder.Length -= CacheKeyTokenSeparator.Length;
|
||||
}
|
||||
|
||||
builder.Append(")");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVaryByRouteKey(StringBuilder builder)
|
||||
{
|
||||
var tokenFound = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(VaryByRoute))
|
||||
{
|
||||
builder.Append(CacheKeyTokenSeparator)
|
||||
.Append(nameof(VaryByRoute))
|
||||
.Append("(");
|
||||
|
||||
foreach (var route in Tokenize(VaryByRoute))
|
||||
{
|
||||
tokenFound = true;
|
||||
|
||||
builder.Append(route)
|
||||
.Append(CacheKeyTokenSeparator)
|
||||
.Append(ViewContext.RouteData.Values[route])
|
||||
.Append(CacheKeyTokenSeparator);
|
||||
}
|
||||
|
||||
if (tokenFound)
|
||||
{
|
||||
builder.Length -= CacheKeyTokenSeparator.Length;
|
||||
}
|
||||
|
||||
builder.Append(")");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> Tokenize(string value)
|
||||
{
|
||||
return value.Split(AttributeSeparator, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(token => token.Trim())
|
||||
.Where(token => token.Length > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
|
||||
"Microsoft.AspNet.Mvc.Razor": ""
|
||||
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
|
||||
"Microsoft.Framework.Cache.Memory": "1.0.0-*",
|
||||
"System.Security.Cryptography.Hashing.Algorithms": "4.0.0-beta-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": {},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
|
|||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Microsoft.AspNet.Security;
|
||||
using Microsoft.Framework.Cache.Memory;
|
||||
using Microsoft.Framework.ConfigurationModel;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.DependencyInjection.NestedProviders;
|
||||
|
|
@ -138,6 +139,9 @@ namespace Microsoft.AspNet.Mvc
|
|||
// Only want one ITagHelperActivator so it can cache Type activation information. Types won't conflict.
|
||||
yield return describe.Singleton<ITagHelperActivator, DefaultTagHelperActivator>();
|
||||
|
||||
// Consumed by the Cache tag helper to cache results across the lifetime of the application.
|
||||
yield return describe.Singleton<IMemoryCache, MemoryCache>();
|
||||
|
||||
// DefaultHtmlGenerator is pretty much stateless but depends on Scoped services such as IUrlHelper and
|
||||
// IActionBindingContextProvider. Therefore it too is scoped.
|
||||
yield return describe.Transient<IHtmlGenerator, DefaultHtmlGenerator>();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" },
|
||||
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*"
|
||||
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
|
||||
"Microsoft.Framework.Cache.Memory": "1.0.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": {},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<h2>Category: Laptops</h2>
|
||||
<h2>Region: North</h2>
|
||||
|
||||
<h2>Cached content</h2>
|
||||
Locations closest to your locale:
|
||||
|
||||
NorthWest Store
|
||||
<div>CorrelationId in View Component: 1</div>
|
||||
|
||||
<partial-title>Listing items</partial-title>
|
||||
|
||||
Cached Content for Laptops
|
||||
<div>CorrelationId in Partial: 1</div>
|
||||
|
||||
<div>CorrelationId in Splash: 1</div>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<h2>Category: Phones</h2>
|
||||
<h2>Region: North</h2>
|
||||
|
||||
<h2>Cached content</h2>
|
||||
Locations closest to your locale:
|
||||
|
||||
NorthWest Store
|
||||
<div>CorrelationId in View Component: 1</div>
|
||||
|
||||
<partial-title>Listing items</partial-title>
|
||||
|
||||
Cached Content for Phones
|
||||
<div>CorrelationId in Partial: 2</div>
|
||||
|
||||
<div>CorrelationId in Splash: 2</div>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<h2>Category: Phones</h2>
|
||||
<h2>Region: East</h2>
|
||||
|
||||
<h2>Cached content</h2>
|
||||
Locations closest to your locale:
|
||||
|
||||
Nationwide Store
|
||||
<div>CorrelationId in View Component: 3</div>
|
||||
|
||||
<partial-title>Listing items</partial-title>
|
||||
|
||||
Cached Content for Phones
|
||||
<div>CorrelationId in Partial: 2</div>
|
||||
|
||||
<div>CorrelationId in Splash: 3</div>
|
||||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
{
|
||||
private readonly IServiceProvider _provider = TestHelper.CreateServices("MvcTagHelpersWebSite");
|
||||
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;
|
||||
private static readonly Assembly _resourcesAssembly = typeof(TagHelpersTests).GetTypeInfo().Assembly;
|
||||
private static readonly Assembly _resourcesAssembly = typeof(MvcTagHelpersTests).GetTypeInfo().Assembly;
|
||||
|
||||
[Theory]
|
||||
[InlineData("Index", null)]
|
||||
|
|
@ -96,5 +96,195 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
expectedContent = string.Format(expectedContent, forgeryToken);
|
||||
Assert.Equal(expectedContent.Trim(), responseContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents()
|
||||
{
|
||||
// Arrange
|
||||
var assertFile =
|
||||
"compiler/resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert";
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("http://localhost");
|
||||
client.DefaultRequestHeaders.Add("Locale", "North");
|
||||
|
||||
// Act - 1
|
||||
// Verify that content gets cached based on vary-by-params
|
||||
var targetUrl = "/catalog?categoryId=1&correlationid=1";
|
||||
var response1 = await client.GetStringAsync(targetUrl);
|
||||
var response2 = await client.GetStringAsync(targetUrl);
|
||||
|
||||
// Assert - 1
|
||||
var expected1 = await _resourcesAssembly.ReadResourceAsStringAsync(assertFile + "1.txt");
|
||||
|
||||
Assert.Equal(expected1, response1.Trim());
|
||||
Assert.Equal(expected1, response2.Trim());
|
||||
|
||||
// Act - 2
|
||||
// Verify content gets changed in partials when one of the vary by parameters is changed
|
||||
targetUrl = "/catalog?categoryId=3&correlationid=2";
|
||||
var response3 = await client.GetStringAsync(targetUrl);
|
||||
var response4 = await client.GetStringAsync(targetUrl);
|
||||
|
||||
// Assert - 2
|
||||
var expected2 = await _resourcesAssembly.ReadResourceAsStringAsync(assertFile + "2.txt");
|
||||
|
||||
Assert.Equal(expected2, response3.Trim());
|
||||
Assert.Equal(expected2, response4.Trim());
|
||||
|
||||
// Act - 3
|
||||
// Verify content gets changed in a View Component when the Vary-by-header parameters is changed
|
||||
client.DefaultRequestHeaders.Remove("Locale");
|
||||
client.DefaultRequestHeaders.Add("Locale", "East");
|
||||
|
||||
targetUrl = "/catalog?categoryId=3&correlationid=3";
|
||||
var response5 = await client.GetStringAsync(targetUrl);
|
||||
var response6 = await client.GetStringAsync(targetUrl);
|
||||
|
||||
// Assert - 3
|
||||
var expected3 = await _resourcesAssembly.ReadResourceAsStringAsync(assertFile + "3.txt");
|
||||
|
||||
Assert.Equal(expected3, response5.Trim());
|
||||
Assert.Equal(expected3, response6.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheTagHelper_ExpiresContent_BasedOnExpiresParameter()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("http://localhost");
|
||||
|
||||
// Act - 1
|
||||
var response1 = await client.GetStringAsync("/catalog/2");
|
||||
|
||||
// Assert - 1
|
||||
var expected1 = "Cached content for 2";
|
||||
Assert.Equal(expected1, response1.Trim());
|
||||
|
||||
// Act - 2
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
var response2 = await client.GetStringAsync("/catalog/3");
|
||||
|
||||
// Assert - 2
|
||||
var expected2 = "Cached content for 3";
|
||||
Assert.Equal(expected2, response2.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheTagHelper_UsesVaryByCookie_ToVaryContent()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("http://localhost");
|
||||
|
||||
// Act - 1
|
||||
var response1 = await client.GetStringAsync("/catalog/cart?correlationid=1");
|
||||
|
||||
// Assert - 1
|
||||
var expected1 = "Cart content for 1";
|
||||
Assert.Equal(expected1, response1.Trim());
|
||||
|
||||
// Act - 2
|
||||
client.DefaultRequestHeaders.Add("Cookie", "CartId=10");
|
||||
var response2 = await client.GetStringAsync("/catalog/cart?correlationid=2");
|
||||
|
||||
// Assert - 2
|
||||
var expected2 = "Cart content for 2";
|
||||
Assert.Equal(expected2, response2.Trim());
|
||||
|
||||
// Act - 3
|
||||
// Resend the cookiesless request and cached result from the first response.
|
||||
client.DefaultRequestHeaders.Remove("Cookie");
|
||||
var response3 = await client.GetStringAsync("/catalog/cart?correlationid=3");
|
||||
|
||||
// Assert - 3
|
||||
Assert.Equal(expected1, response3.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheTagHelper_VariesByRoute()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("http://localhost");
|
||||
|
||||
// Act - 1
|
||||
var response1 = await client.GetStringAsync(
|
||||
"/catalog/north-west/confirm-payment?confirmationId=1");
|
||||
|
||||
// Assert - 1
|
||||
var expected1 = "Welcome Guest. Your confirmation id is 1. (Region north-west)";
|
||||
Assert.Equal(expected1, response1.Trim());
|
||||
|
||||
// Act - 2
|
||||
var response2 = await client.GetStringAsync(
|
||||
"/catalog/south-central/confirm-payment?confirmationId=2");
|
||||
|
||||
// Assert - 2
|
||||
var expected2 = "Welcome Guest. Your confirmation id is 2. (Region south-central)";
|
||||
Assert.Equal(expected2, response2.Trim());
|
||||
|
||||
// Act 3
|
||||
var response3 = await client.GetStringAsync(
|
||||
"/catalog/north-west/Silver/confirm-payment?confirmationId=4");
|
||||
|
||||
var expected3 = "Welcome Silver member. Your confirmation id is 4. (Region north-west)";
|
||||
Assert.Equal(expected3, response3.Trim());
|
||||
|
||||
// Act 4
|
||||
var response4 = await client.GetStringAsync(
|
||||
"/catalog/north-west/Gold/confirm-payment?confirmationId=5");
|
||||
|
||||
var expected4 = "Welcome Gold member. Your confirmation id is 5. (Region north-west)";
|
||||
Assert.Equal(expected4, response4.Trim());
|
||||
|
||||
// Act - 4
|
||||
// Resend the responses and expect cached results.
|
||||
response1 = await client.GetStringAsync(
|
||||
"/catalog/north-west/confirm-payment?confirmationId=301");
|
||||
response2 = await client.GetStringAsync(
|
||||
"/catalog/south-central/confirm-payment?confirmationId=402");
|
||||
response3 = await client.GetStringAsync(
|
||||
"/catalog/north-west/Silver/confirm-payment?confirmationId=503");
|
||||
response4 = await client.GetStringAsync(
|
||||
"/catalog/north-west/Gold/confirm-payment?confirmationId=608");
|
||||
|
||||
// Assert - 4
|
||||
Assert.Equal(expected1, response1.Trim());
|
||||
Assert.Equal(expected2, response2.Trim());
|
||||
Assert.Equal(expected3, response3.Trim());
|
||||
Assert.Equal(expected4, response4.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CacheTagHelper_VariesByUserId()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_provider, _app);
|
||||
var client = server.CreateClient();
|
||||
client.BaseAddress = new Uri("http://localhost");
|
||||
|
||||
// Act - 1
|
||||
var response1 = await client.GetStringAsync("/catalog/past-purchases/test1?correlationid=1");
|
||||
var response2 = await client.GetStringAsync("/catalog/past-purchases/test1?correlationid=2");
|
||||
|
||||
// Assert - 1
|
||||
var expected1 = "Past purchases for user test1 (1)";
|
||||
Assert.Equal(expected1, response1.Trim());
|
||||
Assert.Equal(expected1, response2.Trim());
|
||||
|
||||
// Act - 2
|
||||
var response3 = await client.GetStringAsync("/catalog/past-purchases/test2?correlationid=3");
|
||||
var response4 = await client.GetStringAsync("/catalog/past-purchases/test2?correlationid=4");
|
||||
|
||||
// Assert - 2
|
||||
var expected2 = "Past purchases for user test2 (3)";
|
||||
Assert.Equal(expected2, response3.Trim());
|
||||
Assert.Equal(expected2, response4.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,634 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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 System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Http.Core;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.Cache.Memory;
|
||||
using Microsoft.Framework.Cache.Memory.Infrastructure;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
public class CacheTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public void GenerateKey_ReturnsKeyBasedOnTagHelperUniqueId()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid().ToString();
|
||||
var tagHelperContext = GetTagHelperContext(id);
|
||||
var cacheTagHelper = new CacheTagHelper
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
VaryByQuery = varyByQuery
|
||||
};
|
||||
cacheTagHelper.ViewContext.HttpContext.Request.QueryString =
|
||||
new Http.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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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
|
||||
{
|
||||
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_ReturnsCachedValue_IfVaryByParamIsUnchanged()
|
||||
{
|
||||
// Arrange - 1
|
||||
var id = "unique-id";
|
||||
var childContent = "original-child-content";
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var tagHelperContext1 = GetTagHelperContext(id, childContent);
|
||||
var tagHelperOutput1 = new TagHelperOutput("cache", new Dictionary<string, string>());
|
||||
var cacheTagHelper1 = new CacheTagHelper
|
||||
{
|
||||
VaryByQuery = "key1,key2",
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache
|
||||
};
|
||||
cacheTagHelper1.ViewContext.HttpContext.Request.QueryString = new Http.QueryString(
|
||||
"?key1=value1&key2=value2");
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Null(tagHelperOutput1.PreContent);
|
||||
Assert.Null(tagHelperOutput1.PostContent);
|
||||
Assert.True(tagHelperOutput1.ContentSet);
|
||||
Assert.Equal(childContent, tagHelperOutput1.Content);
|
||||
|
||||
// Arrange - 2
|
||||
var tagHelperContext2 = GetTagHelperContext(id, "different-content");
|
||||
var tagHelperOutput2 = new TagHelperOutput("cache", new Dictionary<string, string>());
|
||||
var cacheTagHelper2 = new CacheTagHelper
|
||||
{
|
||||
VaryByQuery = "key1,key2",
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache
|
||||
};
|
||||
cacheTagHelper2.ViewContext.HttpContext.Request.QueryString = new Http.QueryString(
|
||||
"?key1=value1&key2=value2");
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Null(tagHelperOutput2.PreContent);
|
||||
Assert.Null(tagHelperOutput2.PostContent);
|
||||
Assert.True(tagHelperOutput2.ContentSet);
|
||||
Assert.Equal(childContent, tagHelperOutput2.Content);
|
||||
}
|
||||
|
||||
[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, childContent1);
|
||||
var tagHelperOutput1 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper1 = new CacheTagHelper
|
||||
{
|
||||
VaryByCookie = "cookie1,cookie2",
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache
|
||||
};
|
||||
cacheTagHelper1.ViewContext.HttpContext.Request.Headers["Cookie"] = "cookie1=value1;cookie2=value2";
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Null(tagHelperOutput1.PreContent);
|
||||
Assert.Null(tagHelperOutput1.PostContent);
|
||||
Assert.True(tagHelperOutput1.ContentSet);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content);
|
||||
|
||||
// Arrange - 2
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id, childContent2);
|
||||
var tagHelperOutput2 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper2 = new CacheTagHelper
|
||||
{
|
||||
VaryByCookie = "cookie1,cookie2",
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache
|
||||
};
|
||||
cacheTagHelper2.ViewContext.HttpContext.Request.Headers["Cookie"] = "cookie1=value1;cookie2=not-value2";
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Null(tagHelperOutput2.PreContent);
|
||||
Assert.Null(tagHelperOutput2.PostContent);
|
||||
Assert.True(tagHelperOutput2.ContentSet);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheContext_SetsAbsoluteExpiration_IfExpiresOnIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expiresOn = DateTimeOffset.UtcNow.AddMinutes(4);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheContext = new Mock<ICacheSetContext>();
|
||||
cacheContext.Setup(c => c.SetAbsoluteExpiration(expiresOn))
|
||||
.Verifiable();
|
||||
var cacheTagHelper = new CacheTagHelper
|
||||
{
|
||||
MemoryCache = cache,
|
||||
ExpiresOn = expiresOn
|
||||
};
|
||||
|
||||
// Act
|
||||
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
|
||||
|
||||
// Assert
|
||||
cacheContext.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheContext_SetsAbsoluteExpiration_IfExpiresAfterIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expiresAfter = TimeSpan.FromSeconds(42);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheContext = new Mock<ICacheSetContext>();
|
||||
cacheContext.Setup(c => c.SetAbsoluteExpiration(expiresAfter))
|
||||
.Verifiable();
|
||||
var cacheTagHelper = new CacheTagHelper
|
||||
{
|
||||
MemoryCache = cache,
|
||||
ExpiresAfter = expiresAfter
|
||||
};
|
||||
|
||||
// Act
|
||||
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
|
||||
|
||||
// Assert
|
||||
cacheContext.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheContext_SetsSlidingExpiration_IfExpiresSlidingIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var expiresSliding = TimeSpan.FromSeconds(37);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheContext = new Mock<ICacheSetContext>();
|
||||
cacheContext.Setup(c => c.SetSlidingExpiration(expiresSliding))
|
||||
.Verifiable();
|
||||
var cacheTagHelper = new CacheTagHelper
|
||||
{
|
||||
MemoryCache = cache,
|
||||
ExpiresSliding = expiresSliding
|
||||
};
|
||||
|
||||
// Act
|
||||
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
|
||||
|
||||
// Assert
|
||||
cacheContext.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateCacheContext_SetsCachePreservationPriority()
|
||||
{
|
||||
// Arrange
|
||||
var priority = CachePreservationPriority.High;
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
var cacheContext = new Mock<ICacheSetContext>();
|
||||
cacheContext.Setup(c => c.SetPriority(priority))
|
||||
.Verifiable();
|
||||
var cacheTagHelper = new CacheTagHelper
|
||||
{
|
||||
MemoryCache = cache,
|
||||
Priority = priority
|
||||
};
|
||||
|
||||
// Act
|
||||
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
|
||||
|
||||
// Assert
|
||||
cacheContext.Verify();
|
||||
}
|
||||
|
||||
[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, childContent1);
|
||||
var tagHelperOutput1 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper1 = new CacheTagHelper
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache,
|
||||
ExpiresAfter = TimeSpan.FromMinutes(10)
|
||||
};
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Null(tagHelperOutput1.PreContent);
|
||||
Assert.Null(tagHelperOutput1.PostContent);
|
||||
Assert.True(tagHelperOutput1.ContentSet);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content);
|
||||
|
||||
// Arrange - 2
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id, childContent2);
|
||||
var tagHelperOutput2 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper2 = new CacheTagHelper
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache,
|
||||
ExpiresAfter = TimeSpan.FromMinutes(10)
|
||||
};
|
||||
currentTime = currentTime.AddMinutes(11);
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Null(tagHelperOutput2.PreContent);
|
||||
Assert.Null(tagHelperOutput2.PostContent);
|
||||
Assert.True(tagHelperOutput2.ContentSet);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content);
|
||||
}
|
||||
|
||||
[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, childContent1);
|
||||
var tagHelperOutput1 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper1 = new CacheTagHelper
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache,
|
||||
ExpiresOn = currentTime.AddMinutes(5)
|
||||
};
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Null(tagHelperOutput1.PreContent);
|
||||
Assert.Null(tagHelperOutput1.PostContent);
|
||||
Assert.True(tagHelperOutput1.ContentSet);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content);
|
||||
|
||||
// Arrange - 2
|
||||
currentTime = currentTime.AddMinutes(5).AddSeconds(2);
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id, childContent2);
|
||||
var tagHelperOutput2 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper2 = new CacheTagHelper
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache,
|
||||
ExpiresOn = currentTime.AddMinutes(5)
|
||||
};
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Null(tagHelperOutput2.PreContent);
|
||||
Assert.Null(tagHelperOutput2.PostContent);
|
||||
Assert.True(tagHelperOutput2.ContentSet);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content);
|
||||
}
|
||||
|
||||
[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, childContent1);
|
||||
var tagHelperOutput1 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper1 = new CacheTagHelper
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache,
|
||||
ExpiresSliding = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
// Act - 1
|
||||
await cacheTagHelper1.ProcessAsync(tagHelperContext1, tagHelperOutput1);
|
||||
|
||||
// Assert - 1
|
||||
Assert.Null(tagHelperOutput1.PreContent);
|
||||
Assert.Null(tagHelperOutput1.PostContent);
|
||||
Assert.True(tagHelperOutput1.ContentSet);
|
||||
Assert.Equal(childContent1, tagHelperOutput1.Content);
|
||||
|
||||
// Arrange - 2
|
||||
currentTime = currentTime.AddSeconds(35);
|
||||
var childContent2 = "different-content";
|
||||
var tagHelperContext2 = GetTagHelperContext(id, childContent2);
|
||||
var tagHelperOutput2 = new TagHelperOutput("cache", new Dictionary<string, string> { { "attr", "value" } })
|
||||
{
|
||||
PreContent = "<cache>",
|
||||
PostContent = "</cache>"
|
||||
};
|
||||
var cacheTagHelper2 = new CacheTagHelper
|
||||
{
|
||||
ViewContext = GetViewContext(),
|
||||
MemoryCache = cache,
|
||||
ExpiresSliding = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
// Act - 2
|
||||
await cacheTagHelper2.ProcessAsync(tagHelperContext2, tagHelperOutput2);
|
||||
|
||||
// Assert - 2
|
||||
Assert.Null(tagHelperOutput2.PreContent);
|
||||
Assert.Null(tagHelperOutput2.PostContent);
|
||||
Assert.True(tagHelperOutput2.ContentSet);
|
||||
Assert.Equal(childContent2, tagHelperOutput2.Content);
|
||||
}
|
||||
|
||||
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()),
|
||||
TextWriter.Null);
|
||||
}
|
||||
|
||||
private static TagHelperContext GetTagHelperContext(string id = "testid",
|
||||
string childContent = "some child content")
|
||||
{
|
||||
return new TagHelperContext(new Dictionary<string, object>(),
|
||||
id,
|
||||
() => Task.FromResult(childContent));
|
||||
}
|
||||
|
||||
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,19 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNet.Mvc;
|
||||
|
||||
namespace MvcSample.Web.Components
|
||||
{
|
||||
public class SplashViewComponent : ViewComponent
|
||||
{
|
||||
public ViewViewComponentResult Invoke()
|
||||
{
|
||||
var region = (string)ViewData["Locale"];
|
||||
var model = region == "North" ? "NorthWest Store":
|
||||
"Nationwide Store";
|
||||
|
||||
return View(model: model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
|
||||
namespace MvcTagHelpersWebSite.Controllers
|
||||
{
|
||||
public class Catalog_CacheTagHelperController : Controller
|
||||
{
|
||||
[HttpGet("/catalog")]
|
||||
public ViewResult Splash(int categoryId, int correlationId, [FromHeader]string locale)
|
||||
{
|
||||
var category = categoryId == 1 ? "Laptops" : "Phones";
|
||||
ViewData["Category"] = category;
|
||||
ViewData["Locale"] = locale;
|
||||
ViewData["CorrelationId"] = correlationId;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("/catalog/{id:int}")]
|
||||
public ViewResult Details(int id)
|
||||
{
|
||||
ViewData["ProductId"] = id;
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("/catalog/cart")]
|
||||
public ViewResult ShoppingCart(int correlationId)
|
||||
{
|
||||
ViewData["CorrelationId"] = correlationId;
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("/catalog/{region}/confirm-payment")]
|
||||
public ViewResult GuestConfirmPayment(string region, int confirmationId = 0)
|
||||
{
|
||||
ViewData["Message"] = "Welcome Guest. Your confirmation id is " + confirmationId;
|
||||
ViewData["Region"] = region;
|
||||
return View("ConfirmPayment");
|
||||
}
|
||||
|
||||
[HttpGet("/catalog/{region}/{section}/confirm-payment")]
|
||||
public ViewResult ConfirmPayment(string region, string section, int confirmationId)
|
||||
{
|
||||
var message = "Welcome " + section + " member. Your confirmation id is " + confirmationId;
|
||||
ViewData["Message"] = message;
|
||||
ViewData["Region"] = region;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet("/catalog/past-purchases/{id}")]
|
||||
public ViewResult PastPurchases(string id, int correlationId)
|
||||
{
|
||||
var identity = new ClaimsIdentity();
|
||||
identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, id));
|
||||
|
||||
Context.User = new ClaimsPrincipal(identity);
|
||||
ViewData["CorrelationId"] = correlationId;
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<cache vary-by-route="region, section">
|
||||
@ViewBag.Message. (Region @ViewBag.Region)
|
||||
</cache>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@using Microsoft.Framework.Cache.Memory
|
||||
<cache expires-after="TimeSpan.FromSeconds(1)" priority="CachePreservationPriority.Low">
|
||||
Cached content for @ViewBag.ProductId
|
||||
</cache>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<cache vary-by-user="true">
|
||||
Past purchases for user @Context.User.Identity.Name (@ViewBag.CorrelationId)
|
||||
</cache>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<cache vary-by-cookie="CartId">
|
||||
Cart content for @ViewBag.CorrelationId
|
||||
</cache>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<h2>Category: @ViewBag.Category</h2>
|
||||
<h2>Region: @ViewBag.Locale</h2>
|
||||
<cache vary-by-header="Locale" vary-by-query="categoryid">
|
||||
<h2>Cached content</h2>
|
||||
@await Component.InvokeAsync("Splash")
|
||||
@await Html.PartialAsync("_SplashPartial")
|
||||
<div>CorrelationId in Splash: @ViewBag.CorrelationId</div>
|
||||
</cache>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<partial-title>Listing items</partial-title>
|
||||
<cache vary-by="@ViewBag.Category" data-content="some-content">
|
||||
Cached Content for @ViewBag.Category
|
||||
<div>CorrelationId in Partial: @ViewBag.CorrelationId</div>
|
||||
</cache>
|
||||
|
|
@ -0,0 +1 @@
|
|||
@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
|
||||
@model string
|
||||
Locations closest to your locale:
|
||||
<cache vary-by-header="Locale">
|
||||
@Model
|
||||
<div>CorrelationId in View Component: @ViewBag.CorrelationId</div>
|
||||
</cache>
|
||||
Loading…
Reference in New Issue