From 80ada8d01b099b58456354d1f9b37d38732cc82d Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 7 Jan 2015 12:43:55 -0800 Subject: [PATCH] Introducing 'cache' tag helper Fixes #1552 --- .../CacheTagHelper.cs | 266 ++++++++ .../project.json | 4 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 4 + src/Microsoft.AspNet.Mvc/project.json | 3 +- ...sPartialViewsAndViewComponents.Assert1.txt | 15 + ...sPartialViewsAndViewComponents.Assert2.txt | 15 + ...sPartialViewsAndViewComponents.Assert3.txt | 15 + .../MvcTagHelpersTests.cs | 192 +++++- .../CacheTagHelperTest.cs | 634 ++++++++++++++++++ .../Components/SplashViewComponent.cs | 19 + .../Catalog_CacheTagHelperController.cs | 65 ++ .../ConfirmPayment.cshtml | 3 + .../Catalog_CacheTagHelper/Details.cshtml | 4 + .../PastPurchases.cshtml | 3 + .../ShoppingCart.cshtml | 3 + .../Catalog_CacheTagHelper/Splash.cshtml | 8 + .../_SplashPartial.cshtml | 5 + .../Catalog_CacheTagHelper/_ViewStart.cshtml | 1 + .../Shared/Components/Splash/Default.cshtml | 7 + 19 files changed, 1263 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert1.txt create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert2.txt create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/CacheTagHelper_CanCachePortionsOfViewsPartialViewsAndViewComponents.Assert3.txt create mode 100644 test/Microsoft.AspNet.Mvc.TagHelpers.Test/CacheTagHelperTest.cs create mode 100644 test/WebSites/MvcTagHelpersWebSite/Components/SplashViewComponent.cs create mode 100644 test/WebSites/MvcTagHelpersWebSite/Controllers/Catalog_CacheTagHelperController.cs create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ConfirmPayment.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Details.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/PastPurchases.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/ShoppingCart.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/Splash.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_SplashPartial.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Catalog_CacheTagHelper/_ViewStart.cshtml create mode 100644 test/WebSites/MvcTagHelpersWebSite/Views/Shared/Components/Splash/Default.cshtml diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs new file mode 100644 index 0000000000..6d8e8d288c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs @@ -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 +{ + /// + /// implementation targeting <cache> elements. + /// + public class CacheTagHelper : TagHelper + { + /// + /// Prefix used by instances when creating entries in . + /// + 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[] { ',' }; + + /// + /// Gets or sets the instance used to cache entries. + /// + [Activate] + protected internal IMemoryCache MemoryCache { get; set; } + + /// + /// Gets or sets the for the current executing View. + /// + [Activate] + protected internal ViewContext ViewContext { get; set; } + + /// + /// Gets or sets a to vary the cached result by. + /// + [HtmlAttributeName(VaryByAttributeName)] + public string VaryBy { get; set; } + + /// + /// Gets or sets the name of a HTTP request header to vary the cached result by. + /// + [HtmlAttributeName(VaryByHeaderAttributeName)] + public string VaryByHeader { get; set; } + + /// + /// Gets or sets a comma-delimited set of query parameters to vary the cached result by. + /// + [HtmlAttributeName(VaryByQueryAttributeName)] + public string VaryByQuery { get; set; } + + /// + /// Gets or sets a comma-delimited set of route data parameters to vary the cached result by. + /// + [HtmlAttributeName(VaryByRouteAttributeName)] + public string VaryByRoute { get; set; } + + /// + /// Gets or sets a comma-delimited set of cookie names to vary the cached result by. + /// + [HtmlAttributeName(VaryByCookieAttributeName)] + public string VaryByCookie { get; set; } + + /// + /// Gets or sets a value that determines if the cached result is to be varied by the Identity for the logged in + /// . + /// + [HtmlAttributeName(VaryByUserAttributeName)] + public bool VaryByUser { get; set; } + + /// + /// Gets or sets the exact the cache entry should be evicted. + /// + [HtmlAttributeName(ExpiresOnAttributeName)] + public DateTimeOffset? ExpiresOn { get; set; } + + /// + /// Gets or sets the duration, from the time the cache entry was added, when it should be evicted. + /// + [HtmlAttributeName(ExpiresAfterAttributeName)] + public TimeSpan? ExpiresAfter { get; set; } + + /// + /// Gets or sets the duration from last access that the cache entry should be evicted. + /// + [HtmlAttributeName(ExpiresSlidingAttributeName)] + public TimeSpan? ExpiresSliding { get; set; } + + /// + /// Gets or sets the policy for the cache entry. + /// + [HtmlAttributeName(CachePriorityAttributeName)] + public CachePreservationPriority? Priority { get; set; } + + /// + 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 Tokenize(string value) + { + return value.Split(AttributeSeparator, StringSplitOptions.RemoveEmptyEntries) + .Select(token => token.Trim()) + .Where(token => token.Length > 0); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/project.json b/src/Microsoft.AspNet.Mvc.TagHelpers/project.json index 8ca5e62a7a..9a0034ef94 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/project.json +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/project.json @@ -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": {}, diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 188b5a8ba8..1e2b5aa84f 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -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(); + // Consumed by the Cache tag helper to cache results across the lifetime of the application. + yield return describe.Singleton(); + // DefaultHtmlGenerator is pretty much stateless but depends on Scoped services such as IUrlHelper and // IActionBindingContextProvider. Therefore it too is scoped. yield return describe.Transient(); 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