U4-9121 - improve url perfs

This commit is contained in:
Stephan
2016-10-28 14:33:44 +02:00
parent 7495d89a79
commit c500f98ad8
6 changed files with 268 additions and 199 deletions

View File

@@ -1,20 +1,11 @@
using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Xml;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Diagnostics.Windows;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Validators;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Rdbms;

View File

@@ -1,19 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using BenchmarkDotNet.Running;
namespace Umbraco.Tests.Benchmarks
{
class Program
internal class Program
{
static void Main(string[] args)
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<BulkInsertBenchmarks>();
Console.ReadLine();
if (args.Length == 0)
{
var summary = BenchmarkRunner.Run<BulkInsertBenchmarks>();
Console.ReadLine();
}
else if (args.Length == 1)
{
var type = Assembly.GetExecutingAssembly().GetType("Umbraco.Tests.Benchmarks." +args[0]);
if (type == null)
{
Console.WriteLine("Unknown benchmark.");
}
else
{
var summary = BenchmarkRunner.Run(type);
Console.ReadLine();
}
}
else
{
Console.WriteLine("?");
}
}
}
}

View File

@@ -1,61 +0,0 @@
Version 1.0.0.3 - Initial release to NuGet, pre-release.
TraceEvent has been available from the site http://bcl.codeplex.com/wikipage?title=TraceEvent for some time now
this NuGet Version of the library supersedes that one. WHile the 'core' part of the library is unchanged,
we did change lesser used features, and change the namespace and DLL name, which will cause break. We anticipate
it will take an hour or so to 'port' to this version from the old one. Below are specific details on what
has changed to help in this port.
* The DLL has been renamed from TraceEvent.dll to Microsoft.Diagnostics.Tracing.TraceEvent.dll
* The name spaces for all classes have been changed. The easiest way to port is to simply place
the following using clauses at the top of any file that uses TraceEvent classes
using Microsoft.Diagnostics.Symbols;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Etlx;
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
using Microsoft.Diagnostics.Tracing.Session;
using Microsoft.Diagnostics.Tracing.Stacks;
* Any method with the name RelMSec in it has been changed to be RelativeMSec. The easiest port is to
simply globally rename RelMSec to RelativeMSec
* Any property in the Trace* classes that has the form Max*Index has been renamed to Count.
* A number of methods have been declared obsolete, these are mostly renames and the warning will tell you
how to update them.
* The following classes have been rename
SymPath -> SymbolPath
SymPathElement -> SymbolPathElement
SymbolReaderFlags -> SymbolReaderOptions
* TraceEventSession is now StopOnDispose (it will stop the session when TraceEventSesssion dies), by default
If you were relying on the kernel session living past the process that started it, you must now set
the StopOnDispose explicitly
* There used to be XmlAttrib extensions methods on StringBuilder for use in manifest generated TraceEventParsers
These have been moved to protected members of TraceEvent. The result is that in stead of writing
sb.XmlAttrib(...) you write XmlAttrib(sb, ...)
* References to Pdb in names have been replaced with 'Symbol' to conform to naming guidelines.
***********************************************************************************************
Version 1.0.0.4 - Initial stable release
Mostly this was insuring that the library was cleaned up in preparation
for release the TraceParserGen tool
Improved the docs, removed old code, fixed some naming convention stuff
* Additional changes from the PreRelease copy to the first Stable release
* The arguments to AddCallbackForProviderEvent were reversed!!!! (now provider than event)
* The arguments to Observe(string, string)!!!! (now provider than event)
* Event names for these APIs must include a / between the Task and Opcode names
* Many Events in KernelTraceEventParser were harmonized to be consistent with other conventions
* Events of the form PageFault* were typically renamed to Memory*
* The 'End' suffix was renamed to 'Stop' (its official name)
* PerfInfoSampleProf -> PerfInfoSample
* PerfInfoSampleProf -> PerfInfoSample
* ReadyThread -> DispatcherReadyThread
* StackWalkTraceData -> StackWalkStackTraceData
* FileIo -> FileIO
* DiskIo -> DiskIO
* Many Events in SymbolTraceEventParser were harmonized to be consistent with other conventions
* names with Symbol -> ImageID

View File

@@ -99,20 +99,16 @@
<Compile Include="BulkInsertBenchmarks.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="XmlBenchmarks.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
<None Include="_TraceEventProgrammersGuide.docx" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.Analyzers.dll" />
<Analyzer Include="..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.Analyzers.dll" />
</ItemGroup>
<ItemGroup>
<Content Include="TraceEvent.ReadMe.txt" />
<Content Include="TraceEvent.ReleaseNotes.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
<Project>{31785bc3-256c-4613-b2f5-a1b0bdded8c1}</Project>

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnostics.Windows;
using BenchmarkDotNet.Jobs;
namespace Umbraco.Tests.Benchmarks
{
[Config(typeof(Config))]
public class XmlBenchmarks
{
private class Config : ManualConfig
{
public Config()
{
Add(new MemoryDiagnoser());
//Add(ExecutionValidator.FailOnError);
//The 'quick and dirty' settings, so it runs a little quicker
// see benchmarkdotnet FAQ
Add(Job.Default
.WithLaunchCount(1) // benchmark process will be launched only once
.WithIterationTime(100) // 100ms per iteration
.WithWarmupCount(3) // 3 warmup iteration
.WithTargetCount(3)); // 3 target iteration
}
}
[Setup]
public void Setup()
{
var templateId = 0;
var xmlText = @"<?xml version=""1.0"" encoding=""utf-8""?>
<!DOCTYPE root[
<!ELEMENT Home ANY>
<!ATTLIST Home id ID #REQUIRED>
<!ELEMENT CustomDocument ANY>
<!ATTLIST CustomDocument id ID #REQUIRED>
]>
<root id=""-1"">
<Home id=""1046"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""1"" createDate=""2012-06-12T14:13:17"" updateDate=""2012-07-20T18:50:43"" nodeName=""Home"" urlName=""home"" writerName=""admin"" creatorName=""admin"" path=""-1,1046"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[this/is/my/alias, anotheralias]]></umbracoUrlAlias>
<umbracoNaviHide>1</umbracoNaviHide>
<Home id=""1173"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:06:45"" updateDate=""2012-07-20T19:07:31"" nodeName=""Sub1"" urlName=""sub1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173"" isDoc="""">
<content><![CDATA[<div>This is some content</div>]]></content>
<umbracoUrlAlias><![CDATA[page2/alias, 2ndpagealias]]></umbracoUrlAlias>
<Home id=""1174"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-20T18:07:54"" updateDate=""2012-07-20T19:10:27"" nodeName=""Sub2"" urlName=""sub2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1174"" isDoc="""">
<content><![CDATA[]]></content>
<umbracoUrlAlias><![CDATA[only/one/alias]]></umbracoUrlAlias>
<creatorName><![CDATA[Custom data with same property name as the member name]]></creatorName>
</Home>
<Home id=""1176"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:08"" updateDate=""2012-07-20T19:10:52"" nodeName=""Sub 3"" urlName=""sub-3"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1176"" isDoc="""">
<content><![CDATA[]]></content>
</Home>
<CustomDocument id=""1177"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""custom sub 1"" urlName=""custom-sub-1"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1177"" isDoc="""" />
<CustomDocument id=""1178"" parentID=""1173"" level=""3"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""4"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-16T14:23:35"" nodeName=""custom sub 2"" urlName=""custom-sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1173,1178"" isDoc="""" />
</Home>
<Home id=""1175"" parentID=""1046"" level=""2"" writerID=""0"" creatorID=""0"" nodeType=""1044"" template=""" + templateId + @""" sortOrder=""3"" createDate=""2012-07-20T18:08:01"" updateDate=""2012-07-20T18:49:32"" nodeName=""Sub 2"" urlName=""sub-2"" writerName=""admin"" creatorName=""admin"" path=""-1,1046,1175"" isDoc=""""><content><![CDATA[]]></content>
</Home>
</Home>
<CustomDocument id=""1172"" parentID=""-1"" level=""1"" writerID=""0"" creatorID=""0"" nodeType=""1234"" template=""" + templateId + @""" sortOrder=""2"" createDate=""2012-07-16T15:26:59"" updateDate=""2012-07-18T14:23:35"" nodeName=""Test"" urlName=""test-page"" writerName=""admin"" creatorName=""admin"" path=""-1,1172"" isDoc="""" />
</root>";
_xml = new XmlDocument();
_xml.LoadXml(xmlText);
}
[Cleanup]
public void Cleanup()
{
_xml = null;
}
private XmlDocument _xml;
[Benchmark]
public void XmlWithXPath()
{
var xpath = "/root/* [@isDoc and @urlName='home']//* [@isDoc and @urlName='sub1']//* [@isDoc and @urlName='sub2']";
var elt = _xml.SelectSingleNode(xpath);
if (elt == null) Console.WriteLine("ERR");
}
[Benchmark]
public void XmlWithNavigation()
{
var elt = _xml.DocumentElement;
var id = NavigateElementRoute(elt, new[] {"home", "sub1", "sub2"});
if (id <= 0) Console.WriteLine("ERR");
}
private const bool UseLegacySchema = false;
private int NavigateElementRoute(XmlElement elt, string[] urlParts)
{
var found = true;
var i = 0;
while (found && i < urlParts.Length)
{
found = false;
foreach (XmlElement child in elt.ChildNodes)
{
var noNode = UseLegacySchema
? child.Name != "node"
: child.GetAttributeNode("isDoc") == null;
if (noNode) continue;
if (child.GetAttribute("urlName") != urlParts[i]) continue;
found = true;
elt = child;
break;
}
i++;
}
return found ? int.Parse(elt.GetAttribute("id")) : -1;
}
}
}

View File

@@ -16,7 +16,9 @@ using umbraco;
using System.Linq;
using umbraco.BusinessLogic;
using umbraco.presentation.preview;
using Umbraco.Core.Services;
using GlobalSettings = umbraco.GlobalSettings;
using Task = System.Threading.Tasks.Task;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
@@ -26,6 +28,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
private readonly RoutesCache _routesCache = new RoutesCache(!UnitTesting);
private DomainHelper _domainHelper;
private DomainHelper GetDomainHelper(IDomainService domainService)
{
return _domainHelper ?? (_domainHelper = new DomainHelper(domainService));
}
// for INTERNAL, UNIT TESTS use ONLY
internal RoutesCache RoutesCache { get { return _routesCache; } }
@@ -99,6 +108,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// - non-colliding, adds one complete "by route" lookup, only on the first time a url is computed (then it's cached anyways)
// - colliding, adds one "by route" lookup, the first time the url is computed, then one dictionary looked each time it is computed again
// assuming no collisions, the impact is one complete "by route" lookup the first time each url is computed
//
// U4-9121 - this lookup is too expensive when computing a large amount of urls on a front-end (eg menu)
// ... thinking about moving the lookup out of the path into its own async task, so we are not reporting errors
// in the back-office anymore, but at least we are not polluting the cache
// instead, refactored DeterminedIdByRoute to stop using XPath, with a 16x improvement according to benchmarks
// will it be enough?
var loopId = preview ? 0 : _routesCache.GetNodeId(route); // might be cached already in case of collision
if (loopId == 0)
{
@@ -130,62 +146,141 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
var pos = route.IndexOf('/');
var path = pos == 0 ? route : route.Substring(pos);
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
IEnumerable<XPathVariable> vars;
var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode, out vars);
//check if we can find the node in our xml cache
var content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray());
var id = NavigateRoute(umbracoContext, preview, startNodeId, path, hideTopLevelNode);
if (id > 0) return GetById(umbracoContext, preview, id);
// if hideTopLevelNodePath is true then for url /foo we looked for /*/foo
// but maybe that was the url of a non-default top-level node, so we also
// have to look for /foo (see note in ApplyHideTopLevelNodeFromPath).
if (content == null && hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0)
if (hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0)
{
xpath = CreateXpathQuery(startNodeId, path, false, out vars);
content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray());
var id2 = NavigateRoute(umbracoContext, preview, startNodeId, path, false);
if (id2 > 0) return GetById(umbracoContext, preview, id2);
}
return content;
return null;
}
private int NavigateRoute(UmbracoContext umbracoContext, bool preview, int startNodeId, string path, bool hideTopLevelNode)
{
var xml = GetXml(umbracoContext, preview);
XmlElement elt;
// empty path
if (path == string.Empty || path == "/")
{
if (startNodeId > 0)
{
elt = xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture));
return elt == null ? -1 : startNodeId;
}
elt = null;
var min = int.MaxValue;
foreach (XmlElement e in xml.DocumentElement.ChildNodes)
{
var sortOrder = int.Parse(e.GetAttribute("sortOrder"));
if (sortOrder < min)
{
min = sortOrder;
elt = e;
}
}
return elt == null ? -1 : int.Parse(elt.GetAttribute("id"));
}
// non-empty path
elt = startNodeId <= 0
? xml.DocumentElement
: xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture));
if (elt == null) return -1;
var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries);
if (hideTopLevelNode && startNodeId <= 0)
{
foreach (XmlElement e in elt.ChildNodes)
{
var id = NavigateElementRoute(e, urlParts);
if (id > 0) return id;
}
return -1;
}
return NavigateElementRoute(elt, urlParts);
}
private static bool UseLegacySchema
{
get { return UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema; }
}
private int NavigateElementRoute(XmlElement elt, string[] urlParts)
{
var found = true;
var i = 0;
while (found && i < urlParts.Length)
{
found = false;
foreach (XmlElement child in elt.ChildNodes)
{
var noNode = UseLegacySchema
? child.Name != "node"
: child.GetAttributeNode("isDoc") == null;
if (noNode) continue;
if (child.GetAttribute("urlName") != urlParts[i]) continue;
found = true;
elt = child;
break;
}
i++;
}
return found ? int.Parse(elt.GetAttribute("id")) : -1;
}
string DetermineRouteById(UmbracoContext umbracoContext, bool preview, int contentId)
{
var node = GetById(umbracoContext, preview, contentId);
if (node == null)
return null;
var elt = GetXml(umbracoContext, preview).GetElementById(contentId.ToString(CultureInfo.InvariantCulture));
if (elt == null) return null;
var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService);
var domainHelper = GetDomainHelper(umbracoContext.Application.Services.DomainService);
// walk up from that node until we hit a node with a domain,
// or we reach the content root, collecting urls in the way
var pathParts = new List<string>();
var n = node;
var hasDomains = domainHelper.NodeHasDomains(n.Id);
while (hasDomains == false && n != null) // n is null at root
var eltId = int.Parse(elt.GetAttribute("id"));
var eltParentId = int.Parse(((XmlElement) elt.ParentNode).GetAttribute("id"));
var e = elt;
var id = eltId;
var hasDomains = domainHelper.NodeHasDomains(id);
while (hasDomains == false && id != -1)
{
// get the url
var urlName = n.UrlName;
var urlName = e.GetAttribute("urlName");
pathParts.Add(urlName);
// move to parent node
n = n.Parent;
hasDomains = n != null && domainHelper.NodeHasDomains(n.Id);
e = (XmlElement) e.ParentNode;
id = int.Parse(e.GetAttribute("id"));
hasDomains = id != -1 && domainHelper.NodeHasDomains(id);
}
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (hasDomains == false && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath)
ApplyHideTopLevelNodeFromPath(umbracoContext, node, pathParts);
if (hasDomains == false && GlobalSettings.HideTopLevelNodeFromPath)
ApplyHideTopLevelNodeFromPath(umbracoContext, eltId, eltParentId, pathParts);
// assemble the route
pathParts.Reverse();
var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
var route = (n == null ? "" : n.Id.ToString(CultureInfo.InvariantCulture)) + path;
var route = (id == -1 ? "" : id.ToString(CultureInfo.InvariantCulture)) + path;
return route;
}
static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublishedContent node, IList<string> pathParts)
static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, int nodeId, int parentId, IList<string> pathParts)
{
// in theory if hideTopLevelNodeFromPath is true, then there should be only once
// top-level node, or else domains should be assigned. but for backward compatibility
@@ -195,12 +290,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// "/foo" fails (looking for "/*/foo") we try also "/foo".
// this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but
// that's the way it works pre-4.10 and we try to be backward compat for the time being
if (node.Parent == null)
if (parentId == -1)
{
var rootNode = umbracoContext.ContentCache.GetByRoute("/", true);
if (rootNode == null)
throw new Exception("Failed to get node at /.");
if (rootNode.Id == node.Id) // remove only if we're the default node
if (rootNode.Id == nodeId) // remove only if we're the default node
pathParts.RemoveAt(pathParts.Count - 1);
}
else
@@ -217,12 +312,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
public int Version { get; private set; }
public static string Root { get { return "/root"; } }
public string RootDocuments { get; private set; }
public string DescendantDocumentById { get; private set; }
public string ChildDocumentByUrlName { get; private set; }
public string ChildDocumentByUrlNameVar { get; private set; }
public string RootDocumentWithLowestSortOrder { get; private set; }
public XPathStringsDefinition(int version)
{
@@ -233,19 +323,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// legacy XML schema
case 0:
RootDocuments = "/root/node";
DescendantDocumentById = "//node [@id={0}]";
ChildDocumentByUrlName = "/node [@urlName='{0}']";
ChildDocumentByUrlNameVar = "/node [@urlName=${0}]";
RootDocumentWithLowestSortOrder = "/root/node [not(@sortOrder > ../node/@sortOrder)][1]";
break;
// default XML schema as of 4.10
case 1:
RootDocuments = "/root/* [@isDoc]";
DescendantDocumentById = "//* [@isDoc and @id={0}]";
ChildDocumentByUrlName = "/* [@isDoc and @urlName='{0}']";
ChildDocumentByUrlNameVar = "/* [@isDoc and @urlName=${0}]";
RootDocumentWithLowestSortOrder = "/root/* [@isDoc and not(@sortOrder > ../* [@isDoc]/@sortOrder)][1]";
break;
default:
@@ -421,84 +503,6 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
static readonly char[] SlashChar = new[] { '/' };
protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath, out IEnumerable<XPathVariable> vars)
{
string xpath;
vars = null;
if (path == string.Empty || path == "/")
{
// if url is empty
if (startNodeId > 0)
{
// if in a domain then use the root node of the domain
xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId);
}
else
{
// if not in a domain - what is the default page?
// let's say it is the first one in the tree, if any -- order by sortOrder
// but!
// umbraco does not consistently guarantee that sortOrder starts with 0
// so the one that we want is the one with the smallest sortOrder
// read http://stackoverflow.com/questions/1128745/how-can-i-use-xpath-to-find-the-minimum-value-of-an-attribute-in-a-set-of-elemen
// so that one does not work, because min(@sortOrder) maybe 1
// xpath = "/root/*[@isDoc and @sortOrder='0']";
// and we can't use min() because that's XPath 2.0
// that one works
xpath = XPathStrings.RootDocumentWithLowestSortOrder;
}
}
else
{
// if url is not empty, then use it to try lookup a matching page
var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries);
var xpathBuilder = new StringBuilder();
int partsIndex = 0;
List<XPathVariable> varsList = null;
if (startNodeId == 0)
{
if (hideTopLevelNodeFromPath)
xpathBuilder.Append(XPathStrings.RootDocuments); // first node is not in the url
else
xpathBuilder.Append(XPathStringsDefinition.Root);
}
else
{
xpathBuilder.AppendFormat(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId);
// always "hide top level" when there's a domain
}
while (partsIndex < urlParts.Length)
{
var part = urlParts[partsIndex++];
if (part.Contains('\'') || part.Contains('"'))
{
// use vars, escaping gets ugly pretty quickly
varsList = varsList ?? new List<XPathVariable>();
var varName = string.Format("var{0}", partsIndex);
varsList.Add(new XPathVariable(varName, part));
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlNameVar, varName);
}
else
{
xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part);
}
}
xpath = xpathBuilder.ToString();
if (varsList != null)
vars = varsList.ToArray();
}
return xpath;
}
#endregion
#region Detached