Merge branch 'release' into dev

This commit is contained in:
Pranav K 2015-01-22 16:47:54 -08:00
commit 4d77f670f6
16 changed files with 518 additions and 9 deletions

View File

@ -0,0 +1,44 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.Expiration.Interfaces;
using TagHelperSample.Web.Services;
namespace TagHelperSample.Web.Components
{
[ViewComponent(Name = "FeaturedMovies")]
public class FeaturedMoviesComponent : ViewComponent
{
private MoviesService _moviesService;
public FeaturedMoviesComponent(MoviesService moviesService)
{
_moviesService = moviesService;
}
public IViewComponentResult Invoke()
{
IExpirationTrigger trigger;
var movies = _moviesService.GetFeaturedMovies(out trigger);
// Add custom triggers
EntryLinkHelpers.ContextLink.AddExpirationTriggers(new[] { trigger });
return View(movies);
}
public IViewComponentResult Invoke(string movieName)
{
IExpirationTrigger trigger;
var quote = _moviesService.GetCriticsQuote(out trigger);
// This is invoked as part of a nested cache tag helper.
EntryLinkHelpers.ContextLink.AddExpirationTriggers(new[] { trigger });
return Content(quote);
}
}
}

View File

@ -0,0 +1,45 @@
// 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;
using TagHelperSample.Web.Services;
namespace TagHelperSample.Web.Controllers
{
public class MoviesController : Controller
{
private MoviesService _moviesService;
public MoviesController(MoviesService moviesService)
{
_moviesService = moviesService;
}
// Sample exhibiting the use of nested cache tag helpers with custom user expiration triggers.
// Trigger expirations cascade, expiration of the inner tag helper's content either due to absolute or sliding
// expiration or due to a user specified expiration trigger would cause the outer cache tag helper to also expire.
public ViewResult Index()
{
ViewData["Title"] = "Movies";
return View();
}
[HttpPost]
public ViewResult UpdateMovieRatings()
{
_moviesService.UpdateMovieRating();
ViewData["Title"] = "Movies with updated ratings";
return View("Index");
}
[HttpPost]
public ViewResult UpdateCriticsQuotes()
{
_moviesService.UpdateCriticsQuotes();
ViewData["Title"] = "Movies with updated critics quotes";
return View("Index");
}
}
}

View File

@ -0,0 +1,16 @@
// 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;
namespace TagHelperSample.Web.Models
{
public class FeaturedMovies
{
public string Name { get; set; }
public string Description { get; set; }
public int Rank { get; set; }
}
}

View File

@ -0,0 +1,68 @@
// 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.Threading;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.Expiration.Interfaces;
using TagHelperSample.Web.Models;
namespace TagHelperSample.Web.Services
{
public class MoviesService
{
private readonly Random _random = new Random();
private CancellationTokenSource _featuredMoviesTokenSource;
private CancellationTokenSource _quotesTokenSource;
public IEnumerable<FeaturedMovies> GetFeaturedMovies(out IExpirationTrigger expirationTrigger)
{
_featuredMoviesTokenSource = new CancellationTokenSource();
expirationTrigger = new CancellationTokenTrigger(_featuredMoviesTokenSource.Token);
return GetMovies().OrderBy(m => m.Rank).Take(2);
}
public void UpdateMovieRating()
{
_featuredMoviesTokenSource.Cancel();
_featuredMoviesTokenSource.Dispose();
_featuredMoviesTokenSource = null;
}
public string GetCriticsQuote(out IExpirationTrigger trigger)
{
_quotesTokenSource = new CancellationTokenSource();
var quotes = new[]
{
"A must see for iguana lovers everywhere",
"Slightly better than watching paint dry",
"Never felt more relieved seeing the credits roll",
"Bravo!"
};
trigger = new CancellationTokenTrigger(_quotesTokenSource.Token);
return quotes[_random.Next(0, quotes.Length)];
}
public void UpdateCriticsQuotes()
{
_quotesTokenSource.Cancel();
_quotesTokenSource.Dispose();
_quotesTokenSource = null;
}
private IEnumerable<FeaturedMovies> GetMovies()
{
yield return new FeaturedMovies { Name = "A day in the life of a blue whale", Rank = _random.Next(1, 10) };
yield return new FeaturedMovies { Name = "FlashForward", Rank = _random.Next(1, 10) };
yield return new FeaturedMovies { Name = "Frontier", Rank = _random.Next(1, 10) };
yield return new FeaturedMovies { Name = "Attack of the space spiders", Rank = _random.Next(1, 10) };
yield return new FeaturedMovies { Name = "Rift 3", Rank = _random.Next(1, 10) };
}
}
}

View File

@ -4,6 +4,7 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.Framework.DependencyInjection;
using TagHelperSample.Web.Services;
namespace TagHelperSample.Web
{
@ -18,6 +19,7 @@ namespace TagHelperSample.Web
// Setup services with a test AssemblyProvider so that only the sample's assemblies are loaded. This
// prevents loading controllers from other assemblies when the sample is used in functional tests.
services.AddTransient<IAssemblyProvider, TestAssemblyProvider<Startup>>();
services.AddSingleton<MoviesService>();
services.Configure<MvcOptions>(options =>
{

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<style>
body {
width: 1040px;
}
.main {
width: 800px;
float: left;
}
.sidebar {
width: 200px;
float: left;
}
</style>
</head>
<body>
<div class="main">
<h3>Watch the greatest movies right here!</h3>
Submit your movie rankings:
<form asp-anti-forgery="false" asp-action="UpdateMovieRatings">
Movies + ratings go here
<button type="submit">Update ratings</button>
</form>
<form asp-anti-forgery="false" asp-action="UpdateCriticsQuotes">
Movies + ratings go here
<button type="submit">Update quotes</button>
</form>
</div>
<div class="sidebar">
<cache expires-after="TimeSpan.FromMinutes(20)">
@await Component.InvokeAsync("FeaturedMovies")
</cache>
</div>
<div style="clear: left"></div>
</body>
</html>

View File

@ -0,0 +1,17 @@
@model IEnumerable<TagHelperSample.Web.Models.FeaturedMovies>
<dl>
@foreach (var movie in Model)
{
<dt>(@movie.Rank) @movie.Name</dt>
<dd>
<div>
@movie.Description
</div>
<em>Critics say:</em>
<cache vary-by="@movie.Name">
@Component.Invoke("FeaturedMovies", movie.Name)
</cache>
</dd>
}
</dl>

View File

@ -115,10 +115,17 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
string result;
if (!MemoryCache.TryGetValue(key, out result))
{
result = await context.GetChildContentAsync();
// Create an EntryLink and flow it so that it is accessible via the ambient EntryLinkHelpers.ContentLink
// for user code.
var entryLink = new EntryLink();
using (entryLink.FlowContext())
{
result = await context.GetChildContentAsync();
}
MemoryCache.Set(key, cacheSetContext =>
{
UpdateCacheContext(cacheSetContext);
UpdateCacheContext(cacheSetContext, entryLink);
return result;
});
}
@ -171,7 +178,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
}
// Internal for unit testing
internal void UpdateCacheContext(ICacheSetContext cacheSetContext)
internal void UpdateCacheContext(ICacheSetContext cacheSetContext, EntryLink entryLink)
{
if (ExpiresOn != null)
{
@ -192,6 +199,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{
cacheSetContext.SetPriority(Priority.Value);
}
cacheSetContext.AddEntryLink(entryLink);
}
private static void AddStringCollectionKey(StringBuilder builder,

View File

@ -289,5 +289,45 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expected2, response3.Trim());
Assert.Equal(expected2, response4.Trim());
}
[Fact]
public async Task CacheTagHelper_BubblesExpirationOfNestedTagHelpers()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
client.BaseAddress = new Uri("http://localhost");
// Act - 1
var response1 = await client.GetStringAsync("/categories/Books?correlationId=1");
// Assert - 1
var expected1 =
@"Category: Books
Products: Book1, Book2 (1)";
Assert.Equal(expected1, response1.Trim());
// Act - 2
var response2 = await client.GetStringAsync("/categories/Electronics?correlationId=2");
// Assert - 2
var expected2 =
@"Category: Electronics
Products: Book1, Book2 (1)";
Assert.Equal(expected2, response2.Trim());
// Act - 3
// Trigger an expiration
var response3 = await client.PostAsync("/categories/update-products", new StringContent(string.Empty));
response3.EnsureSuccessStatusCode();
var response4 = await client.GetStringAsync("/categories/Electronics?correlationId=3");
// Assert - 3
var expected3 =
@"Category: Electronics
Products: Laptops (3)";
Assert.Equal(expected3, response4.Trim());
}
}
}

View File

@ -7,14 +7,16 @@ using System.IO;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Core;
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 Microsoft.Framework.Expiration.Interfaces;
using Moq;
using Xunit;
@ -345,7 +347,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
// Arrange
var expiresOn = DateTimeOffset.UtcNow.AddMinutes(4);
var cache = new MemoryCache(new MemoryCacheOptions());
var cacheContext = new Mock<ICacheSetContext>();
var cacheContext = new Mock<ICacheSetContext>(MockBehavior.Strict);
cacheContext.Setup(c => c.SetAbsoluteExpiration(expiresOn))
.Verifiable();
var cacheTagHelper = new CacheTagHelper
@ -355,7 +357,64 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
cacheTagHelper.UpdateCacheContext(cacheContext.Object, new EntryLink());
// Assert
cacheContext.Verify();
}
[Fact]
public void UpdateCacheContext_UsesAbsoluteExpirationSpecifiedOnEntryLink()
{
// Arrange
var expiresOn = DateTimeOffset.UtcNow.AddMinutes(7);
var cache = new MemoryCache(new MemoryCacheOptions());
var cacheContext = new Mock<ICacheSetContext>(MockBehavior.Strict);
cacheContext.Setup(c => c.SetAbsoluteExpiration(expiresOn))
.Verifiable();
var cacheTagHelper = new CacheTagHelper
{
MemoryCache = cache
};
var entryLink = new EntryLink();
entryLink.SetAbsoluteExpiration(expiresOn);
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object, entryLink);
// Assert
cacheContext.Verify();
}
[Fact]
public void UpdateCacheContext_PrefersAbsoluteExpirationSpecifiedOnEntryLinkOverExpiresOn()
{
// Arrange
var expiresOn1 = DateTimeOffset.UtcNow.AddDays(12);
var expiresOn2 = DateTimeOffset.UtcNow.AddMinutes(4);
var cache = new MemoryCache(new MemoryCacheOptions());
var cacheContext = new Mock<ICacheSetContext>();
var sequence = new MockSequence();
cacheContext.InSequence(sequence)
.Setup(c => c.SetAbsoluteExpiration(expiresOn1))
.Verifiable();
cacheContext.InSequence(sequence)
.Setup(c => c.SetAbsoluteExpiration(expiresOn2))
.Verifiable();
var cacheTagHelper = new CacheTagHelper
{
MemoryCache = cache,
ExpiresOn = expiresOn1
};
var entryLink = new EntryLink();
entryLink.SetAbsoluteExpiration(expiresOn2);
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object, entryLink);
// Assert
cacheContext.Verify();
@ -377,7 +436,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
cacheTagHelper.UpdateCacheContext(cacheContext.Object, new EntryLink());
// Assert
cacheContext.Verify();
@ -399,7 +458,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
cacheTagHelper.UpdateCacheContext(cacheContext.Object, new EntryLink());
// Assert
cacheContext.Verify();
@ -421,12 +480,43 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
};
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object);
cacheTagHelper.UpdateCacheContext(cacheContext.Object, new EntryLink());
// Assert
cacheContext.Verify();
}
[Fact]
public void UpdateCacheContext_CopiesTriggersFromEntryLink()
{
// Arrange
var expiresSliding = TimeSpan.FromSeconds(30);
var expected = new[] { Mock.Of<IExpirationTrigger>(), Mock.Of<IExpirationTrigger>() };
var triggers = new List<IExpirationTrigger>();
var cache = new MemoryCache(new MemoryCacheOptions());
var cacheContext = new Mock<ICacheSetContext>();
cacheContext.Setup(c => c.SetSlidingExpiration(expiresSliding))
.Verifiable();
cacheContext.Setup(c => c.AddExpirationTrigger(It.IsAny<IExpirationTrigger>()))
.Callback<IExpirationTrigger>(triggers.Add)
.Verifiable();
var cacheTagHelper = new CacheTagHelper
{
MemoryCache = cache,
ExpiresSliding = expiresSliding
};
var entryLink = new EntryLink();
entryLink.AddExpirationTriggers(expected);
// Act
cacheTagHelper.UpdateCacheContext(cacheContext.Object, entryLink);
// Assert
cacheContext.Verify();
Assert.Equal(expected, triggers);
}
[Fact]
public async Task ProcessAsync_UsesExpiresAfter_ToExpireCacheEntry()
{
@ -604,6 +694,57 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
Assert.Equal(childContent2, tagHelperOutput2.Content);
}
[Fact]
public async Task ProcessAsync_FlowsEntryLinkThatAllowsAddingTriggersToAddedEntry()
{
// Arrange
var id = "some-id";
var expectedContent = "some-content";
var tokenSource = new CancellationTokenSource();
var cache = new MemoryCache(new MemoryCacheOptions());
var tagHelperContext = new TagHelperContext(new Dictionary<string, object>(),
id,
() =>
{
var entryLink = EntryLinkHelpers.ContextLink;
Assert.NotNull(entryLink);
entryLink.AddExpirationTriggers(new[]
{
new CancellationTokenTrigger(tokenSource.Token)
});
return Task.FromResult(expectedContent);
});
var tagHelperOutput = new TagHelperOutput("cache", new Dictionary<string, string>())
{
PreContent = "<cache>",
PostContent = "</cache>"
};
var cacheTagHelper = new CacheTagHelper
{
ViewContext = GetViewContext(),
MemoryCache = cache,
};
var key = cacheTagHelper.GenerateKey(tagHelperContext);
// Act - 1
await cacheTagHelper.ProcessAsync(tagHelperContext, tagHelperOutput);
string cachedValue;
var result = cache.TryGetValue(key, out cachedValue);
// Assert - 1
Assert.Equal(expectedContent, tagHelperOutput.Content);
Assert.True(result);
Assert.Equal(expectedContent, cachedValue);
// Act - 2
tokenSource.Cancel();
result = cache.TryGetValue(key, out cachedValue);
// Assert - 2
Assert.False(result);
Assert.Null(cachedValue);
}
private static ViewContext GetViewContext()
{
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());

View File

@ -0,0 +1,25 @@
// 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;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.Expiration.Interfaces;
namespace MvcTagHelpersWebSite.Components
{
public class ProductsViewComponent : ViewComponent
{
[Activate]
public ProductsService ProductsService { get; set; }
public IViewComponentResult Invoke(string category)
{
IExpirationTrigger trigger;
var products = ProductsService.GetProducts(category, out trigger);
EntryLinkHelpers.ContextLink.AddExpirationTriggers(new[] { trigger });
ViewData["Products"] = products;
return View();
}
}
}

View File

@ -8,6 +8,9 @@ namespace MvcTagHelpersWebSite.Controllers
{
public class Catalog_CacheTagHelperController : Controller
{
[Activate]
public ProductsService ProductsService { get; set; }
[HttpGet("/catalog")]
public ViewResult Splash(int categoryId, int correlationId, [FromHeader]string locale)
{
@ -61,5 +64,21 @@ namespace MvcTagHelpersWebSite.Controllers
ViewData["CorrelationId"] = correlationId;
return View();
}
[HttpGet("/categories/{category}")]
public ViewResult ListCategories(string category, int correlationId)
{
ViewData["Category"] = category;
ViewData["CorrelationId"] = correlationId;
return View();
}
[HttpPost("/categories/update-products")]
public IActionResult UpdateCategories()
{
ProductsService.UpdateProducts();
return new EmptyResult();
}
}
}

View File

@ -0,0 +1,34 @@
// 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.Threading;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.Expiration.Interfaces;
namespace MvcTagHelpersWebSite
{
public class ProductsService
{
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
public string GetProducts(string category, out IExpirationTrigger trigger)
{
var token = _tokenSource.IsCancellationRequested ? CancellationToken.None :
_tokenSource.Token;
trigger = new CancellationTokenTrigger(token);
if (category == "Books")
{
return "Book1, Book2";
}
else
{
return "Laptops";
}
}
public void UpdateProducts()
{
_tokenSource.Cancel();
}
}
}

View File

@ -16,6 +16,7 @@ namespace MvcTagHelpersWebSite
app.UseServices(services =>
{
services.AddMvc(configuration);
services.AddSingleton<ProductsService>();
});
app.UseMvc(routes =>

View File

@ -0,0 +1,4 @@
<cache vary-by-route="category">
Category: @ViewBag.Category
<cache>@Component.Invoke("Products", ViewBag.Category)</cache>
</cache>

View File

@ -0,0 +1 @@
Products: @ViewBag.Products (@ViewBag.CorrelationId)