Tuesday, July 23, 2013

Executing .Net Component Presentations (Web Controls) from the Broker database

In several .Net implementations I have received customers inputs asking why we need to deploy dynamic component presentations that contain .net code in the file system and why we don't just execute them directly form the database.

Actually it is easy to accomplish by using an old .net technology called Virtual Path Provider, this technology has been there since the first days of .Net and it fits perfectly in this scenario.

Solution

We should change both the deployer and web site configuration so that the component presentations are not stored / retrieved from the file system.

  • deployer cd_storage_conf.xml
<Item typeMapping="ComponentPresentation" itemExtension=".Ascx" storageId="database"/>

  • web site cd_storage_conf.xml
<Item typeMapping="ComponentPresentation" itemExtension=".Ascx" storageId="database"/>

Once we have configured the deployer / web site to manage .Net Web Controls in the database the next step is to configure a Virtual Path Provider.

A Virtual Path Provider consist in 3 pieces, a Virtual File, Cache Invalidation and a Virtual Path Provider.

Virtual File

A virtual file is an entity that indicates the ASP .Net runtime where to retrieve the .Net Web Control that needs to be compiled in order to be served to the user.

Here a sample of a Virtual File for .Net Web Controls.

public class TridionVirtualFile : VirtualFile {
    private static readonly int publicationId;

    static TridionVirtualFile() {
        publicationId = int.Parse(WebConfigurationManager.AppSettings["PublicationID"]);
    }

    public TridionVirtualFile(string virtualPath)
        : base(virtualPath) {
    }

    public static bool FileExist(string virtualPath) {
        if (Path.GetExtension(virtualPath).Equals(".ascx")) {
            int componentId = 0;
            int templateId = 0;

            if (TridionVirtualFile.TryGetComponentPresentationIds(virtualPath, out componentId, out templateId)) {
                ComponentPresentationFactory factory = new ComponentPresentationFactory(publicationId);
                return factory.GetComponentPresentation(componentId, templateId) != null;
            }
        }
        return false;
    }

    public bool FileExist(string virtualPath, out ComponentPresentationMeta cpMeta) {
        int componentId = 0;
        int templateId = 0;

        cpMeta = null;
        if (TridionVirtualFile.TryGetComponentPresentationIds(virtualPath, out componentId, out templateId)) {
            ComponentPresentationMetaFactory factory = new ComponentPresentationMetaFactory(publicationId);
            cpMeta = factory.GetMeta(componentId, templateId);

            return cpMeta != null;
        }
        return false;
    }

    public override Stream Open() {
        ComponentPresentationMeta cpMeta;
        if (FileExist(VirtualPath, out cpMeta)) {
            ComponentPresentationFactory factory = new ComponentPresentationFactory(publicationId);
            ComponentPresentation cp = factory.GetComponentPresentation(cpMeta.ComponentId, cpMeta.TemplateId);

            string content = cp.GetContent(false);
            byte[] bytes = Encoding.UTF8.GetBytes(content);
            return new MemoryStream(bytes);
        }
        return new MemoryStream();
    }

    private static bool TryGetComponentPresentationIds(string virtualPath, out int componentId, out int templateId) {
        string dcpName = Path.GetFileNameWithoutExtension(virtualPath);
        string[] dcpNameParts = dcpName.Split('_');

        componentId = 0;
        templateId = 0;

        if (dcpNameParts.Length == 3) {
            if (!int.TryParse(dcpNameParts[1], out componentId)) {
                return false;
            }
            if (!int.TryParse(dcpNameParts[2], out templateId)) {
                return false;
            }
            return true;
        }
        return false;
    }
}


Virtual Path Provider

A Virtual Path provider is the interface between the ASP .Net compiler and in this case a Tridion Virtual File (.Net Web Control), this one will check if the component presentation exist and if it should be compiled / recompiled. In this case I have defined cache invalidation based on the published date so that we will be recompiling Tridion Virtual Files (.Net Web Controls) every time it is republished.

Here a sample of a Virtual Path Provider.

public class TridionVirtualPathProvider : VirtualPathProvider {
    public override bool FileExists(string virtualPath) {
        if (TridionVirtualFile.FileExist(virtualPath)) {
            return true;
        }
        else {
            return Previous.FileExists(virtualPath);
        }
    }

    public override VirtualFile GetFile(string virtualPath) {
        if (TridionVirtualFile.FileExist(virtualPath)) {
            return new TridionVirtualFile(virtualPath);
        }
        else {
            return Previous.GetFile(virtualPath);
        }
    }

    public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, 

DateTime utcStart) {
        if (TridionVirtualFile.FileExist(virtualPath)) {
            return new CacheDependency(
                new string[] { HostingEnvironment.MapPath("~/Web.config") },
                new string[] { Path.GetFileName(virtualPath) } // Cache is dependent in those cache keys
            );
        }
        else {
            return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
        }
    }
}


Execute and Retrieve Tridion .Net DCPs from the database

I was researching on which is the best place to do this without affecting current implementations, I realized that inheriting from the existing Tridion Web Controls like <tridion:ComponentPresentation> will be backwards compatible with previous implementations.

Please notice that any code in this post won't be supported by Tridion R&D or Customer Support since it is a custom developed solution.

I noticed that the class ComponentPresentationAssembler is actually validating if the DCP exist in the file system and failing the request in case it is not present, so the strategy I have taken is to override the Render method so that I can execute my own logic before the ComponentPresentationAssembler is executed.

Here a sample.

public class ComponentPresentation : TridionWeb.ComponentPresentation {
    protected override void Render(HtmlTextWriter writer) {
        if (HttpContext.Current != null && HttpContext.Current.Application != null) {
            DynamicContent.ComponentPresentationFactory factory =
                new DynamicContent.ComponentPresentationFactory(ComponentUri);
          
            DynamicContent.ComponentPresentation cp = factory.GetComponentPresentation(ComponentUri, TemplateUri);
            ComponentPresentationMeta cpMeta = cp.Meta;

            if (cpMeta.ContentType.Equals("ASCX WebControl")) {
                TcmUri componentId = new TcmUri(ComponentUri);
                TcmUri templateId = new TcmUri(TemplateUri);

                ComponentMetaFactory metaFactory = new ComponentMetaFactory(componentId.PublicationId);
                IComponentMeta componentMeta = metaFactory.GetMeta(componentId.ItemId);

                string virtualPath = string.Format("dcp_{0}_{1}.ascx", componentId.ItemId, templateId.ItemId);
                //CacheInvalidation(virtualPath, componentMeta);

                Control control = Page.LoadControl(virtualPath);
                if (control != null) {
                    control.RenderControl(writer);
                }
            }
            else {
                writer.Write(cp.GetContent(false));
            }

            this.RenderChildren(writer);
        }
    }
}


The code above will retrieve and compile the .Net DCP for every request, so it of course is not good for performance, as mentioned before we will use a cache invalidation mechanism in order to invalidate the DCP every time it is published so that the Virtual Path Provider will process it again.

Here a sample of how the cache should be invalidated, in this case I am changing a cache key every time I publish a DCP.

private void CacheInvalidation(string virtualPath, IComponentMeta componentMeta) {
    if (HttpContext.Current.Cache[virtualPath] == null) {
        HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
    }
    else {
        DateTime lastPublishedDate = (DateTime)HttpContext.Current.Cache[virtualPath];
        if (lastPublishedDate < componentMeta.LastPublicationDate) {
            HttpContext.Current.Cache.Remove(virtualPath);
            HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
        }
    }
}


After enabling cache, the performance will increase since it will be just recompiled when there is a new publish date.