Ajax support for the pluggable EPiServer Find UnifiedSearch implementation

This article is based on work described in A way of consolidating EPiServer Find Unified Search over multiple websites as well as Example: Pluggable EPiServer Find UnifiedSearch for selected types.

If you want to use ajax to retrieve more search hits, apply hit filtering or the like, you would need to pay some extra consideration to the API. Since the ajax call from the frontend may be using an ApiController endpoint, the action will have very little idea of what search specifics to apply. This would result in an expected search result on the first request, but some default one (or an exception) when requesting more.

In below code I have stripped away logging and stuff that are not interesting for the example. Keep in mind that we use React and serialize our page and block type instances using the JOS Content Serializer (that’s why you see ignored properties in the block type).

So, first of all we create an interface as below. All EPiServer content types that are responsible for performing the search need to implement it.

The AjaxSearch method will be used to perform the search itself. The SearchOriginId property is a ContentReference string containing the ID of the content type responsible for the initial search.

public interface IUseAjaxSearchApi : IContentData
{
  SearchResult AjaxSearch();
  string SearchOriginId { get; }
}

The implementation may look something like below. The Search-method is described in a previous article.

Since all the data (such as batch size, page number, query and so on) exists in the request URL we can just use the same method when implementing the AjaxSearch method.

[ContentType(DisplayName = "Some search")]
public class LimitedSearchBlock : SearchBlockBase, IUseAjaxSearchApi
{
  [Ignore, ContentSerializerInclude, JsonIgnore]
  public override SearchResult SearchResult => Search();

  private SearchResult Search()
  {
    // ...
    return _searchService.SearchFor(requestBase, specifics);
  }

  public virtual SearchResult AjaxSearch()
  {
    return Search();
  }

  [Ignore, ContentSerializerInclude, JsonIgnore]
  public virtual string SearchOriginId => ((IContent) this).ContentLink.ToReferenceWithoutVersion().ToString();
}

The SearchOriginId parameter could have an implementation such as above. This will return the ID of the LimitedSearchBlock as a string.

Now the frontend ajax call would need to supply the SearchOriginId while calling our API endpoint. We could call the parameter so as below. The rest of the parameters are not used here (but rather inside the search implementation mentioned in a previous article), but I’ve left them in there just to make it clear they are in the query collection.

So, if we don’t have a correct ContentReference value in our so parameter, or if it points to a content that does not implement the IUseAjaxSearchApi interface, we just return a default search.

public class AjaxSearchController : ApiController
{
  private readonly ISearchService _searchService;
  private readonly IContentLoader _contentLoader;

  public AjaxSearchController(
ISearchService searchService,
IContentLoader contentLoader)
  {
    _searchService = searchService ?? throw new ArgumentNullException(nameof(searchService));
    _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
  }

  [HttpGet]
  public SearchResult Search(string so = null, string q = "", string sortOrder = "", int p = 1, int batchSize = 10)
  {
    if (string.IsNullOrEmpty(so) || !ContentReference.TryParse(so, out ContentReference reference))
    {
      return DefaultSearch();
    }

    IUseAjaxSearchApi content = _contentLoader.Get<IUseAjaxSearchApi>(reference);
    if (content == null)
    {
      return DefaultSearch();
    }

    return content.AjaxSearch();
  }

  private SearchResult DefaultSearch()
  {
    HttpRequest request = HttpContext.Current?.Request;
    if (request == null)
    {
      return null;
    }
    HttpRequestBase requestBase = new HttpRequestWrapper(request);
    return _searchService.SearchFor(requestBase, new SpecificSearchBase());
  }
}

If we on the other hand do have a content, we can use it’s implementation of AjaxSearch to retrieve the proper search result depending on the rest of the request.

The default search method is just a basic implementation of a search using the SpecificSearchBase class without customizations.