1

We are using Virtual Path Provider in our Tridion implementation. We run into problems where the cache is not being updated and the web page is showing the old content (although the publishing is successful). Anyone has similar issues with VPP and what is the solution to address this?

1

2 Answers 2

3

In your virtual path provider you need to handle the creation of the cachedependency object as mentioned in earlier comments and answers. What we do to allow ASPX/ASCX code in DCPs to be "executable" is use a virtual path provider which maps to the broker database:

using System.Web.Hosting;
using System.Web.Caching;
using System;
using System.Web;
using System.Linq;
using System.Security.Permissions;

namespace Hinttech.Tridion.Webcontrols.Broker
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Medium)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.High)]
    public class BrokerVirtualPathProvider : VirtualPathProvider
    {
        /// <summary>
        /// Determines if the given path is a path that this virtual path provider should handle
        /// </summary>
        /// <param name="virtualPath">path to check</param>
        /// <returns>true if the given path is a valid virtual path for this virtual path provider, false otherwise</returns>
        private bool IsVirtual(string virtualPath)
        {
            return BrokerVirtualFile.PATTERN.IsMatch(virtualPath);
        }

        /// <summary>
        /// Determines if the given path is an existing virtual resource
        /// </summary>
        /// <param name="virtualPath">path to check</param>
        /// <returns>true if the path maps to an "existing" item; either via this virtual path provider or any other, false otherwise</returns>
        public override bool FileExists(string virtualPath)
        {
            //As the broker has no method for querying the index of all CPs directly we're not even
            //going to bother here and just return true if the pattern matches; 
            //our BrokerVirtualFile will throw a highly specific exception when the DCP doesnt exist 
            //which we can catch from our BrokerHelper methods if desired.
            if (this.IsVirtual(virtualPath))
            {
                return true;
            }
            else
            {
                return Previous.FileExists(virtualPath);
            }
        }

        /// <summary>
        /// Retrieves the VirtualFile object for the given path
        /// </summary>
        /// <param name="virtualPath">path to retrieve file for</param>
        /// <returns>VirtualFile from this provider or any other</returns>
        public override VirtualFile GetFile(string virtualPath)
        {
            if (this.IsVirtual(virtualPath))
            {
                return new BrokerVirtualFile(virtualPath);
            }
            else
            {
                return Previous.GetFile(virtualPath);
            }
        }

        /// <summary>
        /// Generates the cachedependency object for the ASP.Net runtime to use for determining if a code file needs
        /// to be recompiled or not.
        /// </summary>
        /// <param name="virtualPath">The virtual path of the item to watch</param>
        /// <param name="virtualPathDependencies">Established virtual paths that this item depends on; if any of these are changed this item will need to be recompiled</param>
        /// <param name="utcStart">no clue what this is for</param>
        /// <returns>CacheDependency object either from this virtual path provider or any other</returns>
        public override CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
        {
            if (IsVirtual(virtualPath))
            {
                //What we do here is that we establish a cache dependency on another cache key. If we want to force
                //the ASP.Net runtime to recompile the item all we will need to do is update the value of the given
                //cache key. This allows us to implement transparent caching by comparing the output of the broker
                //itself with our cached copy; if not equal override the cached copy which will trigger the compiler
                //to decache it's cached copy of the compiled item.
                string[] pathDependencies = new string[] { "BrokerHelper." + virtualPath };
                return new CacheDependency(null, pathDependencies, utcStart);
            }
            else
            {
                return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
            }
        }
    }
}

This will trigger the ASP.Net compiler to decache whenever the corresponding cache key is updated. Your code that renders the DCPs will need to retrieve the content from the broker and store it in the cache key whenever it is changed (in our solution we do this for every request; because the broker cache ensures this does not result in a DB query every time we request the content from the broker this does not cause any performance issues in our situation).

Our implementation is using a helper class with a static method:

private static ComponentPresentationAssembler _assembler = new ComponentPresentationAssembler();

/// <summary>
/// Writes the content of a single DCP as retrieved from the broker to the given HtmlTextWriter
/// By default ASCX code in the output is executed but this can be disabled for performance.
/// </summary>
/// <param name="page">ASP.Net runtime Page object for the context page</param>
/// <param name="output">HtmlTextWriter where output must be written</param>
/// <param name="componentId">TCMID of the component whose component presentation to output</param>
/// <param name="componentTemplateId">TCMID of the component template whose component presentation to output</param>
/// <param name="isCode">Compile output as ASCX if true; treat as text if false</param>
public void WriteContent(Page page, HtmlTextWriter output, string componentId, string componentTemplateId, bool isCode)
{
    //Get the content from Tridion and compare with cached version
    string content = _assembler.GetContent(componentId, componentTemplateId);
    //If output of DCP is ASCX code load as control; else write output
    if (isCode)
    {
        //Determing cache key for broker output
        //Note that we're not actually caching broker output here; this is just done to cache the compiler output
        //of the .Net runtime using cachedependencies on cache keys!
        string cacheKey = "BrokerHelper./" + componentId.Replace(":", "_") + "_" + componentTemplateId.Replace(":", "_") + "_dcp.ascx";
        //Verify if currently retrieved content is not modified from the cached version
        if ((page.Cache[cacheKey] as string) != content)
        {
            //Reset cache; this should trigger the compiler's own cachedependencies as we're registering the 
            //cache dependency on this key in the virtual path provider!
            HttpContext.Current.Trace.Write("NOTEQUAL CONTENTS :");
            HttpContext.Current.Trace.Write(" - cached copy : " + (page.Cache[cacheKey] as string));
            HttpContext.Current.Trace.Write(" - fresh copy  : " + content);
            page.Cache[cacheKey] = content;
        }
        //Load up the output as a control via the virtual path provider and render to output
        Control c = page.LoadControl("/" + componentId.Replace(":", "_") + "_" + componentTemplateId.Replace(":", "_") + "_dcp.ascx");
        c.RenderControl(output);
    }
    else
    {
        output.Write(content);
    }
}
4

Out of the box ASP.NET will cache the resulting file, but does not know where to check back to renew the cache. Explaining why you see the old file content even after publish.

I have written a VirtualPathProvider, for MVC views stored in the Tridion content delivery database, and with it implemented a custom cache dependency.

So first I override the following method of my VirtualPathProvider, as below:

public override CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        if (IsPathVirtual(virtualPath))
        {
            StreamReader reader = new StreamReader(this.GetFile(virtualPath).Open());
            return new TridionFileCacheDependency(reader.ReadToEnd(), virtualPath);
        }
        else
            return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }

Then, my custom cache dependency looks like:

public class TridionFileCacheDependency : CacheDependency
    {
        //TODO: Update Poll Interval
        private const int PollInterval = 6000;
        private readonly Timer _timer;
        private readonly string _content;
        private readonly string _virtualPath;

        public TridionFileCacheDependency(string content, string virtualPath)
        {
            _timer = new Timer(CheckDependencyCallback, this, PollInterval, PollInterval);
            _content = content;
            _virtualPath = virtualPath;
        }

        private void CheckDependencyCallback(object sender)
        {
            lock (_timer)
            {
                if (_content != GetVirtualFileContent(_virtualPath))
                {
                    NotifyDependencyChanged(sender, EventArgs.Empty);
                }
            }
        }

        private static string GetVirtualFileContent(string virtualPath)
        {
            TridionVirtualFile file = new TridionVirtualFile(virtualPath, new TridionPathProvider());
            StreamReader reader = new StreamReader(file.Open());
            return reader.ReadToEnd();
        }

        protected override void DependencyDispose()
        {
            if (_timer != null) _timer.Dispose();

            base.DependencyDispose();
        }
    }

Next on my todo list is to investigate pushing cache notifications from Tridion. I think Mihai has a couple of blog posts on this here: http://yatb.mitza.net/

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.