Skip to content

Commit a30b85f

Browse files
committed
Merge branch 'feature/cache-profiles' into develop
Introduced the new `[TopicResponseCache]` attribute, which operates similar to the out-of-the-box `[ResponseCache]` attribute, except that it loads the `CacheProfile` from the `CurrentTopic` instead of from the attribute or ASP.NET Core configuration. This allows cache profiles to be centrally configured and managed via OnTopic, and associated with any page. Further, as this is applied to `TopicController` and all descendants, and works directly with the response headers, it is automatically applied without the need for implementers to e.g. customize their models or views. This satisfies the requirements for #89.
2 parents 3e184d4 + 1b4b94e commit a30b85f

10 files changed

Lines changed: 339 additions & 4 deletions

File tree

OnTopic.AspNetCore.Mvc.Host/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
options.MinimumSameSitePolicy = SameSiteMode.None;
2525
});
2626

27+
/*------------------------------------------------------------------------------------------------------------------------------
28+
| Configure: Output Caching
29+
\-----------------------------------------------------------------------------------------------------------------------------*/
30+
builder.Services.AddResponseCaching();
31+
2732
/*------------------------------------------------------------------------------------------------------------------------------
2833
| Configure: MVC
2934
\-----------------------------------------------------------------------------------------------------------------------------*/
@@ -49,7 +54,7 @@
4954
| Configure: Error Pages
5055
\-----------------------------------------------------------------------------------------------------------------------------*/
5156
if (!app.Environment.IsDevelopment()) {
52-
app.UseStatusCodePagesWithReExecute("/Error/{0}");
57+
app.UseStatusCodePagesWithReExecute("/Error/{0}/");
5358
app.UseExceptionHandler("/Error/500/");
5459
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
5560
app.UseHsts();
@@ -63,6 +68,7 @@
6368
app.UseCookiePolicy();
6469
app.UseRouting();
6570
app.UseCors("default");
71+
app.UseResponseCaching();
6672

6773
/*------------------------------------------------------------------------------------------------------------------------------
6874
| Configure: MVC

OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Repositories/StubTopicRepository.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ private static Topic CreateFakeData() {
170170
_ = new Topic("400", "Page", error, currentAttributeId++);
171171
_ = new Topic("Unauthorized", "Page", error, currentAttributeId++);
172172

173+
/*------------------------------------------------------------------------------------------------------------------------
174+
| Establish caching tests
175+
\-----------------------------------------------------------------------------------------------------------------------*/
176+
var cacheProfile = new Topic("CacheProfile", "CacheProfile", rootTopic, currentAttributeId++);
177+
var cachedPage = new Topic("CachedPage", "Page", web, currentAttributeId++);
178+
var uncachedPage = new Topic("UncachedPage", "Page", web, currentAttributeId++);
179+
180+
cacheProfile.Attributes.SetValue("Duration", "10");
181+
cacheProfile.Attributes.SetValue("Location", "Any");
182+
183+
cachedPage.References.SetValue("CacheProfile", cacheProfile);
184+
cachedPage.Attributes.SetValue("View", "Counter");
185+
186+
uncachedPage.Attributes.SetValue("View", "Counter");
187+
173188
/*------------------------------------------------------------------------------------------------------------------------
174189
| Set to cache
175190
\-----------------------------------------------------------------------------------------------------------------------*/

OnTopic.AspNetCore.Mvc.IntegrationTests.Host/Startup.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public Startup(IConfiguration configuration) {
4646
/// </summary>
4747
public void ConfigureServices(IServiceCollection services) {
4848

49+
/*------------------------------------------------------------------------------------------------------------------------------
50+
| Configure: Output Caching
51+
\-----------------------------------------------------------------------------------------------------------------------------*/
52+
services.AddResponseCaching();
53+
4954
/*------------------------------------------------------------------------------------------------------------------------
5055
| Configure: MVC
5156
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -77,13 +82,14 @@ public static void Configure(IApplicationBuilder app) {
7782
| Configure: Error Pages
7883
\-----------------------------------------------------------------------------------------------------------------------*/
7984
app.UseDeveloperExceptionPage();
80-
app.UseStatusCodePagesWithReExecute("/Error/{0}");
85+
app.UseStatusCodePagesWithReExecute("/Error/{0}/");
8186

8287
/*------------------------------------------------------------------------------------------------------------------------
8388
| Configure: Server defaults
8489
\-----------------------------------------------------------------------------------------------------------------------*/
8590
app.UseStaticFiles();
8691
app.UseRouting();
92+
app.UseResponseCaching();
8793

8894
/*------------------------------------------------------------------------------------------------------------------------
8995
| Configure: MVC
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@getCounter()
2+
3+
@functions {
4+
5+
//Establish a counter for each page path
6+
public static Dictionary<string, int> pageCounter = new();
7+
8+
//Increment the counter for each call to a given page path
9+
public int getCounter() {
10+
var path = Context.Request.Path;
11+
if (!pageCounter.ContainsKey(path)) {
12+
pageCounter.Add(path, 0);
13+
}
14+
pageCounter[path] = pageCounter[path]+1;
15+
return pageCounter[path];
16+
}
17+
18+
}

OnTopic.AspNetCore.Mvc.IntegrationTests/ServiceCollectionExtensionsTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
| Client Ignia, LLC
44
| Project Topics Library
55
\=============================================================================================================================*/
6+
using System;
67
using System.Net;
78
using Microsoft.AspNetCore.Routing;
89

@@ -107,5 +108,44 @@ public async Task UseStatusCodePages_ReturnsExpectedStatusCode(string path, Http
107108

108109
}
109110

111+
/*==========================================================================================================================
112+
| TEST: USE RESPONSE CACHING: RETURNS CACHED PAGE
113+
\-------------------------------------------------------------------------------------------------------------------------*/
114+
/// <summary>
115+
/// Evaluates a route with response caching, and confirms that the page remains unchanged after subsequent calls.
116+
/// </summary>
117+
/// <remarks>
118+
/// The <c>Counter.cshtml</c> page will increment a number output for every request to a given path. The <c>CachedPage</c>
119+
/// request will not increment because the cached result is being returned; the <c>UncachedPage</c> will increment because
120+
/// the results are not cached.
121+
/// </remarks>
122+
[Theory]
123+
[InlineData("/Web/CachedPage/", "1", "1", true)]
124+
[InlineData("/Web/UncachedPage/", "1", "2", false)]
125+
public async Task UseResponseCaching_ReturnsCachedPage(
126+
string path,
127+
string firstResult,
128+
string secondResult,
129+
bool validateHeaders
130+
) {
131+
132+
var client = _factory.CreateClient();
133+
var uri = new Uri(path, UriKind.Relative);
134+
135+
var response1 = await client.GetAsync(uri).ConfigureAwait(false);
136+
var content1 = await response1.Content.ReadAsStringAsync().ConfigureAwait(false);
137+
138+
var response2 = await client.GetAsync(uri).ConfigureAwait(false);
139+
var content2 = await response2.Content.ReadAsStringAsync().ConfigureAwait(false);
140+
141+
response1.EnsureSuccessStatusCode();
142+
143+
Assert.StartsWith(firstResult, content1, StringComparison.Ordinal);
144+
Assert.StartsWith(secondResult, content2, StringComparison.Ordinal);
145+
Assert.Equal(validateHeaders? true : null, response1.Headers.CacheControl?.Public);
146+
Assert.Equal(validateHeaders? TimeSpan.FromSeconds(10) : null, response1?.Headers.CacheControl?.MaxAge);
147+
148+
}
149+
110150
} //Class
111151
} //Namespace

OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace OnTopic.AspNetCore.Mvc.Controllers {
1515
/// identifying the topic associated with the given path, determining its content type, and returning a view associated with
1616
/// that content type (with potential overrides for multiple views).
1717
/// </summary>
18+
[TopicResponseCache]
1819
public class TopicController : Controller {
1920

2021
/*==========================================================================================================================
@@ -56,7 +57,7 @@ ITopicMappingService topicMappingService
5657
/// Provides a reference to the Topic Repository in order to gain arbitrary access to the entire topic graph.
5758
/// </summary>
5859
/// <returns>The TopicRepository associated with the controller.</returns>
59-
protected ITopicRepository TopicRepository { get; }
60+
protected internal ITopicRepository TopicRepository { get; }
6061

6162
/*==========================================================================================================================
6263
| CURRENT TOPIC

OnTopic.AspNetCore.Mvc/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for util
88
### Contents
99
- [Components](#components)
1010
- [Controllers and View Components](#controllers-and-view-components)
11+
- [Filters](#filters)
1112
- [View Conventions](#view-conventions)
1213
- [View Matching](#view-matching)
1314
- [View Locations](#view-locations)
@@ -20,7 +21,7 @@ The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for util
2021
- [Error Handling](#error-handling)
2122

2223
## Components
23-
There are five key components at the heart of the ASP.NET Core implementation.
24+
There are six components at the heart of the ASP.NET Core implementation.
2425
- **`TopicController`**: This is a default controller instance that can be used for _any_ topic path. It will automatically validate that the `Topic` exists, that it is not disabled (`!IsDisabled`), and will honor any redirects (e.g., if the `Url` attribute is filled out). Otherwise, it will return a `TopicViewResult` based on a view model, view name, and content type.
2526
- **`TopicRouteValueTransformer`**: A `DynamicRouteValueTransformer` for use with the ASP.NET Core's `MapDynamicControllerRoute()` method, allowing for route parameters to be implicitly inferred; notably, it will use the `area` as the default `controller` and `rootTopic`, if those route parameters are not otherwise defined.
2627
- **`TopicViewLocationExpander`**: Assists the out-of-the-box Razor view engine in locating views associated with OnTopic, e.g. by looking in `~/Views/ContentTypes/{ContentType}.cshtml`, or `~/Views/{ContentType}/{View}.cshtml`. See [View Locations](#view-locations) below.
@@ -45,6 +46,11 @@ There are five main controllers and view components that ship with the ASP.NET C
4546
> ): base(topicRepository, hierarchicalTopicMappingService) {}
4647
> }
4748
49+
## Filters
50+
There are two filters included with the ASP.NET Core implementation, which are meant to work in conjunction with `TopicController`:
51+
- **[`[ValidateTopic]`](_filters/ValidateTopicAttribute.cs)**: A filter attribute that handles topics that aren't intended to be served publicly, such as `PageGroup` and `Container` content types, or topics with `Url` or `IsDisabled` set.
52+
- **[`[TopicResponseCache]`](_filters/TopicResponseCacheAttribute.cs)**: A filter attribute registered on `TopicController` which checks for an affiliated `CacheProfile` topic and sets HTTP response headers accordingly. Compatible with the [ASP.NET Core Response Caching Middleware](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware).
53+
4854
## View Conventions
4955
By default, OnTopic matches views based on the current topic's `ContentType` and, if available, `View`.
5056

0 commit comments

Comments
 (0)