();
diff --git a/src/Microsoft.AspNet.Mvc/project.json b/src/Microsoft.AspNet.Mvc/project.json
index 3d1b2ac692..d4d04fb2d4 100644
--- a/src/Microsoft.AspNet.Mvc/project.json
+++ b/src/Microsoft.AspNet.Mvc/project.json
@@ -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": {},
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert1.txt b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert1.txt
new file mode 100644
index 0000000000..a0a21b6c33
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert1.txt
@@ -0,0 +1,15 @@
+Category: Laptops
+Region: North
+
+ Cached content
+ Locations closest to your locale:
+
+NorthWest Store
+CorrelationId in View Component: 1
+
+ Listing items
+
+Cached Content for Laptops
+CorrelationId in Partial: 1
+
+ CorrelationId in Splash: 1
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert2.txt b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert2.txt
new file mode 100644
index 0000000000..afd10d8186
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert2.txt
@@ -0,0 +1,15 @@
+Category: Phones
+Region: North
+
+ Cached content
+ Locations closest to your locale:
+
+NorthWest Store
+CorrelationId in View Component: 1
+
+ Listing items
+
+Cached Content for Phones
+CorrelationId in Partial: 2
+
+ CorrelationId in Splash: 2
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert3.txt b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert3.txt
new file mode 100644
index 0000000000..2b07849a8b
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert3.txt
@@ -0,0 +1,15 @@
+Category: Phones
+Region: East
+
+ Cached content
+ Locations closest to your locale:
+
+Nationwide Store
+CorrelationId in View Component: 3
+
+ Listing items
+
+Cached Content for Phones
+CorrelationId in Partial: 2
+
+ CorrelationId in Splash: 3
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTests.cs
index 7f39fe9c82..fc975bae58 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTests.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/MvcTagHelpersTests.cs
@@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("MvcTagHelpersWebSite");
private readonly Action _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());
+ }
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs
new file mode 100644
index 0000000000..923eaf9446
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs
@@ -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());
+ 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());
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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();
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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();
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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 { { "attr", "value" } })
+ {
+ PreContent = "",
+ PostContent = ""
+ };
+ 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(),
+ new ViewDataDictionary(new EmptyModelMetadataProvider()),
+ TextWriter.Null);
+ }
+
+ private static TagHelperContext GetTagHelperContext(string id = "testid",
+ string childContent = "some child content")
+ {
+ return new TagHelperContext(new Dictionary(),
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Components/SplashViewComponent.cs b/test/WebSites/MvcTagHelpersWebSite/Components/SplashViewComponent.cs
new file mode 100644
index 0000000000..de643189b6
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Components/SplashViewComponent.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs b/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs
new file mode 100644
index 0000000000..1f83b6e0f4
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs
@@ -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();
+ }
+ }
+}
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ConfirmPayment.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ConfirmPayment.cshtml
new file mode 100644
index 0000000000..13ff1097b4
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ConfirmPayment.cshtml
@@ -0,0 +1,3 @@
+
+@ViewBag.Message. (Region @ViewBag.Region)
+
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Details.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Details.cshtml
new file mode 100644
index 0000000000..6f98ef7171
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Details.cshtml
@@ -0,0 +1,4 @@
+@using Microsoft.Framework.Cache.Memory
+
+Cached content for @ViewBag.ProductId
+
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/PastPurchases.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/PastPurchases.cshtml
new file mode 100644
index 0000000000..f389fb77f0
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/PastPurchases.cshtml
@@ -0,0 +1,3 @@
+
+Past purchases for user @Context.User.Identity.Name (@ViewBag.CorrelationId)
+
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ShoppingCart.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ShoppingCart.cshtml
new file mode 100644
index 0000000000..34e09985cc
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ShoppingCart.cshtml
@@ -0,0 +1,3 @@
+
+Cart content for @ViewBag.CorrelationId
+
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Splash.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Splash.cshtml
new file mode 100644
index 0000000000..f978286a7c
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Splash.cshtml
@@ -0,0 +1,8 @@
+Category: @ViewBag.Category
+Region: @ViewBag.Locale
+
+ Cached content
+ @await Component.InvokeAsync("Splash")
+ @await Html.PartialAsync("_SplashPartial")
+ CorrelationId in Splash: @ViewBag.CorrelationId
+
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_SplashPartial.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_SplashPartial.cshtml
new file mode 100644
index 0000000000..58b292f16f
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_SplashPartial.cshtml
@@ -0,0 +1,5 @@
+Listing items
+
+Cached Content for @ViewBag.Category
+CorrelationId in Partial: @ViewBag.CorrelationId
+
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_ViewStart.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_ViewStart.cshtml
new file mode 100644
index 0000000000..8febe27b2c
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_ViewStart.cshtml
@@ -0,0 +1 @@
+@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
\ No newline at end of file
diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/Shared/Components/Splash/Default.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/Shared/Components/Splash/Default.cshtml
new file mode 100644
index 0000000000..e62c9a6450
--- /dev/null
+++ b/test/WebSites/MvcTagHelpersWebSite/Views/Shared/Components/Splash/Default.cshtml
@@ -0,0 +1,7 @@
+@addtaghelper "Microsoft.AspNet.Mvc.TagHelpers"
+@model string
+Locations closest to your locale:
+
+@Model
+CorrelationId in View Component: @ViewBag.CorrelationId
+
\ No newline at end of file