SharePoint migration is always a challenge. And the more customizations you have the more challenging it is.
In this post I want to describe the solution to fix "404 Page not found" issue that may occur after migrating sites based on custom web templates and custom features.

Let's say you have a product that works both in SharePoint 2010 and 2013.

It contains custom Site Definition, custom Web Templates and a bunch of custom list definitions and instances.
Some of your customers want to migrate from 2010 to 2013.
Everything looks pretty straightforward:
  • backup content database on 2010
  • restore it on 2013
  • install the product's solution with CompatibilityMode 14, 15
  • mount the database
  • proceed Site Collection Upgrade (either from UI or with PowerShell script)
  • happily use migrated content in SharePoint 2013.
But after doing all these steps I've received "404 Page not found" error for each and every page that was related to custom lists defined in the product's package.
Logs contained next message:
Relying on fallback logic in VghostPageManager::getGhostDocument() for document: 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\Template\Features\CustomFeatureName\CustomListName\AllItems.aspx'
The weird thing here was to see 14 Hive folder instead of 15. Potentially it could mean that the list or site was not upgraded for some reason.
Thankfully, there is a separate log file for Site Collection Upgrade process and it contained a lot of warnings like:
Template CustomWebTemplateName#0: Web template upgrade for web/site <url> returned NeedsUpgrade false. Not upgrading. Web template: CustomWebTemplateName#0, web template version: 4.0.0.2, target web template version: 15.0.0.2
This message is logged in SPWebTempateSequence.DoUpgrade() if SPWebTemplateSequence.NeedsUpgrade property returns false.

protected override void DoUpgrade()
{
  if (!this.NeedsUpgrade)
  {
    base.Log.InfoTag(0x258845, string.Format(CultureInfo.InvariantCulture, this.LogPreamble + "Web template upgrade for web/site {0} returned NeedsUpgrade false. Not upgrading. Web template: {1}, web template version: {2}, target web template version: {3}", new object[] { this.Web.Url, this.WebTemplate.Name, this.WebTemplateVersion, this.TargetWebTemplateVersion }));
  }
  // rest of the method
}
NeedsUpgrade getter looks like that:
public override bool NeedsUpgrade
{
  get
  {
    bool flag = false;
    if (((this.WebTemplate != null) &amp;&amp; (this.WebTemplateVersion < this.TargetWebTemplateVersion)) &amp;&amp; (this.XmlConfiguration != null))
    {
      flag = true;
    }
    // Logging goes here
    
    return flag;
  }
}
WebTemplate is not null if the web template's xml is found in c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\TEMPLATE\1033\XML\ (1033 here is an En-US locale identifier)
WebTemplateVersion contains '4' as major revision for SharePoint 2010-based content, TargetWebTemplateVersion contains '15' as major revision as we're upgrading to SharePoint 2013.
So, first 2 conditions are true.
The last condition is the most interesting and complicated.
XmlConfiguration getter implementation looks like that:
internal SPXmlWebTemplateConfiguration XmlConfiguration
{
  get
  {
    if (this.m_xwtcWebTemplate == null)
    {
      foreach (SPXmlConfiguration configuration in SPXmlConfigurationManager.GetInstanceByCompatibilityLevel(this.TargetMajorVersion).SelectXmlConfigurations(SPXmlConfiguration.WebTemplateUpgradeXPath))
      {
        List<spxmlwebtemplateconfiguration> webTemplateConfigurations = configuration.GetWebTemplateConfigurations(this.WebTemplateID, this.LCID);
        int revision = this.TargetWebTemplateVersion.Revision;
        foreach (SPXmlWebTemplateConfiguration configuration2 in webTemplateConfigurations)
        {
          if (configuration2.IsApplicable(this))
          {
            this.m_xwtcWebTemplate = configuration2;
            break;
          }
        }
      }
    }
    return this.m_xwtcWebTemplate;
  }
}
Further investigation of that code leads to next conclusions:
  • SPXmlConfigurationManager loads xml configuration files from c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\CONFIG\UPGRADE\ folder
  • Each file can contain multiple (or none) WebTemplate elements (Upgrade Definition schema is described here)
  • Each found WebTemplate is tested if it is applicable to the Web Template that is being processed by Site Upgrade action.
  • If it is applicable then the Web Template (and the site) will be upgraded.
Then all we need is to add an XML file with a WebTemplate element to c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\CONFIG\UPGRADE\. Also, we need to look at the code of IsApplicable method to understand what values to provide in our WebTemplate element:
flag = ((this.FromProductVersion == webTemplateVersion.Major)
  &amp;&amp; (this.ToSchemaVersion == targetWebTemplateVersion.Revision)) 
  &amp;&amp; ((this.BeginFromSchemaVersion <= webTemplateVersion.Revision) 
  &amp;&amp; (webTemplateVersion.Revision <= this.EndFromSchemaVersion));
FromProductVersion should be equal to '4' as we're upgrading from SharePoint 2010.
ToSchemaVersion should be equal to the Revision number from the site definition onet.xml file applied to SharePoint 2013.
BeginFromSchemaVersion and EndFromSchemaVersion should form a range that contain the Revision from the site definition onet.xml file applied to SharePoint 2010. I would suggest to set BeginFromSchemaVersion to 0 and EndFromSchemaVersion to some large number, for example, 10.
In my case resulting XML looked like that:
<Config xmlns="urn:Microsoft.SharePoint.Upgrade">
<WebTemplate
  ID="<template_id>"
  LocaleId="*"
  FromProductVersion="4"
  BeginFromSchemaVersion="0"
  EndFromSchemaVersion="10"
  ToSchemaVersion="2">
</WebTemplate>
</Config>
</template_id>
This file must be placed to c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\CONFIG\UPGRADE\ on each Web Front-End server before the Site Collection Upgrade has been started.
Either you can just copy the file to needed location as it is one-time operation, or you can include mapped folder to your Visual Studio project and deploy the file during solution installation.
This solution works right at the moment of the migration and Site Collection Upgrade. But if some customer has already migrated the content and upgraded the site collection then the only way to fix the issue is to execute SQL script to update SetupPathVersion value for all the items in the site collection:
UPDATE [dbo.AllDocs]
SET SetupPathVersion = '15'
WHERE SiteId = '<site_collection_id>' AND 'SetupPathVersion' = '4'

UPDATE
  • If you update from 2007 to 2010 and then to 2013 SetupPathVersion will contain '3' instead of '4'. It means that above script should be modified to update entries that have '3' as SetupPathVersion:
    UPDATE [dbo.AllDocs]
    SET SetupPathVersion = '15'
    WHERE SiteId = '<site_collection_id>' AND ('SetupPathVersion' = '4' OR 'SetupPathVersion' = '3')
  • If your Feature for some reason had different name in previous versions you should also update 'SetupPath' with script like:
    UPDATE [dbo.AllDocs]
    SET SetupPath = CAST(REPLACE(CAST(SetupPath as nvarchar(MAX)), 'old_feature_name', 'new_feature_name') as nvarchar(255))
    WHERE SiteId = '<site_collection_id>' AND 'SetupPath' LIKE 'Features\old_feature_nane%'
Hope this will save you some time.
Please, feel free to leave a comment if you have any questions.
Have fun!