Glass v5 properties intermittently null when security applied

Glass Mapper version 5 caches data more aggressively which is beneficial most of the time as it improves site performance. More information on the new caching features can be found here. In order to speed up performance some checks which Sitecore normally does are not performed when an item is returned from the cache. Usually this will not cause any issues but there could be potentially serious issues when security is involved. For example consider below scenario:

  1. User 1 has access to a certain item, but User 2 does not. Security is applied through the regular Sitecore security mechanisms.
  2. User 2 visits the site first, and Glass loads the model. This model has a link to the item which user 2 does not have access to, therefore this link is empty. It will get cached this way.
  3. Now user 1 comes and the model will be returned from cache. Since it was initially cached for user 2 the link is still empty whereas user 1 should be able to view this.

Notice that this behavior is not deterministic, i.e. if user 1 would have hit the site first then the item would be loaded. It would also be loaded for user 2 if that user visits later. This can cause intermittent and hard to find issues especially if the code was working correctly prior to upgrading. Good news is that fixing this is straightforward!

Solution

It is recommended to leave the cache on as much as possible to take advantage of the performance improvements.

Is there is an issue with a certain link which can point to an item where security might be applied, then caching can be turned off through an attribute like this:

[SitecoreType(Cache = Glass.Mapper.Configuration.Cache.Disabled)]
public virtual Link SecureLink { get; set; }

Perhaps this issue only occurs when retrieving items through the SitecoreService or MvcContext. In these cases the cache can be turned off explicitly in the call like this:

T val = mvcContext.GetDataSourceItem<T>(x => x.CacheDisabled())

Fix NullReferenceException in CompositeDataProvider

Sitecore 10 comes with a new Dataprovider which merges items from disk and the database. Jeremy Davis already wrote a great article about this. Sitecore is using this new approach by deploying all their items through an Item Resource file.

TDS now also supports creating item resource files, which makes it convenient to deploy your own items this way too. There are a few uncommon scenarios where this will result in a uncaught NullReferenceException, for example when there is an item without a version. Many areas of Sitecore and your custom solution will most likely be broken if this happens. The stack trace will look something like this:

FATAL Uncaught application error 
Exception: System.NullReferenceException 
Message: Object reference not set to an instance of an object. 
Source: Sitecore.Kernel 
   at Sitecore.Data.DataProviders.CompositeDataProvider.GetItemVersions(ItemDefinition itemDefinition, CallContext context) 
   at Sitecore.Data.DataProviders.DataProvider.GetItemVersions(ItemDefinition item, CallContext context, DataProviderCollection providers) 
   at Sitecore.Data.DataSource.LoadVersions(ItemDefinition definition, Language language) 
   at Sitecore.Data.DataSource.GetVersions(ItemInformation itemInformation, Language language) 
   at Sitecore.Data.DataSource.GetLatestVersion(ItemInformation itemInformation, Language language) 
   at Sitecore.Data.DataSource.GetItemData(ID itemID, Language language, Version version) 
   at Sitecore.Nexus.Data.DataCommands.GetItemCommand.GetItem(ID itemId, Language language, Version version, Database database) 

One way to solve this is by identifying all the items which cause the issue and fix each of them. Another way it to override the CompositeDataProvider and wrap a try/catch block around this logic. The advantage of this approach is that it addresses the root cause and the issue will not reoccur in the future. Following code can be used to catch the exception:

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.DataProviders;
using Sitecore.Data.DataProviders.ReadOnly;
using Sitecore.Diagnostics;
using System;
using System.Collections.Generic;

namespace Foundation.Providers
{
    public class SafeCompositeDataProvider : CompositeDataProvider
    {
        public SafeCompositeDataProvider(IEnumerable<ReadOnlyDataProvider> readOnlyDataProviders, DataProvider headProvider) : base(readOnlyDataProviders, headProvider) { }

        public SafeCompositeDataProvider(ObjectList readOnlyDataProviders, DataProvider headProvider) : base(readOnlyDataProviders, headProvider) { }

        public override VersionUriList GetItemVersions(ItemDefinition itemDefinition, CallContext context)
        {
            try
            {
                return base.GetItemVersions(itemDefinition, context);
            }
            catch (Exception ex)
            {
                Log.Error($"SafeCompositeDataProvider: Caught exception for item {itemDefinition.ID} {itemDefinition.Name} {ex}", this);
                return null;
            }
        }
    }
}

This new provider can be patched in through a configuration file like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
  <sitecore>
    <databases >
      <!-- target to CM or standalone, no master database in CD-->
      <database id="master" role:require="ContentManagement or StandAlone">
        <dataProviders >
          <dataProvider >
            <patch:attribute name="type">Foundation.Providers.SafeCompositeDataProvider, Foundation.Providers</patch:attribute>
          </dataProvider>
        </dataProviders>
      </database>
    </databases>
  </sitecore>
</configuration>

Fix Custom Cache after Sitecore upgrade

Sitecore allows you to create custom caches which can be managed just like all other Sitecore caches. Brian Caos wrote a great blog post about this which can be found here. Recently Sitecore has made some changes to this and if you are upgrading Sitecore you will notice that the code will not compile anymore. For example below code:

 Cache cache = CacheManager.FindCacheByName(cacheName);

will now throw a compile error:

Error CS0411 The type arguments for method 'CacheManager.FindCacheByName<T>(string)' cannot be inferred from the usage. Try specifying the type arguments explicitly. 

Furthermore this now returns an object of type Sitecore.Caching.Generics.ICache<TKey>. My first attempt at fixing this was by just passing the type of my custom cache in the generic which fixes the compile error but now the code throws an exception at runtime:

Exception: System.InvalidCastException 
Message: Unable to cast object of type 'Sitecore.Caching.Cache' to type 'Sitecore.Caching.Generics.ICache`1[MyCustomCache]'.
Source: Sitecore.Kernel
   at Sitecore.Caching.DefaultCacheManager.FindCacheByName[TKey](String name)

The generic now needs the type of the key passed in. In most cases this can just be set to string. To fix this code changes will be required in 2 places.

1. Custom cache object

In the custom cache make sure the base object uses Sitecore.Caching.Generics and also specify string as the key e.g:

 //updated from Sitecore.Caching
using Sitecore.Caching.Generics;

//Specify the generic type of the key
public class MyCustomCache : CustomCache<string> 

2. Custom Cache Clearer

In the method which clears the cache make sure to also pass the same generic for the key, see highlighted line:

private void DoClear()
{
    foreach (string cacheName in Caches)
    {
        //update to pass generic
        Cache cache = CacheManager.FindCacheByName<string>(cacheName); 
        if (cache == null)
            continue;
        Log.Info(this + ". Clearing " + cache.Count + " items from " + cacheName, this);
        cache.Clear();
    }
}

Prevent page error when Datasource is missing after Upgrade

If you have upgraded Sitecore and along with it GlassMapper to version 5 there might be an issue when the Datasource item is not available in the target database, which will throw an error and the page will not load. Prior to the upgrade this would not have been an issue and the invalid Datasource was simply ignored. When this happens you will see an error in the log like this:

Exception: System.InvalidOperationException 
Message: The model item passed into the dictionary is of type 'Sitecore.Mvc.Presentation.RenderingModel', but this dictionary requires a model item of type 'DatasourceNamespace.DatasourceType'.
Source: System.Web.Mvc
   at System.Web.Mvc.ViewDataDictionary`1.SetModel(Object value)
   at System.Web.Mvc.ViewDataDictionary..ctor(ViewDataDictionary dictionary)
   at System.Web.Mvc.WebViewPage`1.SetViewData(ViewDataDictionary viewData)
   at System.Web.Mvc.RazorView.RenderView(ViewContext viewContext, TextWriter writer, Object instance)
   at System.Web.Mvc.Html.PartialExtensions.Partial(HtmlHelper htmlHelper, String partialViewName, Object model, ViewDataDictionary viewData)
   at Sitecore.Mvc.Presentation.ViewRenderer.Render(TextWriter writer) 

Solution

This can be fixed by checking if the Datasource exists and clearing it in case it doesn’t. The RenderRendering processor is a good place to insert this logic:

using Sitecore.Mvc.Pipelines.Response.RenderRendering; 

namespace Custom.Pipelines.RenderRendering
{
    public class ClearInvalidDatasource : RenderRenderingProcessor
    {
        public override void Process(RenderRenderingArgs args)
        {
            var rendering = args.Rendering;

            if (!string.IsNullOrWhiteSpace(rendering.DataSource) && Sitecore.Context.Database.Items.GetItem(rendering.DataSource) == null)
            {
                rendering.DataSource = string.Empty;
            }
        }
    }
}

This customization can be enabled with the following patch file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> 
  <sitecore>
    <pipelines>
      <mvc.renderRendering>
        <processor patch:before="*[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.EnterRenderingContext, Sitecore.Mvc']" type="Custom.Pipelines.RenderRendering.ClearInvalidDatasource, Project.Common" />
      </mvc.renderRendering>
    </pipelines>
  </sitecore>
</configuration>

Now that this change is in place the behavior will be identical to the behavior prior to the upgrade.