Upgrading from Glass.Mapper.Sc v4 to Glass.Mapper.Sc v5

Glass

Version 5 of Glass.Mapper.Sc was released last year.  If you are working with Sitecore 9.1, there isn’t formal support for it with Glass.Mapper.Sc v4.  So, I decided it was time to upgrade.  I also discovered that tutorials and documentation from the Glass team are now a paid training course, which made the learning curve a little more challenging, although it still wasn’t a big deal.  I’m going to outline my steps and the references I found.

Getting Started

Shu Jackson created a pretty kick butt overview of Glass and covers what has changed in version 5.  Shu Jackson and Courtney Dean also created an excellent video that covers just about everything.

Circular References & Model Depth

bloaters

The code base that I am working with has some gnarly model classes that need to be redesigned.  Always easier said than done.  The Glass.Mapper.Sc API changed enough where some of the tricks involving Lazy Loading that allowed these classes to work in the past, no longer worked.  Some classes implemented complex fallback logic and exposed self references via a model property.  That code needed to be removed and, in some cases, refactored as a method.  Ideally, that code will be moved to a service class and out of the model.

Removing Attributes SitecoreNode and SitecoreQuery

Some of the model glasses also relied on the use of Glass.Mapper.Sc model attributes, particularly SitecoreNode and SitecoreQuery.  Some of the queries implemented through these attributes no longer worked as they did previously, most notably, any use of relative and lazy.  I removed these model properties and re-implemented them as methods, removing the use of the attributes.


[SitecoreQuery("Primary/*", IsLazy = true, IsRelative = true)]
public virtual IEnumerable PrimarySubMenu { get; set; }

public virtual IEnumerable PrimarySubMenu
{
    get
    {
        GetItemsByQueryOptions builder = new GetItemsByQueryOptions();
        builder.Query = new Query("Primary/*");
        builder.RelativeItem = this.Item;
        builder.Lazy = Glass.Mapper.LazyLoading.Enabled;
        return _requestContext.SitecoreService.GetItems(builder);
    }
}

Item is the model base class reference to the Sitecore Item.


[SitecoreItem]
public virtual Item Item { get; set; }

The web application started to work after these changes were applied, but the project was still compiling with a dizzying number of warnings.

Removing GlassController

Most of my controller classes inherited from GlassController.


public class MenuController : GlassController
    {

The classes had GlassController calls like:


MenuParameters parameters = GetRenderingParameters();

and


var somedata = GetDataSourceItem();

It’s a pretty easy conversion to Glass Mapper 5.


    public class MenuController : Controller
    {
        private readonly IMvcContext _mvcContext;

        public MenuController()
        {
            _mvcContext = new MvcContext();
        }
        ...

MenuParameters parameters = _mvcContext.GetRenderingParameters();
var somedata = _mvcContext.GetDataSourceItem();

Removing GlassView


@using Glass.Mapper.Sc.Web.Mvc
@using Someproject.Models.Data

@inherits GlassView<SomeModelClass>


@if (Model != null)
{
    <li class="class-of-business-item">

        @if (Model.ArtworkImage != null)
        {
            @Html.Raw(RenderImage<SomeModelClass>(Model, x => x.ArtworkImage, new { Class = "class-of-business-image" }, isEditable: true))
        }
        <li class="class-of-business-content">
            @Editable(Model, x => x.PageTitle)
            @Editable(Model, x => x.Body)
        </li>
    </li>
}

@using Glass.Mapper.Sc.Web.Mvc
@using Someproject.Models.Data

@model SomeModelClass

@if (Model != null)
{
    <li class="class-of-business-item">

        @if (Model.ArtworkImage != null)
        {
            @Html.Raw(Html.Glass().RenderImage<SomeModelClass>(Model, x => x.ArtworkImage, new { Class = "class-of-business-image" }, isEditable: true))
        }
        <li class="class-of-business-content">
            @Html.Glass().Editable(Model, x => x.PageTitle)
            @Html.Glass().Editable(Model, x => x.Body)
        </li>
    </li>
}

Simple Examples

Here are a few simple code snippets:

Create a Reference


SitecoreService _sitecoreService = new SitecoreService(Sitecore.Context.Database);
IRequestContext_requestContext = new RequestContext(_sitecoreService);

Relative Query

Set the RelativeItem property. It does what you expect.


GetItemsByQueryOptions builder = new GetItemsByQueryOptions();
builder.Query = new Query("Primary/*");
builder.RelativeItem = this.Item;
return _requestContext.SitecoreService.GetItems(builder);

Query Item(s) By Path


GetItemByPathOptions builder = new GetItemByPathOptions();
builder.Path = “/sitecore-path/…”;
return _requestContext.SitecoreService.GetItem(builder);

...

GetItemsByQueryOptions builder = new GetItemsByQueryOptions();
builder.Query = new Query("/sitecore-path/…/*");
return _requestContext.SitecoreService.GetItems(builder);

Query Item By Id

GetItemByIdOptions builder = new GetItemByIdOptions();
builder.Id = Guid.Parse(“some-guid-string”);
return _requestContext.SitecoreService.GetItem(builder);

Query Item By Item

Replace any Glass Casting with the following:

return _requestContext.SitecoreService.GetItem(SomeItem);

Other Useful Properties

Request a specific language version of an item:

builder.Language = LanguageManager.DefaultLanguage;

Set Glass caching:

builder.Cache = Glass.Mapper.Configuration.Cache.Enabled;

Set Lazy Loading:

builder.Lazy = Glass.Mapper.LazyLoading.Enabled;

Creating Outcomes in Sitecore 8

Sitecore introduced outcomes in version 8.  While creating and implementing outcomes, I came across an error that I thought was worth noting.

What is an Outcome

According to “Practical Sitecore 8 Configuration and Strategy”:

Outcomes are used to track the lifelong value of a particular customer to your business over time.  An outcome in its most basic form is just a tag on the customer’s profile.  That tag name can be whatever you want it to be and it can represent whatever you want it to represent.  Typically, outcomes represent some combination of events, goals, and/or campaign interactions.

How to Create an Outcome

Sitecore documents how to create an outcome.  It’s pretty simple and the steps can be found in the Sitecore Experience Platform documentation.

Outcome Groups

Here is what is not documented or at least not documented well.  If you create an outcome, and do not add it to an outcome group, the outcome will not work.  The outcome will not only fail to register programmatically, but the exception raised will also cause any goals raised in the same tracking session, to fail to register as well.  When you create a new outcome, the outcome group is defaulted to none.

gotcha

Registering an Outcome

Outcomes can only be registered programmatically.  I’m going to cover how I implemented programmatically registering goals and outcomes in a separate post.

 

Sitecore Wildcard Design Pattern & Profile Cards

A common practice in Sitecore design is to use a wildcard item to handle dynamic URLs.  On the site I am working on, the wildcard item is primary used to abstract content out of the site tree, and to maintain one definition for presentation details (on the * item), rather than define presentation on each individual page.  Abstracted content is received based on the slug.

sitecorewildcard

Experience Analytics

The wildcard design pattern creates all kinds of headaches.  The most recent issue that I stumbled upon was that the wildcard item itself was appearing in Analytics tracking, rather than the abstracted data item.  After doing a little research on the internet, I found Sander Bouwmeester’s blog that outlined a simple solution.  Here is essentially Sander’s code:


using Sitecore;
using Sitecore.Analytics.Tracking;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web;
using System;
using System.Web;
using Sitecore.Analytics.Pipelines.InitializeTracker;

namespace Someproject.Pipelines.InitializeTracker
{
    public class CustomCreatePage : InitializeTrackerProcessor
    {
        private void CreateAndInitializePage(HttpContextBase httpContext, CurrentInteraction visit)
        {
            IPageContext empty = visit.CreatePage();
            empty.SetUrl(WebUtil.GetRawUrl());
            DeviceItem device = Context.Device;
            if (device == null)
            {
                empty.SitecoreDevice.Id = Guid.Empty;
                empty.SitecoreDevice.Name = string.Empty;
            }
            else
            {
                empty.SitecoreDevice.Id = device.ID.Guid;
                empty.SitecoreDevice.Name = device.Name;
            }

            // Default Sitecore implementation
            Item item = Context.Item;

            // Our logic starts here: if the current item is a wildcard
            if (item != null && item.Name == "*")
            {
                // Perform a call to the logic which resolves the correct item
                var resolvedItem = this.ResolveWildcardItem(item);

                if (resolvedItem != null)
                {
                    item = resolvedItem;
                }
            }

            // Resume the default behaviour
            if (item == null)
            {
                return;
            }

            empty.SetItemProperties(item.ID.Guid, item.Language.Name, item.Version.Number);
        }

        public override void Process(InitializeTrackerArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (args.IsSessionEnd)
            {
                return;
            }
            HttpContextBase httpContext = args.HttpContext;
            if (httpContext == null)
            {
                args.AbortPipeline();
                return;
            }
            this.CreateAndInitializePage(httpContext, args.Session.Interaction);
        }

        private Item ResolveWildcardItem(Item item)
        {
            return Someproject.BusinessLogic.AbstactedData.GetDataSource(item, Someproject.BusinessLogic.SlugFactory.GetSlugPreSelected(item));
        }
    }
}

The InitializeTracker CreatePage processor gets overridden with an include file:


< ? xml version="1.0" ?>
< configuration xmlns :patch="http:/ / www.sitecore.net/xmlconfig /" xmlns :set="http: / / www.sitecore.net/xmlconfig/set/" >
  < sitecore >
    < pipelines >
      < initializeTracker >
        < processor type="Someproject.Pipelines.InitializeTracker.CustomCreatePage,Someproject"            patch:instead="*[@type='Sitecore.Analytics.Pipelines.InitializeTracker.CreatePage, Sitecore.Analytics']" />
      < /initializeTracker >
    < /pipelines >
  < /sitecore >
< /configuration >

Profile Cards

This was a good start, but I needed profile cards assigned the abstracted content to get added to the tracking session.  Sitecore Profile cards are used to identify and segment contacts and personalize web site content.  After a little more research, I read Nick Allen’s blog and created the following solution.

WildcardSolution


using Sitecore;
using Sitecore.Analytics;
using Sitecore.Analytics.Pipelines.StartTracking;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Analytics.Pipelines.ProcessItem;

namespace Someproject.Pipelines.StartTracking
{
    public class ProcessItem : Sitecore.Analytics.Pipelines.StartTracking.ProcessItem
    {
        public override void Process(StartTrackingArgs args)
        {
            Assert.ArgumentNotNull(args, "args");

            if (Tracker.Current != null && Tracker.Current.Session != null && Tracker.Current.Session.Interaction != null)
            {
                // Default Sitecore implementation
                Item item = Context.Item;

                // Our logic starts here: if the current item is a wildcard
                if (item != null && item.Name == "*")
                {
                    // Perform a call to the logic which resolves the correct item
                    var resolvedItem = this.ResolveWildcardItem(item);

                    if (resolvedItem != null)
                    {
                        item = resolvedItem;
                    }
                }

                if (item != null)
                {
                    ProcessItemArgs args2 = new ProcessItemArgs(Tracker.Current.Session.Interaction, item);
                    ProcessItemPipeline.Run(args2);
                }
            }
        }

        private Item ResolveWildcardItem(Item item)
        {
            return Someproject.BusinessLogic.AbstactedData.GetDataSource(item, Someproject.BusinessLogic.SlugFactory.GetSlugPreSelected(item));
        }
    }
}

The startTracking ProcessItem processor gets overridden with an include file:


< ? xml version="1.0" ?>
< configuration xmlns :patch="http:/ / www.sitecore.net/xmlconfig /" xmlns :set="http: / / www.sitecore.net/xmlconfig/set/" >
  < sitecore >
    < pipelines >
      < startTracking >
        < processor type="Someproject.Pipelines.StartTracking.ProcessItem,Someproject"            patch:instead="*[@type='Sitecore.Analytics.Pipelines.StartTracking.ProcessItem, Sitecore.Analytics']" />
      < /startTracking >
    < /pipelines >
  < /sitecore >
< /configuration >

Conclusion

It’s possible to maintain the wildcard design pattern in Sitecore 8 Customer Experience without having to resort to large amounts of redesign.  Sitecore profile cards can be assigned to abstracted Sitecore content and programmatically added to the analytics session by overriding the InitializeTracker CreatePage and startTracking ProcessItem pipeline processors.

Rendering Experience Editor Compatible Links Using Glass Mapper and Dynamically Built Properties

I encountered a problem when reviewing some legacy code using Glass Mapper v4.3.  Glass Mapper no longer supports dynamically build properties, while in Sitecore Experience Editor “Edit” mode.

View renderings had expressions like this:

using (BeginRenderLink(Model.GetListHeader(sList), x => x, isEditable: true))
{
   @Html.Raw(Model.GetListHeader(sList).Text);
}

The supporting Model class had the following:

[SitecoreField("List1_Header")]
public virtual Link List1Header { get; set; }

[SitecoreField("List2_Header")]
public virtual Link List2Header { get; set; }

[SitecoreField("List3_Header")]
public virtual Link List3Header { get; set; }

[SitecoreField("List4_Header")]
public virtual Link List4Header { get; set; }

public virtual Link GetListHeader(string sRenderingParameter)
{
   Link oReturn = null;
   int iList = 0;

   iList = Int32.Parse(sRenderingParameter);

   switch (iList)
   {
      case 1:
         oReturn = List1Header;
         break;
      case 2:
         oReturn = List2Header;
         break;
      case 3:
         oReturn = List3Header;
         break;
      case 4:
         oReturn = List4Header;
         break;
      default:
         break;
   }
   return oReturn;
}

While in Experience Editor Edit mode, the following exception was thrown:

Expression doesn't evaluate to a member x
...
at Glass.Mapper.Utilities.GetTargetObjectOfLamba[T](Expression`1 field, T model, MemberExpression& memberExpression) at Glass.Mapper.Sc.GlassHtml.MakeEditable[T](Expression`1 field, Expression`1 standardOutput, T model, Object parameters, Context context, Database database, TextWriter writer)

I did some digging on the web and found a few other references to this issue:

https://stackoverflow.com/questions/18698668/exception-in-glass-mapper-for-sitecore-in-pageeditor-mode

https://stackoverflow.com/questions/44059614/can-i-use-glassmappers-editable-on-a-reflected-property

Glass Mapper checks to make sure the lambda expression is a MemberExpression, and throws an exception when it’s not.  This is necessary due to updates in newer versions of Glass Mapper.

Here is what I did to work around this issue.

Remove the Dynamic Property References?

I could refactor the View rendering, adding an endless number of if statements, effectively pulling the model code into the View rendering, so that the property is statically referenced. That would create an even bigger mess.  This wasn’t going to work for me.  No thank you.

unhappy

Building a Dynamic MemberExpression in a Helper Method

After doing some reading about lambda expressions and coming across this question on Stack Overflow, I added the following method to the model class:

public virtual Expression<Func<T, object>> GetListHeaderExp(string sRenderingParameter)
{
   Expression<Func<T, object>> oReturn = null;
   int iList = 0;

   iList = Int32.Parse(sRenderingParameter);
   PropertyInfo propertyInfo = null;

   switch (iList)
   {
      case 1:
         propertyInfo = typeof(GenericPage).GetProperty("List1Header");
         break;
      case 2:
         propertyInfo = typeof(GenericPage).GetProperty("List2Header");
         break;
      case 3:
         propertyInfo = typeof(GenericPage).GetProperty("List3Header");
         break;
      case 4:
         propertyInfo = typeof(GenericPage).GetProperty("List4Header");
         break;
      default:
         break;
   }

   var entityParam = Expression.Parameter(typeof(GenericPage), "e");
   Expression columnExpr = Expression.Property(entityParam, propertyInfo);

   if (propertyInfo.PropertyType != typeof(Link))
      columnExpr = Expression.Convert(columnExpr, typeof(Link));

   oReturn = Expression.Lambda<Func<T, object>>(columnExpr, entityParam);

   return oReturn;
}

Call the MemberExpression Builder Method from the View

I then refactored the call in the View rendering to:

using (BeginRenderLink(Model, Model.GetListHeaderExp(sList), isEditable: true))
{
   @Html.Raw(Model.GetListHeader(sList).Text);
}

Conclusion

That did the trick.  I can now edit the link using the Sitecore Experience Editor, while keeping the dynamic property reference.

I’m hoping to write about my experience migrating an enterprise Sitecore installation from 7.2 to 8.2.  Hopefully, I’ll get to that soon.

Implementing Search Using Sitecore & Lucene – Part II

In the last post, I discussed aggregating data needed for search in a custom Lucene index. In this post, I’ll review how I implemented the query logic.

Date Conversion Exception

I started accessing the Lucene index and immediately started getting exceptions. Sitecore and Lucene were not happy with how my datetime data was getting stored in the Lucene index. I added a custom IndexFieldDateTimeConverter to manage the exceptions.


public class IndexFieldDateTimeValueConverter : Sitecore.ContentSearch.Converters.IndexFieldDateTimeValueConverter
{
    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        try
        {
             return base.ConvertFrom(context, culture, value);
        }
        catch(Exception e)
        {
             string fieldValue = value as string;

             DateTime dReturn = new DateTime();

             if (DateTime.TryParseExact(fieldValue, "yyyyMMdd", culture, DateTimeStyles.None, out dReturn))
                 return dReturn;
             else
                 throw e;
        }
    }
}

I created an include file to apply the class and had to name it with z (zCustomIndexValueConverters.config) so that it loaded after the Sitecore Content Search Lucene include files.


< configuration xmlns :patch ="http : / /www.sitecore.net /xmlconfig /" >
    < sitecore >
        < contentSearch >
            < indexConfigurations >
                < defaultLuceneIndexConfiguration >
                    < !-- DateTimeConverter -- >
                    < indexFieldStorageValueFormatter type ="Sitecore.ContentSearch.LuceneProvider.Converters.LuceneIndexFieldStorageValueFormatter, Sitecore.ContentSearch.LuceneProvider" >
                        < converters hint ="raw :AddConverter" >
                            < converter handlesType ="System.DateTime" >
                                < patch :attribute name ="typeConverter" >Someproject.ContentSearch.Converters.IndexFieldDateTimeValueConverter, Someproject<  /patch :attribute >
                            <  /converter >
                        <  /converters >
                    <  /indexFieldStorageValueFormatter >
                <  /defaultLuceneIndexConfiguration >
            <  /indexConfigurations >
        <  /contentSearch >
    <  /sitecore >
<  /configuration  >

I then applied the attribute to my data properties.

[TypeConverter(typeof(IndexFieldDateTimeValueConverter))]
 public virtual DateTime MetadataDate { get; set; }

Am I proud of myself? Nope. Did this work. Yep.

POCO and SearchResultItem

I needed classes to store the search results and facet data. I created four classes.

Facet Classes

The facet classes are fairly straight forward. The FacetValue class and the SearchFacet class are POCO (Plain Old CLR Objects) classes to store the facet data and return it to the presentation layer.

[Serializable]
[DataContract(Name = "FacetValue")]
public class FacetValue
{
    [DataMember(Name = "Value")]
    public string Value { get; set; }

    [DataMember(Name = "FacetCount")]
    public int FacetCount { get; set; }
}

[Serializable]
[DataContract(Name = "SearchFacet")]
public class SearchFacet
{
    private List _values;

    public SearchFacet()
    {
        _values = new List();
    }

    [DataMember(Name = "FacetName")]
    public string FacetName { get; set; }

    [DataMember(Name = "Values")]
    public List Values
    {
        get { return _values; }
        set { _values = value; }
    }
}

Search Results & Search Entity Classes

The search entity class stores all of the search result data we want to return to the presentation layer, as well as the properties to where and filter. I inherited from the Sitecore SearchResultItem class and then hid the data I did not want to return to the presentation layer for security and not to bloat the JSON. The search results class is the container for everything.

[Serializable]
[DataContract(Name = "SiteSearchEntity")]
public class SiteSearchEntity : SearchResultItem 
{
    [TypeConverter(typeof(IndexFieldIDValueConverter))]
    [IndexField("_id")]
    public Guid Id { get; set; }

    [DataMember(Name = "ComputedUrl")]
    [IndexField("LinkProviderUrl")]
    public virtual string ComputedUrl { get; set; }

    [DataMember(Name = "ComputedMetaTitle")]
    [IndexField("Title")]
    public virtual string ComputedMetaTitle { get; set; }

    [DataMember(Name = "ComputedMetaDescription")]
    [IndexField("Description")]
    public virtual string ComputedMetaDescription { get; set; }

    [IgnoreDataMember]
    [IndexField("Keywords")]
    public virtual string ComputedKeywords { get; set; }

    [DataMember(Name = "ComputedDocumentDate")]
    [IndexField("DocumentDate")]
    public virtual DateTime ComputedDocumentDate { get; set; }

    [DataMember(Name = "ComputedCategory")]
    [IndexField("ComputedCategory")]
    public virtual List ComputedCategory { get; set; }

    [DataMember(Name = "ComputedImageUrl")]
    [IndexField("ImageURL")]
    public virtual string ComputedImageUrl { get; set; }

    [DataMember(Name = "ComputedSearchUrl")]
    [IndexField("SearchURL")]
    public virtual string ComputedSearchUrl { get; set; }

    #region Hide Some Data Members
    [IgnoreDataMember]
    public new string Version { get; set; }

    [IgnoreDataMember]
    [IndexField("_group")]
    [TypeConverter(typeof(IndexFieldIDValueConverter))]
    public new ID ItemId { get; set; }

    [IgnoreDataMember]
    [IndexField("_uniqueid")]
    [TypeConverter(typeof(IndexFieldItemUriValueConverter))]
    [XmlIgnore]
    public new ItemUri Uri { get; set; }

    [IgnoreDataMember]
    [IndexField("_templatename")]
    public new string TemplateName { get; set; }

    [IgnoreDataMember]
    [IndexField("_template")]
    [TypeConverter(typeof(IndexFieldIDValueConverter))]
    public new ID TemplateId { get; set; }

    [IgnoreDataMember]
    [IndexField("__semantics")]
    [TypeConverter(typeof(IndexFieldEnumerableConverter))]
    public new IEnumerable Semantics { get; set; }

    [IgnoreDataMember]
    [IndexField("_fullpath")]
    public new string Path { get; set; }

    [IgnoreDataMember]
    [IndexField("_path")]
    [TypeConverter(typeof(IndexFieldEnumerableConverter))]
    public new IEnumerable Paths { get; set; }

    [IgnoreDataMember]
    [IndexField("_name")]
    public new string Name { get; set; }

    [IgnoreDataMember]
    [IndexField("_language")]
    public new string Language { get; set; }

    [IgnoreDataMember]
    [IndexField("__smallcreateddate")]
    public new DateTime CreatedDate { get; set; }

    [IgnoreDataMember]
    [IndexField("_content")]
    public new string Content { get; set; }

    [IgnoreDataMember]
    [IndexField("parsedcreatedby")]
    public new string CreatedBy { get; set; }

    [IgnoreDataMember]
    [IndexField("__smallupdateddate")]
    public new DateTime Updated { get; set; }

    [IgnoreDataMember]
    [IndexField("parsedupdatedby")]
    public new string UpdatedBy { get; set; }

    [IgnoreDataMember]
    [IndexField("_datasource")]
    public new string Datasource { get; set; }

    [IgnoreDataMember]
    [IndexField("_database")]
    public new string DatabaseName { get; set; }

    [IgnoreDataMember]
    [IndexField("_parent")]
    public new ID Parent { get; set; }

    [IgnoreDataMember]
    [IndexField("urllink")]
    public new string Url { get; set; }

    #endregion

    #region Work Around for Facets with Spaces
    [IndexField("CategoryFacet")]
    public virtual List CategoryFacet { get; set; }
    #endregion
}

[DataContract(Name = "SearchResults")]
[Serializable]
public class SerializableSearchResults
{
    List _entities = new List();
    List _facets = new List();

    [DataMember(Name = "TotalCount")]
    public int TotalCount { get; set; }

    [DataMember(Name = "SearchTerm")]
    public string SearchTerm { get; set; }

    [DataMember(Name = "entities")]
    public List entities
    {
        get { return _entities; }
        set { _entities = value; }
    }

    [DataMember(Name = "facets")]
    public List facets
    {
        get { return _facets; }
        set { _facets = value; }
    }
}

Search Logic & the Predicate Builder

Now for the fun part. How do we build a search algorithm to return accurate results?  I modeled my work after Matt Burke’s blog post.  I converted the search term into an array of strings, delimiting the term using the space character.  If then built the filter and term predicates separately and joined them together.   The Sitecore PredicateBuilder makes working Lucene fairly easy.  I simplified the search algorithm as it appears below for simplicity.

public static SerializableSearchResults GetSearchResultsLucene(string searchTerm, int page, Dictionary facets)
{
    SerializableSearchResults oReturn = new SerializableSearchResults();
    string sDatabase = "web";
    ISitecoreService service;

    ISearchIndex searchIndex = ContentSearchManager.GetIndex("sitesearch_web");

    SearchResults results = null;
    IQueryable query = null;

    service = new SitecoreService(sDatabase);

    using (IProviderSearchContext searchContext = searchIndex.CreateSearchContext())
    {
        // Parse search term into a collection of strings
        string[] terms = searchTerm.ToLower().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

        Expression filterPredicate = PredicateBuilder.True();

        // Get facets
        string category = GetFacetValue("category", facets);

        // Build facets clauses
        if (!String.IsNullOrEmpty(category))
            filterPredicate = filterPredicate.And(se => se.CategoryFacet.Contains(category));

        Expression termPredicate = PredicateBuilder.False();

        foreach (string term in terms)
        {
            termPredicate = termPredicate
                .Or(p => p.ComputedMetaTitle.Contains(searchTerm)).Boost(5.0f)
                .Or(p => p.ComputedMetaTitle.Like(term, 0.75f)).Boost(2.0f)
                .Or(p => p.ComputedMetaDescription.Contains(searchTerm)).Boost(3.0f)
                .Or(p => p.ComputedMetaDescription.Like(term, 0.75f)).Boost(2.0f)
                .Or(p => p.ComputedKeywords.Contains(searchTerm).Boost(2.5f))
                .Or(p => p.ComputedKeywords.Like(term, 0.75f).Boost(1.5f));
        }

        Expression fullPredicate = filterPredicate.And(termPredicate);

        query = searchContext.GetQueryable().Where(fullPredicate);

        FacetResults searchFacets = searchContext.GetQueryable().Filter(fullPredicate).FacetOn(x => x.CategoryFacet).GetFacets();

        query = query.Page(page - 1, 20);
        results = query.GetResults();
        oReturn.entities = results.Hits.Select(hit => hit.Document).ToList();

        foreach(SiteSearchEntity s in oReturn.entities)
        {
            service.Map(s);
        }

        oReturn.SearchTerm = searchTerm.ToLower();
        oReturn.TotalCount = results.TotalSearchResults;

        oReturn.facets = GetFacetResults(searchFacets);
    }

    return oReturn;
}

private static string GetFacetValue(string FacetName, Dictionary facets)
{
    string sReturn = String.Empty;

    if (facets.ContainsKey(FacetName))
        sReturn = facets[FacetName];

    return sReturn;
}

The method below loads the facet data into the POCO facet objects.

private static List GetFacetResults(FacetResults results)
{
    List f = new List();
    foreach (FacetCategory fc in results.Categories)
    {
        SearchFacet sf = new SearchFacet();
        sf.FacetName = fc.Name;
        foreach(Sitecore.ContentSearch.Linq.FacetValue fv in fc.Values)
        {
            sf.Values.Add(new Someproject.Models.Search.FacetValue() { FacetCount = fv.AggregateCount, Value = fv.Name });
        }
        f.Add(sf);
    }
    return f;
}

Paginated search results and the faceted breakdown of the search results are neatly packaged and are ready to be serialized into JSON for the presentation layer.

Conclusion

The Sitecore PredicateBuilder and Content Search Linq interface makes building a site search solution very managable.  Computed Fields allow the ability to store anything you need into the Lucene index file.

Implementing Search Using Sitecore & Lucene – Part I

Here is an overview of how I recently implemented search for a web site built using Sitecore.  I did not have the option to design the site templates from scratch.  Instead, I inherited a messy template inheritance structure with some inconsistencies in the design. I also had no budget, so Coveo was not an option.

poor

Index Creation

I started by creating a custom index to use for search.  I didn’t like the idea of tacking a large number of computed fields onto the default Sitecore indexes.

The search index needed to include items from the entire content tree as well as the media library.  I added two crawler location definitions.


< locations hint ="list:AddCrawler" >
    < crawler type ="Sitecore.ContentSearch.ExcludeItemCrawler, XL.Website" >
        < Database >master< /Database >
        < Root >/sitecore/content< /Root >
    < /crawler >
< /locations >
< locations hint ="list:AddCrawler" >
    < crawler type ="Sitecore.ContentSearch.ExcludeItemCrawler, XL.Website" >
        < Database >master< /Database >
        < Root >/sitecore/media library< /Root >
    < /crawler >
< /locations >

In the index configuration, I defined all of the templates that I needed to include in the index.


< include hint="list:IncludeTemplate" >
    < Product >{272C2195-AFE6-47CC-9707-BC8FB1909BE4}< /Product >
    < Article >{ABAEACC9-AC67-4A0D-B0DE-54982D2D3246}< /Article >
    ...
< /include >

I also defined any common fields that I would need in the index to search on, as well as display in the UI.


< fieldMap type ="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch" >
    < fieldNames hint ="raw:AddFieldByFieldName" >
        < field fieldName ="MetadataTitle" storageType ="YES" indexType ="UNTOKENIZED" vectorType ="NO" boost ="1f" type ="System.String" settingType ="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" >
            < analyzer type ="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" / >
        < /field >
        < field fieldName ="MetadataKeywords" storageType ="YES" indexType ="UNTOKENIZED" vectorType ="NO" boost ="1f" type ="System.String" settingType ="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" >
            < analyzer type ="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" / >
        < /field >
    < /fieldNames >
< /fieldMap >

Computed Fields

So, what about the data that is not stored consistently across the site?  You can aggregate the needed data using a computed field.

public class MetaDataDescriptionField : IComputedIndexField
{
   public string FieldName { get; set; }
   public string ReturnType { get; set; }

   public object ComputeFieldValue(IIndexable indexable)
   {
      Assert.ArgumentNotNull(indexable, "indexable");
      var indexableItem = indexable as SitecoreIndexableItem;

      if (indexableItem == null)
      {
         Log.Warn(string.Format("{0} : unsupported IIndexable type : {1}", this, indexable.GetType()), this);
         return null;
      }

      string sDescription = String.Empty;
      if (indexableItem.Item.IsDerived(indexableItem.Item, new Sitecore.Data.ID("some-template-guid")))
      {
         Sitecore.Data.Fields.Field stringField = indexableItem.Item.Fields["Somefieldname"];

         if (stringField != null)
         {
            sDescription = indexableItem.Item.Fields["Somefieldname"].Value;
         }
      }
      else
      {
          // Handle other templates ...
      }
      return sDescription;
   }
}

public static class ItemExtensions
{
        public static bool IsDerived([NotNull] this Item item, [NotNull] ID templateId)
        {
            return TemplateManager.GetTemplate(item).IsDerived(templateId);
        }
}

The computed fields get added to the custom Lucene index configuration.


< fields hint="raw:AddComputedIndexField" >
    < field fieldName="Description" storageType="YES" indexType="UNTOKENIZED" >Someproject.Indexes.Computed.MetadataDescriptionField, Somenamespace< /field >
    ...
< /fields >

I added a variety of computed fields.  Some of the fields accessed the LinkManager to store urls to pages or the MediaManager to store urls for images; needed by the presentation layer.  In addition, MultiList fields needed to be converted into a usable format so that they could be used for faceting.

Computed Fields for Facets

If you are using facet values in your presentation layer, rather than GUIDs, you will need to convert your Multilist fields into tokenized facet value data in the Lucene search index.

public class CategoryField : IComputedIndexField
{
   public string FieldName { get; set; }
   public string ReturnType { get; set; }

   public object ComputeFieldValue(IIndexable indexable)
   {
       Assert.ArgumentNotNull(indexable, "indexable");
       var indexableItem = indexable as SitecoreIndexableItem;

       if (indexableItem == null)
       {
           Log.Warn(string.Format("{0} : unsupported IIndexable type : {1}", this, indexable.GetType()), this);
          return null;
       }

       List sReturn = new List();

       if (indexableItem.Item != null)
       {
           if (indexableItem.Item.IsDerived(indexableItem.Item, new Sitecore.Data.ID("some-template-guid")))
           {
               Sitecore.Data.Fields.MultilistField multilistField = currentItem.Fields["somefieldname"];
               if (multilistField != null)
               {
                  sReturn = HelperClass.GetListValues(multilistField, "someotherfieldname");
               }
               else
               {
                ...
               }

               return sReturn;
           }
       }
}

public class HelperClass
{
   public static List GetListValues(MultilistField multiListField, string fieldName)
   {
       List results = new List();
       if (multiListField == null) { return results; }

       foreach (Sitecore.Data.ID sitecoreID in multiListField.TargetIDs)
       {
           Item sitecoreItem = SitecoreHelper.GetItem(sitecoreID.ToString());
           string result = SitecoreHelper.GetFieldValue(sitecoreItem, fieldName);
           if (!string.IsNullOrWhiteSpace(result))
           {
               results.Add(result);
           }
       }
       return results;
   }
}

< fields hint="raw:AddComputedIndexField" >
    < field fieldName="Description" storageType="YES" indexType="UNTOKENIZED" >Someproject.Indexes.Computed.MetadataDescriptionField, Somenamespace< /field >
    < field fieldName="ComputedCategory" storageType="YES" indexType="TOKENIZED" >Someproject.Indexes.Computed.CategoryField, Somenamespace< /field >
    ...
< /fields >

Tokenized versus Untokenized

An important setting in index field configuration is indexType.  Untokenized fields will be stored as one string in the Lucene index.  Tokenized fields will be broken up.

Facet Values Containing Spaces

One major gotcha that I encountered was facet values that contain spaces.  Because the facet fields are tokenized, the spaces in the facet values wrecked havoc with the facet results sets.  I found an excellent blog by Ryan Bailey, referencing a solution provided by Martina Welander.

Adding the computed facet fields to the fieldMap section solved the problem.


< fieldMap type="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch" >
    < fieldNames hint="raw:AddFieldByFieldName" >
        ...
        < field fieldName="ComputedCategory" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" >
            < analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" / >
        < /field >
        ...
    < /fieldNames >
< /fieldMap >

Dynamically Excluding Content

One of the requirements in this site is to be able to explicitly hide content from the site search based on an item field value.  I chose to create a custom crawler for this purpose.  The direct solution would be to directly filter on the results.  I chose to abstract this requirement into a crawler because of the complexity surrounding the search and faceting logic.  I did not want to complicate it further.

public class ExcludeItemCrawler : SitecoreItemCrawler
    {
        protected override bool IsExcludedFromIndex(SitecoreIndexableItem indexable, bool checkLocation = false)
        {
            bool isExcluded = base.IsExcludedFromIndex(indexable, checkLocation);

            if (isExcluded)
                return true;

            Item item = (Item)indexable;

            // If its a wildcard
            if (item.Name == "*")
                return true;

            // Several complex checks
            if (somecondition)
                return true;
	    ...
            return false;
        }
    }

 < locations hint="list:AddCrawler" >
     < crawler type="Somenamespace.ContentSearch.ExcludeItemCrawler, Somenamespace" >
         < Database >master < /Database >
         < Root >/sitecore/content < /Root >
     < /crawler >
 < /locations >
 < locations hint="list:AddCrawler" >
     < crawler type="Somenamespace.ContentSearch.ExcludeItemCrawler, Somenamespace" >
         < Database >master < /Database >
         < Root >/sitecore/media library < /Root >
     < /crawler >
 < /locations >

Ok, so now that we have the data that we want in the Lucene index, I’ll talk about how to query it in Part II.

Upgrading to Glass Mapper v4.3

Glass Mapper is hands down the best ORM available for Sitecore Development.  I have been using it for five years and love it.  It helps you map Sitecore templates and fields to .NET classes, objects and properties.

I recently decided to upgrade to v4.3 from v4.0.1.8.  Upgrading from v3 to v4 was a little bit hairy, but upgrading to v4.3 was pretty easy.

Update from Nuget

First, I loaded the new version of Glass Mapper from Nuget.  I opened the Package Manager Console and ran:

Install-Package Glass.Mapper.Sc -Version 4.3.4.197

Installing the Nuget package wipes out App_Start/GlassMapperScCustom.cs class, so I needed to re-add my custom data mappers. My GlassMapperScCustom class looked like this after I was done.

#region GlassMapperScCustom generated code
using Glass.Mapper.Configuration;
using Glass.Mapper.IoC;
using Glass.Mapper.Maps;
using Glass.Mapper.Sc.IoC;
using IDependencyResolver = Glass.Mapper.Sc.IoC.IDependencyResolver;
using Glass.Mapper.Configuration.Attributes;
using SomeProject.CustomDataHandler;

namespace SomeProject.App_Start
{
    public static  class GlassMapperScCustom
    {
        public static IDependencyResolver CreateResolver()
        {
           var config = new Glass.Mapper.Sc.Config();

	   var dependencyResolver = new DependencyResolver(config);
           // add any changes to the standard resolver here

           dependencyResolver.DataMapperFactory.Insert(0, () => new LinkListDataHandler());
           dependencyResolver.DataMapperFactory.Insert(0, () => new CustomInternalLinkDataHandler());

           return dependencyResolver;
        }

        public static IConfigurationLoader[] GlassLoaders()
        {
           var attributes = new AttributeConfigurationLoader("SomeProject");
           return new IConfigurationLoader[] { attributes };
        }
    
        public static void PostLoad()
        {
           var dbs = Sitecore.Configuration.Factory.GetDatabases();
           foreach (var db in dbs)
           {
               var provider = db.GetDataProviders().FirstOrDefault(x => x is GlassDataProvider) as GlassDataProvider;
               if (provider != null)
               {
                   using (new SecurityDisabler())
                   {
                       provider.Initialise(db);
                   }
               }
           }
        }
        public static void AddMaps(IConfigFactory< IGlassMap >  mapsConfigFactory)
        {
        }
    }
}
#endregion

Ambiguous Reference

I re-built my solution and ran into some compiler errors.

ambiguous reference error

Easy problem to resolve: a simple conflict between a Glass abstraction of the Sitecore Diagnostics log and the Sitecore Diagnostics log.  I considered removing the references to Sitecore.Diagnostics and using the Glass method, but the Glass.Mapper.Sc.Log class requires an instance to be used.  So, I simply globally changed my Log references in code to Sitecore.Diagnostics.Log.

Model Depth Check

The solution now compiled successfully.  When I tested the application, I encountered:

Server Error in ‘/’ Application.

Model too deep. Potential lazy loading loop. Type requested: SomeProject.Models.IGlassBase SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.IGlassBase
SomeProject.Models.Pages.HomePage
SomeProject.Models.Parts.DesktopMenu

After a quick Google search, I re-read the Glass Mapper 4.3 Release Notes.  The depth check mechanism added for the Cachable Changes was breaking the code.  This was another easy fix.  I refactored the CreateResolver method as per the release notes:

public static IDependencyResolver CreateResolver()
{
   var config = new Glass.Mapper.Sc.Config();

   // Needed to avoid Model too deep exception
   config.EnableLazyLoadingForCachableModels = true;
   
   var dependencyResolver = new DependencyResolver(config);
   // add any changes to the standard resolver here

   dependencyResolver.DataMapperFactory.Insert(0, () => new LinkListDataHandler());
   dependencyResolver.DataMapperFactory.Insert(0, () => new CustomInternalLinkDataHandler());

   var factory = dependencyResolver.ObjectConstructionFactory as AbstractConfigFactory < AbstractObjectConstructionTask >;
   factory.Remove< ModelDepthCheck >();

   return dependencyResolver;
}

Naughty GlassCast

The next issue I worked through was the depreciation of the GlassCast method. Mike Edwards, the “Godfather of Glass Mapper”, did an incredible job explaining why GlassCast can lead to performance problems. I had an embarrassing amount of GlassCast method calls sprinkled throughout the code base.

model = MiscContentFactory.GetRelatedContent (Sitecore.Context.Item.Parent.GlassCast< CustomModelClass >().Id.ToString(), parameters.NumberOfItems);

This was straight forward. I removed all references to GlassCast. I chose to replace GlassCast with use of the Cast method from the SitecoreService object.

ISitecoreService service = new SitecoreService(Sitecore.Context.Database);
model = MiscContentFactory.GetRelatedContent (service.Cast< CustomModelClass >(Sitecore.Context.Item.Parent).Id.ToString(), parameters.NumberOfItems);

Embedded Markup in RenderLink Contents Parameter

The next implementation issue was instances of RenderLink being used with embedded markup in the contents attribute.

@RenderLink(x => home.SomeLinkProperty, new System.Collections.Specialized.NameValueCollection() { { "class", "print-logo" } }, contents: "< span class=\"icon-openenvelop\" >< / span > < span class=\"print-logo\">" + home. SomeLinkProperty.Text + " < / span >")

I could not find a way to make this work.  I ended up converting every use of the method RenderLink to BeginRenderLink.

@using (BeginRenderLink(x => home.SomeLinkProperty, new System.Collections.Specialized.NameValueCollection() { { "class", "print-logo" } }, isEditable: true))
{
   < span class="icon-openenvelop" > < /span >< span class="print-logo" > @home. SomeLinkProperty.Text < / span >
}

I suppose I could have created a helper class to work around this to avoid all the rework, but I felt this was clearer long term solution.

Empty Model Values for View Renderings

The last issue that I encountered was View Renderings with blank Model class values.

blank view model value in Sitecore View Rendering

This caused the following error to appear on various pages:

The model item passed into the dictionary is of type ‘Sitecore.Mvc.Presentation.RenderingModel’, but this dictionary requires a model item of type ‘X’

Sloppy development practices left the Model values blank for some View Renderings, even though the View Renderings get bound to specific Model classes in the View.  I reviewed all of the View Renderings and set the Model values that were blank to the appropriate custom model class.

Sitecore View Rendering with valid model value

Conclusion

Upgrading to Glass Mapper v4.3 was fairly straightforward and well worth the effort.