Sitecore LinkManager – Out of context link generation

This is part 2 of 2 posts on detailed Sitecore.Linkmanager configuration.
See part 1 here
Carrying on from the previous post, this post discusses an out of context link generation. This might be from a scheduled task or perhaps a separate thread. This brings its own challenges with site resolution and link formatting.

*** NOTE – If you just want the neat solution, skip to the bottom, as this post explores several dead-ends first ***

Here a simple method that writes links of all items to the Sitecore log, note that we’re specifying absolute urls and writing out the default settings for Options.Site.Name and Options.Siteresolving :

namespace Sitecore641.Business
{
public class Schedule
{
public void CreateLinks()
{
var web = Factory.GetDatabase("web");
var rootItem = web.GetItem("/sitecore/content");

var options = LinkManager.GetDefaultUrlOptions();
options.AlwaysIncludeServerUrl = true;

Log.Info("Options.Site : " + options.Site.Name, this);
Log.Info("Siteresolving : " + options.SiteResolving.ToString(), this);

foreach (var child in rootItem.Axes.GetDescendants())
{

Log.Info(LinkManager.GetItemUrl(child, options), this);
}
}
}
}

We can fire this method from a scheduler entry in web.config

<!-- SCHEDULING -->
<scheduling>
<!-- Time between checking for scheduled tasks waiting to execute -->
<frequency>00:00:30</frequency>
[.....]
<!-- Agent to process tasks from the task database (TaskDatabase) -->
<agent type="Sitecore641.Business.Schedule" method="CreateLinks" interval="00:00:30" />

A reminder of the current sites configuration in web.config :

<site name="subsite" hostName="641subsite.local" rootPath="/sitecore/content/" startItem="/Home/apple/subhome" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
<site name="website2" hostName="641website2.local" rootPath="/sitecore/content/" startItem="/home2" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
<site name="website" hostName="641website.*" rootPath="/sitecore/content/" startItem="/home" targetHostName="641website.local" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" /> 

And our content tree:

This gives the following output in the log:

ManagedPoolThread #15 14:06:32 INFO Job started: Sitecore641.Business.Schedule
ManagedPoolThread #15 14:06:32 INFO Options.Site : scheduler
ManagedPoolThread #15 14:06:32 INFO Siteresolving : False
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home2
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/pear
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/plum
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/apple
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome/blue
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome/green
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome/red
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home2/cat
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home2/dog
ManagedPoolThread #15 14:06:32 INFO http://127.0.0.1/sitecore/content/Home2/rabbit
ManagedPoolThread #15 14:06:32 INFO Job ended: Sitecore641.Business.Schedule (units processed: )

Site resolving doesn’t work – the links are being generated with an ip address rather than hostname, and we have Sitecore paths rather than links relative the each sites root. This behaviour seems strange- although we don’t have a http context, we do know what the item is – we’re directly handing it to LinkManager. Looking further there are a couple of things which are happening which are not what I’d have expected. Firstly, UrlOptions.Site.Name is defaulting to “scheduler”, secondly SiteResolving appears to be false by default.

Enable siteresolving?

So if we force siteresolving to true, perhaps this will work better? Unfortunately not – here is the revised code and output again:

var options = LinkManager.GetDefaultUrlOptions();
options.AlwaysIncludeServerUrl = true;
options.SiteResolving = true;

Log.Info("Options.Site : " + options.Site.Name, this);
Log.Info("Siteresolving : " + options.SiteResolving.ToString(), this);

foreach (var child in rootItem.Axes.GetDescendants())
{

Log.Info(LinkManager.GetItemUrl(child, options), this);
}

Gives this output:

ManagedPoolThread #6 14:15:05 INFO Job started: Sitecore641.Business.Schedule
ManagedPoolThread #6 14:15:05 INFO Options.Site : scheduler
ManagedPoolThread #6 14:15:05 INFO Siteresolving : True
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home2
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/pear
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/plum
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/apple
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome/blue
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome/green
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home/apple/subhome/red
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home2/cat
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home2/dog
ManagedPoolThread #6 14:15:05 INFO http://127.0.0.1/sitecore/content/Home2/rabbit
ManagedPoolThread #6 14:15:05 INFO Job ended: Sitecore641.Business.Schedule (units processed: )

No change. Poking around further reveals that when alwaysIncludeServerUrl=”true” (required for any multi-site setup) and there is no http context, the default behaviour is that the siteresolving process is bypassed by LinkManager even when explicly enabled. Therefore, the site an item belongs to is not evaluated against the site definitions. Instead, a fall-back host of “http://127.0.0.1” is used – not particularly useful in our scenario.

Manually assign UrlOptions.Site?

At this point, we might start going a bit off-piste. Maybe if we assign the site manually as “website” things might work out? Well, we end up with this instead:

options.AlwaysIncludeServerUrl = true;
options.SiteResolving = true;
options.Site = Factory.GetSite("website");

Output:

ManagedPoolThread #0 14:22:06 INFO Job started: Sitecore641.Business.Schedule
ManagedPoolThread #0 14:22:06 INFO Options.Site : website
ManagedPoolThread #0 14:22:06 INFO Siteresolving : True
ManagedPoolThread #0 14:22:06 INFO ://641website.local/
ManagedPoolThread #0 14:22:06 INFO ://641website.local/2
ManagedPoolThread #0 14:22:06 INFO ://641website.local/pear
ManagedPoolThread #0 14:22:06 INFO ://641website.local/plum
ManagedPoolThread #0 14:22:06 INFO ://641website.local/apple
ManagedPoolThread #0 14:22:06 INFO ://641website.local/apple/subhome
ManagedPoolThread #0 14:22:06 INFO ://641website.local/apple/subhome/blue
ManagedPoolThread #0 14:22:06 INFO ://641website.local/apple/subhome/green
ManagedPoolThread #0 14:22:06 INFO ://641website.local/apple/subhome/red
ManagedPoolThread #0 14:22:06 INFO ://641website.local/2/cat
ManagedPoolThread #0 14:22:06 INFO ://641website.local/2/dog
ManagedPoolThread #0 14:22:06 INFO ://641website.local/2/rabbit
ManagedPoolThread #0 14:22:06 INFO Job ended: Sitecore641.Business.Schedule (units processed: )

We have a new problem with malformed urls that are missing the scheme, normally “http”.

Set the hidden site scheme attribute?

Another undocumented attribute exists for the site definition – “scheme”. By default the httpcontext request scheme is used, but strangely when there is no httpcontext the default is an empty string. Specifying the scheme in the site definition allows us the change this:

<site name="website" hostName="641website.*" scheme="http" targetHostName="641website.local" rootPath="/sitecore/content/" startItem="/home" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />

Give us this:

ManagedPoolThread #0 14:47:54 INFO Job started: Sitecore641.Business.Schedule
ManagedPoolThread #0 14:47:54 INFO Options.Site : website
ManagedPoolThread #0 14:47:54 INFO Siteresolving : True
ManagedPoolThread #0 14:47:54 INFO http://641website.local/
ManagedPoolThread #0 14:47:54 INFO http://641website.local/2
ManagedPoolThread #0 14:47:54 INFO http://641website.local/pear
ManagedPoolThread #0 14:47:54 INFO http://641website.local/plum
ManagedPoolThread #0 14:47:54 INFO http://641website.local/apple
ManagedPoolThread #0 14:47:54 INFO http://641website.local/apple/subhome
ManagedPoolThread #0 14:47:54 INFO http://641website.local/apple/subhome/blue
ManagedPoolThread #0 14:47:54 INFO http://641website.local/apple/subhome/green
ManagedPoolThread #0 14:47:54 INFO http://641website.local/apple/subhome/red
ManagedPoolThread #0 14:47:54 INFO http://641website.local/2/cat
ManagedPoolThread #0 14:47:54 INFO http://641website.local/2/dog
ManagedPoolThread #0 14:47:54 INFO http://641website.local/2/rabbit
ManagedPoolThread #0 14:47:54 INFO Job ended: Sitecore641.Business.Schedule (units processed: )

It fixes the url formatting problem but not the bigger issue of site resolution. We could extend the code to resolve the site manually and set the UrlOptions.Site accordingly, however this smells of re-inventing existing functionality (and probably creating new problems in the process).

Allowing siteresolving to work normally

The fundamental issue is that when we are out of httpcontext and have a site specified in UrlOptions with a “matching” startath (remember this is “scheduler” by default in this scenario), the site resolving process is bypassed. To reinstate siteresolving we need to either set the UrlOptions.Site to null or prevent items from matching the startPath of the “scheduler” site definition.

Set UrlOptions.Site to null?

No. If we set UrlOptions.Site = null, the property getter will pull the context site again on next access (back to “scheduler”) which seems to only leaves….

The solution : Set scheduler rootPath property so it cannot match any item paths

The “scheduler” site has no rootpath specified, so is evaluated as an empty string. Because this “matches” any item path i.e. something like (item.path.startswith(site.rootpath), siteresolving is not deemed to be required. By adding a rootpath that cannot match any item paths, we can force “normal” siteresolving to occur. Scheduler now has a rootpath of “#”. I’ve also added “scheme” attributes to the remaining sites:

<site name="subsite" hostName="641subsite.local" scheme="http" rootPath="/sitecore/content/" startItem="/Home/apple/subhome" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
<site name="website2" hostName="641website2.local" scheme="http" rootPath="/sitecore/content/" startItem="/home2" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" />
<site name="website" hostName="641website.*" scheme="http" targetHostName="641website.local" rootPath="/sitecore/content/" startItem="/home" virtualFolder="/" physicalFolder="/" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="10MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="5MB" filteredItemsCacheSize="2MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" /> 
<site name="scheduler" rootPath="#" enableAnalytics="false" domain="sitecore" />

I’ve removed the previous UrlOptions.Site assignment in code, so we are again defaulting to “scheduler”. Explicitly setting UrlOptions.Siteresolving = true is still required. The final code is now:

public class Schedule
{
public void CreateLinks()
{
var web = Factory.GetDatabase("web");
var rootItem = web.GetItem("/sitecore/content");

var options = LinkManager.GetDefaultUrlOptions();
options.AlwaysIncludeServerUrl = true;
options.SiteResolving = true;

Log.Info("Options.Site : " + options.Site.Name, this);
Log.Info("Siteresolving : " + options.SiteResolving.ToString(), this);

foreach (var child in rootItem.Axes.GetDescendants())
{
Log.Info(LinkManager.GetItemUrl(child, options), this);
}
}
}

Which gives this output:

ManagedPoolThread #4 16:36:33 INFO Job started: Sitecore641.Business.Schedule
ManagedPoolThread #4 16:36:33 INFO Options.Site : scheduler
ManagedPoolThread #4 16:36:33 INFO Siteresolving : True
ManagedPoolThread #4 16:36:33 INFO http://641website.local/
ManagedPoolThread #4 16:36:33 INFO http://641website2.local/
ManagedPoolThread #4 16:36:33 INFO http://641website.local/pear
ManagedPoolThread #4 16:36:33 INFO http://641website.local/plum
ManagedPoolThread #4 16:36:33 INFO http://641website.local/apple
ManagedPoolThread #4 16:36:33 INFO http://641subsite.local/
ManagedPoolThread #4 16:36:33 INFO http://641subsite.local/blue
ManagedPoolThread #4 16:36:33 INFO http://641subsite.local/green
ManagedPoolThread #4 16:36:33 INFO http://641subsite.local/red
ManagedPoolThread #4 16:36:33 INFO http://641website2.local/cat
ManagedPoolThread #4 16:36:33 INFO http://641website2.local/dog
ManagedPoolThread #4 16:36:33 INFO http://641website2.local/rabbit
ManagedPoolThread #4 16:36:33 INFO Job ended: Sitecore641.Business.Schedule (units processed: )

Success – all of our domains are now resolving correctly and generting the expected links. To re-cap:

  • UrlOptions.Siteresolving must be enabled
  • Scheme and HostName attributes must be for each site
  • Set the rootPath of the scheduler site to something that will not match any item paths
  • The usual rules still apply :
    • Site order is important
    • targetHostName is required where multiple hostNames or wildcards are used

Hopefully this will help to avoid a few nasty code hacks!

3 thoughts on “Sitecore LinkManager – Out of context link generation

  1. Mark Cassidy

    Wow, very likely one of the most in-depth look at the subject. Kudos for taking the time to walk through all of this, also down the dead-ends where I guess many of us have been down at one point or another 🙂

    Elegant solution. Thanks 🙂

    Reply
  2. Surat Thairdthai

    What an excellent post!!!
    Thanks for writing this up and part 1 as well. Well explanation and helpful a lot. I am sure that many people ran into this issue but couldn’t figure out.
    Thanks!

    Reply

Leave a Reply to Surat Thairdthai Cancel reply

Your email address will not be published. Required fields are marked *