Blog posts by Giuliano Dore2021-05-16T15:50:20.0000000Z/blogs/giuliano-dore/Optimizely WorldHow to migrate from uCommerce to EPiServer Commerce part 1: data models and enums/blogs/giuliano-dore/dates/2021/5/how-to-migrate-from-ucommerce-to-episerver-commerce-part-1-data-models-and-enums/2021-05-16T15:50:20.0000000Z<p><em>This article is following the introduction article I wrote previously <a href="/link/117e1156d1854aa4a1bf1c0a9599e795.aspx">https://world.episerver.com/blogs/giuliano-dore/dates/2021/2/how-to-migrate-from-ucommerce-to-episerver-commerce-part-0-skus-and-tree-structure/</a> regarding migration from uCommerce to EPiServer Commerce. If you are looking for info about tree structure and skus in uCommerce check this out!</em></p>
<h2>Product properties migration</h2>
<p>If you are familiar with CMS where editors can set the definitions for templates, you know that it can be tricky to migrate to CMS where developers are setting definitions for templates like EPiServer.</p>
<p><strong>What does it mean?</strong></p>
<p>It is possible to describe EPiServer as a code-first modelling CMS: the developers (aka the people who have access to the codebase) can create, update and delete pages and products templates by reading and updating code.</p>
<p>If I want to create a page template in EPiServer – like a landing page – I need to create a LandingPage class that inherits from PageData and set some properties inside that class for the properties that I want for the page. If I want a property for the body of the page as html content, I then need to create a public <strong>XhtmlString</strong> property like this:</p>
<pre class="language-csharp"><code>public virtual XhtmlString Content { get; set; }</code></pre>
<p>And EPiServer is smart enough to associate this property in that class with a property in a “landing page” template. <strong> </strong></p>
<p>Unfortunately, some CMS also work the other way around: editors can log into the backoffice and define pages and properties through the backoffice. Once the properties, pages & products have been defined, the developers start writing / generating code to map the definitions that they created in order to add business logic for their projects.</p>
<p>I would call this approach ‘template-first’. Umbraco, Sitecore and uCommerce use template-first data modelling.</p>
<p><em>Is code-first better or worse than template first? I think both solutions have pros and cons. Feel free to add a comment below regarding the question </em>😊</p>
<p>Coming back to uCommerce, how can we migrate data & data models from a template-first project to code-first project?</p>
<p>Regarding this task, we have some good news and some bad news.</p>
<p>The good news is that the data types are often easy to match: properties like simple text, html, numbers are easy to “translate” from one data model to another one. Some examples are available below:</p>
<table>
<tbody>
<tr>
<td>
<p>uCommerce property data type</p>
</td>
<td>
<p>EPiServer Commerce property data type</p>
</td>
</tr>
<tr>
<td>
<p>Boolean</p>
</td>
<td>
<p>C# boolean</p>
</td>
</tr>
<tr>
<td>
<p>ShortText</p>
</td>
<td>
<p>C# string</p>
</td>
</tr>
<tr>
<td>
<p>RichText</p>
</td>
<td>
<p>C# XhtmlString</p>
</td>
</tr>
</tbody>
</table>
<p> </p>
<p>The bad news is that, while it is possible to automate the data migration, <strong>it is not possible to automate the data model migration</strong>.</p>
<p>As mentioned previously, EPiServer data model depends on code. While we can retrieve the products definitions from the uCommerce database, <strong>we would need to generate code on the fly and add it to the project in order to automatically migrate products definitions</strong> <strong>and data in the same thread.</strong></p>
<p>While it is possible, I find that this solution is extremely complex and possibly over-engineered for very little value.</p>
<p>The solution we found was to manually provide data definitions instead of trying to automatically get the definitions and the data in the same thread:</p>
<ol>
<li>Based on the definitions we can find in uCommerce, we manually “translate” the products and properties definitions to EPiServer Commerce using c# classes.</li>
<li>Once all the definitions are available, we start running a scheduled job to migrate data from uCommerce to EPiServer Commerce using the templates we created during step 1.</li>
</ol>
<p>Why is it so important to have all the definitions ready before step 2? The reason is that, if we are attempting to migrate data with missing definitions, <strong>there will be data loss</strong>. As we are trying to avoid data loss, we must make sure all the definitions are ready before starting the second step.</p>
<h2>Enum data migrations</h2>
<p>It isn’t uncommon for products to have properties where the value is available as one choice out of a predefined list. An example would be a scenario where we would want to create a ‘Colour’ property but we would want to offer only options like white, blue, orange, etc.</p>
<p>This can be achieved in uCommerce using Enum data types. Editors can setup Enums inside the Commerce Settings section under the ‘data types’ sub-section:</p>
<p><img src="/link/f15c6f2f5a6a41d495958030f1ad6c0e.aspx" width="641" alt="colour enum" height="336" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p>As you can see, editors can create enum data types and they can also define the potential values to select from. They can add / update / delete values under the enum definition that they just created without breaking their e-commerce website.</p>
<p> <img src="/link/8660b570b0cd40feaed584cec7b05456.aspx" width="241" alt="" height="112" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p>One issue here is that <strong>Enum values in EPiServer are inside the code</strong>. EPiServer developers can define “enum” properties in c# but, due to the nature of code-first data-modelling, it is not possible to allow editors to update enums in c#. The other issue here is that any update from a developer would require code deployment.</p>
<p>In short: <strong>c# Enums are not a good fit for uCommerce Enum data types</strong>.</p>
<p>We started looking for alternative data models where editors could update the list of pre-defined values. We found that we could use the CMS content tree and <strong>SelectionFactories</strong> to “simulate” a “editor friendly” enum property in EPiServer:</p>
<ol>
<li>Inside EPiServer CMS, we would set a “repository” page as “folder”.</li>
<li>The repository page would allow children nodes.</li>
<li>Each children node would contain a simple text property.</li>
<li>On pages where we would expect the “Enum” property type, we would instead have a simple string property with a [<strong>SelectOne</strong>] attribute using a <strong>SelectionFactory</strong> to retrieve the children’s nodes of our “repository” page.</li>
</ol>
<p>Our content tree would look like this:</p>
<ul>
<li>MyWebsite
<ul>
<li>Generic page</li>
<li>Login page</li>
<li>FAQ page</li>
</ul>
</li>
<li>Colour repository (not a real page)
<ul>
<li>Green</li>
<li>Blue</li>
<li>Black</li>
</ul>
</li>
</ul>
<p>And our SelectionFactory would look like this:</p>
<pre class="language-csharp"><code> public class ProductColourSelectionFactory : ISelectionFactory
{
private Injected<ISiteRepositoryService> siteRepositoryService;
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
//this service would find the relevant repository folder for the "enum" we are trying to simulate
//and list all the children nodes
return siteRepositoryService.Service.GetRepositoryPages(x => x.ProductColourRepository)
.Select(x => new SelectItem
{
Text = x.DisplayNameValue,
Value = x.ContentLink
});
}
}</code></pre>
<p>The final result inside EPiServer commerce would look like this:</p>
<p><img src="/link/54503b07aad34a4384f551e0c9237f08.aspx" width="329" alt="" height="230" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p>Once our control is available, the last step is to make sure that the migration script will assign the right value to the Color property and that the dropdown control will display the accurate value.</p>
<p>That's it for today, I hope it was helpful. If you have tips regarding data models or data migrations, feel free to drop a comment below 😊</p>How to integrate Optimizely Feature Rollout with EPiServer/blogs/giuliano-dore/dates/2021/5/how-to-integrate-optimizely-feature-rollout-with-episerver/2021-05-11T10:54:10.0000000Z<p>I have been writing a number of posts about agency clients and use-cases for agencies pain points here on EPiServer World and on other blogging platforms in the past. I also had the chance to work for startups and product companies and I could notice that the process for feature delivery is sometimes a bit different. </p>
<p>Product companies tend to focus on a small number of products and a large number of deployments for features, micro-features and components. One requirement that is listed is that It must be possible to release specific features for specific targets like markets, user groups or audiences without “corrupting” the integrity of the project.</p>
<p>As an example, social networks with a few billion users in hundreds of countries are regularly publishing different features for different countries with full control over the delivery and no downtime.</p>
<p>I have had the opportunity to play with Optimizely during my free time, and I was very pleased with the capabilities of their latest ‘Feature Rollout’ release.</p>
<p>One of the perks regarding feature rollout is that it helps avoiding messy rollbacks and hotfixes as it is possible to use feature flags to roll out features to users with an option to roll back if needed to reduce the risk using a convenient interface.</p>
<h2>Technical limitations</h2>
<p>Let’s get back into EPiServer for a second. We have a wonderful system with flags that can be controlled in a separate backoffice. However, due to the nature of c# and the ‘code first’ approach in EPiServer regarding data models, there are items that we cannot ‘roll out’, I listed a few of them:</p>
<ul>
<li>Page & Block types</li>
<li>Product types in EPiServer Commerce</li>
<li>Selection factories & rules about allowed content</li>
<li>New Scheduled jobs</li>
</ul>
<p>The main reason regarding this limitation is that the flag coming from Optimizely is a <strong>Boolean </strong>and it <strong>is not possible to set wrap ‘if’ statements around classes or attributes</strong>.</p>
<p>The default behaviour in EPiServer regarding components like Scheduled Jobs is that they become available once the class is included in the dll.</p>
<p>While we can’t ship pages or blocks as Optimizely “features”; we still have a wide range of opportunities to use feature rollout:</p>
<p>We can use the flag to hide / show new features in a page like new sections of a page:</p>
<pre class="language-csharp"><code>@{
var optimizelyClient = OptimizelySDK.OptimizelyFactory.NewDefaultInstance(System.Configuration.ConfigurationManager.AppSettings["Optimizely:sdkKey"]);
//testing for all clients
var user = optimizelyClient.CreateUserContext("");
//feature flag
var decision = user.Decide("enable_messenger_chat");
var enableMessengerChat = decision.Enabled;
}
@if (enableMessengerChat)
{
<div>new feature here</div>
}
</code></pre>
<p>We can also use the flag at the page controller level to “disable” pages and blocks from being rendered until the Optimizely feature is marked as enabled:</p>
<pre class="language-csharp"><code>public class NewFeaturePageController : PageController<NewFeaturePage>
{
public ActionResult Index(NewFeaturePage currentPage)
{
var optimizelyClient = OptimizelySDK.OptimizelyFactory.NewDefaultInstance(System.Configuration.ConfigurationManager.AppSettings["Optimizely:sdkKey"]);
var user = optimizelyClient.CreateUserContext("");
var decision = user.Decide("enable_new_feature");
var isEnabled = decision.Enabled;
if(isEnabled)
return View(currentPage);
else
throw new NotImplementedException();
}
}
</code></pre>
<p>With this code, even if the page is deployed, users - like the content team - will not be able to see the page until the feature is enabled in Optimizely.</p>
<p><em>Update: there is a way to "hijack" the creation of pages, blocks and other episerver components using Initialization modules and the right service, solving the "we can't wrap a boolean around a class or an attribute" issue. However this is a lot more advanced and I chose to cover examples that are available using the standard online training classes. </em></p>
<h2>Integration</h2>
<p>I created a test project in github to review the capabilities of Feature Rollout, the code is available here: <a href="https://github.com/giuunit/optimizely-feature-rollout-test">https://github.com/giuunit/optimizely-feature-rollout-test</a> - it's an example of a feature (enabling facebook messenger web plugin) using Optimizely Rollout Feature. Most of the code is inside the master layout as I wanted the plugin to be available for all the pages in the web project. The code below shows how to integrate the FB Messenger chat web plugin using Feature Rollout and a variable for the page id: <br /><br /></p>
<pre class="language-csharp"><code>@{
Layout = "~/Views/Shared/_Layout.cshtml";
//setting up Optimizely;
var optimizelyClient = OptimizelySDK.OptimizelyFactory.NewDefaultInstance(System.Configuration.ConfigurationManager.AppSettings["Optimizely:sdkKey"]);
var user = optimizelyClient.CreateUserContext("");
//feature flag
var decision = user.Decide("enable_messenger_chat");
var enableMessengerChat = decision.Enabled;
//retrieving the page id
var pageId = decision.Variables.GetValue<string>("page_id");
}
@if (enableMessengerChat & !string.IsNullOrWhiteSpace(pageId))
{
<!-- Messenger Chat Plugin Code -->
<div id="fb-root"></div>
<script>
window.fbAsyncInit = function() {
FB.init({
xfbml : true,
version : 'v10.0'
});
};
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = 'https://connect.facebook.net/en_US/sdk/xfbml.customerchat.js';
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<!-- Your Chat Plugin code -->
<div class="fb-customerchat"
attribution="biz_inbox"
page_id="@pageId">
</div>
}</code></pre>
<p><br />Do you have ideas about integrations between Optimizely and EPiServer CMS ? Leave a comment below 😊<br /><br /></p>How to build a decision tree in EPiServer/blogs/giuliano-dore/dates/2021/5/how-to-build-a-decision-tree-in-episerver/2021-05-08T12:12:12.0000000Z<p>During multiple meetings with different clients, we noticed lately that some of our clients were interested / looking for a solution involving a complex user journey with a decision tree for their digital platform.</p>
<p>It is worth noticing that our clients don’t necessary use the word “decision tree” as I believe, it is sometimes more relevant to technical people. We found out that this is the most relevant expression to use based on the needs.</p>
<p><strong>But what is a decision tree?</strong></p>
<p>Our clients were looking for a complex set of forms on their website(s) with pre-defined fields where, based on the answer, we would redirect the user to different questions and so on.</p>
<p>A very simple example of a scenario can be summarised like this:</p>
<ul>
<li>Question 1: What kind of cuisine would you like to eat right now? Options are: Italian food or Chinese food<br />
<ul>
<li>The user selects Italian food
<ul>
<li>Question 2: Would you prefer pizza or pasta?
<ul>
<li>The user selects pizza
<ul>
<li>Outcome: please find our selection of pizzerias in your area.</li>
</ul>
</li>
<li>The user selects pasta
<ul>
<li>Outcome: please find our selection of Italian restaurants serving pasta in your area.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>The user selects Chinese food
<ul>
<li>Question 2: Are you interested in rice dishes or dim sums?
<ul>
<li>The user selects rice dishes
<ul>
<li>Outcome: please find our selection of Chinese restaurants serving rice dishes in your area.</li>
</ul>
</li>
<li>The user selects dim sums
<ul>
<li>Outcome: please find our selection of Chinese restaurants serving dim sums in your area.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>As you can see, the user journey starts with a question as an entry point and each answer redirects the user to a different set of questions, forming a tree of questions and answers, offering the ability to provide different outcomes based on the answers.</p>
<p>For our clients (and potentially more requests in the future) we also need to built an engine that will support dozens of answers at the first level and possibly thousands of branches.</p>
<p>It is implied that decision trees only allow pre-defined answers. Open-ended answers would add a level of complexity as it can become complicated to "translate" plain text into an answer. Our requirements didn’t include open-ended answers.</p>
<p>There are a lot of games out there using a “decision tree engine” – some of them have been known for decades like the <a href="https://en.wikipedia.org/wiki/Choose_Your_Own_Adventure">Choose Your Own Adventure</a> games.</p>
<p><img src="https://upload.wikimedia.org/wikipedia/en/thumb/f/f0/Cave_of_time.jpg/220px-Cave_of_time.jpg" alt="Choose Your Own Adventure - Wikipedia" style="width: 220px; height: 359px; margin: 0px auto; display: block;" /></p>
<p>We started doing some discovery about the capabilities in EPiServer to work with decision trees.</p>
<p>We found out that EPiServer Forms allows some kind of conditional logic where it is possible to display / hide some questions based on the answers of other questions, but unfortunately it is quite linear & wouldn’t scale well for scenarios with hundreds of branches & descendants.</p>
<p>But we also found out that we also have another tree we can use to “simulate” the decision tree: the <strong>content tree </strong>inside the CMS. The perks of going with that approach is that all the visual components are already there; no need to start using Dojo to build complex visual elements. It's also easy for the editors to group questions & answers using the built-in folders / nodes in the content tree.</p>
<p>Using references and the content properties inside EPiServer CMS, it is absolutely possible to store the data at the content tree level:</p>
<p>We can have one entry point (like a page) where we would set a reference to the first question / step.</p>
<p>As per requirement, the first question / step, needs to be a complex object possibly with a label and references to different answers.</p>
<p>Each answer would have a value and possibly an (optional) reference to the next question. If the property linking the answer to a possible next question is empty, we can safely assume that we are reaching one end of the tree.</p>
<p>The schema would look like:</p>
<table>
<tbody>
<tr>
<td>
<p>Page properties</p>
</td>
<td>
<p>Question / step properties</p>
</td>
<td>
<p>Answer properties</p>
</td>
</tr>
<tr>
<td>
<p> </p>
<p>Entry point: single reference to a question.</p>
<p> </p>
</td>
<td>
<p> </p>
<p>Question label: text</p>
<p>Answers: list of references to answer objects.</p>
<p> </p>
</td>
<td>
<p> </p>
<p>Answer label: text</p>
<p>Next question: (optional) reference to a question object.</p>
</td>
</tr>
</tbody>
</table>
<p>Based on our analysis, we can use EPiServer pages or blocks for both questions and answers as pages and blocks are considered as complex customizable objects.</p>
<p>For our project we went with the following approach:</p>
<ul>
<li>Each question is a page.</li>
<li>Each answer is a block, and the list is a ContentArea.</li>
</ul>
<p>It's important to know that this is not the absolute truth, other data models can work just as well, we found this model to be a good fit based on our clients' ability to use the CMS.</p>
<p>We would add the following classes definitions:</p>
<pre class="language-csharp"><code>public class DecisionTreePage : PageData
{
[AllowedTypes(AllowedTypes = new[] { typeof(QuestionPage) })]
[SelectOne(SelectionFactoryType = typeof(QuestionFactory))]
public virtual PageReference StartingPoint { get; set; }
}
Public class QuestionPage: PageData
{
public virtual string Label { get; set; }
[AllowedTypes(AllowedTypes = new[] { typeof(AnswerBlock) })]
public virtual ContentArea AnswersContainer { get; set; }
}
Public class AnswerBlock: BlockData
{
[AllowedTypes(AllowedTypes = new[] { typeof(QuestionPage) })]
public virtual PageReference NextQuestion { get; set; }
}</code></pre>
<p>If you are paying attention, you will notice a selection factory decorating the starting point property. In order to avoid confusion regarding forms, questions and answers, we took the initiative to establish the following convention:</p>
<p>It is only possible to pick questions that are child nodes of the decision tree page. The code below shows a way to do it:</p>
<pre class="language-csharp"><code>public class QuestionFactory : ISelectionFactory
{
private Injected<IContentLoader> _contentLoader;
public IEnumerable<ISelectItem> GetSelections(ExtendedMetadata metadata)
{
var currentPage = metadata.FindOwnerContent();
if (currentPage == null)
return Enumerable.Empty<ISelectItem>();
if (!(currentPage is QuestionPage))
return Enumerable.Empty<ISelectItem>();
//triggered before the page is created - need to check contentlink as well
if (ContentReference.IsNullOrEmpty(currentPage.ContentLink))
return Enumerable.Empty<ISelectItem>();
var children = _contentLoader.Service?.GetChildren<QuestionPage>(currentPage.ContentLink);
return children.Select(x => new SelectItem
{
Text = x.Name,
Value = x.ContentLink
});
}
}</code></pre>
<p>And with this code, we make sure that <strong>only </strong>the child nodes will be selectable as the starting point.</p>
<p>Finally, we need some code to “build” our tree. It can be done using recursive functions available in the pseudo-code below:</p>
<pre class="language-csharp"><code>public DecisionTreeViewModel GetTree(DecisionTreePage page)
{
if (ContentReference.IsNullOrEmpty(page.StartingPoint))
return null;
var startingPoint = _contentLoader.Get<QuestionPage>(page.StartingPoint);
var tree = GetRecursiveItems(startingPoint);
}
Public DecisionTreeViewModel GetRecursiveItems(QuestionPage question)
{
If(question == null)
return null;
var toReturn = new DecisionTreeViewModel
{
questionLabel = question.Label;
Answers = question.Answers.Select( x =>
{
var nextQuestionPage = ContentReference.IsNullOrEmpty(x.NextQuestion) ?
null :
_contentLoader.Get<QuestionPage>(x.NextQuestion);
return new {
nextQuestion = nextQuestionPage == null ? null : GetRecursiveItems(nextQuestionPage);
}
}
}
return toReturn;
}</code></pre>
<p>And this is it! Once your tree view model is loaded, it is possible to move through the decision tree in a web page using the data that we collected before. It’s possible to use a SPA framework like Angular, React or Vue or even vanilla javascript to offer a very smooth user journey without reloading the page.</p>
<p>I thought of setting a plugin for decision trees for the content section of EPiServer, maybe on top of EPiServer Forms? if you are interested to join, please let me know in the comments 😊</p>The ultimate set of extensions to navigate EPiServer content trees/blogs/giuliano-dore/dates/2021/3/the-ultimate-set-of-extensions-to-navigate-episerver-content-trees/2021-03-13T14:16:09.0000000Z<p>As I have been working with CMS for a while, I always found content tree navigation to be one of the most important aspects of development for professional projects.</p>
<p>I have been writing about functions to do content tree navigations using EPiServer core functions <a href="/link/659e47f141bf4ba2b5360dc991529402.aspx">here</a> and <a href="/link/ff0ee884fa5e46c2981d79580e81681e.aspx">here</a> and with my team we finally decided to open-source and package some of our most popular functions for database-free content search using <strong>IContentLoader</strong> with our new (beta) package:</p>
<p><span style="color: #2dc26b;"><strong><a href="https://www.nuget.org/packages/Dotcentric.Extensions/">Dotcentric.Extensions</a></strong></span></p>
<p>The current library is focused on extending IContentLoader to offer a range of helper functions for IContentData and IContent objects:</p>
<ul>
<li>FirstChild - retrieve the first child of a content item - possibly filtered based on an optional predicate.</li>
<li>FirstSibling - retrieve the first sibling of a content item - possibly filtered based on an optional predicate.</li>
<li>FollowingSibling - retrieve the following sibling of a content item based on a sorting parameter like the name or sort index, etc.</li>
<li>FollowingSiblings - retrieve the following siblings of a content item based on a sorting parameter. Examples are above.</li>
<li>GetAncestor - retrieve the first ancestor of a content item matching some criteria.</li>
<li>GetAncestorOrSelf - retrieve the first ancestor or the current content item based on some criteria.</li>
<li>GetAncestors - retrieve a list of ancestors - possibly filtered by an optional predicate.</li>
<li>GetDescendent - retrieve the first descendent of the current content item based on some criteria.</li>
<li>GetDescendentOrSelf - retrieve the first descendent or the current content item based on some criteria.</li>
<li>GetDescendents - retrieve the list of descendents from the current content item, possibly filtered by an optional predicate.</li>
<li>LastChild - retrieve the last child of a content item - possibly filtered based on an optional predicate.</li>
<li>OrderedSiblingsAndSelf - retrieve the ordered list of siblings + the current content item based on a sorting parameter.</li>
<li>PreviousSibling - retrieve the previous sibling of a content item based on a sorting parameter.</li>
<li>PreviousSiblings - retrieve the list of previous siblings of a content item based on a sorting parameter.</li>
<li>Siblings - retrieve the list of siblings of a content item</li>
<li>SiblingsAndSelf - retrieve the list of siblings of a content item + the item itself.</li>
</ul>
<p>Some notes about our micro-library:</p>
<p>As we are working with content tree items and concepts like parents, ancestors, children and descendents, most of the functions include a generic type constraint where the content item must inherit from <strong>IContent</strong>. The reason being that this is the highest abstraction available that contains a <strong>ParentLink</strong> property, mandatory for us to do bottom up navigation for functions like <strong>GetAncestor</strong>.</p>
<p>The project is available on github: <a href="https://github.com/dotcentric/Dotcentric.EPiServer.Extensions">https://github.com/dotcentric/Dotcentric.EPiServer.Extensions</a> - I added an Alloy website project that I extended to offer a TestsPage where I tried some scenarios like:</p>
<ul>
<li><span class="pl-c">find all the ancestors of the current page with more than 2 children</span></li>
<li><span class="pl-c">find all the "standard page" ancestors of the 'Find a reseller' page</span></li>
<li><span class="pl-c">find the next sibling by name of the page named 'alloy plan'</span></li>
</ul>
<p><span class="pl-c">The "tests" are available here: <a href="https://github.com/dotcentric/Dotcentric.EPiServer.Extensions/blob/main/Dotcentric.Extensions/Dotcentric.Extensions.Website/Controllers/TestsPageController.cs">https://github.com/dotcentric/Dotcentric.EPiServer.Extensions/blob/main/Dotcentric.Extensions/Dotcentric.Extensions.Website/Controllers/TestsPageController.cs</a> - I didn't use unit-tests as it felt like a massive amount of work. </span></p>
<p>As we used the best abstraction we could find, we can use the helper functions for the CMS content tree but also for the EPiServer Commerce tree and enable the behaviors like these:</p>
<ul>
<li>I want to retrieve all the products of a specific type.</li>
<li>I want to retrieve the siblings of a category based on a specific name</li>
<li>I want to retrieve all the catalogs that have more than 2 categories.</li>
</ul>
<p>If you find a bug or a possible improvement feel free to send a PR, feedback is more than welcome 😁</p>
<p>Cheers,</p>
<p>Giuliano</p>How to migrate from uCommerce to EPiServer Commerce part 0: skus and tree structure/blogs/giuliano-dore/dates/2021/2/how-to-migrate-from-ucommerce-to-episerver-commerce-part-0-skus-and-tree-structure/2021-02-20T19:30:05.0000000Z<h2>Intro</h2>
<p>For one of our projects, we have the opportunity to migrate data from a uCommerce database / project into EPiServer Commerce. Migrating from one e-commerce platform to another is often a bumpy ride; the conventions are not the same, the data might not fit within the new data structures, and testing can take quite some time. I am hoping that this article will help developers who are facing the same challenges that we did.</p>
<p><strong>Disclaimer</strong>: the versions of the tools / frameworks discussed in this article are listed below:</p>
<ul>
<li>uCommerce for Sitecore 6.6.6.15141</li>
<li>Sitecore 8.2</li>
<li>EPiServer CMS 11.20</li>
<li>EPiServer Commerce 13.25</li>
</ul>
<p><strong>Disclaimer</strong>: the statements in this article are based on the version that we used for our migration; some definitions might have been updated in the latest versions of the e-commerce platforms listed above. Feel free to drop a comment if you are doing a migration and you want to share some nuggets of wisdom 😊</p>
<p>Now that we have the basics covered, let’s get started 😊.</p>
<h2>Products and variants skus: it’s different</h2>
<p>In uCommerce, products and variants are stored in the same table called Product. In this table, a “product” (as row) can have a <strong>sku</strong> and a <strong>variant sku</strong>. What this structure means is that <strong>products and their variants share the same sku</strong>.</p>
<p>The following structure is possible in UCommerce:</p>
<ul>
<li>Product 1 – sku: product_1
<ul>
<li>Product 1 variant 1 – sku: product_1</li>
<li>Product 1 variant 2 – sku: product_1</li>
</ul>
</li>
<li>Product 2 – sku: product_2</li>
</ul>
<p>The main issue with this approach is that the <strong>sku property in UCommerce is not a unique identifier</strong>. How do we differentiate all the products and variant then? By using the <strong>variant sku</strong> property:</p>
<ul>
<li>Product 1 – sku: product_1, variant sku: null (not a variant)
<ul>
<li>Product 1 variant 1 – sku: product_1, <strong>variant sku: var_1</strong></li>
<li>Product 1 variant 2 – sku : product_1, <strong>variant sku: var_2</strong></li>
</ul>
</li>
</ul>
<p>At this stage, we are thinking: that’s fine, I will use the variant sku as unique identifier. <strong>WRONG.</strong><strong></strong></p>
<p>The variant sku is not a unique identifier either.</p>
<p>First, it can be null, which is never a good sign for a unique identifier. Second, the variant sku can be shared between variants as long as they don’t depend on the same product:</p>
<ul>
<li>Product 1
<ul>
<li>Product 1 variant 1 – variant sku: <strong>var_1</strong></li>
</ul>
</li>
<li>Product 2
<ul>
<li>Product 2 variant 1 – variant sku: <strong>var_1</strong></li>
</ul>
</li>
</ul>
<p>People familiar with the EPiServer Commerce conventions already know that <strong>it’s not possible to share a sku between products and variants</strong>. It must be unique and non-nullable regardless of your parent product.</p>
<p>So, what’s a unique identifier in uCommerce? It’s <strong>the combination between a sku and their variant sku</strong>.</p>
<p><em>As a side note, this ‘a unique code is actually a combination of 2 properties’ is not really my cup of tea. It sounds like a very challenging technical design to maintain.</em></p>
<p>So, what’s the move regarding the migration? I went with the following approach in EPiServer:</p>
<ol>
<li>We must extend products and variants using meta classes to allow 2 properties: <strong>legacy sku</strong> and <strong>legacy variant sku</strong> that will be mapped with the properties in that we discussed previously.</li>
<li>When importing, we let EPiServer generate the ‘code’ property to avoid issues with duplicates.</li>
<li>The content / commerce team can then look at the code property inside the EPiServer Commerce backoffice and update it accordingly.</li>
</ol>
<p>While we are running the script, we might be thinking ‘why are we even importing those properties?’ the reason is very simple:</p>
<ol>
<li>During migrations, it’s a good practice to import as much data as you can, you can always decide to delete unnecessary data after successfully migrating your content.</li>
<li>Tables / objects like orders in uCommerce are using the sku and variant sku as identifier for the content of the order items. <strong>Without those properties, it will be impossible to import order history</strong>.</li>
</ol>
<p>Finally, it’s important to notice that products and variants in EPiServer offer a slightly different approach to data definition. EPiServer Commerce allows the ability to create distinct definitions for products and variants as meta classes. As the definitions are separated, it’s possible to have different properties between products and variants.</p>
<p>As an example:</p>
<ul>
<li>A ‘CleaningProduct’ will have the following properties
<ul>
<li>Property1</li>
<li>Property2</li>
</ul>
</li>
</ul>
<p>And it will allow the following variant types:</p>
<ul>
<li>‘CleaningVariantXY’ and will have the following properties:
<ul>
<li>Property3</li>
<li>Property4</li>
</ul>
</li>
<li>‘CleaningVariantRT’ will have the following properties:
<ul>
<li>Property5</li>
<li>Property6</li>
</ul>
</li>
</ul>
<p>In this example, the definition for CleaningVariantXY doesn’t have a definition for Property1 and Property2 by default.</p>
<p>In uCommerce, there’s no separate definition available for variants; variants are extensions of products; they share the same definitions. If we want to “flag” a property as available at the variant level, we must define the property as ‘variant property’. Once this is done, we can create a variant for the product we selected and the property will be available and editable.</p>
<h2>The catalogue tree structure</h2>
<p>uCommerce has the following tree structure:</p>
<ul>
<li>Stores (Called <strong>Product Catalog Groups</strong> in database)
<ul>
<li>Catalogues
<ul>
<li>Categories
<ul>
<li>Products</li>
<li>Categories
<ul>
<li>Products</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>EPiServer has the following tree structure:</p>
<ul>
<li>Catalogues
<ul>
<li>Categories
<ul>
<li>Products</li>
<li>Categories
<ul>
<li>Products</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>We also know the following:</p>
<ul>
<li>Every item template (stores, catalogues, categories, products) in the uCommerce tree can have <strong>a custom definition </strong>where we can set custom properties for the template to fit our needs. In EPiServer Commerce we can add definitions by using MetaClasses to extend the list of properties that a commerce object can contain.</li>
<li>EPiServer Catalogues (<a href="/link/735d13cd61d945fea9af717653d07737.aspx">CatalogContent</a>) do not inherit from IMetaData; I couldn’t find a way online to use <a href="/link/c0ff35df86cf4042a8dbc20dc00ae6ce.aspx">MetaClasses</a> with catalogues so I assumed it was not possible. Luckily for me, the stores that I was pulling from the database didn’t contain any custom properties.</li>
<li>It is possible to have as many layers of categories as needed. Categories can have sub-categories and sub-sub-categories without any limit to the number of levels we need.</li>
</ul>
<p>If there is information inside your uCommerce stores objects that you need to keep, the following options are available:</p>
<ul>
<li>Use a separate persistent entity to match the properties that are needed like a DataStore or custom tables with a direct relation to the CatalogContent</li>
<li>Move the properties one level down as categories in EPiServer can use MetaClasses to extend the list of properties.</li>
</ul>
<p>I believe the goal of a migration is to migrate data while offering a smooth transition; it involves keeping as much data as possible in a familiar structure. My goal here was not to challenge the previous data structure but making sure all the data was successfully migrated. It is absolutely possible to challenge a data structure but I would recommend to migrate first and then offer to refactor once the migration is complete. </p>
<p>Once we have an understanding of our legacy commerce tree and we have the definitions for the new tree, we can start to programmatically add items in our commerce tree. In my scenario, this is the before/after:</p>
<p><strong>Before</strong></p>
<ul>
<li>Store1
<ul>
<li>Catalog1
<ul>
<li>Category1
<ul>
<li>Product1</li>
<li>Product2</li>
</ul>
</li>
<li>Category2
<ul>
<li>Product3</li>
<li>Product4</li>
</ul>
</li>
<li>Catalog2
<ul>
<li>Category3
<ul>
<li>Product5</li>
</ul>
</li>
<li>Store2
<ul>
<li>Catalog3
<ul>
<li>Category4
<ul>
<li>Product6</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p> </p>
<p><strong>After</strong></p>
<ul>
<li>Catalog1 (previously identified as Store1)
<ul>
<li>Category1 (previously identified as Catalog1)
<ul>
<li>SubCategory1 (previously identified as Category1)
<ul>
<li>Product1</li>
<li>Product2</li>
</ul>
</li>
<li>SubCategory2 (previously identified as Category2)
<ul>
<li>Product3</li>
<li>Product4</li>
</ul>
</li>
<li>Category2 (previously identified as Catalog2)
<ul>
<li>SubCategory3 (previously identified as Category3)
<ul>
<li>Product5</li>
</ul>
</li>
<li>Catalog2 (previously identified as Store2)
<ul>
<li>Category3 (previously identified as Catalog3)
<ul>
<li>SubCategory4 (previously identified as Category4)
<ul>
<li>Product6</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>A note about migration strategies</h2>
<p>Migration scripts can be tricky depending on the volume of data to transfer. For some lucky scenarios, you might be able to have a single script that imports everything in one go, happy days.</p>
<p>Unfortunately, most enterprise databases are pretty heavy and sometimes your only option will be <strong>to split your migration into multiple scripts</strong>. </p>
<p> We could have the following scenarios:</p>
<ul>
<li>A script for catalogs and categories, a script for products, a script for product “custom” properties or</li>
<li>A script for catalogs, a script for categories, a script for products and their properties or</li>
<li>A script for every single list of data to transfer, catalogs, categories, sub categories, products, product properties, category properties, etc.</li>
</ul>
<p>If you have to run multiple scripts one after another – for example adding products to categories that we just created – your best bet is to save the previous database items ids inside your new EPiServer Commerce definitions.</p>
<p>Examples: </p>
<ul>
<li>Are you looking for the parent category of your product? Define a ‘<strong>legacy category id</strong>’ property in your category meta class so you can find refer to that property when saving a product.</li>
<li>Are you looking to update a product with properties from the legacy database? A ‘<strong>Legacy product id</strong>’ is your best bet to find the product.</li>
</ul>
<p>That’s it for today 😊</p>
<p>For the next articles, I will be looking to write about the functions to use to save data inside the Commerce database, pricing and how to export data from a database at a code level.</p>
<p>I hope it was helpful. If you are familiar with migrations please reach out, I would love to hear your feedback about this article.</p>
<p>Many thanks to the EPiServer Community and especially <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=741a7e8c-2586-e711-810b-70106faab5f1">Praful</a>, <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=9b7dad33-b57b-e011-a4b8-0018717a8c82">Quan</a> and <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=ded905b5-f4eb-e911-a812-000d3a228515">Sanjay</a> for their help.</p>How to setup a custom login redirection in EPiServer for a multi-tenants project/blogs/giuliano-dore/dates/2021/1/how-to-setup-a-custom-login-redirection-in-episerver/2021-01-27T10:16:16.0000000Z<p>Our journey of discovery inside EPiServer continues with a requirement coming from one of our clients regarding one of the projects we are managing. The project we are talking about has the following setup:</p>
<ul>
<li>Multi-sites solution with
<ul>
<li>Multiple home pages per sites and</li>
<li>Multiple frontend roles</li>
</ul>
</li>
</ul>
<p>For one of the websites that we are creating, we must create a "portal" for users to log in / register and check information about their profile like name, billing address, etc. As we have multiple "home pages" we must make sure that we redirect the user to the right login page, then to the right portal on the right website.</p>
<p>The content tree is expected to look like this:</p>
<ul>
<li>Site1
<ul>
<li>HomePage1
<ul>
<li>LoginPage1</li>
<li>MyPortal1 (restricted)
<ul>
<li>AccountPage1 (restricted)</li>
<li>OtherPage1 (restricted)</li>
</ul>
</li>
</ul>
</li>
<li>HomePage2
<ul>
<li>LoginPage2</li>
<li>MyPortal2 (restricted)
<ul>
<li>AccountPages2 (restricted)</li>
<li>OtherPage2 (restricted)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Site2
<ul>
<li>Random pages</li>
</ul>
</li>
</ul>
<p>In a nutshell, users accessing <strong>MyPortal1 </strong>must be redirected to <strong>LoginPage1 </strong>while users accessing <strong>MyPortal2</strong> must be redirected to <strong>LoginPage2. </strong>For the sake of this example, we can assume that the portals share the same page type. We cannot allow users to be redirected to the standard EPiServer login page.</p>
<p>As the portals share the same page type, they also share the same <strong>PageController </strong>so we started working on a function inside the <strong>Index </strong>function for when we render the page.</p>
<pre class="language-csharp"><code>//the code has been simplified for clarity purposes
public ActionResult Index(MyAccountPage currentPage)
{
//test if authenticated
if (User.Identity.IsAuthenticated && User.IsInRole("authenticatedMember"))
return View(currentPage);
else
return Redirect(loginPage);
}</code></pre>
<p>But we quickly realized: </p>
<ol>
<li>There was no code reusability here</li>
<li>Other controller actions were not restricted so it was technically possible to trigger other actions (like a form submit inside the MyAccountPage) while being anonymous 😱</li>
<li>We would need to do that for every page type that needs to be restricted 😱 😱</li>
</ol>
<p>So we started looking at a cleaner approach. To solve the point 2. we could override the <strong>OnAuthorization </strong>function but that was not solving the first and third point.</p>
<p>Coming from asp.net MVC, I knew that there was a possibility to inherit from <strong>AuthorizeAttribute </strong>and add the redirection logic in the newly created class. We were solving point 1 and 3 ! 😀</p>
<pre class="language-csharp"><code>//the code has been simplified for clarity purpose
public class MyAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
//authentication is fine
if (filterContext.Result == null)
return;
//edit mode we do not need to redirect
if (PageEditing.PageIsInEditMode)
return;
//if authentication didn't work
if (filterContext.Result.GetType() == typeof(HttpUnauthorizedResult))
{
filterContext.Result = new RedirectResult(loginPage);
}
}
}
//And for each controller we just need to add one line/attribute to describe the controller class so that every action needs to be coming from an authenticated //user
[MyAuthorize(Roles = "authenticatedMember")]
public class MyAccountPageController : PageController<MyAccountPage>
{
public ActionResult Index(KarndeanAccountPage currentPage)
{
return View(currentPage);
}
}</code></pre>
<p><strong>However</strong>, even though the code appeared to be working well, we quickly realised 2 issues with it:</p>
<ol>
<li>I didn't check if there was a built-in feature / API in EPiServer, I went straight with the ASP.NET MVC solution</li>
<li>My code was "breaking" <strong>Access Rights</strong></li>
</ol>
<p>By using access rights in Episerver, you can control the content that a website visitor sees, as well as what editors can do and where they can do it in the content structure. More info available <a href="https://webhelp.episerver.com/17-2/cms-admin/access-rights.htm">here</a>.</p>
<p>The issue with my code is that it was ignoring / overriding Access Rights permissions. <strong>We needed to override the default redirection behavior while maintaining Acess Rights integrity</strong>.</p>
<p>The first step was to restrict the <strong>MyPortal1 </strong>page and their descendants to disallow anonymous members. It is possible by unchecking the 'Read' option for the 'Everyone' group for the page we wanted to restrict & their descendants and allow this page for the relevant group:</p>
<figure class="image"><img src="/link/537c22b7a8f0427c8f63f131279e75e1.aspx" width="503" alt="Access Rights Tree" height="415" style="display: block; margin-left: auto; margin-right: auto;" />
<figcaption></figcaption>
</figure>
<p><img src="/link/29220012ff7b4a39aefa0beacb00f3c2.aspx" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p>Once this operation was successful, trying to access the page as anonymous user would redirect us to the default EPiServer login page:</p>
<p><img src="/link/b0f6d02da4e940d2aea74087610a4703.aspx" width="582" alt="" height="506" style="display: block; margin-left: auto; margin-right: auto;" /></p>
<p>While we were successfully redirecting unauthorized users, we still needed to redirect to the "right" login page. This is where the <strong>AuthorizeContentAttribute </strong>comes handy.</p>
<p>This class allows the following behaviour: <br /><em>When added to an MVC controller this action filter checks authorization for a previously routed node available through the request context.</em></p>
<p>What it means: it will check access rights for the requested node while allowing an option for custom behaviour, with a native IContentLoader property, this is just what we need ! 🤩 </p>
<pre class="language-cpp"><code>//the code has been simplified for clarity purpose
public sealed class MyAuthorizeContentAttribute : AuthorizeContentAttribute, IAuthorizationFilter
{
public new void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
if (filterContext == null)
return;
//authentication is fine
if (filterContext.Result == null)
return;
//edit mode we do not need to redirect
if (PageEditing.PageIsInEditMode)
return;
//if authentication didn't work
if (filterContext.Result.GetType() == typeof(HttpUnauthorizedResult))
{
//we find the login page and setup the redirection
filterContext.Result = new RedirectResult(loginPage);
}
}
}
//and for the controller:
[MyAuthorizeContent(RequiredLevel = EPiServer.Security.AccessLevel.Read)]
public class MyAccountPageController : PageController<MyAccountPage>
{
public ActionResult Index(MyAccountPage currentPage)
{
return View(currentPage);
}
}
</code></pre>
<p>And we now redirect to our custom login page every time an anonymous user tries to access the portal page ! WOOHOO ! 🥳</p>
<p>The last part of this article is about selecting the right login page. Based on our requirements we must navigate the content tree to find the right login page based on the page we are trying to access.</p>
<p>There are 2 schools of thought regarding content tree navigation:</p>
<ul>
<li>Bottom up Navigation</li>
<li>Top down Navigation</li>
</ul>
<p>I was a big believer in top down navigation due to the simplicity of the code involved. With a start from the top & a few .FirstOrDefault() based on page type and the allowed children, it is possible to find any node in a few lines of code. Adding some validations on top of that approach to allow 'singleton' restrictions for specific nodes (like login pages, portal pages, etc.) and we could wrap it up. However this approach doesn't go that well in the following scenarios: </p>
<ul>
<li>With multi-sites projects or websites with multiple entry points (like multiple home pages).</li>
<li>The 'singleton' validation could be ignored by programmatically adding pages to the tree</li>
</ul>
<p>For those reasons I decided to use bottom up navigation more often. The approach I follow is:</p>
<ol>
<li>Allow a top node to define the 'default' page reference that we want to access, it can be the default login page, default search page or any other page that has value as singleton page.</li>
<li>From that node, navigate all the way to that top node to find the default page reference that we need.</li>
</ol>
<p>Based on our current content tree, the best place to set a 'default login page' property would be at the home page level. From there, the only remaining bit was to update our AuthorizeContentAttribute:</p>
<pre class="language-csharp"><code>public sealed class MyAuthorizeContentAttribute : AuthorizeContentAttribute, IAuthorizationFilter
{
private Injected<IPageRouteHelper> _pageRouteHelperProxy;
private Injected<IUrlResolver> _urlResolverProxy;
public new void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
if (filterContext == null)
return;
//authentication is fine
if (filterContext.Result == null)
return;
//edit mode we do not need to redirect
if (PageEditing.PageIsInEditMode)
return;
//if authentication didn't work
if (filterContext.Result.GetType() == typeof(HttpUnauthorizedResult))
{
var currentPage = _pageRouteHelperProxy.Service.Page;
var loginPageReference = this.ContentLoader.GetAncestorOrSelf<HomePage>(currentPage).DefaultLoginPage;
if (ContentReference.IsNullOrEmpty(loginPageReference))
filterContext.Result = new RedirectResult("/");
var loginUrl = _urlResolverProxy.Service.GetUrl(loginPageReference);
var currentPageUrl = _urlResolverProxy.Service.GetUrl(currentPage.ContentLink);
var redirectUrl = $"{loginUrl}?returnUrl={currentPageUrl}";
filterContext.Result = new RedirectResult(redirectUrl);
}
}
}</code></pre>
<p>With that code, we can dynamically redirect from any restricted page to the corresponding login page in complex scenarios where restricted pages might be associated with specific login pages while keeping <strong>Access Rights</strong> integrity.</p>
<p><strong>IPageRouteHelper </strong>is a great helper to quickly find the current page regardless of the context. As we are using <a href="/link/ebeb7390a6de4c16a2530221b226e292.aspx">Dependency Injection</a> in our project, it is recommended to use a constructor parameter to inject a dependency. As it was possible when inheriting from AuthorizeContentAttribute we had to settle for the property injection using Injected<>.</p>
<p>Do you use another method to redirect to a custom login page ? I would love to hear about it in the comments ! 😊<br /><br />Many thanks to Jon S. and Al for the feedback regarding my article and code. </p>Navigate the content tree like a pro with the GetDescendentOrSelf function/blogs/giuliano-dore/dates/2020/11/navigate-the-content-tree-like-a-pro-with-the-getdescendantorself-function/2020-11-22T16:51:37.0000000Z<p>In the '<a href="/link/659e47f141bf4ba2b5360dc991529402.aspx">Navigate the content tree like a pro with the GetAncestorOrSelf function</a>' article, we started discussing about how important it is to be able to navigate a content tree and the importance of performance.</p>
<p>With the new set of functions, we can now navigate all the way to the top, but what if we need to navigate all the way to <strong>the bottom of the tree</strong> to retrieve a specific page ?</p>
<p>As you know now, the native IContentLoader.GetDescendents function doesn't use the cache and calls the database every time we call the function, can we imagine adding a function like this one on a page where we get 10.000 users hourly ? 😱</p>
<p><img src="/link/92fb6d99a27647c79a8e41ab057eed30.aspx" /></p>
<p><em><span style="color: #808080;">IContentLoader helper table</span></em></p>
<p>That's why I came out with my own cached implementation of GetDescendents. It's a very simple tree flattening algorith: we set a 'pivot' as being a list of items. The first version being a list of one item (the starting point) and we start collecting all the children of those items with the <strong>GetChildren</strong> function from <strong>contentLoader</strong> as this function is using the cache.</p>
<p>Every time we collect the children, we move to the level below, and we can set the pivot to the new list that we retrieved. We can stop going through the tree once there's no more child nodes.</p>
<pre class="language-csharp"><code> public static IEnumerable<IContent> GetDescendents(this IContentLoader contentLoader,
IContent content,
int maxLevel = defaultMaxLevel)
{
var toReturn = new List<IContent>();
IEnumerable<IContent> pivot = new List<IContent>
{
content
};
for(var i = 0; i < maxLevel; i++)
{
var children = pivot.SelectMany(x => contentLoader.GetChildren<IContent>(x.ContentLink));
if (!children.Any())
break;
toReturn.AddRange(children);
pivot = children;
}
return toReturn;
}</code></pre>
<p>And that's it, folks.</p>
<p>If I am being honest, the heavy lifting was done thanks to the <a href="https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.selectmany?view=net-5.0">SelectMany</a> function, especially useful for flattening lists like the ones we are receiving at each level.</p>
<p>But now I realise I missed something. If we want to <strong>find a specific descendent</strong> <strong>item</strong>, we might not want to load all the descendents. we might want to <strong>filter before</strong> we load the next set of descendents.</p>
<p>So what do we need ? Like the <strong>GetAncestorOrSelf </strong>function, we need a predicate, we need to filter before we "swap" our pivot and we need to return a single object instead of a list:</p>
<pre class="language-csharp"><code>public static IContent GetDescendentOrSelf(this IContentLoader contentLoader,
IContent content,
Func<IContent, bool> predicate,
int maxLevel = defaultMaxLevel)
{
if (predicate.Invoke(content))
return content;
IEnumerable<IContent> pivot = new List<IContent>
{
content
};
for (var i = 0; i < maxLevel; i++)
{
var children = pivot.SelectMany(x => contentLoader.GetChildren<IContent>(x.ContentLink));
if (!children.Any())
break;
var res = children.FirstOrDefault(predicate);
//we can return the object if the value isn't the default one nor null
if(EqualityComparer<T>.Default.Equals(res, default(T))) {
return res;
}
pivot = children;
}
//we couldnt find a result so we just return the default value for the return
return default;</code></pre>
<p>Some notes regarding this function:</p>
<ul>
<li>As we want to test descendents <strong>and</strong> the current object, I run the predicate before getting inside the loop.</li>
<li>We can combine FirstOrDefault with our predicate for a very efficient expression.</li>
<li>It's possible for struct to inherit from interfaces. <a href="https://stackoverflow.com/questions/63671/is-it-safe-for-structs-to-implement-interfaces">It is not recommended</a>, but it's possible. Because of that scenario & because we are building a function that should cover all scenarios, it's better to return default instead of null.</li>
</ul>
<p>I hope this was helpful and I am looking forward to your comments 😊</p>How to create a Robots.txt handler for a multi-site episerver project/blogs/giuliano-dore/dates/2020/10/how-to-create-a-simple-robots-txt-handler-for-a-multi-site-episerver-project/2020-10-17T16:57:46.0000000Z<p>With my team we had the opportunity to start working on a new multi-site project using the EPiServer 11.20.0 & DXP and it's been a very exciting journey so far. We are planning to release 4 websites over the next months, each website with different templates, behaviors and routes.</p>
<p>While we are busy defining the different templates that each website will use, we also noticed that some components could be created once and be available for each website without a need to "override" their default behavior. I am talking about the following components:</p>
<ul>
<li>Robots.txt file</li>
<li>Sitemap (xml) file</li>
</ul>
<p>While the sitemap component is also worth an article on its own, today I want to explore the idea of a single entrypoint per website to generate a robots.txt in EPiServer.</p>
<p>It might sound controversial, but I do love being a lazy developer. I do love working with such developers. They don't write that much code. They might spent the time to look for plugins or examples online that have been battle-tested but in the end it's a methodology that is brings <strong>dividends over time</strong>.</p>
<p>So I started looking for plugins online. But we need to have rules. Without rules there's chaos.</p>
<p><img src="https://media4.giphy.com/media/iB4PoTVka0Xnul7UaC/giphy.gif" alt="" /></p>
<p>So here's my set of rules:</p>
<ul>
<li>The plugin / package must not be a beta.</li>
<li>The plugin / package must be up to date.</li>
<li>The plugin can be open-source but it has to come from a trusted source and must be maintained properly.<br />There's a possibility to add it from a package manager or to copy the parts that we need.</li>
<li>If the plugin is not open-source then there must be a contract involving maintenance. <br />It's often better to pay a license to make sure there's support instead of adding some dlls with little transparency. We do not like ticking bombs.</li>
<li>For this scenario, the plugin / package must be working in a <strong>multi-site environment</strong>.</li>
</ul>
<p>Unfortunately, I couldn't find what I was looking for 😭 so I began to think of a way to put the pieces together. </p>
<p>Before starting our coding journey, let's list our requirements:</p>
<ul>
<li>We want a unique "/routes.txt" endpoint available for each website we are hosting and get a different result based on the website we are visiting.</li>
<li>We want to be able to edit the content of our robots.txt file inside the CMS - and in <strong>one place only </strong>(per website).</li>
<li>We want to write as little code as possible.</li>
</ul>
<p>First step was to allow Mvc attribute routes in our EPiServer project:</p>
<pre class="language-csharp"><code> [InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class AttributeRoutingInitializationModule : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
var routes = RouteTable.Routes;
routes.MapMvcAttributeRoutes();
}
public void Uninitialize(InitializationEngine context)
{
}
}</code></pre>
<p>This code will allow us to bind an MVC action to a specific route. In our scenario, I want the "/robots.txt" route to become an endpoint for my project.</p>
<p>Second step was to setup an interface that will contain the content of our robots.txt file:</p>
<pre class="language-csharp"><code>public interface ISitePage
{
string RobotsTxtContent { get; set; }
}</code></pre>
<pre class="language-csharp"><code>public class MyFreshWebsiteSitePage : PageData, ISitePage
{
[UIHint(UIHint.Textarea)]
public virtual string RobotsTxtContent { get; set; }
}</code></pre>
<p>Each of my websites will have a site page that will be the first ancestor for all the content that I want. It looks like the right place to store a property that is <strong>unique for each website</strong>. I also want to edit this property inside a <strong>textarea</strong>:<br />We are almost there. Final step is to setup our MVC endpoint:</p>
<pre class="language-csharp"><code>//after calling UrlResolver and IContentLoader in the controller constructor using Dependency injection
[Route("robots.txt")]
[HttpGet]
public ActionResult RenderRobotsTxt()
{
var site = _contentLoader.Get<ISitePage>(ContentReference.StartPage);
if (site == null)
return new HttpNotFoundResult();
var robotsContent = site.RobotsTxtContent;
if (string.IsNullOrWhiteSpace(robotsContent))
return new HttpNotFoundResult();
return Content(robotsContent, "text/plain", Encoding.UTF8);
}</code></pre>
<p>Before we wrap up and go home, let's analyse the code we have here. For the sake of my example I allowed some assumptions in order to find the page containing the Robots.txt content:</p>
<ul>
<li>All the site pages are at the root level. It's easy to find the site using the <span style="text-decoration: line-through;">GetRoot extension method</span> <em>ContentReference.StartPage </em>as we know that the start page is the site page that contains the property we are looking for.</li>
</ul>
<p>It is recommended to allow only a specific group of people to edit this property as it is a very sensitive one. We do not want Google to decide that one of our website should no longer be indexed. I would also recommend to add a "robots.txt" validator to make sure the syntax is the right one.</p>
<p><em><strong>Update</strong>: I replaced the GetRoot and Url.GetLeftPart by a call to the cache with ContentReference.StartPage as reference. Credits to <a href="/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userId=4b7f901b-8d52-e311-9350-0050568d002c">Stefan Holm Olsen</a>.</em><br /><br />From there we can locate the site page, find the content of our robots.txt and send it as a text ! Hurrayyyyyy 🥳🥳</p>
<p>Please feel free to provide feedback & comments in the comment section & don't forget to like the article if it was useful 🤩</p>
<p><em>Ps: While this solution will work for our project, we are currently considering the idea of having a more generic solution inside a NuGet package where we would "resolve" the path to the Robots.txt content property inside the CMS. </em></p>
<p><em>It could be some configuration in the code, a [RobotsTxtContent] attribute to decorate the property, etc. If you have a clever implementation in mind we would love to hear more about it in the comment section 😊</em></p>Navigate the content tree like a pro with the GetAncestorOrSelf function/blogs/giuliano-dore/dates/2020/9/navigate-the-content-tree-like-a-pro-with-the-getancestororself-function/2020-09-27T16:33:02.0000000Z<p>Navigating a content tree inside a CMS can be quite a challenge depending on performance and the functions that are available. I have been navigating trees for different CMS for a while and I noticed that some functions tends to come back often.</p>
<p>Let's say we have a multisite project. Any page can belong to any of the websites that are listed inside your EPiServer project. What do you do when you want to programatically know which website your page belongs to ? How do you generate a page that lists all the pages that are visible for the visitor without adding a direct reference to the "root" website page ?</p>
<p>You have to navigate the tree.</p>
<p>So I have been reading more about navigation inside EPiServer 11 and the functions available to navigate the content tree.</p>
<p>Based on the documentation I've seen, there are functions available inside <strong>IContentLoader</strong> that can be extremely useful to retrieve components and navigate inside the content tree:</p>
<ul>
<li><strong>Get</strong><T> - to retrieve any (unique) object based on a reference - preferred for performance as this function uses the <strong>cache</strong> and <strong>doesn't call the database</strong>.</li>
<li><strong>GetChildren</strong><T> - used to retrieve children of a node in the content tree - another function using the <strong>cache</strong>, recommended by EPiServer for performance.</li>
<li><strong>TryGet</strong><T> - same as Get<T> but returning a boolean, useful for scenarios when you are not sure if the casting is going to be successful.</li>
<li><strong>GetDescendents - </strong>Provide a list of all the "descendents" of a tree node all the way until we reach pages without children nodes. This function is <strong>not cached</strong> and hit the database every time you call it. As we want to navigate all the way <strong>up</strong>, this function is not the one we need at the moment. </li>
<li><strong>GetAncestors - </strong>Provide a list of all the "ancestors" of a tree node all the way until the "root" item. Wonderful ! Unfortunately this function doesn't use the cache and <strong>hit the database</strong> every time you call it noooooooooo 😭😭 - we were really close.</li>
</ul>
<p><img src="/link/92fb6d99a27647c79a8e41ab057eed30.aspx" /></p>
<p><span style="color: #999999;"><em>Information based on the EPiServer training for version 11. For previous versions please see the documentation.</em> </span></p>
<p>So what do we want ? We want a function that helps us <strong>navigate the tree all the way up</strong>, if possible with an option to filter to retrieve the element that we want (root website page, in that scenario) <strong>with only cache functions</strong>.</p>
<p>For a little bit more clarity, this is what I mean by "root website page", my tree looks like that:</p>
<ul>
<li>Root
<ul>
<li>Website1 - that's a root website page</li>
<li>Website2 - that's another one</li>
<li>Website3
<ul>
<li>childPageOfWeb3
<ul>
<li>DescendentOfWeb3
<ul>
<li>Another descendent - I am currently here, I wanna get back to <strong>Website3</strong>, help me !🆘</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>So I came up with a function to navigate the tree: an extension method for <strong>GetAncestorOrSelf(predicate)</strong> for <strong>IContentLoader</strong>.</p>
<p>As we are only using IContentLoader functions and we want this function to be as generic as possible, we decided to extend the IContentLoader capabilities with this helper function. We need a predicate because we wanna be able to provide a condition to filter the page that we are looking for.</p>
<p>How does it work ? From a specific node, we want to be able to loop all the way to the top while searching for the item that we are looking for.</p>
<p>The declaration will look like that:</p>
<pre class="language-csharp"><code> public static IContent GetAncestorOrSelf(this IContentLoader contentLoader,
IContent content,
Func<IContent, bool> predicate)</code></pre>
<p>Why Ancestor<strong>OrSelf</strong> ? I want to be able to locate the node I am searching for regardless of my position in the tree. I can be at the root or at the bottom, the condition will still be the same. </p>
<p>The rest is fairly straightforward, we loop all the way up and we run the condition to see if the condition works out:</p>
<pre class="language-csharp"><code>public static IContent GetAncestorOrSelf(this IContentLoader contentLoader,
IContent content,
Func<IContent, bool> predicate,
int maxLevel = defaultMaxLevel)
{
if (content == null)
return null;
//pivot will be the variable that will be used to navigate up the content tree
var contentPivot = content;
//I prefer 'for loops' but while loops are just fine.
for (var i = 0; i < maxLevel; i++)
{
if (predicate.Invoke(contentPivot))
return contentPivot;
var parent = ContentReference.IsNullOrEmpty(contentPivot.ParentLink) ?
null :
contentLoader.Get<PageData>(contentPivot.ParentLink);
//if the parent is null - end of navigation
if (parent == null)
return null;
//we are still inside the tree, we continue to loop
contentPivot = parent;
}
//unlikely to happen but there to avoid stackoverflow exception. we return null
return null;
}</code></pre>
<p>As you can see, I am running a loop that will move from the child to the parent, run the condition & continue to loop if I don't find anything.</p>
<p>At that point you surely have spotted the <strong>Get</strong><> function - mandatory cache function to retrieve information about the parent as ParentLink is only a <strong>reference </strong>and we need the full parent object.</p>
<p>Why am I using a for() loop instead of a while() ? I do not like the <strong>while</strong> keyword.</p>
<p>In short, those loops are a proper trap for developers to generate infinite loops, stackoverflow exceptions and other nightmares. Forgetting about a 'break' clause shouldn't be that fatal of a mistake. From a performance POV I want to be able to "control" how long my loop will run, this is why it is possible to add a <strong>maxLevel</strong> property to guarantee my loop will stop regardless.</p>
<p>Let's get back to my problem, I want to locate <strong>Website3 </strong>programmatically without decorating my object with interfaces or decorators. My condition will be:<br /><br /><em>The node I am looking for is the one of my ancestors and its parent is the EPiServer 'root' node - the root of all websites.</em></p>
<p>How does it translate in code ? Something like this:</p>
<pre class="language-csharp"><code> (content) =>
{
var parent = ContentReference.IsNullOrEmpty(content.ParentLink) ?
null :
contentLoader.Get<PageData>(content.ParentLink);
if (!(parent is PageData typedParentPivot))
return false;
return typedParentPivot.PageTypeName == "SysRoot";
});</code></pre>
<p>My condition is: <br /><br />I will load the parent node information using <strong>Get</strong><> and check that the parent PageTypeName <strong>= SysRoot</strong>, the default type name for the root node in EPiServer CMS.</p>
<p>And it works ! Hoorayyyy 🥳🥳</p>
<p><span style="color: #ff0000;"><strong>But we have to stop the party</strong></span>. There's an issue with that condition. I am calling Get<>(parentLink) twice for each loop 😱😱 so much for performance !</p>
<p>How do we solve it ? That's why we need a <strong>second version</strong> of <strong>GetAncestorOrSelf() </strong>with the parent inside the predicate:</p>
<pre class="language-csharp"><code>public static IContent GetAncestorOrSelf(this IContentLoader contentLoader,
IContent content,
Func<IContent, IContent, bool> predicate,
int maxLevel = defaultMaxLevel)
{
if (content == null)
return null;
//pivot will be the variable that will be used to navigate up the content tree
var contentPivot = content;
//please do not use 'while' loops
for (var i = 0; i < maxLevel; i++)
{
var parent = ContentReference.IsNullOrEmpty(contentPivot.ParentLink) ?
null :
contentLoader.Get<PageData>(contentPivot.ParentLink);
//we can allow the predicate to work even with a null parent
if (predicate.Invoke(contentPivot, parent))
return contentPivot;
//if the parent is null - end of navigation
if (parent == null)
return null;
//we are still inside the tree, we continue to loop
contentPivot = parent;
}
//unlikely to happen but there to avoid stackoverflow exception. we return null
return null;
}</code></pre>
<p>By including the parent inside the predicate, we don't need to call Get<> for the parent twice as it is already available !</p>
<p>We can finally finish with our helper function to retrive the node tha we were looking for:</p>
<pre class="language-csharp"><code> public static IContent GetRootSite(this IContentLoader contentLoader, IContent content)
{
//predicate here is: parent must be 'sysroot'
var rootSite = contentLoader.GetAncestorOrSelf(content,
(pivot, parentPivot) =>
{
if (!(parentPivot is PageData typedParentPivot))
return false;
return typedParentPivot.PageTypeName == "SysRoot";
});
return rootSite;
}</code></pre>
<p>And that's a wrap ! We have our helper function to retrieve the node we were looking for and in the process we got a helper function GetAncestorOrSelf with 2 method signatures. Happy days !</p>
<p>And because we are feeling generous, why not include a <strong>GetAncestor(predicate)</strong> function as well ? It's quite easy with <strong>GetAncestorOrSelf</strong>() :</p>
<pre class="language-csharp"><code>///for this function we move to the first parent then call GetAncestorOrSelf
public static IContent GetAncestor(this IContentLoader contentLoader,
IContent content,
Func<IContent, bool> predicate,
int maxLevel = defaultMaxLevel)
{
if (content == null)
return null;
var parent = ContentReference.IsNullOrEmpty(content.ParentLink) ?
null :
contentLoader.Get<PageData>(content.ParentLink);
//pivot will be the variable that will be used to navigate up the content tree
//we start with the parent
var contentPivot = parent;
return GetAncestorOrSelf(contentLoader, contentPivot, predicate, maxLevel);
}</code></pre>
<p>Now that we have those functions ready, the possibilities to filter are limitless, we could search for:</p>
<ul>
<li>The first ancestor / parent / current page with a specific page type</li>
<li>The first ancestor that was published after a specific date</li>
<li>The first ancestor that has 5 child nodes</li>
<li>The first ancestor with a specific interface</li>
</ul>
<p>The sky is the limit 😊 I hope this article was helpful to you. Stay safe </p>