Fluent API Best Practices for Children Nodes

Posted by Community Admin on 03-Aug-2018 19:05

Fluent API Best Practices for Children Nodes

All Replies

Posted by Community Admin on 15-May-2011 00:00

I had a question regarding the best practice to acquire the children nodes for a specific page within the front end site map.  Consider this code snippet :

1.using (var fluent = App.WorkWith())
2.
3.    var parentPage = fluent.Pages()
4.        .Where(node => node.Id == parentPageId).Get().FirstOrDefault();
5.    var childrenPages = parentPage.Nodes;
6.

We do some foreach'ing through the childrenPages to load some of the details in a simple class to populate a rad tree control.

Now for my question.  This page only has four children nodes -- why is this generating 1858 database calls?  Is our query poorly optimized?

Posted by Community Admin on 17-May-2011 00:00

Hi James,

There is a problem with the plural pages facade. I would recommend that you use the 'native' API:

var parentPageId = Guid.Empty; // set to some real ID
  
var parentPage2 = PageManager.GetManager().GetPageNodes().Where(p => p.Id == parentPageId).FirstOrDefault();
  
if (parentPage2 != null)
  
  
    var childNodes = parentPage2.Nodes;
  

If you do know that there is a page by this ID, you will really speed thigs up by taking advantage of OpenAcces' first-level cache:

var parentPageId = Guid.Empty; // set to some real ID
  
var childNodes2 = PageManager.GetManager().GetPageNode(parentPageId).Nodes;

The difference between the two is that the first query always goes to the DB, while the second takes advantage of OA's first-level cache (e.g. when you retrieve items by just their PK and not a full-fledged query).

We are aware that there are performance issues, and we are working on them. I do not know how many issues will be addressed in the next Q, but we have dedicated a lot of our internal resources to performance improvements.

Greetings,

Dido
the Telerik team

Do you want to have your say in the Sitefinity development roadmap? Do you want to know when a feature you requested is added or when a bug fixed? Explore the Telerik Public Issue Tracking system and vote to affect the priority of the items

Posted by Community Admin on 17-May-2011 00:00

This was incredibly valuable information and EXACTLY what we were needing!  I had optimized some fluent API queries that were taking SECONDS to run down to about 400-800ms and now with your change all of our queries are running in 10-20ms which is absolutely phenomenal!

Thanks so much for the help and insight, we will be changing out the rest of our queries over the coming days and will report back if anything else looks strange.

Thanks again.

James

Posted by Community Admin on 19-May-2011 00:00

I can get a PageManaer() a couple of different ways.  Are the sfPm1 and sfPm2 objects below, the same w respect to performance?

var sfPm1 = PageManager.GetManager().

using (var fluent = App.WorkWith())
    var pageFacade = fluent.Page();
    var sfPm2 = pageFacade.PageManager;

I sorta like sfPm2 because in the same code block of code I have to create a new page, and I do that like this:

pageFacade.CreateNewStandardPage(myPageNode)

Thanks

Posted by Community Admin on 20-May-2011 00:00

I can focus my question now that I have dug into this a little more.  The way that we are currently creating PageNodes is using memory to such an extent that we can only create 20 or so at a time. The memory usage of the IIS process continues to increase until the machine has no more to provide.   We would like to create 180 pages at a time.  

What is the best way to create pages?  

The following code block is what we are currently doing.  It is based on code that this forum gave us several months back.

using (var fluent = App.WorkWith())
    var pageFacade = fluent.Page();
    var pm = pageFacade.PageManager;
    LoadVars(pm, sfModelPageId, parentGuid);
     pageFacade.CreateNewStandardPage(parentNode)
        .Do(pn =>
        
            pn.Title = childPageName;
            pn.UrlName = childPageName;
            pn.Page.Title = childPageName;
            pn.InheritsPermissions = true;
            // pn.Ordinal = theOrdinal;
        )
        .CheckOut() // creates a draft version and locks the page
        .SetTemplateTo(guidTemplate)
        .Publish()
        .SaveAndContinue()
        .SaveChanges();

What code can we use to create pages without the memory usage running out of control?

Thanks.

Posted by Community Admin on 24-May-2011 00:00

Hello Phil,

If you get the manager through the pages facade, it will be in a named transaction. In Sitefinity, named transactions are an artificial way to work in transaction with the objects of several providers/managers. The only difference to the end user is that you can no longer call SaveChanges on the manager, you will have to invoke TransactionManager.CommitTransaction(transactionName), where transactionName is the name of the named transaction. You can always check if a provider is working in a named transaction mode by checking the value of Manager.Provider.TransactionName. If it is null or empty, you work in a regular transaction (per-manager). Otherwise, you should pass that value to TransactionManager.CommitTransaction.
For this to work, the underlying providers must implement FlushTransaction, RollbackTransaction and CommitTransaction (and in the stock providers they are implemented via decorators).

As far as page creation performance - I can't really tell, as I haven't really tested. In fact, that's what a team of ours is doing this Q, but they are profiling other areas in the pages API/services. I can offer some general advice, however:

  • Make sure that you create pages in the same transaction (or at least 100 of them). Every call to App.WorkWith().FacadeName() will initiate a new named transaction, which is expensive. You don't want to do that. If you are doing this in a loop, try something like this (pseudo code):
    const int gcCollectFrequency = 100;
    const int commitFrequency = 100;
    var fluentPages = App.WorkWith().Page();
    for (int i = 0; i < pagesToCreate.Count; i++) 
       // your logic for creating pages
       if (i > 0)
          if (i % commitFrequency == 0)
             fluentPages.SaveChanges();
             fluentPages = App.WorkWith().Page(); // create a new named transaction
          
          if (i % gcCollectFrequency == 0)
             GC.Collect(); 
          

    Calling the garbage collector can deteriorate performance, but will save memory. After all, .net runtime won't start cleaning up unless you are going out of memory.
  • Because of content lifecycle, you create a new object (and copy object trees to and fro) every time you perform an operation like CheckOut, CheckIn, Publish, etc. The fluent API goes a few steps further and creates binary versions of your pages for every draft and public version. Since the road from Draft o Live is defined by a workflow, publishing a page might invoke the workflow service as well. If there are registered publishing pipes for pages (e.g. searching or rss), the publishing system will get called, too. All these are nice, because they autamote a lot of steps that the end user needn't worry about. After all, the purpose of the fluent API is to simplify a common scenario. If you need full control over what's happening - you use the 'native' API.
    If you only want to create once a batch of pages, you don't need to go through the whole process.If your pages will be public, it will go easier on the memory, as in Pages lifecycle only the live page (PageData) is mandatory; the rest (PageDraft-s, accesible through PageData.Drafts) are optional. Since you can't truely turn workflow off, you will need to manually set PageData.WorkflwoApprovalStatus for the appropriate cultures (if your site is multilingual; the property is an Lstring). I don't really know the specifics of your case, and therefore can't give an example, but using the native API would definitely make things easier on CPU and memory (since you are going to omit workflow, lifecycle operations and create only one version - the final one).
CMS-es are expected to have a lot of features (lifecycle, workflow, granular permissions, versioning), and the API is expected to be easy and flexible. Well, the last two don't go well together, and therefore we have two APIs for the same task: one that is easier and prettier (fluent), and one that isn't shy of complexity, but allows great flexibility. The rule on the thumb with 'native' API is that one manager should not call another (there are very few exceptions, and it is only for query-ing , not for manipulation), which means that you have to do everything by yourself. Here is something like a quick cheat sheet about the native pages API:
  • Manage pages:
    • PageNode holds information that is necessary to build a site map.
      • What in the UI is called 'group page' is actually a page node with its PageNode.Page (PageData) set to null
      • The UI has a 'redirect' page, which is again a PageNode with no page data
    • PageData is the 'public' page. It contains all the information Sitefinity needs to build an .aspx and pass it to the ASP.NET BuildManager (i.e. template parser). If its Visible property is set to false, it is what the UI calls 'unpublished'. The Version field alse plays role, but you shouldn't manage it yourself.
    • PageDraft is again used to hold information needed to render the page (e.g. controls, title, <head> contents, etc), but it is used to represent a draft (when IsTempDraft is false), or a page data that is visible only in the page editor (IsTempDraft == true).
    • The relation goes like this: PageNode.Page [PageData]. PageDrafts [PageDraft, at most two - one with IsTempDraft set to true, and one with IsTempDraft set to false].
  • Manage workflow
    • This is something rather complex. We use WF, which is hosted by a web service. To access that web service, you have to use WorkflowManager.MessageWorkflow. I won't discuss the parameters, because they are dependent ont he workflow and you won't be messaging the workflow in your batch page creation
    • We don't use the feature of WF that persists the current state of the workflow for numerous reasons. Instead, our workflows (.xamlx) are finite state machines that receive messages to go from one state to another. The current state is stored in PageData.ApprovalWorkflowState. In your batch creation, you will want to set it either to "Draft" or "Published". Since this is an Lstring, it can have different values per culture.
  • Pages lifecycle
    • Do not manage pages licecycle by yourself. Use PageManager.PublishPageDraft and PageManager.EditPage.
    • PageManger doesn't set ApprovalWorkflowState. For your batch creation, setting the feild is required, as the backend will assume your pages are drafts (this is the default state for workflow items)
  • Versioning
    • To create a new version, simply pass the data item to VersionManager.CreateVersion.
  • Manage transactions
    • You will want to perform all steps in a single transaction. First, you will need to create a unique name for your transaction. To obtain a manager in named transaction you get it like Manager.GetManager(providerName, transactionName). Save changes for all managers at once with TransactionManager.CommitTransaction

All the best,
Dido
the Telerik team
Do you want to have your say in the Sitefinity development roadmap? Do you want to know when a feature you requested is added or when a bug fixed? Explore the Telerik Public Issue Tracking system and vote to affect the priority of the items

This thread is closed