Caching of a TreeNode's Pages

George Tselios asked on March 31, 2020 10:28

Dear Sirs,

In our current project (Kentico 12.0.51 MVC) we have decided to use the MultiDocumentQuery and the un-typed version of DocumentQuery, so all our document queries return either a TreeNode or an IEnumerable of TreeNode.

In cases that a content only page has a field of "Pages" Data Type (w/ Pages form control), we initially load the corresponding TreeNode of the content only page, and then we execute a query similar to the bellow in order to also load the referenced pages:

  FormFieldInfo formField = FormHelper.GetFormInfo(node.NodeClassName, false, true, false).GetFormField(propertyName);

        var adHocRelationshipNameCodeName = RelationshipNameInfoProvider
            .GetAdHocRelationshipNameCodeName(node.NodeClassName, formField);

        var relationshipNameInfo = RelationshipNameInfoProvider
            .GetRelationshipNameInfo(adHocRelationshipNameCodeName);

        var query = RelationshipInfoProvider.ApplyRelationshipOrderData(
            DocumentHelper.GetDocuments()
            .Culture(new string[] { node.DocumentCulture })
            .WithCoupledColumns(true)
            .Published(!node.IsLastVersion)
            .PublishedVersion(!node.IsLastVersion)
            .InRelationWith(node.NodeGUID, adHocRelationshipNameCodeName, RelationshipSideEnum.Left), node.NodeID, relationshipNameInfo.RelationshipNameId);

In terms of caching the above query, we have used the following dependency key:

"nodeid|<NodeID>|relationships"

but it does not seem to work as expected.

What we have found out is that when a "referenced" page is modified, this modification does not clear the above cache, so an older version of data is still available. In order to force the above cache to be cleared, one has to re-save the content-only page in Kentico Admin, resulting in the entire cache of the content-only page to be cleared.

Please provide clarifications regarding the correct way to cache such document queries.

Thanks in advance,
George

Correct Answer

Dmitry Bastron answered on April 1, 2020 13:45

Dear George,

Yes, you are right, there was a mistake in my code, apologies for that. Here is the correct one:

public IEnumerable<TreeNode> GetRelatedDocuments(TreeNode mainNode, string relatedProperty)
{
    IEnumerable<TreeNode> result = null;

    var dataCacheKey = $"custom|{mainNode.NodeID}|{relatedProperty}";

    using (var cs = new CachedSection<IEnumerable<TreeNode>>(ref result, 10, true, dataCacheKey))
    {
        if (cs.LoadData)
        {
            var formField = FormHelper.GetFormInfo(mainNode.NodeClassName, false, true, false)
                .GetFormField(relatedProperty);

            var adHocRelationshipNameCodeName = RelationshipNameInfoProvider
                .GetAdHocRelationshipNameCodeName(mainNode.NodeClassName, formField);

            var relationshipNameInfo = RelationshipNameInfoProvider
                .GetRelationshipNameInfo(adHocRelationshipNameCodeName);

            var query = RelationshipInfoProvider.ApplyRelationshipOrderData(
                DocumentHelper.GetDocuments().Culture(new string[] { mainNode.DocumentCulture })
                    .WithCoupledColumns(true).Published(!mainNode.IsLastVersion)
                    .PublishedVersion(!mainNode.IsLastVersion).InRelationWith(mainNode.NodeGUID,
                        adHocRelationshipNameCodeName, RelationshipSideEnum.Left), mainNode.NodeID,
                relationshipNameInfo.RelationshipNameId);

            result = query.ToList();
            cs.Data = result;

            // related pages by ID
            var dependencies = result.Select(x => $"nodeid|{x.NodeID}").ToList();
            // main page dependencies by ID
            dependencies.Add($"nodeid|{mainNode.NodeID}|relationships");

            cs.CacheDependency = CacheHelper.GetCacheDependency(dependencies);
        }
    }

    return result;
}

Regarding the second point, even if it's not an explicit relationship, CMS_Relationship is used behind the scenes. My expectation was it should work as well. Probably regarding this you may contact support as there may be a bug or something. Anyway, as a workaround, you can use "main" Node ID. I know it's not ideal, and will invalidate the cache when other properties of that page change, but should work:

dependencies.Add($"nodeid|{mainNode.NodeID}");
// instead of
dependencies.Add($"nodeid|{mainNode.NodeID}|relationships");
0 votesVote for this answer Unmark Correct answer

Recent Answers


Dmitry Bastron answered on March 31, 2020 11:40

Hi George,

The dummy key you mentioned

"nodeid|<NodeID>|relationships"

is working the following way: it is triggered when relationships change for the main page, i.e. you add or remove a new relationship, reorder them. But instead, you need to use IDs of related pages to achieve what you want. Iterate through the query from your example and combine the following list to use as dependencies:

nodeid|<related page 1 node id>
nodeid|<related page 2 node id>
...
0 votesVote for this answer Mark as a Correct answer

George Tselios answered on March 31, 2020 11:53

Dear Dmitry,

Thanks for your answer.

If I understand correctly, I still have to use the above cache dependency key, for any modifications regarding the "main" page but I have also to add dependency keys for each referenced node. Right?

How do I get the required NodeIds of the referenced Pages prior to executing the query?

Thanks in advance,
George

0 votesVote for this answer Mark as a Correct answer

Dmitry Bastron answered on March 31, 2020 12:58

Dear George,

Yes, the full set of dependency keys will be the following:

nodeid|<main page node id>
nodeid|<main page node id>|relationships
nodeid|<related page 1 node id>
nodeid|<related page 2 node id>
...

Also worth mentioning that this approach with Node IDs is not the best one if you have multilingual site and separate caches per language. If you have let's say en-GB and de-DE cultures and amend the main page on en-GB culture the approach above will flush the cache for both en-GB and de-DE cultures. If you need to keep it culture-specific - use document ID instead, here is the full list of dummy keys you can use, just in case.

0 votesVote for this answer Mark as a Correct answer

George Tselios answered on March 31, 2020 13:11

Dear Dmitry,

Thanks again for your answer.

What we need in order to apply the mentioned dummy keys are the node ids or document ids of the nodes that are referenced by the "main" page. How do we get these ids before we execute the query?

Regards,
George

0 votesVote for this answer Mark as a Correct answer

Dmitry Bastron answered on March 31, 2020 14:58

Dear George,

Hmm.. now it's not clear to me how do you store those related pages for main page. Do you request it separately only when neede? Or do you always request those and store in the "main" page custom property in the code? What do you do with the query in your example?

If the code you've posted is a separate method - you can add caching logic into this method with the key containing "main" page ID and dependencies to "main" page ID and "query" pages. And yes, those dependencies need to be setup when you first call the query and after you get the result so that you can use returned IDs.

0 votesVote for this answer Mark as a Correct answer

George Tselios answered on March 31, 2020 16:16

Dear Dmitry,

Regarding the related pages storage, in the Page Type of a "main" page we create a field of "Pages" Data Type and "Pages" Form Control.

When we need to get the data of both the "main" page and its "related" pages we do the following two queries:

  1. Given the "main" page's NodeID we execute a DocumentQuery in order to load the TreeNode of the "main" page. In this query we introduce the dummy cache keys "node|main page node Id" and "node|main page node id|relationships".
  2. After we get the TreeNode of the "main" page, for the property of "Pages" form field, we execute the query I have posted in order to get the "related" pages TreeNodes.

Our question has to do with a way to provide caching of the second query. How can we introduce a dummy cache key like "nodeid|related page node Id" for each "related" page since we don't know a way to get the required "related page node Ids" before executing the second query. Could you please provide a solution for acquiring the "related page node Ids"?

Thanks in advance,
George

0 votesVote for this answer Mark as a Correct answer

Dmitry Bastron answered on March 31, 2020 18:37

Dear George,

The following code demonstrates the caching example:

public IEnumerable<TreeNode> GetRelatedDocuments(TreeNode mainNode, string relatedProperty)
{
    IEnumerable<TreeNode> result = null;

    var dataCacheKey = $"custom|{mainNode.NodeID}|{relatedProperty}";

    using (var cs = new CachedSection<IEnumerable<TreeNode>>(ref result, 10, true, dataCacheKey))
    {
        if (cs.LoadData)
        {
            var formField = FormHelper.GetFormInfo(mainNode.NodeClassName, false, true, false)
                .GetFormField(relatedProperty);

            var adHocRelationshipNameCodeName = RelationshipNameInfoProvider
                .GetAdHocRelationshipNameCodeName(mainNode.NodeClassName, formField);

            var relationshipNameInfo = RelationshipNameInfoProvider
                .GetRelationshipNameInfo(adHocRelationshipNameCodeName);

            var query = RelationshipInfoProvider.ApplyRelationshipOrderData(
                DocumentHelper.GetDocuments().Culture(new string[] {mainNode.DocumentCulture})
                    .WithCoupledColumns(true).Published(!mainNode.IsLastVersion)
                    .PublishedVersion(!mainNode.IsLastVersion).InRelationWith(mainNode.NodeGUID,
                        adHocRelationshipNameCodeName, RelationshipSideEnum.Left), mainNode.NodeID,
                relationshipNameInfo.RelationshipNameId);

            var relatedPages = query.ToList();
            cs.Data = relatedPages;

            // related pages by ID
            var dependencies = relatedPages.Select(x => $"nodeid|{x.NodeID}").ToList();
            // main page dependencies by ID
            dependencies.Add($"nodeid|{mainNode.NodeID}|relationships");

            cs.CacheDependency = CacheHelper.GetCacheDependency(dependencies);
        }
    }

    return result;
}

This method:

  • Will always return related pages for "main" page stored in specific "relatedProperty" from cache (if it is in the cache, or load otherwise)
  • Will flush this specific cache if dependencies of the "main" page change (add new related page in CMS admin for example)
  • Will flush this specific cache if any of these related pages change

I hope it gives you a better idea of setting up dependencies. But typically I'd recommend strong typing with specific properties added via partial classes, bake some of this logic there and cache the entire result with property filled. It works better from my experience.

0 votesVote for this answer Mark as a Correct answer

George Tselios answered on April 1, 2020 11:53

Dear Dmitry,

Thank you very much for your code snippet regarding the use of the CacheSection.

I used the above code but faced the following two issues:

  1. During the first request to the above method, when the code enters the cs.LoadData block, although the relatedPages variable holds actual TreeNodes that then assigns to the cs.Data, the result variable remains null and thus the method does not return any TreeNode. On the second request the result variable holds the correct results retrieved from the cache. Perhaps, in the code inside cs.LoadData block, the value of the result variable should be set explicitly, e.g. result = relatedPages ?
  2. The cache is not cleared when you make changes in the "main" page related to the "relatedProperty" field. For example, if you add, remove or change the sort order of a related page, of the "relatedProperty" field, the cache is not cleared. Please keep in mind that the "relatedProperty" is a field of "Pages" data type and "Pages" form control. It is not an explicitly defined relationship between page types.

Please advise on the above issues.

Thanks in advance,
George

0 votesVote for this answer Mark as a Correct answer

George Tselios answered on April 1, 2020 17:17

Dear Dmitry,

In regards to the result variable, I made the suggested change and it works fine. I have also changed the dependency key and it work as expected for both the "main node" and the related pages.

Regards,
George

0 votesVote for this answer Mark as a Correct answer

   Please, sign in to be able to submit a new answer.