Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Render View to String caching issue

For my project I currently implemented a ViewRender for my asp.net core application. It generates Views without a controller to html, this works fine using the following code:

public class ViewRenderService : IViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;

    public ViewRenderService(IRazorViewEngine razorViewEngine,
        ITempDataProvider tempDataProvider,
        IServiceProvider serviceProvider)
    {
        _razorViewEngine = razorViewEngine;
        _tempDataProvider = tempDataProvider;
        _serviceProvider = serviceProvider;
    }

    public async Task<string> RenderToStringAsync(string viewName, object model)
    {
        var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };

        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        string viewgerendered = "";
        try
        {
            using (var sw = new StringWriter())
            {
                var viewResult = _razorViewEngine.GetView(viewName, viewName, false);

                if (viewResult.View == null)
                {
                    throw new ArgumentNullException($"{viewName} does not match any available view");
                }

                var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    Model = model
                };        

                var viewContext = new ViewContext(
                    actionContext,
                    viewResult.View,
                    viewDictionary,
                    new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                    sw,
                    new HtmlHelperOptions()
                );

                await viewResult.View.RenderAsync(viewContext);

                viewgerendered = sw.ToString();
                return viewgerendered;
            }
        }
        catch (Exception e)
        {
            object temp = e.Message + " - " + e.StackTrace;
            return temp.ToString();
        }
    }

    public Task RenderToStringAsync(string v)
    {
        throw new NotImplementedException();
    }
}

Source: https://ppolyzos.com/2016/09/09/asp-net-core-render-view-to-string/

Changes which are made to views which use this renderer are not updated without restarting the application itself. Diving further into it, the views are cached. A comment within the source mentions using the _razorViewEngine.GetView method should get rid of my caching issue. However this doesn't work.

What I got, trying to figure out a way to register a new ViewRender, with a slight modification of the ViewRenderService.

//Seems not to be available on asp.net core 2.0...
services.AddMvc().Configure<MvcViewOptions>(options =>
            {
                options.ViewEngines.Clear();
                options.ViewEngines.Add(typeof(CustomViewEngine));
            });

And to overload the RazorViewEngine to expose the ViewLookupCache, where supposedly the view cache is located.

  public class CustomViewEngine : RazorViewEngine
    {
        public CustomViewEngine(
            IRazorPageFactoryProvider pageFactory, 
            IRazorPageActivator pageActivator,
            HtmlEncoder htmlEncoder, 
            IOptions<RazorViewEngineOptions> optionsAccessor, 
            Microsoft.AspNetCore.Razor.Language.RazorProject razorProject, 
            ILoggerFactory loggerFactory, 
            System.Diagnostics.DiagnosticSource diagnosticSource) : 
            base(pageFactory, pageActivator, htmlEncoder, optionsAccessor,razorProject,loggerFactory, diagnosticSource){ }

        public void RemoveCachedView(string view)
        { 
            this.ViewLookupCache.Remove(view);
        }
    }

There's not a lot to find on how caching is done within asp.net core 2.0 for views and clearing a particular view / set of. Basically I want to find a way how I can flush an entire selection of cached views as a command, for performance reasons.

Edit 13-04-2018

As suggested by K Finley, I tried emptying the ViewLookupCache as suggested. The code in short;

In my startup.cs ConfigureServices (not entirely sure if this is how a custom viewengine is registered).

    services.AddSingleton<IRazorViewEngine, CustomViewEngine>();
    services.AddSingleton<IViewRenderService, ViewRenderService>();

The custom view engine:

public class CustomViewEngine : RazorViewEngine
{
    public CustomViewEngine(
        IRazorPageFactoryProvider pageFactory,
        IRazorPageActivator pageActivator,
        HtmlEncoder htmlEncoder,
        IOptions<RazorViewEngineOptions> optionsAccessor,
        Microsoft.AspNetCore.Razor.Language.RazorProject razorProject,
        ILoggerFactory loggerFactory,
        System.Diagnostics.DiagnosticSource diagnosticSource) :
        base(pageFactory, pageActivator, htmlEncoder, optionsAccessor, razorProject, loggerFactory, diagnosticSource)
    { }


    public void RemoveViewFromCache(string viewName, string controller, bool isLayout, bool isPartial = false, string pageName = null, string areaName = null)
    {   
        var key = new ViewLocationCacheKey(viewName, controller, areaName, pageName, !isLayout | !isPartial, isLayout ? null : new Dictionary<string, string>(StringComparer.Ordinal));        
        base.ViewLookupCache.Remove(key);
    }

    public void RemoveViewFromCache(string viewName, bool isLayout)
    {
        //Code uses this one
        var key = new ViewLocationCacheKey(viewName, isLayout);  
        base.ViewLookupCache.Remove(key);    
    }
}

And modified the original ViewRenderService...

    public class ViewRenderService : IViewRenderService
    {
        private CustomViewEngine _razorViewEngine;
        private ITempDataProvider _tempDataProvider;
        private IServiceProvider _serviceProvider;
        private IHostingEnvironment _hostingEnvironment;


        public ViewRenderService(IRazorViewEngine razorViewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider, 
            IHostingEnvironment hostingEnvironment)
        {
            _razorViewEngine = (CustomViewEngine)razorViewEngine;
...

try
            {
                using (var sw = new StringWriter())
                {
                    _razorViewEngine.RemoveViewFromCache(viewName, false);
                    var viewResult = _razorViewEngine.GetView(viewName, viewName, false);

These modifications do delete the ViewLookupCache using the second method. However it still doesn't properly update my views. I do have to note the views don't have their own controller.

like image 808
usselite Avatar asked Mar 06 '23 21:03

usselite


2 Answers

You need to enable file watcher:
Add the environment variable DOTNET_USE_POLLING_FILE_WATCHER=true or ENV DOTNET_USE_POLLING_FILE_WATCHER=true in the Dockerfile.

like image 163
Andy Avatar answered Mar 15 '23 00:03

Andy


If you look at the RazorViewEngine source you can see how views are cached. I'll explain how to remove a view from the cache but you'll need some knowledge about the views in order to make it work.

In your example you're looking for the view in the cache based on just a name (I'm assuming the view name). This won't work because you need to look for the view in the cache using a ViewLocationCacheKey. There are 2 constructors on the ViewLocationCacheKey struct.

    public ViewLocationCacheKey(
        string viewName,
        bool isMainPage)

    public ViewLocationCacheKey(
        string viewName,
        string controllerName,
        string areaName,
        string pageName,
        bool isMainPage,
        IReadOnlyDictionary<string, string> values)

In the RazorViewEngine each of these are called depending on how the view is loaded (FromPath or FromViewLocations). With my situation I have a CMS that has full Razor support where any kind of content is just a a Razor View that gets rendered through the RazorViewEngine and therefore cached in Memory. When an update is made to a piece of content then I remove that view from the ViewLookupCache and allow it to get repopulated on the next load. Unfortunately the RazorViewEngine doesn't (currently) allow you to swap out the Cache to a distributed caching option (Redis, Memcached, etc.).

Here's how I'm handle it on my custom view engine.

public void RemoveViewFromCache(string viewName, string controller, bool isLayout, bool isPartial = false, string pageName = null, string areaName = null) {

     var key = new ViewLocationCacheKey(viewName, controller, areaName, pageName, !isLayout | !isPartial, isLayout ? null : new Dictionary<string, string>(StringComparer.Ordinal))

     base.ViewLookupCache.Remove(key);
}

I remove the view when an update is made and let the view get reloaded and cached naturally the next time it's requested.

If your app is doing anything with ViewLocationExpanderValues you'll have to make changes. Just inspect the ViewLookupCache collection while the app is running and you'll start to get a feel for what's going on here.

like image 34
K Finley Avatar answered Mar 15 '23 00:03

K Finley