HomeDev guideAPI ReferenceGraphQL
Dev guideUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

Alloy MVC with Optimizely Graph Client

Create a standard Alloy MVC sample site and use Optimizely Graph Service and Optimizely Graph Client (OG Client).

Use a few lines of C# code and a dedicated package to build GraphQL search queries for Optimizely Graph, similar to Search & Navigation.

This tutorial uses Visual Studio Code with extensions. You can use whatever IDE you prefer.

Add the Optimizely Graph Client tools

This tool allows you to generate schema's models after indexing your data.

Create a new folder called ContentGraph:

mkdir ContentGraph

Go to the folder ContentGraph:

cd ContentGraph

Create a manifest file by running the dotnet new command:

dotnet new tool-manifest

Install Strawberry Shake tools locally:

dotnet tool install Optimizely.Graph.Client.Tools

Create Alloy MVC site

Create a new folder for the site called AlloyMvcGraphQL:

mkdir AlloyMvcGraphQL

Go to the folder AlloyMvcGraphQL:

cd AlloyMvcGraphQL

Create the Alloy MVC site:

dotnet new epi-alloy-mvc

Add the latest CMS package:

dotnet add package EPiServer.Cms

Start the site:

dotnet run

Open your browser, go to the site https://localhost:5000, add the admin user, and make sure the site is working properly.

Install the required packages for Optimizely Graph and OG Client

Add the Optimizely Graph Client:

dotnet add package Optimizely.Graph.Client

(Optional) Generate the model classes that will be used:

dotnet ogschema appsettings.json

Add Optimizely.ContentGraph.Cms:

dotnet add package Optimizely.ContentGraph.Cms

Update Startup.cs to use Optimizely Graph and OG Client

Add using:

using EPiServer.DependencyInjection;

Update constructor to handle IConfiguration:

private readonly IConfiguration _configuration;

public Startup(IWebHostEnvironment webHostingEnvironment, IConfiguration configuration)
{
  _webHostingEnvironment = webHostingEnvironment;
   _configuration = configuration;
 }

In ConfigureServices method add:

        services.ConfigureContentApiOptions(o =>
        {
            o.IncludeInternalContentRoots = true;
            o.IncludeSiteHosts = true;
            o.EnablePreviewFeatures = true;
            o.SetValidateTemplateForContentUrl(true);
        });
        services.AddContentDeliveryApi(); // required, for further configurations, please visit: https://docs.developers.optimizely.com/content-cloud/v1.5.0-content-delivery-api/docs/configuration
        services.AddContentGraph(_configuration);
        services.AddContentGraphClient(_configuration); //use graph client

Update the SitePageData and Content Delivery Model Filter

Add 2 properties to SitePageData type to make it available for query after runing the indexing job (you can add this after the indexing job is finished)

[ContentType(AvailableInEditMode = false)]
public class SitePageData : PageData, ICustomCssInContentArea
{
  public virtual string Url { get; set; } //add Url prop for query
  public virtual IEnumerable<string> ContentType { get; set; } //add ContentType prop for query
  ...
  //original properties
}

Create a new class CustomContentApiModelFilter to customize ContentApiModelFilter for additional content type on Optimizely Graph.

[ServiceConfiguration(typeof(IContentApiModelFilter), Lifecycle = ServiceInstanceScope.Singleton)]
    internal class CustomContentApiModelFilter : ContentApiModelFilter<ContentApiModel>
    {
        private readonly IContentTypeRepository _contentTypeRepository;
        private readonly IContentLoader _contentLoader;

        public CustomContentApiModelFilter()
            : this(ServiceLocator.Current.GetInstance<IContentTypeRepository>(),
                    ServiceLocator.Current.GetInstance<IContentLoader>())
        {
        }

        public CustomContentApiModelFilter(
            IContentTypeRepository contentTypeRepository,
            IContentLoader contentLoader)
        {
            _contentLoader = contentLoader;
            _contentTypeRepository = contentTypeRepository;
        }
        public override void Filter(ContentApiModel contentApiModel, ConverterContext converterContext)
        {
            var contentType = GetContentType(contentApiModel);
            if (contentType != null)
            {
                var abstractTypes = new List<Type>();
                AddBaseTypes(contentType.ModelType, ref abstractTypes);
                contentApiModel.ContentType.AddRange(abstractTypes.Select(x => x.Name));
                contentApiModel.ContentType.Add("Content");
            }
        }

        private void AddBaseTypes(Type type, ref List<Type> types)
        {
            if (type?.BaseType != null && type.BaseType != type && type.BaseType != typeof(IContent) && type.BaseType != typeof(Object))
            {
                types.Add(type.BaseType);
                AddBaseTypes(type.BaseType, ref types);
            }
        }

        private ContentType GetContentType(ContentApiModel contentApiModel)
        {
            if (contentApiModel?.ContentLink?.Id is null or 0)
            {
                return null;
            }

            var contentReference = new ContentReference(contentApiModel.ContentLink.Id.Value, contentApiModel.ContentLink?.WorkId.GetValueOrDefault() ?? 0, contentApiModel.ContentLink?.ProviderName);
            if (_contentLoader.TryGet<IContent>(contentReference, out var content))
            {
                var contentType = _contentTypeRepository.Load(content.ContentTypeID);
                if (contentType != null)
                {
                    return contentType;
                }
            }

            return null;
        }
    }

Update search functionality in Alloy

In Alloy, use Optimizely Graph in the search controller.

Update SearchPageController in Controller folder to use Optimizely query

using AlloyMvcGraphQL.Models.Pages;
using AlloyMvcGraphQL.Models.ViewModels;
using EPiServer.ContentGraph.Extensions;
using EPiServer.ContentGraph.Api;
using Microsoft.AspNetCore.Mvc;

namespace AlloyMvcGraphQL.Controllers;

public class SearchPageController : PageControllerBase<SearchPage>
{
    private readonly GraphQueryBuilder _contentGraphClient;
    private static Lazy<LocaleSerializer> _lazyLocaleSerializer = new Lazy<LocaleSerializer>(() => new LocaleSerializer());

    public SearchPageController(GraphQueryBuilder contentGraphClient)
    {
        _contentGraphClient = contentGraphClient;
    }

    public ViewResult Index(SearchPage currentPage, string q)
    {
        var searchHits = new List<SearchContentModel.SearchHit>();
        var total = 0;
				var facets = new List<SearchContentModel.SearchFacet>();
        if (q != null)
        {
            var locale = _lazyLocaleSerializer.Value.Parse(currentPage.Language.TwoLetterISOLanguageName.ToUpper());

            var result = _contentGraphClient
            .OperationName("Alloy_Sample_Query")
            .ForType<SitePageData>()
            .Fields(_=>_.Name, _=>_.Url, _=> _.MetaDescription)
            .Facet(_=>_.ContentType)
            .Total()
            .GetResultAsync<SitePageData>().Result;
            
            var searchWords = q.Split(" ");
            foreach (var item in result.Content.Hits)
            {
                searchHits.Add(new SearchContentModel.SearchHit()
                {
                    Title = item.Name,
                    Url = item.Url,
                    Excerpt = GetExcerpt(q, searchWords, item._fulltext)
                }); ;
            }
            facets = result.Content.Facets["ContentType"].Select(x=> new SearchContentModel.SearchFacet { 
                    Name = x.Name,
                    Count = x.Count
                });
            total = result.Content.Total;
        }

        var model = new SearchContentModel(currentPage)
        {
            Hits = searchHits,
            NumberOfHits = total,
            Facets = facets,
            SearchedQuery = q
        };

        return View(model);
    }

    private string GetExcerpt(string query, IEnumerable<string> querySplitInWords, IEnumerable<string> resultTextsForHit)
    {
        var matchingTexts = new Dictionary<string, int>();
        foreach (var resultText in resultTextsForHit)
        {
            foreach (var searchedWord in querySplitInWords)
            {
                if (resultText.Contains(searchedWord))
                {
                    if (!matchingTexts.ContainsKey(resultText))
                    {
                        matchingTexts[resultText] = 0;
                    }

                    matchingTexts[resultText]++;
                }
            }

            if (resultText.Contains(query))
            {
                matchingTexts[resultText] = matchingTexts[resultText] * 10;
            }
        }

        string excerpt;
        if (matchingTexts.Any())
        {
            excerpt = string.Join(" .", matchingTexts.OrderByDescending(x => x.Value).Select(x => x.Key));
        }
        else
        {
            excerpt = string.Join(" .", resultTextsForHit);
        }

        return excerpt?[0..Math.Min(excerpt.Length, 500)];
    }
}

Go to to the search page and try it out

Start the site

dotnet run

Start the site, open your browser, and go to https://localhost:5000/en/search