-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #128 from cloudscribe/feature/127
#127 caching nav view component
- Loading branch information
Showing
9 changed files
with
355 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using Microsoft.Extensions.Caching.Distributed; | ||
using Microsoft.Extensions.Caching.Memory; | ||
using Microsoft.Extensions.Logging; | ||
using System; | ||
using System.Threading.Tasks; | ||
|
||
namespace cloudscribe.Web.Navigation.Caching | ||
{ | ||
public class DOMTreeCache : IDOMTreeCache | ||
{ | ||
private readonly IDistributedCache _cache; | ||
private readonly ILogger<DistributedTreeCache> _logger; | ||
|
||
public DOMTreeCache( | ||
IDistributedCache cache, | ||
ILogger<DistributedTreeCache> logger) | ||
{ | ||
_cache = cache; | ||
_logger = logger; | ||
} | ||
|
||
public async Task<string> GetDOMTree(string cacheKey) | ||
{ | ||
var dom = await _cache.GetAsync<string>(cacheKey); | ||
return dom; | ||
} | ||
|
||
public async Task StoreDOMTree(string cacheKey, string tree, int expirationSeconds) | ||
{ | ||
try | ||
{ | ||
var options = new DistributedCacheEntryOptions(); | ||
options.SetSlidingExpiration(TimeSpan.FromSeconds(expirationSeconds)); | ||
await _cache.SetAsync<string>(cacheKey, tree, options); | ||
_logger.LogDebug($"Added navigation DOM tree to distributed cache: {cacheKey}"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, $"Failed to add navigation DOM tree to distributed cache: {cacheKey}"); | ||
} | ||
} | ||
|
||
public async Task ClearDOMTreeCache(string cacheKey) | ||
{ | ||
await _cache.RemoveAsync(cacheKey); | ||
// ((MemoryCache)_cache).Compact(1); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using System.Threading.Tasks; | ||
|
||
namespace cloudscribe.Web.Navigation.Caching | ||
{ | ||
public interface IDOMTreeCache | ||
{ | ||
Task ClearDOMTreeCache(string cacheKey); | ||
Task<string> GetDOMTree(string cacheKey); | ||
Task StoreDOMTree(string cacheKey, string tree, int expirationSeconds); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.ModelBinding; | ||
using Microsoft.AspNetCore.Mvc.Rendering; | ||
using Microsoft.AspNetCore.Mvc.ViewEngines; | ||
using Microsoft.AspNetCore.Mvc.ViewFeatures; | ||
using Microsoft.Extensions.Logging; | ||
using System; | ||
using System.IO; | ||
using System.Threading.Tasks; | ||
|
||
namespace cloudscribe.Web.Navigation.Caching | ||
{ | ||
/// <summary> | ||
/// produce an html string representing the site nav - for use incaching - using | ||
/// Razor templates and models | ||
/// | ||
/// JimK - this is based on the main CS version, but simplified - | ||
/// passing in to it the actionContext etc etc. from the consuming method | ||
/// rather than relying on DI services in here | ||
/// seems to prevent a proliferation of "object disposed" errors. | ||
/// </summary> | ||
public class NavViewRenderer | ||
{ | ||
public NavViewRenderer(ILogger<NavViewRenderer> logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
private readonly ILogger<NavViewRenderer> _logger; | ||
|
||
public async Task<string> RenderViewAsStringWithActionContext<TModel>(string viewName, | ||
TModel model, | ||
ViewEngineResult viewResult, | ||
ActionContext actionContext, | ||
TempDataDictionary tempData) | ||
{ | ||
var viewData = new ViewDataDictionary<TModel>( | ||
metadataProvider: new EmptyModelMetadataProvider(), | ||
modelState: new ModelStateDictionary()) | ||
{ | ||
Model = model | ||
}; | ||
|
||
|
||
try | ||
{ | ||
using (StringWriter output = new StringWriter()) | ||
{ | ||
ViewContext viewContext = new ViewContext( | ||
actionContext, | ||
viewResult.View, | ||
viewData, | ||
tempData, | ||
output, | ||
new HtmlHelperOptions() | ||
); | ||
|
||
await viewResult.View.RenderAsync(viewContext); | ||
|
||
return output.GetStringBuilder().ToString(); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_logger.LogError(ex, "NavViewRenderer - error in view rendering for view " + viewName); | ||
throw ex; | ||
} | ||
} | ||
} | ||
} |
173 changes: 173 additions & 0 deletions
173
src/cloudscribe.Web.Navigation/CachingNavigationViewComponent.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// Copyright (c) Source Tree Solutions, LLC. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using cloudscribe.Web.Navigation.Caching; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.Infrastructure; | ||
using Microsoft.AspNetCore.Mvc.Razor; | ||
using Microsoft.AspNetCore.Mvc.Routing; | ||
using Microsoft.AspNetCore.Mvc.ViewEngines; | ||
using Microsoft.AspNetCore.Mvc.ViewFeatures; | ||
using Microsoft.Extensions.Logging; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
|
||
namespace cloudscribe.Web.Navigation | ||
{ | ||
public class CachingNavigationViewComponent : ViewComponent | ||
{ | ||
public CachingNavigationViewComponent( | ||
NavigationTreeBuilderService siteMapTreeBuilder, | ||
IEnumerable<INavigationNodePermissionResolver> permissionResolvers, | ||
IEnumerable<IFindCurrentNode> nodeFinders, | ||
IUrlHelperFactory urlHelperFactory, | ||
IActionContextAccessor actionContextAccesor, | ||
INodeUrlPrefixProvider prefixProvider, | ||
ILogger<NavigationViewComponent> logger, | ||
IDOMTreeCache DomCache, | ||
IRazorViewEngine viewEngine, | ||
NavViewRenderer viewRenderer, | ||
ITempDataProvider tempDataProvider) | ||
{ | ||
_builder = siteMapTreeBuilder; | ||
_permissionResolvers = permissionResolvers; | ||
_nodeFinders = nodeFinders; | ||
_urlHelperFactory = urlHelperFactory; | ||
_actionContextAccesor = actionContextAccesor; | ||
_prefixProvider = prefixProvider; | ||
_log = logger; | ||
_domCache = DomCache; | ||
_viewEngine = viewEngine; | ||
_viewRenderer = viewRenderer; | ||
_tempDataProvider = tempDataProvider; | ||
} | ||
|
||
private ILogger _log; | ||
private readonly IDOMTreeCache _domCache; | ||
private readonly IRazorViewEngine _viewEngine; | ||
private readonly NavViewRenderer _viewRenderer; | ||
private readonly ITempDataProvider _tempDataProvider; | ||
private NavigationTreeBuilderService _builder; | ||
private IEnumerable<INavigationNodePermissionResolver> _permissionResolvers; | ||
private IEnumerable<IFindCurrentNode> _nodeFinders; | ||
private IUrlHelperFactory _urlHelperFactory; | ||
private IActionContextAccessor _actionContextAccesor; | ||
private INodeUrlPrefixProvider _prefixProvider; | ||
|
||
|
||
// intention here is not to re-compute the whole navigation DOM tree repeatedly | ||
// when you get a large number of unauthenticated page requests e.g. from a PWA | ||
// The main problem is clearing this cache again on new page creation etc | ||
// since .Net memory cache has no method for enumerating its keys - | ||
// you need to know the specific key name. | ||
// In a simplecontent system I'd probably need to use the IHandlePageCreated (etc) | ||
// hooks to clear a navcache of known name. | ||
|
||
public async Task<IViewComponentResult> InvokeAsync(string viewName, | ||
string filterName, | ||
string startingNodeKey, | ||
int expirationSeconds = 60, | ||
bool testMode = false) | ||
{ | ||
NavigationViewModel model = null; | ||
|
||
string cacheKey = $"{viewName}_{filterName}_{startingNodeKey}"; | ||
|
||
// authenticated users - always do what the stadard version of this component does: | ||
// build the tree afresh | ||
if (User.Identity.IsAuthenticated) | ||
{ | ||
// maybe kill cache key here under certain circumstances? | ||
// if(User.IsInRole("Administrators") || User.IsInRole("Content Administrators")) | ||
// { | ||
// // await _domCache.ClearDOMTreeCache(cacheKey); | ||
// } | ||
|
||
model = await CreateNavigationTree(filterName, startingNodeKey); | ||
return View(viewName, model); | ||
} | ||
else | ||
{ | ||
var result = await _domCache.GetDOMTree(cacheKey); // use the viewname as the key in the cache | ||
|
||
if (string.IsNullOrEmpty(result)) | ||
{ | ||
model = await CreateNavigationTree(filterName, startingNodeKey); | ||
|
||
ViewEngineResult viewResult = null; | ||
var actionContext = _actionContextAccesor.ActionContext; | ||
var tempData = new TempDataDictionary(actionContext.HttpContext, _tempDataProvider); | ||
|
||
string fullViewName = $"Components/CachingNavigation/{viewName}"; | ||
try | ||
{ | ||
// beware the 'IFeatureCollection has been disposed' System.ObjectDisposedException error here | ||
viewResult = _viewEngine.FindView(actionContext, fullViewName, true); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_log.LogError(ex, $"CachingNavigationViewComponent: Failed to search for View {fullViewName}"); | ||
} | ||
|
||
if (viewResult == null || !viewResult.Success || viewResult.View == null) | ||
{ | ||
_log.LogError($"CachingNavigationViewComponent: Failed to find a matching view {fullViewName}"); | ||
} | ||
else | ||
{ | ||
try | ||
{ | ||
result = await _viewRenderer.RenderViewAsStringWithActionContext(fullViewName, | ||
model, | ||
viewResult, | ||
actionContext, | ||
tempData | ||
); | ||
|
||
if (!string.IsNullOrEmpty(result)) | ||
{ | ||
if(testMode) | ||
{ | ||
await _domCache.StoreDOMTree(cacheKey, $"<h2>Cached copy from {cacheKey}</h2> {result}", expirationSeconds); | ||
} | ||
else | ||
{ | ||
await _domCache.StoreDOMTree(cacheKey, result, expirationSeconds); | ||
} | ||
} | ||
|
||
_log.LogInformation($"CachingNavigationViewComponent: Rendered view successfully for {fullViewName}"); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_log.LogError(ex, $"CachingNavigationViewComponent: Failed to render view for {fullViewName}"); | ||
throw (ex); | ||
} | ||
} | ||
} | ||
return View("CachedNav", result); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// The expensive thing... | ||
/// </summary> | ||
private async Task<NavigationViewModel> CreateNavigationTree(string filterName, string startingNodeKey) | ||
{ | ||
var rootNode = await _builder.GetTree(); | ||
var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccesor.ActionContext); | ||
NavigationViewModel model = new NavigationViewModel( | ||
startingNodeKey, | ||
filterName, | ||
Request.HttpContext, | ||
urlHelper, | ||
rootNode, | ||
_permissionResolvers, | ||
_nodeFinders, | ||
_prefixProvider.GetPrefix(), | ||
_log); | ||
return model; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
...on/Views/Shared/Components/CachingNavigation/Bootstrap5TopNavWithDropdowns_Caching.cshtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
@using cloudscribe.Web.Navigation | ||
@using System.Text | ||
@model NavigationViewModel | ||
@using Microsoft.Extensions.Localization | ||
@inject IStringLocalizer<cloudscribe.Web.Navigation.MenuResources> sr | ||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | ||
@addTagHelper *, cloudscribe.Web.Navigation | ||
|
||
@{ | ||
Layout = null; | ||
} | ||
|
||
<ul class="navbar-nav me-auto" role="menubar" aria-label="@sr["Top menu"]"> | ||
@if (await Model.ShouldAllowView(Model.RootNode)) | ||
{ | ||
<li role="none" cwn-data-attributes="@Model.RootNode.Value.DataAttributes" class='@Model.GetClass(Model.RootNode.Value, "nav-item")'><a role="menuitem" class="nav-link" href="@Url.Content(Model.AdjustUrl(Model.RootNode))">@Html.Raw(Model.GetIcon(Model.RootNode.Value))@sr[Model.AdjustText(Model.RootNode)]</a></li> | ||
} | ||
|
||
@if (await Model.HasVisibleChildren(Model.RootNode)) | ||
{ | ||
@foreach (var node in Model.RootNode.Children) | ||
{ | ||
if (!await Model.ShouldAllowView(node)) { continue; } | ||
if (!await Model.HasVisibleChildren(node)) | ||
{ | ||
<li role="none" class='@Model.GetClass(node.Value, "nav-item")' cwn-data-attributes="@node.Value.DataAttributes"><a role="menuitem" class="nav-link" href="@Url.Content(Model.AdjustUrl(node))">@Html.Raw(Model.GetIcon(node.Value))@sr[Model.AdjustText(node)]</a></li> | ||
} | ||
else | ||
{ | ||
<li role="none" class='@Model.GetClass(node.Value, "nav-item dropdown", "active", true)' cwn-data-attributes="@node.Value.DataAttributes"> | ||
<a role="menuitem" class="nav-link dropdown-toggle" id="[email protected]" aria-haspopup="true" aria-expanded="false" href="@Url.Content(Model.AdjustUrl(node))">@Html.Raw(Model.GetIcon(node.Value))@sr[Model.AdjustText(node)] </a> | ||
@Model.UpdateTempNode(node) <partial name="Bootstrap5NavigationNodeChildDropdownPartial" model="@Model" /> | ||
</li> | ||
} | ||
} | ||
} | ||
</ul> |
2 changes: 2 additions & 0 deletions
2
src/cloudscribe.Web.Navigation/Views/Shared/Components/CachingNavigation/CachedNav.cshtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@model string | ||
@Html.Raw(Model) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters