Tweak CacheTagHelper sample to use a token provider service for
cache expiration. Fixes #3573
This commit is contained in:
parent
21e815cb36
commit
56c6975a21
|
|
@ -7,6 +7,7 @@ using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Testing;
|
using Microsoft.AspNet.Testing;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -445,17 +446,19 @@ Products: Book1, Book2 (1)";
|
||||||
Assert.Equal(expected2, response2.Trim(), ignoreLineEndingDifferences: true);
|
Assert.Equal(expected2, response2.Trim(), ignoreLineEndingDifferences: true);
|
||||||
|
|
||||||
// Act - 3
|
// Act - 3
|
||||||
// Trigger an expiration
|
// Trigger an expiration of the nested content.
|
||||||
var response3 = await Client.PostAsync("/categories/update-products", new StringContent(string.Empty));
|
var content = @"[{ productName: ""Music Systems"" },{ productName: ""Televisions"" }]";
|
||||||
response3.EnsureSuccessStatusCode();
|
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/categories/Electronics");
|
||||||
|
requestMessage.Content = new StringContent(content, Encoding.UTF8, "application/json");
|
||||||
|
(await Client.SendAsync(requestMessage)).EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var response4 = await Client.GetStringAsync("/categories/Electronics?correlationId=3");
|
var response3 = await Client.GetStringAsync("/categories/Electronics?correlationId=3");
|
||||||
|
|
||||||
// Assert - 3
|
// Assert - 3
|
||||||
var expected3 =
|
var expected3 =
|
||||||
@"Category: Electronics
|
@"Category: Electronics
|
||||||
Products: Laptops (3)";
|
Products: Music Systems, Televisions (3)";
|
||||||
Assert.Equal(expected3, response4.Trim(), ignoreLineEndingDifferences: true);
|
Assert.Equal(expected3, response3.Trim(), ignoreLineEndingDifferences: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using Microsoft.AspNet.Mvc;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace HtmlGenerationWebSite.Components
|
|
||||||
{
|
|
||||||
public class ProductsViewComponent : ViewComponent
|
|
||||||
{
|
|
||||||
public ProductsViewComponent(ProductsService productsService, IMemoryCache cache)
|
|
||||||
{
|
|
||||||
ProductsService = productsService;
|
|
||||||
Cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ProductsService ProductsService { get; }
|
|
||||||
|
|
||||||
public IMemoryCache Cache { get; }
|
|
||||||
|
|
||||||
public IViewComponentResult Invoke(string category)
|
|
||||||
{
|
|
||||||
string products;
|
|
||||||
if (!Cache.TryGetValue(category, out products))
|
|
||||||
{
|
|
||||||
IChangeToken changeToken;
|
|
||||||
products = ProductsService.GetProducts(category, out changeToken);
|
|
||||||
Cache.Set(category, products, new MemoryCacheEntryOptions().AddExpirationToken(changeToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewData["Products"] = products;
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using HtmlGenerationWebSite.Models;
|
||||||
using Microsoft.AspNet.Mvc;
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
namespace HtmlGenerationWebSite.Controllers
|
namespace HtmlGenerationWebSite.Controllers
|
||||||
|
|
@ -71,10 +73,14 @@ namespace HtmlGenerationWebSite.Controllers
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/categories/update-products")]
|
[HttpPost("/categories/{category}")]
|
||||||
public void UpdateCategories([FromServices] ProductsService productsService)
|
public IActionResult UpdateProducts(
|
||||||
|
[FromServices] ProductsService productService,
|
||||||
|
string category,
|
||||||
|
[FromBody] List<Product> products)
|
||||||
{
|
{
|
||||||
productsService.UpdateProducts();
|
productService.UpdateProducts(category, products);
|
||||||
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/catalog/GetDealPercentage/{dealPercentage}")]
|
[HttpGet("/catalog/GetDealPercentage/{dealPercentage}")]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace HtmlGenerationWebSite
|
||||||
|
{
|
||||||
|
public interface ISignalTokenProviderService<TKey>
|
||||||
|
{
|
||||||
|
IChangeToken GetToken(object key);
|
||||||
|
|
||||||
|
void SignalToken(object key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,33 +1,59 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Threading;
|
using System.Collections.Generic;
|
||||||
using Microsoft.Extensions.Primitives;
|
using System.Linq;
|
||||||
|
using HtmlGenerationWebSite.Models;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
namespace HtmlGenerationWebSite
|
namespace HtmlGenerationWebSite
|
||||||
{
|
{
|
||||||
public class ProductsService
|
public class ProductsService
|
||||||
{
|
{
|
||||||
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
private readonly ISignalTokenProviderService<Product> _tokenProviderService;
|
||||||
public string GetProducts(string category, out IChangeToken changeToken)
|
private readonly Dictionary<string, Product[]> _products = new Dictionary<string, Product[]>
|
||||||
{
|
{
|
||||||
var token = _tokenSource.IsCancellationRequested ?
|
["Books"] = new[]
|
||||||
CancellationToken.None : _tokenSource.Token;
|
|
||||||
changeToken = new CancellationChangeToken(token);
|
|
||||||
if (category == "Books")
|
|
||||||
{
|
{
|
||||||
return "Book1, Book2";
|
new Product { ProductName = "Book1" },
|
||||||
}
|
new Product { ProductName = "Book2" }
|
||||||
else
|
},
|
||||||
|
["Electronics"] = new[]
|
||||||
{
|
{
|
||||||
return "Laptops";
|
new Product { ProductName = "Laptops" }
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public ProductsService(
|
||||||
|
IMemoryCache memoryCache,
|
||||||
|
ISignalTokenProviderService<Product> tokenProviderService)
|
||||||
|
{
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
_tokenProviderService = tokenProviderService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateProducts()
|
public IEnumerable<string> GetProductNames(string category)
|
||||||
{
|
{
|
||||||
_tokenSource.Cancel();
|
IEnumerable<Product> products;
|
||||||
|
var key = typeof(ProductsService).FullName;
|
||||||
|
if (!_memoryCache.TryGetValue(key, out products))
|
||||||
|
{
|
||||||
|
var changeToken = _tokenProviderService.GetToken(key);
|
||||||
|
products = _memoryCache.Set<IEnumerable<Product>>(
|
||||||
|
key,
|
||||||
|
_products[category],
|
||||||
|
new MemoryCacheEntryOptions().AddExpirationToken(changeToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
return products.Select(p => p.ProductName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateProducts(string category, IEnumerable<Product> products)
|
||||||
|
{
|
||||||
|
_products[category] = products.ToArray();
|
||||||
|
var key = typeof(ProductsService).FullName;
|
||||||
|
_tokenProviderService.SignalToken(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace HtmlGenerationWebSite
|
||||||
|
{
|
||||||
|
public class SignalTokenProviderService<TKey> : ISignalTokenProviderService<TKey>
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<object, ChangeTokenInfo> _changeTokens
|
||||||
|
= new ConcurrentDictionary<object, ChangeTokenInfo>();
|
||||||
|
|
||||||
|
public IChangeToken GetToken(object key)
|
||||||
|
{
|
||||||
|
return _changeTokens.GetOrAdd(
|
||||||
|
key,
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
var changeToken = new CancellationChangeToken(cancellationTokenSource.Token);
|
||||||
|
return new ChangeTokenInfo(changeToken, cancellationTokenSource);
|
||||||
|
}).ChangeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SignalToken(object key)
|
||||||
|
{
|
||||||
|
ChangeTokenInfo changeTokenInfo;
|
||||||
|
if (_changeTokens.TryRemove(key, out changeTokenInfo))
|
||||||
|
{
|
||||||
|
changeTokenInfo.TokenSource.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChangeTokenInfo
|
||||||
|
{
|
||||||
|
public ChangeTokenInfo(IChangeToken changeToken, CancellationTokenSource tokenSource)
|
||||||
|
{
|
||||||
|
ChangeToken = changeToken;
|
||||||
|
TokenSource = tokenSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IChangeToken ChangeToken { get; }
|
||||||
|
|
||||||
|
public CancellationTokenSource TokenSource { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ namespace HtmlGenerationWebSite
|
||||||
// null which is interpreted as true unless element includes an action attribute.
|
// null which is interpreted as true unless element includes an action attribute.
|
||||||
services.AddMvc().InitializeTagHelper<FormTagHelper>((helper, _) => helper.Antiforgery = false);
|
services.AddMvc().InitializeTagHelper<FormTagHelper>((helper, _) => helper.Antiforgery = false);
|
||||||
|
|
||||||
|
services.AddSingleton(typeof(ISignalTokenProviderService<>), typeof(SignalTokenProviderService<>));
|
||||||
services.AddSingleton<ProductsService>();
|
services.AddSingleton<ProductsService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<cache vary-by-route="category">
|
@inject HtmlGenerationWebSite.ProductsService ProductsService
|
||||||
|
<cache vary-by-route="category">
|
||||||
Category: @ViewBag.Category
|
Category: @ViewBag.Category
|
||||||
<cache>@Component.Invoke("Products", ViewBag.Category)</cache>
|
<cache>Products: @string.Join(", ", ProductsService.GetProductNames(ViewBag.Category)) (@ViewBag.CorrelationId)</cache>
|
||||||
</cache>
|
</cache>
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Products: @ViewBag.Products (@ViewBag.CorrelationId)
|
|
||||||
Loading…
Reference in New Issue