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.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Testing;
|
||||
using Xunit;
|
||||
|
|
@ -445,17 +446,19 @@ Products: Book1, Book2 (1)";
|
|||
Assert.Equal(expected2, response2.Trim(), ignoreLineEndingDifferences: true);
|
||||
|
||||
// Act - 3
|
||||
// Trigger an expiration
|
||||
var response3 = await Client.PostAsync("/categories/update-products", new StringContent(string.Empty));
|
||||
response3.EnsureSuccessStatusCode();
|
||||
// Trigger an expiration of the nested content.
|
||||
var content = @"[{ productName: ""Music Systems"" },{ productName: ""Televisions"" }]";
|
||||
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
|
||||
var expected3 =
|
||||
@"Category: Electronics
|
||||
Products: Laptops (3)";
|
||||
Assert.Equal(expected3, response4.Trim(), ignoreLineEndingDifferences: true);
|
||||
Products: Music Systems, Televisions (3)";
|
||||
Assert.Equal(expected3, response3.Trim(), ignoreLineEndingDifferences: true);
|
||||
}
|
||||
|
||||
[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.
|
||||
// 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 HtmlGenerationWebSite.Models;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
|
||||
namespace HtmlGenerationWebSite.Controllers
|
||||
|
|
@ -71,10 +73,14 @@ namespace HtmlGenerationWebSite.Controllers
|
|||
return View();
|
||||
}
|
||||
|
||||
[HttpPost("/categories/update-products")]
|
||||
public void UpdateCategories([FromServices] ProductsService productsService)
|
||||
[HttpPost("/categories/{category}")]
|
||||
public IActionResult UpdateProducts(
|
||||
[FromServices] ProductsService productService,
|
||||
string category,
|
||||
[FromBody] List<Product> products)
|
||||
{
|
||||
productsService.UpdateProducts();
|
||||
productService.UpdateProducts(category, products);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[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.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HtmlGenerationWebSite.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace HtmlGenerationWebSite
|
||||
{
|
||||
public class ProductsService
|
||||
{
|
||||
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
|
||||
|
||||
public string GetProducts(string category, out IChangeToken changeToken)
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ISignalTokenProviderService<Product> _tokenProviderService;
|
||||
private readonly Dictionary<string, Product[]> _products = new Dictionary<string, Product[]>
|
||||
{
|
||||
var token = _tokenSource.IsCancellationRequested ?
|
||||
CancellationToken.None : _tokenSource.Token;
|
||||
changeToken = new CancellationChangeToken(token);
|
||||
if (category == "Books")
|
||||
["Books"] = new[]
|
||||
{
|
||||
return "Book1, Book2";
|
||||
}
|
||||
else
|
||||
new Product { ProductName = "Book1" },
|
||||
new Product { ProductName = "Book2" }
|
||||
},
|
||||
["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.
|
||||
services.AddMvc().InitializeTagHelper<FormTagHelper>((helper, _) => helper.Antiforgery = false);
|
||||
|
||||
services.AddSingleton(typeof(ISignalTokenProviderService<>), typeof(SignalTokenProviderService<>));
|
||||
services.AddSingleton<ProductsService>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<cache vary-by-route="category">
|
||||
@inject HtmlGenerationWebSite.ProductsService ProductsService
|
||||
<cache vary-by-route="category">
|
||||
Category: @ViewBag.Category
|
||||
<cache>@Component.Invoke("Products", ViewBag.Category)</cache>
|
||||
<cache>Products: @string.Join(", ", ProductsService.GetProductNames(ViewBag.Category)) (@ViewBag.CorrelationId)</cache>
|
||||
</cache>
|
||||
|
|
@ -1 +0,0 @@
|
|||
Products: @ViewBag.Products (@ViewBag.CorrelationId)
|
||||
Loading…
Reference in New Issue