<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: WiredViews</title>
    <description>The latest articles on Forem by WiredViews (@wiredviews).</description>
    <link>https://forem.com/wiredviews</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F779%2F9e5935b0-1bee-4832-bbb5-72ba2f3c38c4.png</url>
      <title>Forem: WiredViews</title>
      <link>https://forem.com/wiredviews</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/wiredviews"/>
    <language>en</language>
    <item>
      <title>Bits of Xperience: Displaying Rich Text After a Form Submission</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Tue, 06 Sep 2022 12:54:54 +0000</pubDate>
      <link>https://forem.com/wiredviews/bits-of-xperience-displaying-rich-text-after-a-form-submission-144j</link>
      <guid>https://forem.com/wiredviews/bits-of-xperience-displaying-rich-text-after-a-form-submission-144j</guid>
      <description>&lt;p&gt;Kentico Xperience's &lt;a href="https://docs.xperience.io/managing-website-content/forms/composing-forms" rel="noopener noreferrer"&gt;Form Builder&lt;/a&gt; enables marketing professionals to quickly build and manage forms in a no-code UI for use across their website 👩🏾‍💼.&lt;/p&gt;

&lt;p&gt;The experience for site visitors submitting a form can be customized for successful submissions 👍🏽.&lt;/p&gt;

&lt;p&gt;Let's look at what's possible and how a few lines of customized code can extend the functionality even more 😮!&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 What Will We Learn?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What happens after a Form is submitted&lt;/li&gt;
&lt;li&gt;How to customize the Form management UI&lt;/li&gt;
&lt;li&gt;How to customize Razor Class Libraries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ❓ What Happens After a Form is Submitted?
&lt;/h2&gt;

&lt;p&gt;Forms on a website are one of the most common ways for marketers to engage with visitors.&lt;/p&gt;

&lt;p&gt;Let's take the Dancing Goat demo site Contact Form as an example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffx5wm7t5o91e7qq632pg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffx5wm7t5o91e7qq632pg.png" alt="Filled out Contact form on a website"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a visitor fills out this Form and submits it, what exactly happens 🤔?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If the form is not valid (based on &lt;a href="https://docs.xperience.io/managing-website-content/forms/composing-forms#Composingforms-Addingfieldvalidation" rel="noopener noreferrer"&gt;form field validation&lt;/a&gt;) Xperience will re-render the form with validation error UI and messages&lt;/li&gt;
&lt;li&gt;If the form is valid, Xperience looks for an existing form entry for the submitter (if the visitor is &lt;a href="https://docs.xperience.io/managing-website-content/forms/using-smart-fields-in-forms#Usingsmartfieldsinforms-Contacttrackingrequirement" rel="noopener noreferrer"&gt;identified as an existing Contact&lt;/a&gt;) and updates it, or adds a new Form submission to the database.&lt;/li&gt;
&lt;li&gt;If the Form's Redirect Url is populated, redirect the page to a new URL (usually to display some "thank you" content).&lt;/li&gt;
&lt;li&gt;If Form's Display Text is populated, return Display Text and replace the Form with the Display Text.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Redirecting to a new page gives a marketer a lot of control over the visitor's experience 💪🏻, but it also requires maintaining an additional page on the site 😞.&lt;/p&gt;

&lt;p&gt;Using the Display Text is simpler and more convenient 🤗!&lt;/p&gt;

&lt;p&gt;Unfortunately, the Display Text field only accepts plain text. What if we want to respond to a form submission with a helpful message, links, and maybe a Call To Action with an accessible design 🤷🏼‍♂️?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqkivan8kfc51orqo9qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqkivan8kfc51orqo9qg.png" alt="Form metadata in Kentico Xperience administration interface"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It seems as though we'll need to customize what kind of content we can author here and how it's displayed on the site after a submission 🤓!&lt;/p&gt;

&lt;h2&gt;
  
  
  🗃 How to Customize the Form Management UI
&lt;/h2&gt;

&lt;p&gt;The first thing we need to look for is a way to change the Display Text field's Text input to a Rich Text input.&lt;/p&gt;

&lt;p&gt;This UI element can be found at &lt;code&gt;CMS\CMSModules\BizForms\Tools\BizForm_Edit_General.aspx&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t4vphca6nnmgrajitbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t4vphca6nnmgrajitbf.png" alt="Visual Studio Solution Explorer showing path to Form metadata UI control file"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key customization we need to make to this file is to register a new "Rich Text" User Control (called &lt;code&gt;HtmlAreaControl&lt;/code&gt;) at the top of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%@&lt;/span&gt; &lt;span class="na"&gt;Register&lt;/span&gt; 
    &lt;span class="na"&gt;Src=&lt;/span&gt;&lt;span class="s"&gt;"~/CMSFormControls/Basic/HtmlAreaControl.ascx"&lt;/span&gt;
    &lt;span class="na"&gt;TagName=&lt;/span&gt;&lt;span class="s"&gt;"HtmlArea"&lt;/span&gt; 
    &lt;span class="na"&gt;TagPrefix=&lt;/span&gt;&lt;span class="s"&gt;"cms"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxylfv4fk9ip0zwq4xdlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxylfv4fk9ip0zwq4xdlp.png" alt="ASPX markup for Form metadata UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that registration added, we can refer to the control further down the file, replacing the &lt;code&gt;&amp;lt;cms:LocalizableTextBox /&amp;gt;&lt;/code&gt; currently being used for Display Text input with a &lt;code&gt;&amp;lt;cms:HtmlAreaControl /&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;cms:CMSRadioButton&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"selector-subitem"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;cms:LocalizableTextBox&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"txtDisplay"&lt;/span&gt; &lt;span class="na"&gt;runat=&lt;/span&gt;&lt;span class="s"&gt;"server"&lt;/span&gt; &lt;span class="na"&gt;MaxLength=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;is replaced by:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;cms:CMSRadioButton&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"selector-subitem"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;cms:HtmlArea&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"txtDisplay"&lt;/span&gt; &lt;span class="na"&gt;runat=&lt;/span&gt;&lt;span class="s"&gt;"server"&lt;/span&gt; &lt;span class="na"&gt;MaxLength=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzngwgnl0hqfg00hkg2x6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzngwgnl0hqfg00hkg2x6.png" alt="Change to ASPX markup with HtmlArea control"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With these two changes, we can rebuild the CMS application and go back to the Form "General" tab and add Rich Text as our Display Text 🦾!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqj2ye2ewy2bgg4hkphm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqj2ye2ewy2bgg4hkphm.png" alt="Rich Text editor replacing a text field for Form Display Text value"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  📅 How to Customize Razor Class Libraries
&lt;/h2&gt;

&lt;p&gt;Even though we're now storing Rich Text as our Form's Display Text, we won't get very far if we don't change how that Display Text is rendered on the live site 😏.&lt;/p&gt;

&lt;p&gt;Fortunately, ASP.NET Core includes a feature that allows us to &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-6.0&amp;amp;tabs=visual-studio#override-views-partial-views-and-pages" rel="noopener noreferrer"&gt;override a View included in our project from a Razor Class Library&lt;/a&gt; by adding a View file with the exact same name and path as the file from the Razor Class Library 🧐.&lt;/p&gt;

&lt;p&gt;The View that controls how the Form Display Text is rendered after a successful submission is in the Razor Class Library at the following path:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Views/Shared/Kentico/Widgets/FormWidget/_FormWidgetTextConfirmation.cshtml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We don't have a file at that path in our project, so let's create a new empty one 🙃.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure you use the same path and filename, otherwise ASP.NET Core will see your View as a new one and not a replacement for the View coming from the library!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Razor in the original View from the library looks this like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@model string

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"formwidget-submit-text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;@Model&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Which we are going to replace with the following:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@model string

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"formwidget-submit-text"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;@Html.Raw(Model)&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwnv00jhkixmo2lkhrgg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwnv00jhkixmo2lkhrgg.png" alt="Authoring new Razor View file"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By rendering the &lt;code&gt;Model&lt;/code&gt; with &lt;code&gt;Html.Raw()&lt;/code&gt; we ensure our Rich Text Display Text won't be escaped when displayed on the page.&lt;/p&gt;

&lt;p&gt;At this point we can rebuild our ASP.NET Core application and we're all set 🙌🏿!&lt;/p&gt;
&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;With our 2 simple customizations, we can try out the Contact form to see our Rich Text message displayed on the page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnyam3hns0ujze31wmm8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnyam3hns0ujze31wmm8c.png" alt="Thank you message with a link displayed on a website"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It worked 🎉!&lt;/p&gt;

&lt;p&gt;We can use any content or design to our Display Text that can be authored in Rich Text and craft a "richer" experience (pun intended 😆) without having to go through the process of creating a completely separate page to redirect to.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.xperience.io/managing-website-content/forms/composing-forms" rel="noopener noreferrer"&gt;Form Builder&lt;/a&gt; - Xperience Docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.xperience.io/managing-website-content/forms/composing-forms#Composingforms-Addingfieldvalidation" rel="noopener noreferrer"&gt;Form Builder field validation&lt;/a&gt; - Xperience Docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.xperience.io/managing-website-content/forms/using-smart-fields-in-forms#Usingsmartfieldsinforms-Contacttrackingrequirement" rel="noopener noreferrer"&gt;Form Builder Smart Fields&lt;/a&gt; - Xperience Docs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-6.0&amp;amp;tabs=visual-studio#override-views-partial-views-and-pages" rel="noopener noreferrer"&gt;Overriding Razor Class Library Views&lt;/a&gt; - ASP.NET Core Docs&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md" rel="noopener noreferrer"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__57801"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/xperience" class="ltag__tag__link"&gt;xperience&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my &lt;a href="https://dev.to/seangwright/series"&gt;Kentico Xperience blog series&lt;/a&gt;, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/8185"&gt;Kentico Xperience Xplorations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/9483"&gt;Kentico Xperience MVC Widget Experiments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/10963"&gt;Kentico Xperience Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>csharp</category>
      <category>aspnet</category>
    </item>
    <item>
      <title>Bits of Xperience: Reducing CLS when Rendering Images</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Tue, 31 May 2022 16:10:05 +0000</pubDate>
      <link>https://forem.com/wiredviews/bits-of-xperience-reducing-cls-when-rendering-images-2nmc</link>
      <guid>https://forem.com/wiredviews/bits-of-xperience-reducing-cls-when-rendering-images-2nmc</guid>
      <description>&lt;p&gt;Kentico Xperience sites allow marketers and content managers to author content and build pages that display text, images, and anything else that HTML supports 😁.&lt;/p&gt;

&lt;p&gt;There are multiple ways to add images to content in Xperience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embed in Rich Text&lt;/li&gt;
&lt;li&gt;Custom Page Builder Component (Widget or Section)&lt;/li&gt;
&lt;li&gt;Rendered with Razor in a Page view or Template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These images can come from multiple sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Media Library&lt;/li&gt;
&lt;li&gt;Attachments&lt;/li&gt;
&lt;li&gt;Meta files (for SKUs)&lt;/li&gt;
&lt;li&gt;Form Submissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No matter which rendering approach or content source combination we are using for images on our sites, we'll want to make sure we always render those images with the appropriate HTML attributes, and these days that includes the &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes.&lt;/p&gt;

&lt;p&gt;But why 🤔?&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 What Will We Learn?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is Cumulative Layout Shift (CLS)?&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; attributes prevent Layout Shift?&lt;/li&gt;
&lt;li&gt;How do we get this information programmatically?&lt;/li&gt;
&lt;li&gt;Why should we always retrieve image content from the database?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🌊 Cumulative Layout Shift (CLS)
&lt;/h2&gt;

&lt;p&gt;Have you ever heard of &lt;a href="https://web.dev/cls/"&gt;Cumulative Layout Shift&lt;/a&gt;? It is defined (by Google) as follows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CLS is a measure of the largest burst of layout shift scores for every unexpected layout shift that occurs during the entire lifespan of a page.&lt;/p&gt;

&lt;p&gt;A layout shift occurs any time a visible element changes its position from one rendered frame to the next. (See below for details on how individual layout shift scores are calculated.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We've all experienced this on websites. Something on the page finally loads and a button moves, or the text we're reading shifts out of the viewport.&lt;/p&gt;

&lt;p&gt;Not only is this extremely annoying 😖, but Google uses it as a quality indicator for websites. More CLS means a worse user experience and Google takes that into account for &lt;a href="https://web.dev/cls-web-tooling/"&gt;scoring in PageSpeed Insights&lt;/a&gt;, which means it impacts search ranking 😮.&lt;/p&gt;

&lt;p&gt;So, now that it's clear we want to reduce CLS on our sites, how does this factor into displaying images with Kentico Xperience?&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing CLS with Images and &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; HTML Attributes
&lt;/h2&gt;

&lt;p&gt;Images can be a major cause of CLS 🙁 because the browser doesn't know an image's dimensions until it's been fully downloaded. While the image is being downloaded, the browser's rendering engine doesn't know how much space to allocate for it on the page. Once it finishes downloading, the browser re-renders, moving other content around to give the image the space it needs 😒.&lt;/p&gt;

&lt;p&gt;It should be clear now why we shouldn't be rendering images in Xperience with &lt;em&gt;just&lt;/em&gt; a &lt;code&gt;src&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"@Model.ImagePath"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This will all but guarantee CLS in most situations 😭.&lt;/p&gt;

&lt;p&gt;However, &lt;a href="https://twitter.com/jensimmons/status/1405528938147848204"&gt;browsers have been using width and height attributes&lt;/a&gt; to pre-allocate space for images for awhile now 🧐.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check out this great article on the topic on Smashing: &lt;a href="https://www.smashingmagazine.com/2020/03/setting-height-width-images-important-again/"&gt;Setting Height And Width On Images Is Important Again&lt;/a&gt; and it's follow up &lt;a href="https://www.smashingmagazine.com/2021/06/how-to-fix-cumulative-layout-shift-issues/#set-width-and-heights-on-images-and-iframes"&gt;How To Fix Cumulative Layout Shift (CLS) Issues&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we know the &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; values for an image and add those attributes/values when rendering it, we can help prevent CLS by letting the browser know how much space (vertically) the image will need based on the image's aspect ratio computed from these two values 👏🏾.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting an Image's Dimensions in Xperience
&lt;/h2&gt;

&lt;p&gt;Thankfully, Xperience stores all uploaded image's dimensions in the database 🙏🏽. However, since there are multiple ways to store and display images in Xperience, we'll need multiple techniques for retrieving an image's dimensions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Xperience doesn't treat &lt;code&gt;.svg&lt;/code&gt; files as images, which means we need to customize its behavior to capture SVG image dimensions when they are uploaded to a site.&lt;/p&gt;

&lt;p&gt;We can use the &lt;a href="https://github.com/wiredviews/xperience-svg-media-dimensions"&gt;Xperience SVG Media Dimensions&lt;/a&gt; library to do this for us automatically!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Image Dimensions and Media Files
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.xperience.io/x/UwqRBg"&gt;Media Library&lt;/a&gt; is probably the best place to store and manage images in Xperience so we'll use it as an example 👍🏿.&lt;/p&gt;

&lt;p&gt;To select files from the Media Library for Page Type fields, you'll probably use the Media Selection Form Control:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4nDaSGoI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8b6e3rujkfkk0yjpqn9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4nDaSGoI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8b6e3rujkfkk0yjpqn9n.png" alt="Media Selection Form Control UI for a Page Type field" width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will display a path on the page's Content tab for this control and a preview of the image:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RvgSVhrp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/845c9k6tpysrpx30yy8s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RvgSVhrp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/845c9k6tpysrpx30yy8s.png" alt="Page Content tab for Media Selection Form Control" width="750" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Retrieving an image from the media library by full path isn't a great developer experience, so fortunately &lt;a href="https://dev.to/mattnield"&gt;@mattnield&lt;/a&gt; wrote a &lt;a href="https://stackoverflow.com/questions/47024602/how-can-i-get-guid-from-permanent-url-media-selector/47026186#47026186"&gt;Regex several years ago&lt;/a&gt; that is able to retrieve the media file's &lt;code&gt;FileGUID&lt;/code&gt; from the path 💪🏼.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;FileGUID&lt;/code&gt; retrieved, we can query the image's content from the database:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pageRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetrieveAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HomePage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
   &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TopN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
   &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; 
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeroImageMediaFilePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@"[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;mediaFileGUID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HeroImageMediaFilePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;RegexOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IgnoreCase&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;MediaFileInfo&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mediaFileInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MediaFileInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileGUID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
        &lt;span class="n"&gt;mediaFileGUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnumerableTypedResultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// use Media File values ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now that we have our full &lt;code&gt;MediaFileInfo&lt;/code&gt; we can use all of its data to render our &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element 🙌.&lt;/p&gt;

&lt;p&gt;Assuming we have an &lt;code&gt;ImageViewModel&lt;/code&gt; class defined like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageViewModel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ImageViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;MediaFileInfo&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IMediaFileUrlRetriever&lt;/span&gt; &lt;span class="n"&gt;urlRetriever&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urlRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;RelativePath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;AltText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileDescription&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileTitle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileImageWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileImageHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;AltText&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Width&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Height&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can render our image in Razor as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"@Model.Path"&lt;/span&gt;
     &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"@Model.AltText"&lt;/span&gt;
     &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"@Model.Title"&lt;/span&gt;
     &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"@Model.Width"&lt;/span&gt;
     &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"@Model.Height"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Awesome! Layout shift avoided and CLS reduced! 🎉🥳🎊&lt;/p&gt;
&lt;h2&gt;
  
  
  Always Retrieve the Full Image!
&lt;/h2&gt;

&lt;p&gt;You might think, "This makes sense for images at the top of the page, but what about images at the bottom? They won't impact CLS, so this is a lot of extra work for little benefit."&lt;/p&gt;

&lt;p&gt;First, if we display an image as part of a Widget, we don't know where on the page that Widget will be added and different devices' viewports might place that content in different locations on the page. We should code defensively and always retrieve these values from the database 😊.&lt;/p&gt;

&lt;p&gt;Second, and far more importantly, we should &lt;em&gt;always&lt;/em&gt; retrieve the full image content from the database for accessibility 😎!&lt;/p&gt;

&lt;p&gt;All images in Xperience have &lt;code&gt;Title&lt;/code&gt; and &lt;code&gt;Description&lt;/code&gt; fields, which translate to the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;alt&lt;/code&gt; HTML attributes. If we want accessible sites (I'd argue that &lt;a href="https://www.wiredviews.com/news/2022/03/7-things-you-can-t-skip-when-building-a-website"&gt;accessibility is one of the 7 things we can't skip&lt;/a&gt; when building a website), then we need to include these values on &lt;em&gt;every&lt;/em&gt; informational image we render (background images that are for design don't have the same requirements).&lt;/p&gt;

&lt;p&gt;This means we are going to have to retrieve the image content anyway 🤷🏽‍♀️, so it's really no extra work to get the &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; values.&lt;/p&gt;
&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;Cumulative Layout Shift (CLS) is "a measure of the largest burst of layout shift scores for every unexpected layout shift that occurs during the entire lifespan of a page". This measurement represents some of the user experience visitors will have when they visit a page and can negatively impact our page's search rank (SEO) 🙄.&lt;/p&gt;

&lt;p&gt;The browser doesn't know how much space to allocate for images until they are fully downloaded, so images often impact CLS on sites. To help prevent Layout Shift for images we can add the image's &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; values as HTML attributes to the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element. This tells the browser how much space to allocate for the image based on its aspect ratio 🧠.&lt;/p&gt;

&lt;p&gt;To add these values with a Kentico Xperience site, we'll need to retrieve the image's content from the database (as opposed to &lt;em&gt;only&lt;/em&gt; the image's path).&lt;/p&gt;

&lt;p&gt;To improve the accessibility of our sites, we want to include &lt;code&gt;alt&lt;/code&gt; text on our images. This content, authored by content managers and marketers, is stored in the image content in the database 🤗.&lt;/p&gt;

&lt;p&gt;So, while adding &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; values to an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; might add some complexity to our code, we should be retrieving an image's content from the database anyway to ensure our sites are accessible 😉!&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/jensimmons/status/1405528938147848204"&gt;Twitter: Jen Simmons - Images and CLS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.smashingmagazine.com/2020/03/setting-height-width-images-important-again/"&gt;Smashing Magazine: Setting Height And Width On Images Is Important Again&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.smashingmagazine.com/2021/06/how-to-fix-cumulative-layout-shift-issues/"&gt;Smashing Magazine: How To Fix Cumulative Layout Shift (CLS) Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/x/UwqRBg"&gt;Kentico Xperience: Media Library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/x/LA2RBg"&gt;Kentico Xperience: Displaying content from media libraries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio"&gt;MDN: Aspect-ratio CSS property&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__57801"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/xperience" class="ltag__tag__link"&gt;xperience&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my &lt;a href="https://dev.to/seangwright/series"&gt;Kentico Xperience blog series&lt;/a&gt;, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/8185"&gt;Kentico Xperience Xplorations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/9483"&gt;Kentico Xperience MVC Widget Experiments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/10963"&gt;Kentico Xperience Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>csharp</category>
      <category>html</category>
    </item>
    <item>
      <title>Bits of Xperience: Supercharged Page Custom Data Fields</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 27 Sep 2021 14:12:57 +0000</pubDate>
      <link>https://forem.com/wiredviews/bits-of-xperience-supercharged-page-custom-data-fields-2h4h</link>
      <guid>https://forem.com/wiredviews/bits-of-xperience-supercharged-page-custom-data-fields-2h4h</guid>
      <description>&lt;p&gt;Kentico Xperience's custom Page Types let us model a site's content using a combination of powerful built-in Form Controls and the flexible structuring of information in a site's Content Tree.&lt;/p&gt;

&lt;p&gt;Usually, with Page Type fields, each field maps to one value (as a column in a database table), and each Page Type has a unique set of fields. But, what if we want to store multiple fields in a single database column, or have multiple Page Types that store data in a single location, making it easy to query 🤔?&lt;/p&gt;

&lt;p&gt;There might not be an out-of-the-box solution, but fortunately, with a little code and configuration 🤓, we can use Page "Custom Data" to achieve both of these things.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to jump to the solution, check out our new NuGet package that does all the coding for you, &lt;a href="https://github.com/wiredviews/xperience-page-custom-data-control-extender" rel="noopener noreferrer"&gt;Xperience Page Custom Data Control Extender&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  📚 What Will We Learn?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What are Page "Custom Data" fields?&lt;/li&gt;
&lt;li&gt;What do the "Custom Data" Page fields lack?&lt;/li&gt;
&lt;li&gt;Using Global Events with Page "Custom Data"&lt;/li&gt;
&lt;li&gt;Using Custom Form Controls and a Control Extender&lt;/li&gt;
&lt;li&gt;Storing Page data directly in "Custom Data" fields&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ❓ What are Page "Custom Data" fields?
&lt;/h2&gt;

&lt;p&gt;Before we get going, let's level set. Page "Custom Data" fields are the &lt;code&gt;DocumentCustomData&lt;/code&gt; column in the &lt;code&gt;CMS_Document&lt;/code&gt; table and the &lt;code&gt;NodeCustomData&lt;/code&gt; column in the &lt;code&gt;CMS_Tree&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Anything in, or related to, the &lt;code&gt;CMS_Tree&lt;/code&gt; table is going to apply to all cultures for a Page in the Content Tree, and likewise, anything in, or related to, the &lt;code&gt;CMS_Document&lt;/code&gt; table is going to be specific to a single culture. Many sites only have a single culture, so this distinction might not be something you're used to thinking about 🤨.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read more about the &lt;a href="https://docs.xperience.io/custom-development/working-with-pages-in-the-api/page-database-structure" rel="noopener noreferrer"&gt;Kentico Xperience Page database architecture&lt;/a&gt; 🧐.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All "Custom Data" database columns (there are many in non-Page ones Xperience) have an XML structure, and the C# API to interact with them works with a &lt;code&gt;XmlData&lt;/code&gt; container behind the scenes, almost like a &lt;code&gt;string&lt;/code&gt;-keyed dictionary.&lt;/p&gt;

&lt;p&gt;"Custom Data" columns let us switch from a relational database architecture to more of a document structure where the schema of the data isn't defined in the database, but instead in our code.&lt;/p&gt;

&lt;p&gt;It would be great 👍🏾 to leverage this alternate way of storing data to achieve what was discussed earlier (multiple values per column and Pages storing field values for multiple Page Types in the same location, instead of separate Page Type database tables).&lt;/p&gt;

&lt;h2&gt;
  
  
  🗃 What do the Page "Custom Data" Page Fields Lack?
&lt;/h2&gt;

&lt;p&gt;First, let's review why we can't 😞 use Page "Custom Data" fields as they currently exist in Xperience.&lt;/p&gt;

&lt;p&gt;If we look at the the documentation on the features of the &lt;a href="https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/reference-field-editor#ReferenceFieldeditor-Creatingnewfields" rel="noopener noreferrer"&gt;field editor&lt;/a&gt;, which is used for creating fields for custom Page Types, we can see there are a couple options for the "Field type". The one we are interested in is the "Page Field":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Page field – only available for page type fields. Allows you to choose a general page column from the &lt;code&gt;CMS_Tree&lt;/code&gt; or &lt;code&gt;CMS_Document&lt;/code&gt; table, and link it to the page field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When creating a new Page Type field and selecting a &lt;em&gt;Field type&lt;/em&gt; of &lt;em&gt;Page field&lt;/em&gt; we can select either &lt;code&gt;Page fields&lt;/code&gt; or &lt;code&gt;Node fields&lt;/code&gt; for the Group. This equates to columns from the &lt;code&gt;CMS_Document&lt;/code&gt; and &lt;code&gt;CMS_Tree&lt;/code&gt; tables.&lt;/p&gt;

&lt;p&gt;If we select &lt;code&gt;Page fields&lt;/code&gt; and then pick the &lt;em&gt;Field name&lt;/em&gt; of &lt;code&gt;DocumentCustomData&lt;/code&gt;, we can start interacting directly with this value for each Page of the given Page Type 😀.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5qfvguiohzrzczm4leg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh5qfvguiohzrzczm4leg.jpg" alt="Page Type field editor using DocumentCustomData directly"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the XML schema of these fields is flexible, there's no special "Custom Data" Form Control 😦 that let's us modify that XML in a way that is friendly to Content Managers.&lt;/p&gt;

&lt;p&gt;The best we can do is a Rich Text editor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ms5sdvphpwjb4zh2yie.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ms5sdvphpwjb4zh2yie.jpg" alt="Rich text editor showing plain text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But it falls way too short of something usable for Content Managers 😣:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj7o5omucm08on69wizry.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj7o5omucm08on69wizry.jpg" alt="Rich text editor showing XML"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what are we going to do if we want to leverage the schema flexibility of "Custom Data" fields for a Page? What Form Control gives us a good Content Management experience? Do we have to build a bunch of custom Web Forms Form Controls ☠?&lt;/p&gt;

&lt;h2&gt;
  
  
  📅 Using Global Events and Page "Custom Data"
&lt;/h2&gt;

&lt;p&gt;Fortunately there's a couple different ways we can approach this problem 😅.&lt;/p&gt;

&lt;p&gt;We already have a bunch of pre-built Form Controls which are designed for ease-of-use for Content Managers. Let's make sure our solution includes those 👏🏼 and doesn't require us to rewrite them!&lt;/p&gt;

&lt;p&gt;Let's create a new field on our Page Type named &lt;code&gt;ArticleIsInSitemap&lt;/code&gt;, using all the standard Page Type field functionality:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotf3ssd2rnx87e2t6t3o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotf3ssd2rnx87e2t6t3o.jpg" alt="Page Type field dialog"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we create new fields on our Page Type and use the appropriate standard Form Controls for those fields, we can get a solid Content Management experience, but the values go into individual database columns in each Page Type's database table 🤦🏽‍♀️ instead of the Page "Custom Data" columns.&lt;/p&gt;

&lt;p&gt;Thankfully, Kentico Xperience has a full set of &lt;a href="https://docs.xperience.io/custom-development/handling-global-events" rel="noopener noreferrer"&gt;Global Events&lt;/a&gt; that allow developers to react to things happening within the system 👨🏿‍🔬. We can use these events to copy data from our Page Type fields to the "Custom Data" XML structures of the Page.&lt;/p&gt;

&lt;p&gt;Let's create a Custom Module that will give us a place to react to these events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;CMS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;CMS.DataEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;CMS.DocumentEngine&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;CMSApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;RegisterModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DocumentEventsModule&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;CMSApp&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocumentEventsModule&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Module&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DocumentEventsModule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; 
            &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DocumentEventsModule&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnInit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="n"&gt;DocumentEvents&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Insert&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Before&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Insert_Before&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="n"&gt;DocumentEvents&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Before&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Update_Before&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update_Before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DocumentEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
            &lt;span class="nf"&gt;SetValuesInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Insert_Before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DocumentEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="nf"&gt;SetValuesInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both of the event handlers above let us run our logic when any Page is inserted or updated with the following method:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetValuesInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DocumentEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Article&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;article&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DocumentCustomData&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsInSitemap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
            &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsInSitemap&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And that's it! Every time an Article is inserted into the Content Tree or updated, the value in &lt;code&gt;ArticleIsInSitemap&lt;/code&gt; will be copied to an XML element in &lt;code&gt;CMS_DocumentCustomData&lt;/code&gt; 😊, which will look like this in the database:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;CustomData&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;IsInSitemap&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/IsInSitemap&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/CustomData&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What's the benefit here? Don't we already have the value in the &lt;code&gt;DancingGoatCore_Article&lt;/code&gt; table's &lt;code&gt;ArticleIsInSitemap&lt;/code&gt; column?&lt;/p&gt;

&lt;p&gt;Well, for data that determines whether or not a Page is in the sitemap, we want to be able to query across &lt;strong&gt;all&lt;/strong&gt; Pages of the site, not just  Articles, so that we generate the correct &lt;a href="https://developers.google.com/search/docs/advanced/sitemaps/overview" rel="noopener noreferrer"&gt;XML sitemap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If we have a column in each table for all of our custom Page Types, we'd end up with a real ugly SQL &lt;code&gt;UNION&lt;/code&gt; to get all the Pages in the sitemap. By copying the value to &lt;code&gt;DocumentCustomData&lt;/code&gt;, we ensure the full sitemap can be generated by &lt;em&gt;only&lt;/em&gt; querying the &lt;code&gt;CMS_Document&lt;/code&gt; table 💪🏻:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;CMS_Document&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DocumentCustomData&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;XML&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'(//CustomData/IsInSitemap/text())[1]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bit'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;Checkout Microsoft's documentation to read about &lt;a href="https://docs.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver15" rel="noopener noreferrer"&gt;querying XML in SQL&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is great 👍🏾! But it would be &lt;em&gt;really&lt;/em&gt; great if we didn't have to have an extra database table column per-Page Type and duplicate this data. We would prefer to write directly to the Page "Custom Data" field 😏.&lt;/p&gt;
&lt;h2&gt;
  
  
  🎛 Custom Form Controls and Control Extenders
&lt;/h2&gt;

&lt;p&gt;Lucky for us, Kentico Xperience provides a convenient feature in the CMS architecture - &lt;a href="https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/inheriting-from-existing-form-controls" rel="noopener noreferrer"&gt;Control Extenders&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Control Extenders let us enhance the functionality of inherited Form Controls. But what does that mean?&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/OQSNhk5ICTI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;We can create new Form Controls in the Administration application, with either new code or inheriting the code and functionality from an existing Form Control. The 2nd option is preferable because it means less work for us 😄!&lt;/p&gt;

&lt;p&gt;When we create a new Form Control that inherits from another, we can apply a Control Extender to it. A Control Extender is a component that wraps the original Form Control and gets to intercept interactions with the Control 🧐.&lt;/p&gt;

&lt;p&gt;This is a valuable feature for us, because it will let us source the Control's value from &lt;code&gt;DocumentCustomData&lt;/code&gt; when it is read and write it to &lt;code&gt;DocumentCustomData&lt;/code&gt; when the Control value is updated - all without modifying the code or functionality of the original control. We can also apply this Control Extender to &lt;em&gt;any&lt;/em&gt; inheriting Form Control 😮.&lt;/p&gt;

&lt;p&gt;In summary, this is what we want to accomplish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✔ Create a Control Extender that redirects interactions with the Form Control's value to the &lt;code&gt;DocumentCustomData&lt;/code&gt; field and store the value in an XML element with the same name as the field's name&lt;/li&gt;
&lt;li&gt;✔ Create a new Form Control that inherits from the standard Check box Form Control and apply the Control Extender to the new Form Control&lt;/li&gt;
&lt;li&gt;✔ Use the new Form Control as the control for our &lt;code&gt;Article&lt;/code&gt; Page Type &lt;code&gt;IsInSitemap&lt;/code&gt; field&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🏗 Control Extender
&lt;/h3&gt;

&lt;p&gt;The code for the Control Extender is pretty simple:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomDataControlExtender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; 
    &lt;span class="n"&gt;ControlExtender&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FormEngineUserControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// logic here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There are multiple events the Control emits that we can register event handlers for and when the underlying Form Control initializes, we register our event handlers in &lt;code&gt;Control_Init&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Control_Init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Control_Init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnGetControlValue&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Form_OnGetControlValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnAfterDataLoad&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;Form_OnAfterDataLoad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FieldControls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We are going to be creating our Page Type field as a "Field without database representation", which means it won't be listed in the &lt;code&gt;FieldControls&lt;/code&gt; or &lt;code&gt;ColumnNames&lt;/code&gt; collections that get processed when we load/save our Form, so we explicitly add it.&lt;/p&gt;

&lt;p&gt;This way the Form treats our field as though it needs to be persisted/retrieved just like the other ones.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Form_OnAfterDataLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DocumentCustomData&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Form_OnGetControlValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FormEngineUserControlEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCultureIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DocumentCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then we define two event handlers - the first supplies the Form Control value from the correct Page "Custom Data" field when the value is loaded by the Control, and the second accepts the value coming from the Control and stores it in the correct Page "Custom Data" field.&lt;/p&gt;

&lt;p&gt;After our interception, the Page gets created or saved and the field's value is saved along with it, except it's inside &lt;code&gt;DocumentCustomData&lt;/code&gt; and not a Page Type specific database column 🤩.&lt;/p&gt;
&lt;h3&gt;
  
  
  👵🏽 Inheriting a Form Control
&lt;/h3&gt;

&lt;p&gt;Inheriting from an existing Form Control and applying a Control Extender only takes a few steps!&lt;/p&gt;

&lt;p&gt;First, navigate to the "Administration interface" module in the CMS application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8r470gaphltyfcxin3zy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8r470gaphltyfcxin3zy.jpg" alt="Administration interface menu item"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then create a new "inheriting" Form Control, using the Check box Form Control as the source:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfay5cva2ijs9wg65kwh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfay5cva2ijs9wg65kwh.jpg" alt="New Form Control dialog"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Be sure to select the Control Extender you just created!&lt;/p&gt;

&lt;p&gt;Finally, select the places where this Form Control can be used. For our example it will be for "Boolean (Yes/No)" fields for "Page Types":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwvdysx1zwykg7ihvufs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnwvdysx1zwykg7ihvufs.jpg" alt="Form Control assignment configuration"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  📃 Using a Page "Custom Data" Form Control
&lt;/h3&gt;

&lt;p&gt;Now we can create the new field for our Page Type 😊!&lt;/p&gt;

&lt;p&gt;Be sure to select &lt;strong&gt;"Field without database representation"&lt;/strong&gt; for the Field type (otherwise the value will be saved in a newly created database table column for the Page Type 😬) and use our new extended Form Control (otherwise the value won't be saved &lt;em&gt;at all&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Whatever we name this new field will end up being the XML element that the value is stored in, so a field named &lt;code&gt;ABC&lt;/code&gt; would end up as &lt;code&gt;&amp;lt;CustomData&amp;gt;&amp;lt;ABC&amp;gt;value&amp;lt;/ABC&amp;gt;&amp;lt;/CustomData&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivhidsqpv6ahumn8v2vy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivhidsqpv6ahumn8v2vy.jpg" alt="New Page Type field dialog"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it! When we save a Page of this Page Type, this specific field will only be saved to &lt;code&gt;DocumentCustomData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can add as many "Custom Data" fields to a Page Type as we want, and if we define the same field on multiple Page Types, they'll all put data of the same XML schema in our &lt;code&gt;CMS_Document.DocumentCustomData&lt;/code&gt; database column.&lt;/p&gt;
&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope by now both the motivation and process for using Page "Custom Data" fields as the backing store of Page Type fields are clear 🔍.&lt;/p&gt;

&lt;p&gt;There's a few steps to set everything up - create a Control Extender, define new inheriting Form Controls using the Control Extender, and add a fields to Page Types using the extended Form Control - but the initial setup definitely pays off. It's worth noting the first step (creating a Control Extender) only needs performed once, and the second (creating an extended Form Control) only once per Form Control type (eg Text Box, Check Box, Page Selector).&lt;/p&gt;

&lt;p&gt;Our "Sitemap" example saves us from performing a large SQL &lt;code&gt;UNION&lt;/code&gt; when we generate a site's XML sitemap, but that's not the only use case.&lt;/p&gt;

&lt;p&gt;What about &lt;a href="https://ogp.me/" rel="noopener noreferrer"&gt;Open Graph&lt;/a&gt; metadata values for a Page - wouldn't it be nice to not have to create a separate database column for each value?&lt;/p&gt;

&lt;p&gt;Or, a standard field inherited from a base Page Type that we aren't going to be likely to filter in SQL - like a 'primary image path'.&lt;/p&gt;

&lt;p&gt;We could even make mixins, letting multiple Page Types share sets of fields and then access those field values across Page Types by querying the &lt;code&gt;CMS_Document&lt;/code&gt; table only 🧐.&lt;/p&gt;

&lt;p&gt;Are you thinking about implementing this yourself? Well, it's dangerous to go alone... &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.giphy.com%2Fmedia%2FElSNi8FdSB7RS%2Fgiphy.gif%3Fcid%3Decf05e47ef4jrqak87upnitkvd0qjmetu2ynz0iu87w40p54%26rid%3Dgiphy.gif%26ct%3Dg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia.giphy.com%2Fmedia%2FElSNi8FdSB7RS%2Fgiphy.gif%3Fcid%3Decf05e47ef4jrqak87upnitkvd0qjmetu2ynz0iu87w40p54%26rid%3Dgiphy.gif%26ct%3Dg" alt="zelda meme"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, take this... the &lt;a href="https://github.com/wiredviews/xperience-page-custom-data-control-extender" rel="noopener noreferrer"&gt;Xperience Page Custom Data Control Extender&lt;/a&gt;, a NuGet package containing an enhanced version of the above Control Extender with detailed setup instructions 👐.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/wiredviews/xperience-page-custom-data-control-extender" rel="noopener noreferrer"&gt;Xperience Page Custom Data Control Extender&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/reference-field-editor#ReferenceFieldeditor-Creatingnewfields" rel="noopener noreferrer"&gt;Creating new fields in the Kentico Xperience field editor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/custom-development/handling-global-events" rel="noopener noreferrer"&gt;Kentico Xperience Global Events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/advanced/sitemaps/overview" rel="noopener noreferrer"&gt;Google - XML sitemaps Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/sql/t-sql/xml/xml-data-type-methods?view=sql-server-ver15" rel="noopener noreferrer"&gt;SQL Server - xml Data Type Methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/custom-development/extending-the-administration-interface/developing-form-controls/inheriting-from-existing-form-controls" rel="noopener noreferrer"&gt;Kentico Xperience - Control Extenders&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md" rel="noopener noreferrer"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__57801"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/xperience" class="ltag__tag__link"&gt;xperience&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my &lt;a href="https://dev.to/seangwright/series"&gt;Kentico Xperience blog series&lt;/a&gt;, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/8185"&gt;Kentico Xperience Xplorations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/9483"&gt;Kentico Xperience MVC Widget Experiments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/10963"&gt;Kentico Xperience Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Bits of Xperience: The Hidden Cost of IPageUrlRetriever.Retrieve</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 28 Jun 2021 13:19:38 +0000</pubDate>
      <link>https://forem.com/wiredviews/bits-of-xperience-the-hidden-cost-of-ipageurlretriever-retrieve-3oj1</link>
      <guid>https://forem.com/wiredviews/bits-of-xperience-the-hidden-cost-of-ipageurlretriever-retrieve-3oj1</guid>
      <description>&lt;p&gt;There's a lot of new, helpful types and methods in Kentico Xperience 13.0... but it can sometimes be difficult to know when each should be used 🤔.&lt;/p&gt;

&lt;p&gt;Let's look at the simple (or is it?) example of the &lt;code&gt;IPageUrlRetriever&lt;/code&gt; interface and its &lt;code&gt;Retrieve()&lt;/code&gt; method.&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 What Will We Learn?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What &lt;code&gt;IPageUrlRetriever&lt;/code&gt; does&lt;/li&gt;
&lt;li&gt;The multiple overloads of the &lt;code&gt;Retrieve()&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;The hidden difference between each overload&lt;/li&gt;
&lt;li&gt;The best way to retrieve Page URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🎣 What is the IPageUrlRetriever?
&lt;/h2&gt;

&lt;p&gt;Using Kentico Xperience's &lt;a href="https://docs.xperience.io/developing-websites/implementing-routing/content-tree-based-routing"&gt;Content Tree based routing&lt;/a&gt; is the option most developers choose. It enables the ability to have some Pages in the Content Tree not participate in URL generation, and also ensures that &lt;a href="https://docs.xperience.io/developing-websites/implementing-routing/content-tree-based-routing#Contenttreebasedrouting-GenerationMultilingualURLgenerationonmultilingualsites"&gt;culture is included in URLs&lt;/a&gt; based on the site's settings.&lt;/p&gt;

&lt;p&gt;All of this means that generating URLs correctly can get a little tricky 😅!&lt;/p&gt;

&lt;p&gt;Fortunately, Kentico Xperience helps us out by providing the &lt;code&gt;IPageUrlRetriever&lt;/code&gt; interface which has 1 method, &lt;code&gt;Retrieve()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This method returns an instance of the &lt;code&gt;PageUrl&lt;/code&gt; type which is defined as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Encapsulates page relative path and absolute URL.&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PageUrl&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PageUrl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// Relative path (starting with ~/) of the page.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RelativePath&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="c1"&gt;// Absolute URL of the page.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;AbsoluteUrl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can use this &lt;code&gt;PageUrl&lt;/code&gt; instance to render links to other Pages in our Razor Views.&lt;/p&gt;

&lt;p&gt;However, there are multiple overloads of the &lt;code&gt;Retrieve()&lt;/code&gt; method and they have very different use-cases 😮.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧙🏽‍♀️ Which Method Do We Choose?
&lt;/h2&gt;

&lt;p&gt;Here are all the overloads of &lt;code&gt;IPageUrlRetriever.Retrieve()&lt;/code&gt;, with their XML doc comments summary included:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Retrieves URL for given page&lt;/span&gt;
&lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;keepCurrentCulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieves URL for the given page in the given culture.&lt;/span&gt;
&lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cultureCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieves URL for a page based on given properties.&lt;/span&gt;
&lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;nodeAliasPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;keepCurrentCulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieves URL for a page based on given properties.&lt;/span&gt;
&lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;nodeAliasPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cultureCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It looks like Kentico Xperience is giving us a lot of flexibility here. We can either supply a full &lt;code&gt;TreeNode&lt;/code&gt; instance or, just the &lt;code&gt;nodeAliasPath&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's convenient! If we know there's always an &lt;code&gt;/About&lt;/code&gt; Page in the Content Tree, we can make a call like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="n"&gt;pageUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/About"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This will give us access to the correctly generated Page URL, &lt;em&gt;and&lt;/em&gt; we don't have to query for the &lt;code&gt;TreeNode&lt;/code&gt;, which means one less database round-trip 😃 (or does it?)&lt;/p&gt;

&lt;p&gt;So, we probably feel pretty confident 😤 in using &lt;code&gt;Retrieve(TreeNode node, ...)&lt;/code&gt; when we have the &lt;code&gt;TreeNode&lt;/code&gt; instance anyway, and using &lt;code&gt;Retrieve(string nodeAliasPath, ...)&lt;/code&gt; when we only know where in the Content Tree the Page is or when we get the Node Alias Path from some other Page's field.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚔ Retrieve(string) vs Retrieve(TreeNode)
&lt;/h2&gt;

&lt;p&gt;In this situation Xperience asks for what it needs but lets us provide less, however &lt;a href="https://en.wikipedia.org/wiki/There_ain%27t_no_such_thing_as_a_free_lunch"&gt;there's no free lunch&lt;/a&gt; and the convenience provided to us has a cost 🤨!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IPageUrlRetriever&lt;/code&gt; is implemented by the &lt;code&gt;Kentico.Content.Web.Mvc.PageUrlRetriever&lt;/code&gt; internal class. When we call &lt;code&gt;Retrieve(string nodeAliasPath, ...)&lt;/code&gt; the &lt;code&gt;PageUrlRetriever&lt;/code&gt; uses &lt;code&gt;IPageSystemDataContextRetriever.Retrieve()&lt;/code&gt; internally to get the 'page data' that matches the &lt;code&gt;nodeAliasPath&lt;/code&gt; we provided:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Provides an interface for retrieving the page based on given parameters for system purposes.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPageSystemDataContextRetriever&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

   &lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SiteInfoIdentifier&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;nodeAliasPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cultureCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;IPageSystemDataContextRetriever.Retrieve()&lt;/code&gt; is fortunately cached for 10 minutes, so repeated uses of &lt;code&gt;IPageUrlRetrieve.Retrieve(string nodeAliasPath, ...)&lt;/code&gt; won't result in multiple database calls, but the first call absolutely does hit the database because it needs more information, than what we provided, to generate the correct URL.&lt;/p&gt;

&lt;p&gt;This is an important point to understand... Kentico Xperience provides &lt;em&gt;many&lt;/em&gt; different ways to accomplish the same goal, which is a good thing because we can choose the right one for our use-case. At the same time, if we choose the wrong approach for our use-case, we might end up taking a performance hit we didn't intend 😬!&lt;/p&gt;

&lt;p&gt;How bad can it get? Let's say we are generating URLs for 100 products displayed on a Page using &lt;code&gt;IPageUrlRetriever.Retrieve(string nodeAliasPath, ...)&lt;/code&gt;. This means we are executing &lt;strong&gt;at least&lt;/strong&gt; 100 database queries just to get URLs! Add on to this all the querying we did to get the Product information and images! Ooof 😖!&lt;/p&gt;

&lt;p&gt;This is commonly known at the &lt;a href="https://stackoverflow.com/a/97253/939634"&gt;N + 1 Querying Problem&lt;/a&gt; and is often seen with &lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping"&gt;Object-Relational Mapping tools&lt;/a&gt; like &lt;a href="https://docs.microsoft.com/en-us/ef/core/"&gt;Entity Framework Core&lt;/a&gt; or ... Kentico Xperience's APIs 😋.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔥 More Pitfalls 💣
&lt;/h2&gt;

&lt;p&gt;Let say we've avoided the N + 1 query by using the alternative overload of &lt;code&gt;IPageUrlRetriever.Retrieve(TreeNode node, ...)&lt;/code&gt; so that Kentico Xperience doesn't have to go and fetch all the nodes independently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If we still have to get our Pages from &lt;code&gt;string nodeAliasPath&lt;/code&gt; values, we can use the &lt;code&gt;WhereIn(string columnName, ICollection&amp;lt;string&amp;gt; values)&lt;/code&gt; method defined on &lt;code&gt;WhereConditionBase&lt;/code&gt; to query for all &lt;code&gt;TreeNode&lt;/code&gt; objects that match set of &lt;code&gt;nodeAliasPath&lt;/code&gt; values we have. This would be a big query, but at least it's 1 query and not 100.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since we've found the correct API for our use-case, we should be all set now, right? &lt;code&gt;git commit&lt;/code&gt; and deploy 😎!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/143vPc6b08locw/giphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/143vPc6b08locw/giphy.gif" alt="Ship falling into the water" width="396" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, we could now run into a second problem 😫.&lt;/p&gt;

&lt;p&gt;In Kentico Xperience MVC (compared to older Portal Engine sites), the &lt;code&gt;nodeAliasPath&lt;/code&gt; is not the true URL even if parts of it match a URL for a Page. Instead, all generated URL values are stored in the &lt;code&gt;CMS_PageUrlPath&lt;/code&gt; table 🤓 in the database and the actual path is in the &lt;code&gt;PageUrlPathUrlPath&lt;/code&gt; column (what a tongue twister!)&lt;/p&gt;

&lt;p&gt;Without this data, we cannot generate valid Page URLs.&lt;/p&gt;

&lt;p&gt;This means that when we pass a &lt;code&gt;TreeNode&lt;/code&gt; to &lt;code&gt;IPageUrlRetriever.Retrieve(TreeNode node, ...)&lt;/code&gt;, internally it has to check if the &lt;code&gt;PageUrlPathUrlPath&lt;/code&gt; field is in the &lt;code&gt;TreeNode&lt;/code&gt;'s internal data set of field/value pairs. If the value is not populated, then Kentico Xperience has to query the database for it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;PageUrlPathInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PageUrlPathNodeID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NodeID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PageUrlPathCulture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cultureCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In addition to having this field populated, the culture of the &lt;code&gt;TreeNode&lt;/code&gt; retrieved from the database needs to match the culture of the URL we are trying to generate.&lt;/p&gt;

&lt;p&gt;If either the &lt;code&gt;PageUrlPathUrlPath&lt;/code&gt; is missing or the cultures don't match, we have to make yet another database call for the URL data, and as far as I can tell, this query &lt;strong&gt;is not cached&lt;/strong&gt; 😨.&lt;/p&gt;

&lt;p&gt;So we're in another situation where we could be making an additional 100 database queries (and if we were using &lt;code&gt;nodeAliasPath&lt;/code&gt; that means at least 200 database calls!) to get Page URLs.&lt;/p&gt;
&lt;h2&gt;
  
  
  🏆 Optimal URL Generation
&lt;/h2&gt;

&lt;p&gt;Fortunately, there's a nice extension method &lt;code&gt;WithPageUrlPaths()&lt;/code&gt;, in the &lt;code&gt;CMS.DocumentEngine.Routing&lt;/code&gt; namespace, for &lt;code&gt;IDocumentQuery&lt;/code&gt; that ensures the &lt;code&gt;CMS_PageUrlPath&lt;/code&gt; table is joined when querying for our &lt;code&gt;TreeNode&lt;/code&gt;s 🤩.&lt;/p&gt;

&lt;p&gt;If we had a collection of &lt;code&gt;NodeGUID&lt;/code&gt; values (or &lt;code&gt;string nodeAliasPath&lt;/code&gt; values) that referenced Pages in the Content Tree that we wanted to generate URLs for, I think this would be the best approach:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;linkedNodeGuids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;linkedDocuments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DocumentHelper&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDocuments&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithPageUrlPaths&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NodeGUID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;linkNodeGuids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnumerableTypedResultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nodesAndURLs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linkedDocuments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With our &lt;code&gt;nodesAndURLs&lt;/code&gt; array of tuples we have all the data we need to create links to all those Pages 👏!&lt;/p&gt;

&lt;p&gt;Of course, if we instead use Kentico Xperience's &lt;code&gt;IPageRetriever&lt;/code&gt; service, the &lt;code&gt;.WithPageUrlPaths()&lt;/code&gt; extension gets applied for us automatically:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;linkedNodeGuids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;linkedDocuments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pageRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetrieveAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NodeGUID&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;linkNodeGuids&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="n"&gt;TreeNode&lt;/span&gt; &lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PageUrl&lt;/span&gt; &lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nodesAndURLs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linkedDocuments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Isn't that nice 😁!&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The convenience of the Kentico Xperience libraries help us developers create the applications our businesses need, quickly and with a lot of flexibility 💪🏽.&lt;/p&gt;

&lt;p&gt;That flexibility can come at a cost that we might not notice during local development or when a site isn't under heavy load.&lt;/p&gt;

&lt;p&gt;Caching helps solve a lot of inevitable performance limitations and mistakes, but it's best if we can make the right choices the first time (especially if its only the difference of 1 method overload vs another 😉).&lt;/p&gt;

&lt;p&gt;When trying to get URLs for Pages, especially in bulk, our best choice is to use &lt;code&gt;IPageUrlRetriever.Retrieve(TreeNode node, ...)&lt;/code&gt; and then make sure the &lt;code&gt;TreeNode&lt;/code&gt; being passed was retrieved from the database using a &lt;code&gt;DocumentQuery&lt;/code&gt; that called &lt;code&gt;.WithPageUrlPaths()&lt;/code&gt; with the correct culture... otherwise we might end up causing N + 1 (or worse!) querying against the database.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IPageRetriever.RetrieveAsync()&lt;/code&gt; method can at least make sure &lt;code&gt;.WithPageUrlPaths()&lt;/code&gt; is applied to our query, so we don't have to remember to do it 😄.&lt;/p&gt;

&lt;p&gt;If there are any other APIs you have questions about, let me know in the comments below.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/developing-websites/implementing-routing/content-tree-based-routing"&gt;Kentico Xperience - Content Tree Routing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/There_ain%27t_no_such_thing_as_a_free_lunch"&gt;There's No Free Lunch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/97253/939634"&gt;N + 1 Querying&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping"&gt;Object-Relational Mapping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/ef/core/"&gt;Entity Framework Core&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__57801"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/xperience" class="ltag__tag__link"&gt;xperience&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my &lt;a href="https://dev.to/seangwright/series"&gt;Kentico Xperience blog series&lt;/a&gt;, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/8185"&gt;Kentico Xperience Xplorations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/9483"&gt;Kentico Xperience MVC Widget Experiments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/10963"&gt;Kentico Xperience Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>aspnetcore</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Bits of Xperience: Localizing XHR Requests in Kentico Xperience 13.0</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 18 Jan 2021 15:13:43 +0000</pubDate>
      <link>https://forem.com/wiredviews/bits-of-xperience-localizing-xhr-requests-in-kentico-xperience-13-0-4i3j</link>
      <guid>https://forem.com/wiredviews/bits-of-xperience-localizing-xhr-requests-in-kentico-xperience-13-0-4i3j</guid>
      <description>&lt;p&gt;Application and content localization can be tricky, but fortunately, Kentico Xperience 13.0 makes it easy 🤗!&lt;/p&gt;

&lt;p&gt;Using Xperience's &lt;a href="https://docs.xperience.io/multilingual-websites/setting-up-multilingual-websites/configuring-urls-for-multilingual-websites"&gt;Content Tree-based routing&lt;/a&gt;, we let the platform generate our URLs, inserting the culture code as part of the path.&lt;/p&gt;

&lt;p&gt;When our application handles a localized URL, assuming we use the &lt;code&gt;CultureInfo.CurrentCulture&lt;/code&gt; (or more likely &lt;code&gt;IPageRetriever&lt;/code&gt;) to retrieve our Page's content, we'll be sure display the localized content 💪🏿!&lt;/p&gt;

&lt;p&gt;In this post I want to talk about a different scenario...&lt;/p&gt;

&lt;p&gt;How do we make sure that any XHR requests made to an API, running in our Xperience application, are also localized, working with and returning the correct content for the current page 🤔?&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 What Will We Learn?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What is Localization?&lt;/li&gt;
&lt;li&gt;How does Xperience set the Page's request culture?&lt;/li&gt;
&lt;li&gt;How do our custom API endpoints find the request culture?&lt;/li&gt;
&lt;li&gt;What tools can we use to pass the Page's culture on to the API requests?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🌍 What is Localization?
&lt;/h2&gt;

&lt;p&gt;Good ol' &lt;a href="https://en.wikipedia.org/wiki/Language_localisation"&gt;Wikipedia&lt;/a&gt; says that localization "is the process of adapting a product's translation to a specific country or region."&lt;/p&gt;

&lt;p&gt;When we "localize" a response to an HTTP request for a web page, we are making sure that the content returned with that response is correct for the country or region associated with that request. This is often simplified to be the language associated with the request.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For the rest of this post I'm going to use this simplified definition of localization and refer to the language, but there are plenty of other bits of content that need to be localized, like &lt;a href="https://en.wikipedia.org/wiki/Decimal_separator"&gt;number&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Date_format_by_country"&gt;date&lt;/a&gt; formatting.&lt;/p&gt;

&lt;p&gt;Also, the &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-5.0#globalization-and-localization-terms-2"&gt;.NET docs&lt;/a&gt; have a great discussion on Globalization (building an app so it can support multiple languages) and Localization (adding support for a specific language(s)).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🔍 How does Xperience Figure Out a Request's Culture?
&lt;/h2&gt;

&lt;p&gt;Xperience, like any globalized platform/website, needs to decide on a culture by looking at the incoming HTTP request, because that culture determines the localization of the Page.&lt;/p&gt;

&lt;p&gt;There's a lot of places this information could be stored, but the most common are cookies 🍪 or the URL itself.&lt;/p&gt;

&lt;p&gt;Xperience's Content Tree-based routing let's developers pick from 2 options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using the host: &lt;code&gt;https://site.es/products&lt;/code&gt; - Spanish language top level domain&lt;/li&gt;
&lt;li&gt;Using the path: &lt;code&gt;https://site.com/es-MX/products&lt;/code&gt; - Mexican Spanish language path prefix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scenario I'm interested in is the latter because it supports having 2 cultures on the same domain.&lt;/p&gt;




&lt;p&gt;When looking for the culture in the URL path, Xperience uses route constraints and tokens to pull the culture value out. It has a specific URL pattern, using the path prefix (&lt;code&gt;https://stuff/en-US/more-stuff&lt;/code&gt;) which makes it easy to pinpoint the culture 🧐.&lt;/p&gt;

&lt;p&gt;In the Xperience ASP.NET Core integration, Xperience has some &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-5.0"&gt;middleware&lt;/a&gt; 🤓 that looks at the incoming request context and figures out what culture to associate with it (based on the &lt;a href="https://docs.xperience.io/multilingual-websites/setting-up-multilingual-websites/configuring-urls-for-multilingual-websites"&gt;settings&lt;/a&gt; in the application).&lt;/p&gt;

&lt;p&gt;Once it figures out the culture for the request URL, it then sets the culture for the thread/context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentCulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestCulture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Culture&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentUICulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestCulture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UICulture&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  🧭 Finding the Request Culture in API Endpoints
&lt;/h2&gt;

&lt;p&gt;When routing to an ASP.NET Controller or View associated with a Page in the content tree, Xperience does all the traffic management.&lt;/p&gt;

&lt;p&gt;But our custom API endpoints aren't associated with anything in the content tree, which means Xperience is hands off 👐!&lt;/p&gt;



&lt;p&gt;Let's assume we have a content tree route to a Product Page that is handled by Xperience and we are visiting the (Mexican) Spanish language version of that Page:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET https://site.com/es-MX/product/apples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We rendered the product id for "apples" into the Razor View for this page, and we use that value to make an XHR request back to our API:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@model ProductViewModel

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;@Model.Name&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt; @* This will be "Manzanas" *@

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here's the code that makes up our &lt;code&gt;ProductsApiController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/[controller]"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsApiController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{productId:int}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;culture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentCulture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Our original Razor View and HTML document were rendered using the Spanish language content and the URL this page is loaded from has the &lt;code&gt;/es-MX&lt;/code&gt; path prefix.&lt;/p&gt;

&lt;p&gt;When this &lt;code&gt;GetProduct&lt;/code&gt; method is called from the XHR made by our JavaScript &lt;code&gt;fetch()&lt;/code&gt; request, what will the value of &lt;code&gt;culture&lt;/code&gt; be 🤔😕?&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;It's a trick question 🤦🏽‍♂️! I haven't told you what the &lt;a href="https://docs.xperience.io/multilingual-websites/setting-up-multilingual-websites/setting-default-languages-for-users-and-visitors"&gt;default culture&lt;/a&gt; is for our site.&lt;/p&gt;

&lt;p&gt;Since there's no culture code in the XHR request's URL, Xperience will associate it with whatever the default culture is for the site that the domain is associated with 😮.&lt;/p&gt;

&lt;p&gt;In my specific use-case, it's English. This means that the &lt;code&gt;culture&lt;/code&gt; variable will have the value &lt;code&gt;en-US&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How can we get that to be &lt;code&gt;es-MX&lt;/code&gt; so that any data we retrieve from the Xperience database and return from our API endpoint will be localized to the Page's culture 🤷🏼‍♀️?&lt;/p&gt;
&lt;h2&gt;
  
  
  🤝🏻🤝🤝🏾 Passing the Culture
&lt;/h2&gt;

&lt;p&gt;There are many ways to pass the culture information in a URL and they would all work - really all we need to do is get the culture code to be a parameter (probably query string) that our &lt;code&gt;GetProduct()&lt;/code&gt; method can directly access.&lt;/p&gt;

&lt;p&gt;Then we can the &lt;code&gt;CultureInfo&lt;/code&gt; constructor and the code pattern that Xperience already uses to set the culture on the current thread 👍🏾:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;culture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cultureInfo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCultureInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;culture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentCulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cultureInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentUICulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cultureInfo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The problem with this approach is we are now cluttering up our API action method with assigning the thread culture and we have this weird query string parameter we have to remember to include in all our other API requests and action methods.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We also don't have access to the correct culture until our Controller action method executes, which means Action filters and anything else that executes before the action are using our default culture 😣.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A better approach is to use the same feature that Xperience relies on for parsing out and assigning a culture to a Page's HTTP request.&lt;/p&gt;

&lt;p&gt;We just need to make sure our API URL includes a special &lt;code&gt;{culture}&lt;/code&gt; token in the route definition:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/{culture}/[controller]"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- new route token&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsApiController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{productId:int}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ActionResult&lt;/span&gt; &lt;span class="nf"&gt;GetProduct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;culture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentCulture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// ^ culture will have the correct value now&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And, then include that in the &lt;code&gt;fetch()&lt;/code&gt; request URL (we will source the culture value from &lt;code&gt;CultureInfo.CurrentCulture.Name&lt;/code&gt; which is correctly set when rendering our Page):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;culture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@CultureInfo.CurrentCulture.Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;culture&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/products/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I think this is a much nicer solution 👏🏽!&lt;/p&gt;

&lt;p&gt;Our API URLs match the same culture path prefix pattern that our Page URLs have and we don't have to worry about extra culture related values and logic in our API action methods 👍🏼.&lt;/p&gt;
&lt;h2&gt;
  
  
  🧠 Conclusion
&lt;/h2&gt;

&lt;p&gt;Globalization and localization are important features when they're needed, but they tend to create new problems that need solutions, especially if we're used to not worrying about other languages (like me!).&lt;/p&gt;

&lt;p&gt;Kentico Xperience supports localization as a first class feature and, for the most part, handles it all for us 😉.&lt;/p&gt;

&lt;p&gt;However, when we start to come up with clever technical solutions for our application requirements, we need to be sure to carry all the existing features forward.&lt;/p&gt;

&lt;p&gt;Fortunately, culture-aware XHR requests can be enabled in an elegant and simple way with Kentico Xperience 13.0 sites.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;
&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization"&gt;.NET Documentation on Localization and Globalization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.xperience.io/multilingual-websites/setting-up-multilingual-websites/"&gt;Xperience 13.0 Documentation on Setting up Multilingual Websites&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__57801"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/xperience" class="ltag__tag__link"&gt;xperience&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my &lt;a href="https://dev.to/seangwright/series"&gt;Kentico Xperience blog series&lt;/a&gt;, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/8185"&gt;Kentico Xperience Xplorations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/9483"&gt;Kentico Xperience MVC Widget Experiments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/1046"&gt;Kentico 12 - Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>aspnetcore</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Kentico CMS Quick Tip: Using Font Awesome Icons for Custom Page Types</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Tue, 22 Sep 2020 14:30:06 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-cms-quick-tip-using-font-awesome-icons-for-custom-page-types-3fcb</link>
      <guid>https://forem.com/wiredviews/kentico-cms-quick-tip-using-font-awesome-icons-for-custom-page-types-3fcb</guid>
      <description>&lt;h2&gt;
  
  
  Our Requirements
&lt;/h2&gt;

&lt;p&gt;We want to give our Content Management team the ability to quickly identify the Custom Page Types we create for them, both in the Content Tree and when creating new Pages, using the huge &lt;a href="https://fontawesome.com/icons?d=gallery" rel="noopener noreferrer"&gt;Font Awesome 5&lt;/a&gt; font icons set, integrated into Kentico Xperience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TLDR; I created a .NET CLI global tool anyone can install, called &lt;a href="https://github.com/wiredviews/xperience-font-awesome-integrator" rel="noopener noreferrer"&gt;Xperience Font Awesome Integrator&lt;/a&gt;, that will do all the hard work, described below, for you 🤗.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How Can We Assign Icons to Custom Page Types?
&lt;/h2&gt;

&lt;p&gt;Kentico Xperience focuses most of the application's content management tasks on the Content Tree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6e9p5q33gv5xuopusgw1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6e9p5q33gv5xuopusgw1.jpg" alt="Xperience Content Tree structure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The items in this tree structure are identified by their position in the hierarchy, their name, and the icon that displays next to each of them.&lt;/p&gt;

&lt;p&gt;We can &lt;a href="https://docs.kentico.com/developing-websites/defining-website-content-structure/managing-page-types/changing-page-type-icons" rel="noopener noreferrer"&gt;change those icons&lt;/a&gt; for each Page Type by editing the settings in the Page Types module 👍🏾.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fia7uarlamgw5r0rshmbw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fia7uarlamgw5r0rshmbw.jpg" alt="Custom Page Type General settings view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing Solution
&lt;/h2&gt;

&lt;p&gt;Xperience provides us with a &lt;a href="https://devnet.kentico.com/docs/icon-list/index.html" rel="noopener noreferrer"&gt;pre-defined set of icons&lt;/a&gt; we can use to help both developers and content editors identify Custom Page Types.&lt;/p&gt;

&lt;p&gt;These icons are always used in the Dancing Goat demo application, and for good reason - they make it much easier to identify content types and content organization 🧐.&lt;/p&gt;

&lt;p&gt;While the list of included icons is pretty nice, it's definitely been curated to be used with Xperience's demo sites and other visual elements in the Content Management application.&lt;/p&gt;

&lt;p&gt;What if we can't find an appropriate icon in the list 🤔?&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution - Use Font Awesome 5
&lt;/h2&gt;

&lt;p&gt;We can follow &lt;a href="https://docs.kentico.com/k12sp/custom-development/working-with-font-icons" rel="noopener noreferrer"&gt;the instructions in the Xperience docs&lt;/a&gt; that explain how to add custom font icons to the application.&lt;/p&gt;

&lt;p&gt;The process requires the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the folder &lt;code&gt;CMS\App_Themes\Default\Custom&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a sub-folder that will contain the custom font files (ex: &lt;code&gt;CMS\App_Themes\Default\Custom\fonts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;style.css&lt;/code&gt; file that contains the style rules to assign the font icons to DOM elements, taking into account the specificity requirements of the Content Management application&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;icon-css-classes.txt&lt;/code&gt; manifest file which will populate the icon selector controls with our custom icons&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's look at how we'd apply this steps for the (awesome 🤣) &lt;a href="https://fontawesome.com/" rel="noopener noreferrer"&gt;Font Awesome 5&lt;/a&gt; icons!&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract the Font Awesome Assets
&lt;/h3&gt;

&lt;p&gt;Whether we &lt;a href="https://fontawesome.com/download" rel="noopener noreferrer"&gt;download the Free icons&lt;/a&gt; or we have access to the Pro versions, we will end up with a &lt;code&gt;.zip&lt;/code&gt; file which we can extract to a folder.&lt;/p&gt;

&lt;p&gt;The extracted folders might be nested - we are interested in the one containing all the asset folders (&lt;code&gt;\css&lt;/code&gt;, &lt;code&gt;\js&lt;/code&gt;, ect...):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fw76uipc8bgz9dy51oyz1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fw76uipc8bgz9dy51oyz1.jpg" alt="Windows Explorer view of Font Awesome assets folder"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Copy the Webfonts folder to Xperience
&lt;/h3&gt;

&lt;p&gt;We need the font files, that are used to display the icons, in our application.&lt;/p&gt;

&lt;p&gt;This is easily accomplished by copying the &lt;code&gt;\webfonts&lt;/code&gt; folder from the Font Awesome folder to &lt;code&gt;CMS\App_Themes\Default\Custom&lt;/code&gt; 😉.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convert the Metadata to a Manifest
&lt;/h3&gt;

&lt;p&gt;Open the &lt;code&gt;\metadata\icons.json&lt;/code&gt; file and take a look at the metadata for all icons included what we downloaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"500px"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ligatures"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"styles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"brands"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unicode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f26e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500px"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"voted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"svg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"free"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"brands"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We are most interested in the keys of the top level object (ex: &lt;code&gt;500px&lt;/code&gt;) and the &lt;code&gt;styles&lt;/code&gt; array of each of the child objects (ex: &lt;code&gt;["brands"]&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;We want to convert each one of the keys and their styles to a line in the &lt;code&gt;icon-css-classes.txt&lt;/code&gt; that we need to create.&lt;/p&gt;

&lt;p&gt;We can now create our empty icon manifest file at &lt;code&gt;CMS\App_Themes\Default\Custom\icon-css-classes.txt&lt;/code&gt; and start adding entries.&lt;/p&gt;

&lt;p&gt;The example JSON above would result in 1 line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fa fab-500px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If we scroll down the &lt;code&gt;icons.json&lt;/code&gt; file and find the entry for &lt;code&gt;address-book&lt;/code&gt;, we can see it is available in multiple styles (&lt;code&gt;["solid", "regular"]&lt;/code&gt;). This means we should create a line in our &lt;code&gt;icon-css-classes.txt&lt;/code&gt; for each style for this icon 🧐:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fa fab-500px
...
fa fas-address-book
fa far-address-book
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We will need entries in our &lt;code&gt;icon-css-classes.txt&lt;/code&gt; file for every icon we want available in the Xperience Content Management 😲.&lt;/p&gt;
&lt;h3&gt;
  
  
  Adapt the CSS File
&lt;/h3&gt;

&lt;p&gt;The Font Awesome download comes with a set of &lt;code&gt;.css&lt;/code&gt; files that define the style rules to use the icons in our application.&lt;/p&gt;

&lt;p&gt;If we open up &lt;code&gt;css\all.css&lt;/code&gt; we can see all the rules defined for the entire icon set:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/*!
 * Font Awesome Free 5.14.0 by @fontawesome - https://fontawesome.com
 * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
 */&lt;/span&gt;
&lt;span class="nc"&gt;.fa&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.fas&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.far&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.fal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.fad&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.fab&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;-moz-osx-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grayscale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;antialiased&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-rendering&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.fa-lg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.33333em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;vertical-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-.0667em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To ensure we override the specificity rules of the icon CSS included in Xperience, we need to modify the rules in &lt;code&gt;.all.css&lt;/code&gt; to be more specific.&lt;/p&gt;

&lt;p&gt;Let's copy &lt;code&gt;all.css&lt;/code&gt; to &lt;code&gt;CMS\App_Themes\Default\Custom\style.css&lt;/code&gt; and begin 📝 editing it.&lt;/p&gt;

&lt;p&gt;First, we need to augment the selector for the general icon CSS rules:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fa&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;.fas&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;.far&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;.fal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;.fad&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nc"&gt;.fab&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fa&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fa&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fab&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fab&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fad&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fad&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fal&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.far&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.far&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fas&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fas&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;-moz-osx-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grayscale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;-webkit-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;antialiased&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Adding these additional selectors ensures that our Font Awesome icons use the correct CSS rules when displayed in the application 🙄.&lt;/p&gt;

&lt;p&gt;We need to go through this file and replace each individual collection (Regular, Brand, Light, Solid, Duoton) selector with one that includes the CMS selectors.&lt;/p&gt;

&lt;p&gt;Here is an example of the updated selector for Brand icons:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fab&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;'icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fab&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="nc"&gt;.cms-bootstrap&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s2"&gt;' icon-'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="nc"&gt;.fab&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Finally, we need to update the paths to the webfont files in the &lt;code&gt;@font-face&lt;/code&gt; rules, which are slightly different when integrated into Xperience, changing &lt;code&gt;url("../webfonts&lt;/code&gt; to &lt;code&gt;url("./webfonts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Font Awesome 5 Brands'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("./webfonts/fa-brands-400.eot")&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("./webfonts/fa-brands-400.eot?#iefix")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"embedded-opentype"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sx"&gt;url("./webfonts/fa-brands-400.woff2")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff2"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sx"&gt;url("./webfonts/fa-brands-400.woff")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sx"&gt;url("./webfonts/fa-brands-400.ttf")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"truetype"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sx"&gt;url("./webfonts/fa-brands-400.svg#fontawesome")&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"svg"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Update our Project
&lt;/h3&gt;

&lt;p&gt;The last step is to ensure all the new files we added to our application are &lt;a href="http://f5debug.net/how-to-add-existing-folder-or-directory-to-visual-studio-project-solution/" rel="noopener noreferrer"&gt;included in the ASP.NET project&lt;/a&gt;, so that when we deploy our site, these files are deployed as well 😉.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When we run our application and navigate to the icon selector for a Custom Page Type, we can see the vastly enhanced list of icons available, which now included the entire set from Font Awesome 5!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjwtpnt5wfwq56eb8sokp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjwtpnt5wfwq56eb8sokp.gif" alt="Huge scrollable list of icons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If some of these steps seem confusing and you think creating a 1600 line long icon manifest file by hand (for the Free version) is a ridiculous idea (I know I do 🤪!), then fear not!&lt;/p&gt;

&lt;p&gt;I created a .NET Core global tool called &lt;a href="https://github.com/wiredviews/xperience-font-awesome-integrator" rel="noopener noreferrer"&gt;Xperience Font Awesome Integrator&lt;/a&gt; that does all this work for you and takes about 1 second to execute 🍻🎊🥳.&lt;/p&gt;

&lt;p&gt;Running it from the command line looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; xperience-fa-integrator &lt;span class="nt"&gt;-fpath&lt;/span&gt; C:&lt;span class="se"&gt;\d&lt;/span&gt;ev&lt;span class="se"&gt;\f&lt;/span&gt;ontawesome-free-5.14.0-web &lt;span class="nt"&gt;-cpath&lt;/span&gt; C:&lt;span class="se"&gt;\d&lt;/span&gt;ev&lt;span class="se"&gt;\X&lt;/span&gt;perience&lt;span class="se"&gt;\C&lt;/span&gt;MS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pretty cool!&lt;/p&gt;

&lt;p&gt;It works with both the Free and Pro versions of the Font Awesome 5 icons.&lt;/p&gt;

&lt;p&gt;Give it a try and let me know what you think 👋🏼.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;



&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md" rel="noopener noreferrer"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tags here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__57801"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/xperience" class="ltag__tag__link"&gt;xperience&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my &lt;a href="https://dev.to/seangwright/series"&gt;Kentico Xperience blog series&lt;/a&gt;, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/7366"&gt;Kentico Xperience 13 Beta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/seangwright/series/8185"&gt;Kentico Xperience Xplorations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12 - Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>css</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Kentico CMS Quick Tip: Azure Active Directory Authentication</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 20 Jul 2020 18:00:10 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-cms-quick-tip-azure-active-directory-authentication-n9j</link>
      <guid>https://forem.com/wiredviews/kentico-cms-quick-tip-azure-active-directory-authentication-n9j</guid>
      <description>&lt;h2&gt;
  
  
  Our Requirements
&lt;/h2&gt;

&lt;p&gt;Give our Content Management team a way to authenticate into &lt;a href="https://xperience.io/" rel="noopener noreferrer"&gt;Kentico CMS&lt;/a&gt; using their existing Azure Active Directory (AD) accounts and also &lt;em&gt;keep&lt;/em&gt; individual account authentication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This article applies to Kentico 12. If you are using Kentico Xperience 13, please see the note at the end of the article to see how configuration is different in v13.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How Does the Built In Authentication Work?
&lt;/h2&gt;

&lt;p&gt;Authenticating into the Kentico CMS content management application is a pretty simple process 😉:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to &lt;code&gt;/Admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Unauthenticated users are redirected to &lt;code&gt;/CMSPages/logon.aspx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After entering a valid username and password, the application responds with an Authentication cookie.&lt;/li&gt;
&lt;li&gt;Requests to the application check for this cookie when accessing routes requiring authentication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This process relies on ASP.NET's &lt;a href="https://docs.microsoft.com/en-us/aspnet/web-forms/overview/older-versions-security/introduction/an-overview-of-forms-authentication-cs" rel="noopener noreferrer"&gt;Forms Authentication&lt;/a&gt; and all credentials are stored in the application's database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Existing Solutions
&lt;/h2&gt;

&lt;p&gt;If we want to integrate Azure AD into Kentico CMS for authentication, maybe we can use an existing open source solution or one blogged about by the Kentico CMS community 🧐!&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-In Claims Based Authentication
&lt;/h3&gt;

&lt;p&gt;First off, there is a solution that Kentico Xperience supports out of the box and it's detailed &lt;a href="https://devnet.kentico.com/articles/integrate-azure-active-directory-with-kentico" rel="noopener noreferrer"&gt;on Kentico's DevNet&lt;/a&gt; by a Kentico employee 👏🏽. Jeroen Furst, a fellow Kentico MVP 🤓, also detailed &lt;a href="https://blogs.jeroenfurst.nl/blog/november-2018/claims-based-authentication-using-azure-ad" rel="noopener noreferrer"&gt;this process awhile ago on his blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, this solution completely replaces Kentico Xperience's Forms Authentication with Azure AD authentication because &lt;a href="https://docs.kentico.com/k12sp/managing-users/user-registration-and-authentication/claims-based-authentication" rel="noopener noreferrer"&gt;"Mixed authentication mode of claims-based and forms authentication is also not supported."&lt;/a&gt; 😢.&lt;/p&gt;

&lt;p&gt;This means all users of the application will need to have Azure AD accounts in the tenant specified by the App Registration in Azure AD.&lt;/p&gt;

&lt;p&gt;As an agency, we want to use Azure AD for our authentication into our client's sites, but we also want to allow them to continue using Forms Authentication for their access. We definitely don't want to have all our clients accounts managed in our Azure AD tenant 😅!&lt;/p&gt;

&lt;h3&gt;
  
  
  Portal Engine Public Site Authentication
&lt;/h3&gt;

&lt;p&gt;Kentico partner, &lt;a href="https://www.deleteagency.com/" rel="noopener noreferrer"&gt;Delete Agency&lt;/a&gt;, also has &lt;a href="https://github.com/kate-orlova/azure-ad-auth-in-kentico" rel="noopener noreferrer"&gt;a solution up on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This approach is much closer to what we want, as it augments rather than replaces the normal Forms Authentication 💪🏾.&lt;/p&gt;

&lt;p&gt;However, the use-case is slightly different. This code provides authentication for Portal Engine based Kentico applications and the authentication is for live site users, not content management users 😣.&lt;/p&gt;

&lt;p&gt;We want to follow the authentication steps that were detailed at the beginning of this post, but allow our employees to authenticate with Azure AD instead of application credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution
&lt;/h2&gt;

&lt;p&gt;Despite the fact that the Delete approach is for live site authentication, it follows the general outline of our approach 👍🏼.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing the Logon Form
&lt;/h3&gt;

&lt;p&gt;First, let's update the normal back end login form found in &lt;code&gt;CMSApp/CMSPages/logon.aspx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Replace the last &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; inside the &lt;code&gt;&amp;lt;asp:Panel&amp;gt;&lt;/code&gt; (starting on line  100) with the following:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt; 
     &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display: flex; justify-content: space-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;cms:LocalizedButton&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"btnLogonAzureAD"&lt;/span&gt; 
            &lt;span class="na"&gt;ButtonStyle=&lt;/span&gt;&lt;span class="s"&gt;"Primary"&lt;/span&gt; 
            &lt;span class="na"&gt;CssClass=&lt;/span&gt;&lt;span class="s"&gt;"login-btn"&lt;/span&gt;
            &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"Logon With Azure AD"&lt;/span&gt; &lt;span class="na"&gt;runat=&lt;/span&gt;&lt;span class="s"&gt;"server"&lt;/span&gt;
            &lt;span class="na"&gt;OnClick=&lt;/span&gt;&lt;span class="s"&gt;"btnLogonAzureAD_Click"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;cms:LocalizedButton&lt;/span&gt; &lt;span class="na"&gt;ID=&lt;/span&gt;&lt;span class="s"&gt;"LoginButton"&lt;/span&gt; 
            &lt;span class="na"&gt;runat=&lt;/span&gt;&lt;span class="s"&gt;"server"&lt;/span&gt; 
            &lt;span class="na"&gt;CommandName=&lt;/span&gt;&lt;span class="s"&gt;"Login"&lt;/span&gt; &lt;span class="na"&gt;ValidationGroup=&lt;/span&gt;&lt;span class="s"&gt;"Login1"&lt;/span&gt;
            &lt;span class="na"&gt;ButtonStyle=&lt;/span&gt;&lt;span class="s"&gt;"Primary"&lt;/span&gt; &lt;span class="na"&gt;CssClass=&lt;/span&gt;&lt;span class="s"&gt;"login-btn"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;All we did here was add a new button with the text "Logon With Azure AD" and some styles and markup to layout the buttons correctly.&lt;/p&gt;

&lt;p&gt;Now let's add the C# in &lt;code&gt;logon.aspx.cs&lt;/code&gt; to wire the button up:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;btnLogonAzureAD_Click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We will uncomment this line once we've created the&lt;/span&gt;
    &lt;span class="c1"&gt;// handler class&lt;/span&gt;

    &lt;span class="c1"&gt;// AzureADAuthenticationHandler.OnLogin(ReturnUrl);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Pretty easy 😅!&lt;/p&gt;

&lt;p&gt;If we start up the content management application and navigate to &lt;code&gt;/Admin&lt;/code&gt; (assuming we are not logged in), we will see this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffy5ucpr02uln6xld2zjd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffy5ucpr02uln6xld2zjd.jpg" alt="Kentico logon form with additional Azure AD logon button"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating a Custom &lt;code&gt;IHttpHandler&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Now we can create the handler class referenced in our &lt;code&gt;logon.aspx.cs&lt;/code&gt; above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are adding this directly the CMS code, you can create the class in &lt;code&gt;Old_App_Code&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Handles both the initial OAuth request to Azure AD and the redirect response&lt;/span&gt;
&lt;span class="c1"&gt;/// to authenticate the user into Kentico.&lt;/span&gt;
&lt;span class="c1"&gt;/// &lt;/span&gt;
&lt;span class="c1"&gt;/// Operates according to the flow described here https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AzureADAuthenticationHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHttpHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;redirectUriPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;groupClaimId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;stateCookieName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"azure_ad_sso_state"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsReusable&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;AzureADAuthenticationHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConfigurationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppSettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;clientId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:client-id"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;redirectUriPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:redirect-uri-path"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:tenant-id"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;clientSecret&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:client-secret"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;groupClaimId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:group-claim-id"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;First we read in a bunch of values from App Settings when the application starts up. These will all be populated from configuration in the &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app" rel="noopener noreferrer"&gt;Azure AD App Registration&lt;/a&gt; that we will create later 😎.&lt;/p&gt;

&lt;p&gt;Now we will create the &lt;code&gt;Logon&lt;/code&gt; method that will be called by &lt;code&gt;logon.aspx.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UriBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"https://login.microsoftonline.com/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/oauth2/v2.0/authorize"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="cm"&gt;/*
    * This state is both stored in a cookie and sent to the Azure AD authenticate URL
    * via query string.
    * 
    * When the redirect comes back after the Azure AD sign-in, we want to ensure
    * the request isn't an XSRF or replay attach so we pull the state values
    * out of the cookie and ensure they match what's in the query string.
    * 
    * The "ReturnUrl" captured by the Kentico login page allows us to send the user back
    * to their original destination if they were redirected to login for authentication.
    * 
    * After logging in we clear the state cookie.
    */&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OIDCState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ReturnUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;encodedState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParseQueryString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NameValueCollection&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"response_type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"response_mode"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// We request the 'profile' scope to get access to the user's Azure AD groups&lt;/span&gt;
        &lt;span class="c1"&gt;// to ensure they are allowed to SSO into Kentico&lt;/span&gt;
        &lt;span class="c1"&gt;// https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims#configuring-groups-optional-claims&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"openid profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLeftPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UriPartial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authority&lt;/span&gt;&lt;span class="p"&gt;)}{&lt;/span&gt;&lt;span class="n"&gt;redirectUriPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encodedState&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="cm"&gt;/*
    * We can't use CookieHelper.SetValue() because it requires an expiration
    * date and Chrome won't process the set-cookie header on 302 responses
    * when an Expires cookie value is set
    * https://bugs.chromium.org/p/chromium/issues/detail?id=696204
    * 
    * This problem does not appear in localhost requests or in other browsers
    */&lt;/span&gt;
    &lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateCookieName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;HttpOnly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encodedState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SameSite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SameSiteMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Secure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This code creates a security cookie / state value to protect against XSRF attacks 😮 and also generates a redirect request to Azure AD to ensure the user is authenticated there.&lt;/p&gt;

&lt;p&gt;We now create an &lt;code&gt;Authenticate()&lt;/code&gt; method that handles the request back to our application, redirected from Azure AD after the user is authenticated there:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;Authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthenticationHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsAuthenticated&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryString&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;encodedRequestState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;encodedCookieState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CookieHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateCookieName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encodedRequestState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encodedCookieState&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;requeststate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeserializeObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OIDCState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nf"&gt;Base64Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encodedRequestState&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cookiestate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeserializeObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OIDCState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nf"&gt;Base64Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encodedCookieState&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requeststate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cookiestate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"https://login.microsoftonline.com/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/oauth2/v2.0/token"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;NameValueCollection&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"openid profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redirect_uri"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLeftPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UriPartial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authority&lt;/span&gt;&lt;span class="p"&gt;)}{&lt;/span&gt;&lt;span class="n"&gt;redirectUriPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"grant_type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"client_secret"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientSecret&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;responseString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebClient&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UploadValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;responseString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;EventLogProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AzureADAuthenticationHandler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"AUTH_FAILURE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseString&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeserializeObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OIDCResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;responseString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JwtSecurityTokenHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="cm"&gt;/*
    * Here are the claims available in each token type
    * https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
    * https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
    * 
    * The difference between the token types is important when using them to access
    * APIs on behalf of the user, but we only use them for their payload data.
    */&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadJwtToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;idToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadJwtToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"groups"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupClaimId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;upn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetClaimValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"upn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetClaimValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="cm"&gt;/*
    * Guest accounts (invited from another Azure AD tenant/domain) don't expose the 'upn' claim
    * so we have to get the username/email from the 'email' claim
    */&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upn&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UserInfo&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;IsExternal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetClaimValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"given_name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetClaimValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"family_name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FullName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetClaimValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;SiteIndependentPrivilegeLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserPrivilegeLevelEnum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GlobalAdmin&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="n"&gt;UserInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;UserInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUserToSite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SiteContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentSiteName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;UserInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUserToRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;AuthenticationHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AuthenticateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;CookieHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stateCookieName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requeststate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReturnUrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/Admin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requeststate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReturnUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To get all the compilation errors to go away we need a couple utility methods and classes:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Base64Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;valueBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Base64Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;valueBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetClaimValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JwtSecurityToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;claimType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Claims&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
          &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claimType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OIDCState&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ReturnUrl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OIDCResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;AccessToken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"token_type"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;TokenType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expires"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;Expires&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Scope&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RefreshToken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;JsonProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id_token"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;IdToken&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We also need to install a NuGet package, &lt;a href="https://www.nuget.org/packages/System.IdentityModel.Tokens.Jwt/" rel="noopener noreferrer"&gt;System.IdentityModel.Tokens.Jwt&lt;/a&gt;, to help us parse the Jwt that comes back from Azure AD when we request it to verify the token sent to us in the URL query string.&lt;/p&gt;



&lt;p&gt;Finally, we need to implement &lt;code&gt;IHttpHandler&lt;/code&gt; and add a &lt;code&gt;ProcessRequest()&lt;/code&gt; method:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ProcessRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;EventLogProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Azure AD Authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"AUTHENTICATIONFAIL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"Could not log in user"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;EventLogProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Azure AD Authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"AUTHENTICATIONSUCCESS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"Authentication success"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;URLHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LocalRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;returnUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We can now go back to &lt;code&gt;logon.aspx.cs&lt;/code&gt; and comment the line &lt;code&gt;AzureADAuthenticationHandler.OnLogin(ReturnUrl);&lt;/code&gt; 👍🏻.&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding the Handler to the &lt;code&gt;web.config&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We want our handler to handle any requests matching a specific URL pattern, which in following &lt;a href="https://auth0.com/docs/protocols/oidc" rel="noopener noreferrer"&gt;OIDC authentication&lt;/a&gt; conventions will be &lt;code&gt;/oidc-callback&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since our handler class inherits from &lt;code&gt;IHttpHandler&lt;/code&gt;, ASP.NET will pass it requests matching a pattern by registering it in the application &lt;code&gt;web.config&lt;/code&gt; correctly:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

&lt;span class="nt"&gt;&amp;lt;system.webServer&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;handlers&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"AzureADOIDCCallback"&lt;/span&gt; 
         &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"oidc-callback"&lt;/span&gt; &lt;span class="na"&gt;verb=&lt;/span&gt;&lt;span class="s"&gt;"GET"&lt;/span&gt; 
        &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"Namespace.AzureADAuthenticationHandler, CMSApp"&lt;/span&gt;
        &lt;span class="na"&gt;preCondition=&lt;/span&gt;&lt;span class="s"&gt;"integratedMode,runtimeVersionv4.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/handlers&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;In the above code, &lt;code&gt;Namespace.AzureADAuthenticationHandler&lt;/code&gt; should be the namespace of your &lt;code&gt;AzureADAuthenticationHandler&lt;/code&gt; class and &lt;code&gt;CMSApp&lt;/code&gt; should be the name of the assembly your class is located in.&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating an Application Registration in Azure
&lt;/h3&gt;

&lt;p&gt;Now we can create a new App Registration in Azure AD.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;https://portal.azure.com&lt;/a&gt;, open the Azure AD blade (or search for Azure AD in the search bar), select "App Registrations" from the left side menu, and click "+ New Registration" at the top-left of the screen.&lt;/p&gt;

&lt;p&gt;Now, fill in the details, giving it a friendly name and specifying the full Redirect URI with the path matching what we defined in the &lt;code&gt;web.config&lt;/code&gt; for the handler (eg: &lt;code&gt;https://localhost:44332/oidc-callback&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4yj0mc9ox40cn4vhjjn9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4yj0mc9ox40cn4vhjjn9.jpg" alt="Setting application Redirect URI in Azure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, collect the identifiers from the "Overview" blade accessed from the left side menu. We want the "Application (client) ID" and "Directory (tenant) ID":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fq92c34ghql6kkppzo0lv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fq92c34ghql6kkppzo0lv.jpg" alt="App registration properties"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This implementation restricts authentication to users that are in a specific Azure AD group, but to get access to a user's groups, we need to add an "Optional claim" by selecting the "Token configuration" option from the left menu 🧐.&lt;/p&gt;

&lt;p&gt;Select "+ Add groups claim" and check "Security groups" and save:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fge7adz9thahhjihyvyu5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fge7adz9thahhjihyvyu5.jpg" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last step for the App Registration is to create a client secret that the Kentico application can use to verify the OIDC callback token and get the ID and Access tokens from the Azure AD API.&lt;/p&gt;

&lt;p&gt;Select "Certificates &amp;amp; secrets" from the left side menu and click "+ New client secret":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu42miet848h4adikgnew.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu42miet848h4adikgnew.jpg" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have to make sure we copy the client secret and save it as we won't be able to see it again later 😏.&lt;/p&gt;

&lt;p&gt;Finally, create an Azure AD Group and assign some users to it - only users in this group will be allowed to use the Azure AD SSO functionality in Kentico. Copy its "Object Id" from the Groups list and save this for our App Settings.&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding Our Application Settings
&lt;/h3&gt;

&lt;p&gt;Now we only need to configure the application settings by copying the values from eariler.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tenant-id&lt;/code&gt; from the App Registration Overview&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client-id&lt;/code&gt; from the App Registration Overview&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirect-uri-path&lt;/code&gt; matches the &lt;code&gt;web.config&lt;/code&gt; &lt;code&gt;path&lt;/code&gt; for the handler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client-secret&lt;/code&gt; from the client secret we created in the "Certificats &amp;amp; secrets" blade&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;group-claim-id&lt;/code&gt; from the "Object Id" in the Azure AD Groups blade&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;

  &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:tenant-id"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:client-id"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:redirect-uri-path"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"/oidc-callback"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:client-secret"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; &lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:group-claim-id"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The only other setting we need is to tell Kentico to ignore the &lt;code&gt;/oidc-callback&lt;/code&gt; path and not try to match it up with a path handled by the content management application URL processing used in Portal Engine sites 😉.&lt;/p&gt;

&lt;p&gt;Log into the content management app, and navigate to the Settings module. Click "URLs and SEO", scroll to "URL format", find the Excluded URLs setting and add &lt;code&gt;/oidc-callback&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kentico Xperience 13 configuration
&lt;/h3&gt;

&lt;p&gt;If you are using Kentico Xperience 13, you'll notice that the "Excluded URLs" field in the Settings module no longer exists. There is a "Excluded Alternative URLs" field, but this configures the live site ASP.NET Core application, not the CMS administration application.&lt;/p&gt;

&lt;p&gt;To hook into the request pipeline and tell Xperience to ignore handling requests starting with &lt;code&gt;/oidc-callback&lt;/code&gt;, you can add this custom module to your &lt;code&gt;CMSApp&lt;/code&gt; project.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;RegisterModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AzureADAuthenticationRoutingModule&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

   &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;AppCodeName.Management.Web.Authentication&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AzureADAuthenticationRoutingModule&lt;/span&gt; 
           &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Module&lt;/span&gt;
       &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AzureADAuthenticationRoutingModule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
               &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AzureADAuthenticationRoutingModule&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
           &lt;span class="p"&gt;{&lt;/span&gt;
               &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;appSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConfigurationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppSettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

               &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;redirectUriPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appSettings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"authentication:azure-ad:redirect-uri-path"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

               &lt;span class="n"&gt;URLRewritingEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessRewritingResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Before&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
               &lt;span class="p"&gt;{&lt;/span&gt;
                   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RelativePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redirectUriPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                   &lt;span class="p"&gt;{&lt;/span&gt;
                       &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                   &lt;span class="p"&gt;}&lt;/span&gt;
               &lt;span class="p"&gt;};&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;
       &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;This code tells the administration application to &lt;em&gt;not&lt;/em&gt; try to handle (rewrite) requests starting with whatever value is stored in our &lt;code&gt;authentication:azure-ad:redirect-uri-path&lt;/code&gt; &lt;code&gt;web.config&lt;/code&gt; key. In this post's example, that would be &lt;code&gt;/oidc-callback&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Our Azure AD SSO integration is complete. We can now go to the login page of the content management app and click the "Logon With Azure AD", go through the OAuth redirection flow, and be logged straight into the application without having to supply any credentials 🎉🎉🥳.&lt;/p&gt;

&lt;p&gt;In addition to the above workflow, new users in our Azure AD tenant that don't already have accounts in Kentico, will have accounts automatically created for them during sign-in.&lt;/p&gt;

&lt;p&gt;The above code sets all SSO users as Global Administrators 🤠 when new accounts are being created, but we could instead add Azure AD Groups as Kentico Roles and then ensure the Roles of the authenticating user inside Kentico matched the Azure AD Groups.&lt;/p&gt;

&lt;p&gt;If you run into any issues setting this up, leave a comment below and we'll figure it out. There's a lot of separate steps here and some tricky configuration.&lt;/p&gt;

&lt;p&gt;This Azure AD authentication integration has worked great for us at  &lt;a href="https://www.wiredviews.com" rel="noopener noreferrer"&gt;WiredViews&lt;/a&gt;, reducing the complexity and tedium of user management across all our Kentico applications. I hope others find it useful as well.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;


Photo by &lt;a href="https://unsplash.com/@gabewasylko" rel="noopener noreferrer"&gt;Gabriel Wasylko
&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;





&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md" rel="noopener noreferrer"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tags here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;div class="ltag__tag ltag__tag__id__34321"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kontent" class="ltag__tag__link"&gt;kontent&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my Kentico Xperience blog series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=kentico%20ems%3A%20mvc%20widget%20experiements"&gt;Kentico EMS: MVC Widget Experiements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12: Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip"&gt;Kentico Quick Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>xperience</category>
      <category>kentico</category>
      <category>azure</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Kentico CMS Quick Tip: Creating an Email Whitelist For Testing Kentico EMS</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 13 Apr 2020 13:47:54 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-cms-quick-tip-creating-an-email-whitelist-for-testing-kentico-ems-3pn0</link>
      <guid>https://forem.com/wiredviews/kentico-cms-quick-tip-creating-an-email-whitelist-for-testing-kentico-ems-3pn0</guid>
      <description>&lt;h2&gt;
  
  
  Our Requirements
&lt;/h2&gt;

&lt;p&gt;Imagine we need to configure Kentico for testing and we have real customer or user data in the database.&lt;/p&gt;

&lt;p&gt;We need to allow for testers and stakeholders to fully test the system - this means when they perform operations that would send emails, those emails should be sent 👍🏾!&lt;/p&gt;

&lt;p&gt;However, we can't allow emails to be sent out to those real users from our testing environment 😨.&lt;/p&gt;

&lt;p&gt;Telling everyone "Don't click this button because that could send out an email to a non-test user" isn't a great solution 🙁.&lt;/p&gt;

&lt;p&gt;We need a way to whitelist a set of addresses or domains to receive emails, and intercept attempts to send emails to all others 🤔.&lt;/p&gt;

&lt;p&gt;First, we need to identify all features in Kentico EMS that could result in emails being sent...&lt;/p&gt;




&lt;h2&gt;
  
  
  What Features in Kentico EMS Send Emails?
&lt;/h2&gt;

&lt;p&gt;Kentico uses its email system to support a lot of functionality within the application. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.kentico.com/k12sp/on-line-marketing-features/managing-your-on-line-marketing-features/email-marketing"&gt;Email marketing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kentico.com/k12sp/multilingual-websites/configuring-translation-services/human-translation-services-email-translation"&gt;Page translation requests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kentico.com/k12sp/e-commerce-features/configuring-on-line-stores/configuring-e-commerce-email-notifications"&gt;E-Commerce notifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kentico.com/k12sp/securing-websites/designing-secure-websites/configuring-email-confirmations"&gt;Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kentico.com/k12sp/configuring-kentico/configuring-the-environment-for-content-editors/configuring-workflows/configuring-workflow-email-notifications"&gt;Publishing workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.kentico.com/k12sp/managing-website-content/forms/form-notification-emails"&gt;Form submissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;... and more ...&lt;/p&gt;

&lt;p&gt;That's a lot of different email configuration to manage!&lt;/p&gt;

&lt;p&gt;So, it's unlikely that we'll be able to customize all these parts of the application to whitelist emails 😒.&lt;/p&gt;

&lt;p&gt;Maybe there's a single, central service that Kentico uses to send emails where we could make our customizations?&lt;/p&gt;




&lt;h2&gt;
  
  
  CMS.EmailEngine.EmailProvider
&lt;/h2&gt;

&lt;p&gt;Fortunately for us, Kentico exposes a Provider class that does exactly what we need!&lt;/p&gt;

&lt;p&gt;Looking through the &lt;a href="https://docs.kentico.com/k12sp/custom-development/customizing-providers/custom-email-provider-example"&gt;Kentico EMS documentation&lt;/a&gt; we can find information about the &lt;code&gt;CMS.EmailEngine.EmailProvider&lt;/code&gt; class 💪🏾.&lt;/p&gt;

&lt;p&gt;The documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Customizing the email provider allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Execute custom actions when sending emails (for example logging the sent emails for auditing purposes)&lt;/li&gt;
&lt;li&gt;Use third‑party components for sending emails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you create and register your custom email provider, it is used to process all emails sent out by Kentico.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perfect!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;EmailProvider&lt;/code&gt; class exposes (3) protected methods we can override, however we only need to focus on (2) of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Synchronously sends an email through the SMTP server.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendEmailInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SMTPServerInfo&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Asynchronously sends an email through the SMTP server.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendEmailAsyncInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;SMTPServerInfo&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailToken&lt;/span&gt; &lt;span class="n"&gt;emailToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating Our &lt;code&gt;WhitelistEmailProvider&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To override and intercept Kentico's email processing, we need to create our sub-class and &lt;a href="https://docs.kentico.com/k12sp/custom-development/customizing-providers/custom-info-provider-example"&gt;register it as a custom provider&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Don't forget this attribute&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;RegisterCustomProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WhitelistEmailProvider&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Sandbox.Infrastructure.Emails&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WhitelistEmailProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailProvider&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendEmailAsyncInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;SMTPServerInfo&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailToken&lt;/span&gt; &lt;span class="n"&gt;emailToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// ... insert custom functionality&lt;/span&gt;

            &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendEmailAsyncInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emailToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendEmailInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SMTPServerInfo&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;insert&lt;/span&gt; &lt;span class="n"&gt;custom&lt;/span&gt; &lt;span class="n"&gt;functionality&lt;/span&gt;

            &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendEmailInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We still call the parent class methods (&lt;code&gt;base.SendEmailInternal()&lt;/code&gt;) because we don't want to write &lt;em&gt;all&lt;/em&gt; the email processing logic - just a small part.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configuring an Email Whitelist
&lt;/h2&gt;

&lt;p&gt;Now that we have a spot in our code base to whitelist and intercept outgoing emails, we need to decide how we are actually going to do that whitelisting.&lt;/p&gt;

&lt;p&gt;Let's use Kentico's support for &lt;a href="https://docs.kentico.com/k12sp/custom-development/creating-custom-modules/adding-custom-website-settings"&gt;Custom Settings&lt;/a&gt; to define a place where site administrators can update the email whitelist settings 🙂.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Take a look at Kentico's documentation on &lt;a href="https://docs.kentico.com/k12sp/custom-development/creating-custom-modules"&gt;Creating custom modules&lt;/a&gt; for more information on what Custom Modules are and how they work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the screenshot below you can see I've created a Custom Module named &lt;code&gt;Sandbox&lt;/code&gt; and I'm about to modify the &lt;code&gt;Settings -&amp;gt; System -&amp;gt; Emails&lt;/code&gt; node from within this module:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8RBcTiKk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/m52rbl3l274ugv51zi1d.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8RBcTiKk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/m52rbl3l274ugv51zi1d.jpg" alt="Kentico Settings tree within the Sandbox Custom Module" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I enter some values for a "New settings group":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--869ALJ6M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/wnf84ktzzbiw6kwxcmlc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--869ALJ6M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/wnf84ktzzbiw6kwxcmlc.jpg" alt="New settings group named Email Whitelist" width="682" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then I create several "New settings key" entries:&lt;/p&gt;

&lt;p&gt;Setting 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display Name: Is Whitelisting Enabled&lt;/li&gt;
&lt;li&gt;Code name: &lt;code&gt;SANDBOX_EMAIL_WHITELIST_IS_WHITELIST_ENABLED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Description: Check to enable email whitelisting&lt;/li&gt;
&lt;li&gt;Type: Boolean&lt;/li&gt;
&lt;li&gt;Editing control: Default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display Name: Whitelisted Emails&lt;/li&gt;
&lt;li&gt;Code name: &lt;code&gt;SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Description: Domain suffixes or specific email addresses, semi-colon delimited that will be whitelisted from email interception&lt;/li&gt;
&lt;li&gt;Type: Text&lt;/li&gt;
&lt;li&gt;Editing control: Form Control -&amp;gt; Text area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display Name: Intercepting Email Address&lt;/li&gt;
&lt;li&gt;Code name: &lt;code&gt;SANDBOX_EMAIL_WHITELIST_INTERCEPTING_ADDRESS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Description: When email whitelisting is enabled, any non-whitelisted emails will be send to this address instead of the intended recipients&lt;/li&gt;
&lt;li&gt;Type: Text&lt;/li&gt;
&lt;li&gt;Editing control: Default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our resulting UI for these custom settings should look like the following screenshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jiBxgwti--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/e83nssudgttzgp24s0zr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jiBxgwti--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/e83nssudgttzgp24s0zr.jpg" alt="Email whitelist settings UI" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Creating &lt;code&gt;EmailWhitelistSettings&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Now that we have a place to store our settings for email whitelisting and a custom class to override email sending functionality, we can add our custom code.&lt;/p&gt;

&lt;p&gt;First, let's create a &lt;code&gt;EmailWhitelistSettings&lt;/code&gt; class to abstract out our custom settings.&lt;/p&gt;

&lt;p&gt;We create a nested class to hold our settings keys. By using the &lt;code&gt;nameof()&lt;/code&gt; operator, I get the string value of the key to match the key name automatically 🤓:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailWhitelistSettings&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SettingKeys&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_IS_WHITELIST_ENABLED&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_IS_WHITELIST_ENABLED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_INTERCEPTING_ADDRESS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
            &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_INTERCEPTING_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We make (2) private fields to hold some default values:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fallbackWhitelist&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="s"&gt;"&amp;lt;YOUR FALLBACK WHITELIST&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fallbackInterceptingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="s"&gt;"&amp;lt;YOUR FALLBACK INTERCEPTING ADDRESS&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now we make several &lt;code&gt;static&lt;/code&gt; methods to retrieve the values from settings:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;WhitelistedRecipients&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;whitelistedRecipients&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SettingsKeyInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SettingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SANDBOX_EMAIL_WHITELIST_WHITELISTED_EMAILS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsDebug&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;whitelistedRecipients&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;whitelistedRecipients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;fallbackWhitelist&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;whitelistedRecipients&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;whitelistedRecipients&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;InterceptingAddress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;interceptingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SettingsKeyInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SettingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZEL01_EMAIL_WHITELIST_INTERCEPTING_ADDRESS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsDebug&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;interceptingAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interceptingAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;fallbackInterceptingAddress&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;interceptingAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;interceptingAddress&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsWhitelistEnabled&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;IsDebug&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SettingsKeyInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBoolValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;SettingKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ZEL01_IS_EMAIL_WHITELIST_ENABLED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What about that &lt;code&gt;IsDebug()&lt;/code&gt; method?&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsDebug&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDebug&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#if DEBUG
&lt;/span&gt;    &lt;span class="n"&gt;isDebug&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#endif
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;isDebug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can see here, that I use a &lt;a href="https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if"&gt;pre-processor directive&lt;/a&gt; to ensure that if I'm doing a &lt;code&gt;DEBUG&lt;/code&gt; build (running the site locally), I'm guaranteed to have whitelisting (and fallbacks) enabled.&lt;/p&gt;

&lt;p&gt;This prevents me from accidentally running some code locally that sends out emails to real users 😏!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We could also use a local email server for this (like &lt;a href="https://www.hmailserver.com/"&gt;hMailServer&lt;/a&gt;, &lt;a href="http://mailslurper.com/"&gt;MailSlurper&lt;/a&gt;, or &lt;a href="https://github.com/ChangemakerStudios/Papercut-SMTP"&gt;Papercut&lt;/a&gt; but a code level check is double-safe!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, we add a utility method to determine if a given email address has been whitelisted:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsWhitelistedRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;whitelistedEmails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This method will match on full address or a domain suffix. For example, if the following are whitelisted, "&lt;a href="mailto:test@test.com"&gt;test@test.com&lt;/a&gt;;@wiredviews.com", all of my co-workers could receive emails normally, along with "&lt;a href="mailto:test@test.com"&gt;test@test.com&lt;/a&gt;". All other addresses are going to be captured and re-routed 🧐.&lt;/p&gt;


&lt;h2&gt;
  
  
  Customizing the &lt;code&gt;WhitelistEmailProvider&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Ok! We're finally ready to custom the &lt;code&gt;WhitelistEmailProvider&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm only going to show the &lt;code&gt;SendEmailInternal&lt;/code&gt; method, since the other will be implemented the same way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The override is pretty simple because all the functionality is going to be in a &lt;code&gt;ProcessEmail&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendEmailInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SMTPServerInfo&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendEmailInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;siteName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;ProcessEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;smtpServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can see in &lt;code&gt;ProcessEmail&lt;/code&gt; that determines if the email should be handled or directly returned:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="nf"&gt;ProcessEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;EmailWhitelistSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsWhitelistEnabled&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;IsWhitelisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ModifyMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We use the &lt;code&gt;IsWhitelisted(MailMessage message)&lt;/code&gt; method to determine if the given message has any non-whitelisted email addresses in all of its sending fields (To, CC, Bcc):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsWhitelisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;whitelisted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmailWhitelistSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhitelistedRecipients&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmailWhitelistSettings&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsWhitelistedRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;whitelisted&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmailWhitelistSettings&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsWhitelistedRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;whitelisted&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bcc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmailWhitelistSettings&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsWhitelistedRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;whitelisted&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Finally we have a larger method, &lt;code&gt;ModifyMessage(MailMessage message)&lt;/code&gt;, which is called when the recipients of the email are not whitelisted.&lt;/p&gt;

&lt;p&gt;In this method we want to change the recipients to use the intercepting address, and record the original recipients at the bottom of the email's body:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Replaces the recipients fields with intercepting email address and appends&lt;/span&gt;
&lt;span class="c1"&gt;/// the original recipients to the end of the email&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;param name="message"&amp;gt;&amp;lt;/param&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="nf"&gt;ModifyMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ✅ Grab all the original recipients&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;originalTo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;originalCc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;originalBcc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bcc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Now clear them out&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bcc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Add our intercepting address as the only recipient&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailWhitelistSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InterceptingAddress&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Indicate in the subject this email has been overriden&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: override"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Find the HTML email (ignore plain text for now)&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nullableView&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AlternateViews&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
            &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MediaType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MediaTypeNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;nullableView&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;AlternateView&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Remove the existing HTML content from the email&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AlternateViews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;p&amp;gt;{0}&amp;lt;/p&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Read out the HTML content as a string&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentStream&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;originalContents&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadToEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;originalContents&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Create all the override information&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"--- Email Override ---"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="s"&gt;$"original to: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;originalTo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;originalCc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="s"&gt;$"original cc: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;originalCc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;originalBcc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="s"&gt;$"original bcc: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;originalBcc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;updatedBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;originalContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MediaTypeNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Create new HTML content and attach it to the email&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;newView&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AlternateView&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAlternateViewFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updatedBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;originalContentType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AlternateViews&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newView&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;AlternateView&lt;/code&gt; that makes up the content of the &lt;code&gt;MailMessage&lt;/code&gt; is not editable 😕, so we instead have to remove it from the &lt;code&gt;MailMessage&lt;/code&gt;, read its content as a string and append our override messages to it.&lt;/p&gt;

&lt;p&gt;We then create a new &lt;code&gt;AlternateView&lt;/code&gt; that has our updated content as the email body, and add it to the &lt;code&gt;MailMessage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's definitely some extra processing going on here that you don't want to run during production while sending out bulk emails. However, that's definitely not a scenario for this email whitelisting 😋.&lt;/p&gt;

&lt;p&gt;Assuming our settings are correctly configured in the CMS and the custom provider is registered with Kentico (by using the assembly attribute), when our application attempts to send emails, they will be checked by the whitelisting process and either re-routed or sent out correctly 😄!&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As we develop our Kentico applications we might find the need to test parts of the system that will send out emails using Kentico's underlying email processing.&lt;/p&gt;

&lt;p&gt;If we have any real email addresses in the database, we run the risk of emails being sent to users from non-production sites 😦.&lt;/p&gt;

&lt;p&gt;We could use SMTP servers that capture emails, but that might not allow testers and stakeholders to verify the functionality of the site as it would be in a production scenario. It also adds another tool that testers would need to have access to while testing common workflows 😔.&lt;/p&gt;

&lt;p&gt;Instead, we can leverage Kentico's robust overriding/intercepting patterns of its internal providers 😅.&lt;/p&gt;

&lt;p&gt;By designing an email "whitelist" feature, we can enable email whitelisting for testers and stakeholders while still intercepting and re-routing emails destined for real, non-test addresses.&lt;/p&gt;

&lt;p&gt;Kentico's custom settings functionality allows site administrators easy access to these settings within the CMS, which is great for enabling new domains or email addresses as the testing phase of a site proceeds 👏🏾.&lt;/p&gt;

&lt;p&gt;Finally, once the site is ready to go live, we can either disable whitelisting in the CMS settings, remove the custom provider attribute to un-register it in Kentico or delete the class entirely, at which point email processing by the CMS will return to normal.&lt;/p&gt;

&lt;p&gt;Pretty cool 😎.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading 🙏!&lt;/p&gt;


Photo by &lt;a href="https://unsplash.com/@joannakosinska"&gt;Joanna Kosinska&lt;/a&gt; on &lt;a href="https://unsplash.com"&gt;Unsplash&lt;/a&gt;





&lt;p&gt;We've put together a list over on &lt;a href="https://github.com/Kentico/Home/blob/master/RESOURCES.md"&gt;Kentico's GitHub account&lt;/a&gt; of developer resources. Go check it out!&lt;/p&gt;

&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Or my Kentico blog series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12: Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip"&gt;Kentico CMS Quick Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kentico</category>
      <category>cms</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Kentico CMS Quick Tip: Understanding E-Commerce XML Structures</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 18 Nov 2019 14:40:07 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-cms-quick-tip-understanding-e-commerce-xml-structures-lmf</link>
      <guid>https://forem.com/wiredviews/kentico-cms-quick-tip-understanding-e-commerce-xml-structures-lmf</guid>
      <description>Photo by &lt;a href="https://unsplash.com/@thisisramiro"&gt;Ramiro Mendes&lt;/a&gt; on &lt;a href="https://unsplash.com"&gt;Unsplash&lt;/a&gt;

&lt;h2&gt;
  
  
  E-Commerce Types and XML Data
&lt;/h2&gt;

&lt;p&gt;Kentico's &lt;a href="https://docs.kentico.com/api12sp/e-commerce/orders"&gt;documentation provides examples&lt;/a&gt; of ways that developers can interact with the &lt;code&gt;OrderInfo&lt;/code&gt; and &lt;code&gt;OrderItemInfo&lt;/code&gt; types 👍.&lt;/p&gt;

&lt;p&gt;These are the primary objects developers retrieve order information from after a customer completes checkout with their shopping cart and an &lt;code&gt;OrderInfo&lt;/code&gt; instance is created from the &lt;code&gt;ShoppingCartInfo&lt;/code&gt; instance belonging to the customer.&lt;/p&gt;

&lt;p&gt;However, it's not so clear from the documentation how all the fields in these objects get populated and what values we should expected to be stored in them 🤷🏽‍♀️.&lt;/p&gt;

&lt;p&gt;This is especially true with the XML structures that Kentico uses for Summary and Custom Data fields, which we might find ourselves needing to parse for business logic customizations 🤨.&lt;/p&gt;

&lt;p&gt;Below, we'll look at where these XML values come from and what schema we should expect them to use 🤓.&lt;/p&gt;

&lt;h2&gt;
  
  
  OrderInfo fields
&lt;/h2&gt;

&lt;h3&gt;
  
  
  OrderCustomData
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is one of a couple fields that is transparently copied from the &lt;code&gt;ShoppingCartInfo&lt;/code&gt; to the &lt;code&gt;OrderInfo&lt;/code&gt; when the order is created, so we can use either the &lt;code&gt;ShoppingCartInfo.ShoppingCartCustomData&lt;/code&gt; or &lt;code&gt;OrderInfo.OrderCustomData&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Via the ShoppingCartInfo&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;ShoppingCartInfo&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShoppingCartCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataNumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShoppingCartCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataString"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Via the OrderInfo&lt;/span&gt;
&lt;span class="n"&gt;OrderInfo&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrderInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataNubmer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataString"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_Order.OrderCustomData&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;CustomData&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CustomDataNumber&amp;gt;&lt;/span&gt;42&lt;span class="nt"&gt;&amp;lt;/CustomDataNumber&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CustomDataString&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/CustomDataString&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/CustomData&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderPaymentResult
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To set a payment result on an order we need to use the &lt;code&gt;OrderInfo.UpdateOrderStatus()&lt;/code&gt; method, passing a valid &lt;code&gt;PaymentResultInfo&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;paymentResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PaymentResultInfo&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PaymentDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PaymentDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Payment completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PaymentIsCompleted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PaymentIsAuthorized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;PaymentTransactionID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;PaymentStatusValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;PaymentMethodName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"TestPaymentName"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="n"&gt;OrderInfo&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrderInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpdateOrderStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can see below that each value populated in the &lt;code&gt;PaymentResultInfo&lt;/code&gt; creates an element in the XML stored in the database.&lt;/p&gt;

&lt;p&gt;The values like &lt;code&gt;{$PaymentGateway.Result.TransactionID$}&lt;/code&gt; are unresolved &lt;a href="https://docs.kentico.com/k12sp/macro-expressions/macro-syntax"&gt;localization macros&lt;/a&gt; that would be populated with translated values of the specific payment gateway / payment method being used for this order 🧐.&lt;/p&gt;

&lt;p&gt;However, in my examples I have not populated those localization strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_Order.OrderPaymentResult&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;result&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.Date$}"&lt;/span&gt; 
        &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"11/17/2019 10:16:30 PM"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"method"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.PaymentMethod$}"&lt;/span&gt; 
        &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"TestPaymentName"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.Status$}"&lt;/span&gt; 
        &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"6a418f48-2aee-4bde-86ef-c6e3eef6667e"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"transactionid"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.TransactionID$}"&lt;/span&gt; 
        &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"237b5af1-bc28-4187-b9d4-0ecf1084b6ec"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.Description$}"&lt;/span&gt; 
        &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Test Payment completed"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"completed"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.IsCompleted$}"&lt;/span&gt; 
        &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.PaymentCompleted$}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"authorized"&lt;/span&gt; 
        &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.IsAuthorized$}"&lt;/span&gt; 
        &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"{$paymentgateway.result.paymentauthorized$}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;item&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"failed"&lt;/span&gt; &lt;span class="na"&gt;header=&lt;/span&gt;&lt;span class="s"&gt;"{$PaymentGateway.Result.IsFailed$}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/result&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderDiscounts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Order discounts come from coupon codes defined in the CMS under the "Order Discounts" module.&lt;/p&gt;

&lt;p&gt;In the example below I've created an Order Discount named &lt;code&gt;Test Discount 10%&lt;/code&gt; that applies a 10% discount to the entire order.&lt;/p&gt;

&lt;p&gt;Then I created a Coupon Code, &lt;code&gt;TEST_10_OFF&lt;/code&gt;, for this discount.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCouponCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TEST_10_OFF"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_Order.OrderDiscounts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Summary&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Test Discount 10%&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;0.30&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Summary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderOtherPayments
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This field stores gift cards applied to the order, and in Kentico gift cards are basically coupons that reduce the final cost of the entire order and also the total balance of the gift card.&lt;/p&gt;

&lt;p&gt;So, it shouldn't be much of a surprise that gift cards are added to an order the same way coupon codes are 😀.&lt;/p&gt;

&lt;p&gt;For the example below, in the CMS "Gift Cards" module, I created a gift card named "Two Dollar Gift Card" with a coupon code &lt;code&gt;TWO_DOLLARS_PLEASE&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCouponCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TWO_DOLLARS_PLEASE"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_Order.OrderOtherPayments&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Summary&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Two Dollar Gift Card&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;2.00&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Summary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderTaxSummary
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Taxes for an order are calculated by an &lt;code&gt;ITaxCalculationService&lt;/code&gt; which has a pretty simple signature, &lt;code&gt;TaxCalculationResult CalculateTaxes(TaxCalculationRequest taxRequest)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Kentico's documentation &lt;a href="https://docs.kentico.com/k12sp/e-commerce-features/customizing-on-line-stores/customizing-tax-calculation"&gt;has a good example&lt;/a&gt; of how an implementation of this interface might look 💪🏿.&lt;/p&gt;

&lt;p&gt;Here's a trivial example of an &lt;code&gt;ITaxCalculationService&lt;/code&gt; that sets some values in the &lt;code&gt;TaxCalculationResult.Summary&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomTaxCalculationService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITaxCalculationService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TaxCalculationResult&lt;/span&gt; &lt;span class="nf"&gt;CalculateTaxes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TaxCalculationRequest&lt;/span&gt; &lt;span class="n"&gt;taxRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TaxCalculationResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;itemsTotal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ItemsTax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;itemsTotal&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;01&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Custom tax"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itemsTotal&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;027&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;shippingTaxRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.15&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingTax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;taxRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingPrice&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;shippingTaxRate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Custom shipping tax"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingTax&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"County Tax"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;itemsTotal&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;06&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can see below how each of the &lt;code&gt;string&lt;/code&gt; keys used in our call to &lt;code&gt;Summary.Sum()&lt;/code&gt; (like &lt;code&gt;"County Tax"&lt;/code&gt;) results in a element in the XML summary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_Order.OrderTaxSummary&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Summary&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Custom tax&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;0.270&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Custom shipping tax&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;0.000&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;County Tax&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;0.1620&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Summary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderCouponCodes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By applying any coupons to an order we can populate the &lt;code&gt;OrderCouponCodes&lt;/code&gt; field - this include both order discount coupons and gift card coupons.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These are the coupons that result in the &lt;code&gt;OrderOtherPayments&lt;/code&gt; and &lt;code&gt;OrderDiscounts&lt;/code&gt; fields being populated 🧐.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This field will also contain any "Product Coupons" (see &lt;code&gt;OrderItemDiscountSummary&lt;/code&gt; below) applied to the order, even though these are order item specific.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Apply an order discount&lt;/span&gt;
&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCouponCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TEST_10_OFF"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Apply a gift card&lt;/span&gt;
&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCouponCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TWO_DOLLARS_PLEASE"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_Order.OrderCouponCodes&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;CouponCodes&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CouponCode&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;TEST_10_OFF&lt;span class="nt"&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/CouponCode&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CouponCode&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;TWO_DOLLARS_PLEASE&lt;span class="nt"&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ValueInMainCurrency&amp;gt;&lt;/span&gt;2.00&lt;span class="nt"&gt;&amp;lt;/ValueInMainCurrency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/CouponCode&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/CouponCodes&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  OrderItem fields
&lt;/h2&gt;
&lt;h3&gt;
  
  
  OrderItemCustomData
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This field works just like &lt;code&gt;OrderInfo.OrderCustomData&lt;/code&gt;, except it allows us to store custom data per line-item in the order. This means we can use either &lt;code&gt;ShoppingCartItemInfo.CartItemCustomData&lt;/code&gt; or &lt;code&gt;OrderItemInfo.OrderItemCustomData&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Via the ShoppingCartItemInfo&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;ShoppingCartInfo&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;ShoppingCartItemInfo&lt;/span&gt; &lt;span class="n"&gt;cartItem&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;cartItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CartItemCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataNumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cartItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CartItemCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataString"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;cartItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Via the OrderItemInfo&lt;/span&gt;
&lt;span class="n"&gt;ICollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderItemInfo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orderItems&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderItemInfoProvider&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrderItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;OrderItemInfo&lt;/span&gt; &lt;span class="n"&gt;orderItem&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;orderItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderItemCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataNumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;orderItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderItemCustomData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CustomDataString"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;orderItem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_OrderItem.OrderItemCustomData&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;CustomData&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CustomDataNumber&amp;gt;&lt;/span&gt;10&lt;span class="nt"&gt;&amp;lt;/CustomDataNumber&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CustomDataString&amp;gt;&lt;/span&gt;Hello&lt;span class="nt"&gt;&amp;lt;/CustomDataString&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/CustomData&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderItemProductDiscounts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This field is populated by any catalog discounts we have defined for our store in the CMS. These can be managed in the "Catalog Discounts" module.&lt;/p&gt;

&lt;p&gt;For the example below, I've created a catalog discount named "Test Product 5% off" which applies a percentage discount of 5% to a single product named "Test Product".&lt;/p&gt;

&lt;p&gt;This means the "Test Product" needs to be in my order for this discount to be applied and the field to be populated ✔.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;SKUInfo&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKUInfoProvider&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSKUs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SKUInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKUName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"TestProduct"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TopN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;ShoppingCartInfo&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItemToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;Note that this is the one discount/coupon related field where the discount value and not the discounted amount is stored.&lt;/p&gt;

&lt;p&gt;So, for example, a &lt;code&gt;Value&lt;/code&gt; of "0.05" for a catalog discount of 5% would be stored and not "1.00" for a 10% discount on a $10.00 product.&lt;/p&gt;

&lt;p&gt;All other discount/coupon fields store the amount to be subtracted from the order or item price.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_OrderItem.OrderItemProductDiscounts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Summary&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Test Product 5% off&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;0.05&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Summary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  OrderItemDiscountSummary
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;How do we populate it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This field is populated when the order has items that are part of a Buy-&amp;gt;Get type discount, which are defined in the CMS "Buy X Get Y" module.&lt;/p&gt;

&lt;p&gt;These kinds of discounts state that if a customer purchases some set of products they will get a discount of some amount either on specific items (in order to encourage buying products together) or on the cheapest items in the cart (as a promotion) 🤔.&lt;/p&gt;

&lt;p&gt;In the example below I have a discount defined that applies a 20% discount named "Test Buy X Get Y" on the product named "Test Accessory" if the order also has a product named "Test Product".&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;SKUInfo&lt;/span&gt; &lt;span class="n"&gt;skuProduct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKUInfoProvider&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSKUs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SKUInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKUName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"TestProduct"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TopN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;SKUInfo&lt;/span&gt; &lt;span class="n"&gt;skuAccessory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKUInfoProvider&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSKUs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SKUInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKUName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"TestAccessory"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TopN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;ShoppingCartInfo&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItemToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItemToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuAccessory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SKUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can also populate this field with coupons created in the "Product Coupons" CMS module.&lt;/p&gt;

&lt;p&gt;If we add a coupon to the shopping cart that applies to a product in the cart, the Product Coupon display name and the discount amount will be stored in the XML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where is it stored in the db?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COM_OrderItem.OrderItemDiscountSummary&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will it look like?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Summary&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Name&amp;gt;&lt;/span&gt;Test Buy X Get Y&lt;span class="nt"&gt;&amp;lt;/Name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Value&amp;gt;&lt;/span&gt;0.20&lt;span class="nt"&gt;&amp;lt;/Value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Item&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Summary&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Kentico has all sorts of nooks and crannies, hiding implementation details. Some times the defaults fulfill the needs of our business logic, but other times we need to explore these thins to understand how we can best apply customizations 🤓.&lt;/p&gt;

&lt;p&gt;We've looked at how the XML fields of &lt;code&gt;ShoppingCartInfo&lt;/code&gt;, &lt;code&gt;ShoppingCartItemInfo&lt;/code&gt;, &lt;code&gt;OrderInfo&lt;/code&gt;, and &lt;code&gt;OrderItemInfo&lt;/code&gt; are populated, used, and stored in the database.&lt;/p&gt;

&lt;p&gt;These fields can usually be ignored until we explicitly need them, and in those cases it's nice to have some concrete examples, like the ones above, to ensure the results we are seeing in our applications are the correct ones.&lt;/p&gt;

&lt;p&gt;Hopefully this blog post can be a little cheat-sheet for you (and me! 😎) when working with Kentico's E-Commerce functionality.&lt;/p&gt;

&lt;p&gt;Thanks for reading 🙏!&lt;/p&gt;



&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Or my Kentico blog series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12: Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip"&gt;Kentico CMS Quick Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kentico</category>
      <category>cms</category>
      <category>csharp</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Kentico CMS Quick Tip: Faking A Live Site For Integration Testing</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 11 Nov 2019 13:50:10 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-cms-quick-tip-faking-a-live-site-for-integration-testing-c98</link>
      <guid>https://forem.com/wiredviews/kentico-cms-quick-tip-faking-a-live-site-for-integration-testing-c98</guid>
      <description>Photo by &lt;a href="https://unsplash.com/@mihaly_koles"&gt;Mihály Köles&lt;/a&gt; on &lt;a href="https://unsplash.com"&gt;Unsplash&lt;/a&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Integration tests give confidence in complex code interactions&lt;/li&gt;
&lt;li&gt;Some of Kentico's APIs are difficult to test, because they depend on a live ASP.NET application context&lt;/li&gt;
&lt;li&gt;We can fake this context if we know which parts are accessed during our test&lt;/li&gt;
&lt;li&gt;Testing Kentico's &lt;code&gt;ShoppingService&lt;/code&gt; lets us explore how the E-Commerce functionality works and verify the results of our custom code&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Kentico Integration Tests
&lt;/h2&gt;

&lt;p&gt;Integration tests in Kentico projects are perfect for simulating interactions between classes that depend on external resources, namely the database.&lt;/p&gt;

&lt;p&gt;Many interactions with Kentico's APIs can be performed in unit tests, but some (like &lt;code&gt;UserInfoProvider.IsUserInRole&lt;/code&gt;) require a real database connection 🤔.&lt;/p&gt;

&lt;p&gt;Other times, our code performs a complex series of steps, working with Kentico's types in specific ways defined by our business requirements.&lt;/p&gt;

&lt;p&gt;Unit tests might not be enough to guarantee that these interactions will operate how we expect at runtime, and integration tests might be more appropriate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can read an example of what we might want in an integration test in my post &lt;em&gt;Kentico CMS Quick Tip: Integration Testing Roles&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/wiredviews" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ANGJ2hOP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://res.cloudinary.com/practicaldev/image/fetch/s--8CyKf2ga--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/organization/profile_image/779/9e5935b0-1bee-4832-bbb5-72ba2f3c38c4.png" alt="WiredViews" width="150" height="150"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3visu6H---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://res.cloudinary.com/practicaldev/image/fetch/s--UJc-YbIk--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/152179/1a3ce219-703d-46cb-b473-dcadd80152f4.jpg" alt="" width="150" height="150"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/wiredviews/kentico-cms-quick-tip-integration-testing-roles-h6i" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Kentico CMS Quick Tip: Integration Testing Roles&lt;/h2&gt;
      &lt;h3&gt;Sean G. Wright for WiredViews ・ Jul 29 '19 ・ 4 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#kentico&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#dotnet&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#testing&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#csharp&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;

&lt;/blockquote&gt;

&lt;p&gt;However, there are some APIs that don't even work 😯 in an integration test scenario because they depend on a live running ASP.NET application, interacting with things like &lt;code&gt;HttpContext&lt;/code&gt; and &lt;code&gt;Session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fortunately there's a solution to this predicament 😅!&lt;/p&gt;

&lt;p&gt;Let's look at an example.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing &lt;code&gt;ShoppingService&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Kentico provides many service types behind interfaces that are registered with its own &lt;a href="https://en.wikipedia.org/wiki/Service_locator_pattern"&gt;Service Locator&lt;/a&gt;, &lt;code&gt;Service&lt;/code&gt;, found in the &lt;code&gt;CMS.Core&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;If we are using &lt;a href="https://en.wikipedia.org/wiki/Dependency_injection"&gt;Dependency Injection&lt;/a&gt;, we could always mock these interfaces in our unit tests, but that doesn't fulfill the requirement of getting validation that the runtime interactions of multiple classes (and the database) will work as we expect.&lt;/p&gt;

&lt;p&gt;Below we will look at running an integration test that works with &lt;code&gt;ShoppingService&lt;/code&gt; 💰.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating The Test Class
&lt;/h3&gt;

&lt;p&gt;First, we create a &lt;a href="https://docs.kentico.com/k12sp/custom-development/writing-automated-tests/creating-integration-tests-with-a-connection-string"&gt;standard Kentico integration test&lt;/a&gt;, applying the correct &lt;code&gt;NUnit&lt;/code&gt; test attributes and creating a test method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TestFixture&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShoppingServiceTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IntegrationTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Test&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ShoppingService_AddItemsToCart_Will_Update_CartItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;You can read more about setting up integration tests in Kentico in my post &lt;em&gt;Kentico 12: Design Patterns Part 8 - Setting Up Integration Tests&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/seangwright" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3visu6H---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://res.cloudinary.com/practicaldev/image/fetch/s--UJc-YbIk--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/152179/1a3ce219-703d-46cb-b473-dcadd80152f4.jpg" alt="seangwright"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/seangwright/kentico-12-design-patterns-part-8-setting-up-integration-tests-14jg" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Kentico 12: Design Patterns Part 8 - Setting Up Integration Tests&lt;/h2&gt;
      &lt;h3&gt;Sean G. Wright ・ Jul 22 '19 ・ 10 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#dotnet&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#kentico&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#integrationtests&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#testing&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;br&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Our Initial Attempt
&lt;/h3&gt;

&lt;p&gt;In our test method, we are going to work with &lt;code&gt;CMS.Ecommerce.ShoppingService&lt;/code&gt; (through the &lt;code&gt;IShoppingService&lt;/code&gt; interface), calling &lt;code&gt;AddItemToCart()&lt;/code&gt;, which we would expect to update the database, inserting a row in &lt;code&gt;COM_ShoppingCartSKU&lt;/code&gt; (for a new shopping cart).&lt;/p&gt;

&lt;p&gt;Below we can see how we retrieve an &lt;code&gt;ShoppingService&lt;/code&gt; instance from the &lt;code&gt;Service&lt;/code&gt; service locator class and then immediately call &lt;code&gt;AddItemToCart()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;skuId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;118&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItemToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This test will fail because this method requires that the execution context is a live running ASP.NET application, not a test application 😢.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;AddItemToCart()&lt;/code&gt; method assumes there is a user associated with the current request, whether that is an anonymous user or an authenticated one.&lt;/p&gt;

&lt;p&gt;It also assumes there is a site that the request is being processed under. That site must have a valid Kentico license to allow for the E-Commerce functionality to be accessed 😵.&lt;/p&gt;

&lt;p&gt;In our test method there is no "current request", so trying to access it for a user (by checking the &lt;code&gt;HttpContext.User.Identity&lt;/code&gt;), or a site (by checking &lt;code&gt;HttpContext.Request.Url.Host&lt;/code&gt;) will fail.&lt;/p&gt;

&lt;p&gt;The above code will throw an exception at the line &lt;code&gt;shoppingService.AddItemToCart(skuId, quantity);&lt;/code&gt; due to a foreign key constraint caused by the shopping cart not being persisted to the database correctly 🤦🏿‍♀️.&lt;/p&gt;


&lt;h3&gt;
  
  
  Faking a Live Site
&lt;/h3&gt;

&lt;p&gt;So, how can we "fake" a live ASP.NET application? By faking the parts that get used by the &lt;code&gt;ShoppingService&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, we assign a valid instance of &lt;code&gt;HttpContext&lt;/code&gt; to &lt;code&gt;HttpContext.Current&lt;/code&gt;, only filling in the values needed by the &lt;code&gt;ShoppingService.AddItemToCart()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteUri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://localhost:44397"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;siteUri&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ClaimsPrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ClaimsIdentity&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;siteUri&lt;/code&gt; that is listed above has to be a domain that has a valid license in the CMS (in this case, &lt;code&gt;localhost&lt;/code&gt;) 👍.&lt;/p&gt;

&lt;p&gt;We also fill in the &lt;code&gt;HttpContext.User&lt;/code&gt;, not with anything valid, but just ensuring nothing is &lt;code&gt;null&lt;/code&gt; (this is due to the &lt;code&gt;ShoppingService&lt;/code&gt; requiring this value to not be &lt;code&gt;null&lt;/code&gt; internally).&lt;/p&gt;

&lt;p&gt;Next, we use Kentico's &lt;code&gt;VirtualContext&lt;/code&gt; static class to assign some values that would normally be populated by the current HTTP request.&lt;/p&gt;

&lt;p&gt;Most of the &lt;code&gt;*Context&lt;/code&gt; static classes source some data from &lt;code&gt;VirtualContext&lt;/code&gt;, so setting values here will ensure they flow to the rest of Kentico's classes 🤯:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;siteCodeName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Sandbox"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;currentUserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sean@wiredviews.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;VirtualContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VirtualContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PARAM_SITENAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;siteCodeName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;VirtualContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VirtualContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PARAM_USERNAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentUserName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I assign a real username under the &lt;code&gt;VirtualContext.PARAM_USERNAME&lt;/code&gt; key, so that when data is pulled from the database for the "current user", real information comes back. This user is also a valid Customer within the system.&lt;/p&gt;

&lt;p&gt;I set the site name to match the site with a domain alias matching the &lt;code&gt;siteUri&lt;/code&gt; I used above 🧐.&lt;/p&gt;

&lt;p&gt;Now, we can write some code to validate our faked data is being passed around Kentico's libraries and giving us the state we desire.&lt;/p&gt;

&lt;p&gt;Specifically, there should be an authenticated user in the system, and that user should be the same one I set with the &lt;code&gt;VirtualContext&lt;/code&gt; above 🙂:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isAuthenticated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AuthenticationHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsAuthenticated&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;BeTrue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentUser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MembershipContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticatedUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentUserName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now that we have our simulated live ASP.NET context within our test method we can start making calls to &lt;code&gt;ShoppingService.AddItemToCart()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;skuId1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;118&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;skuId2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShoppingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItemToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddItemToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoppingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentShoppingCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Be&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Above, we add an item to the cart, ensure the &lt;code&gt;CartItems&lt;/code&gt; count is &lt;code&gt;1&lt;/code&gt;, and then add a different item, asserting the &lt;code&gt;CartItems&lt;/code&gt; count is &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we were to debug this code, we'd see that the value returned by &lt;code&gt;shoppingService.GetCurrentShoppingCart()&lt;/code&gt; would be a cart assigned to the site and Customer / User matching the username we populated in the &lt;code&gt;VirtualContext&lt;/code&gt; 🤯.&lt;/p&gt;

&lt;p&gt;This also means that if we skip deleting the cart when the test ends, and run it again, there will already be items in the cart from the previous test run - all of this data is coming from the database 🤓.&lt;/p&gt;

&lt;p&gt;Pretty cool ⚡⚡👏!&lt;/p&gt;


&lt;h2&gt;
  
  
  Pros and Cons
&lt;/h2&gt;

&lt;p&gt;Faking a live environment can definitely be helpful, but its not easy. We have to know what dependencies the Kentico classes have - like &lt;code&gt;VirtualContext&lt;/code&gt; and &lt;code&gt;HttpContext&lt;/code&gt; - to ensure we fake the correct data.&lt;/p&gt;

&lt;p&gt;Using a decompiler, or viewing the Kentico source code (if you have access to it), is really the only option here 🤷🏽‍♀️.&lt;/p&gt;

&lt;p&gt;That said, it allows us to automate the interaction of many complex parts of our application - potentially scripting out an entire E-Commerce process from "Add To Cart", to "Payment", and finally to "Create Order" 😮.&lt;/p&gt;

&lt;p&gt;If you've tried to test these processes before by stepping through the checkout process on the live site, manually clicking things until something succeeds or fails, an integration test like the one above might be a nice alternative 🤗.&lt;/p&gt;

&lt;p&gt;It also can be helpful to see exactly how different combinations of Kentico's libraries interact together and what all the different APIs really do.&lt;/p&gt;

&lt;p&gt;An integration test is like a mini-application where we can explore ideas, but without having to pay the cost of waiting for a site to start-up with every change we make 😊.&lt;/p&gt;


&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;In this post we started out explaining the benefits of integration tests compared to unit tests, but realized that even integration tests can have issues when the code being tested needs to be run in an environment different than what the test simulates.&lt;/p&gt;

&lt;p&gt;Some Kentico APIs expect a live ASP.NET request context to function correctly, which means if we want to test these we need to simulate them in our test.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;HttpContext.Current&lt;/code&gt; and &lt;code&gt;VirtualContext&lt;/code&gt;, we can simulate what Kentico's &lt;code&gt;ShoppingService&lt;/code&gt; does when a real customer adds an item to their shopping cart 💪.&lt;/p&gt;

&lt;p&gt;Figuring out these hidden dependencies can be difficult, but the reward is an integration test that both helps us better understand Kentico and verifies the behavior of our custom code that uses Kentico's APIs.&lt;/p&gt;

&lt;p&gt;In the future I'd like to expand this &lt;code&gt;ShoppingService&lt;/code&gt; integration test to include discounts, coupons, and creating an order from a shopping cart, so &lt;a href="https://dev.to/seangwright"&gt;follow me&lt;/a&gt; to be alerted when I publish here on DEV 🤓.&lt;/p&gt;

&lt;p&gt;Thanks for reading 🙏!&lt;/p&gt;



&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Or my Kentico blog series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12: Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip"&gt;Kentico CMS Quick Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kentico</category>
      <category>testing</category>
      <category>cms</category>
      <category>aspnet</category>
    </item>
    <item>
      <title>Kentico CMS Quick Tip: Minimal JSON Web APIs with IHttpHandler and .ashx Files</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Mon, 28 Oct 2019 14:00:21 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-cms-quick-tip-minimal-json-web-apis-with-ihttphandler-and-ashx-files-5dho</link>
      <guid>https://forem.com/wiredviews/kentico-cms-quick-tip-minimal-json-web-apis-with-ihttphandler-and-ashx-files-5dho</guid>
      <description>Photo by &lt;a href="https://unsplash.com/@bnwaneampeh" rel="noopener noreferrer"&gt;Benjamin Nwaneampeh&lt;/a&gt; on &lt;a href="https://unsplash.com" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was inspired by a discussion I had recently with Chris Bass, an inspiring member of the Kentico developer community. Check out &lt;a href="https://chrisbass.wakeflyexperts.com/blog" rel="noopener noreferrer"&gt;his blog&lt;/a&gt; for more Kentico content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Kentico Portal Engine CMS + API
&lt;/h2&gt;

&lt;p&gt;When we need to make calls to our &lt;a href="https://docs.kentico.com/k12sp/developing-websites/portal-engine-development-overview" rel="noopener noreferrer"&gt;Kentico CMS Portal Engine&lt;/a&gt; web application from the browser over XHR or from another web service, we need an API to communicate with.&lt;/p&gt;

&lt;p&gt;There are several ways to accomplish this, each with pros and cons, depending on our requirements 🤔.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://www.kenticotricks.com/blog/pretty-urls-for-web-handlers" rel="noopener noreferrer"&gt;another great blog post&lt;/a&gt; about this topic from Kristian Bortnik.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Web API 2 - The Sports Car 🏎
&lt;/h3&gt;

&lt;p&gt;Kentico's documentation explains the steps for &lt;a href="https://docs.kentico.com/k12sp/integrating-3rd-party-systems/using-asp-net-web-api-with-kentico" rel="noopener noreferrer"&gt;integrating Web API 2&lt;/a&gt; into the CMS.&lt;/p&gt;

&lt;p&gt;The approach is great if you need a large and robust custom API surface standing in front of the CMS - and it's an approach I've used many, many times 👍.&lt;/p&gt;

&lt;p&gt;However, the setup isn't trivial and the solution effectively runs the Web API OWIN-based application within the CMS - which leads to some sharp edges 🔪.&lt;/p&gt;




&lt;h3&gt;
  
  
  Kentico REST API - Public Transportation 🚍
&lt;/h3&gt;

&lt;p&gt;Kentico has a &lt;a href="https://docs.kentico.com/k12sp/integrating-3rd-party-systems/kentico-rest-service/getting-data-using-rest" rel="noopener noreferrer"&gt;REST API built-in&lt;/a&gt;, which can be used to query, and modify all kinds of data within the application 🧐.&lt;/p&gt;

&lt;p&gt;It does provide security through a &lt;a href="https://en.wikipedia.org/wiki/Basic_access_authentication" rel="noopener noreferrer"&gt;Basic Authentication&lt;/a&gt; HTTP header and but authenticates against the normal User accounts created within Kentico.&lt;/p&gt;

&lt;p&gt;The REST API exposes the Kentico Page data and &lt;code&gt;*Info&lt;/code&gt; objects directly, effectively creating a projection of the database over HTTP.&lt;/p&gt;

&lt;p&gt;Given the above, the caveat of this built-in solution is that it's verbose, not customizable, and a leaky abstraction 😞.&lt;/p&gt;




&lt;h3&gt;
  
  
  IHttpHandler - The Commuter Car 🚗
&lt;/h3&gt;

&lt;p&gt;For those simple scenarios where we only need a handful of endpoints, exposing a limited set of data, curated and filtered for us, we would like a way to build an API... without all the API.&lt;/p&gt;

&lt;p&gt;A nice solution to this problem is the ASP.NET &lt;code&gt;IHttpHandler&lt;/code&gt;, which can be exposed through an &lt;code&gt;.ashx&lt;/code&gt; file in our CMS project.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can read more about &lt;a href="https://docs.microsoft.com/en-us/previous-versions/bb398986(v=vs.140)?redirectedfrom=MSDN" rel="noopener noreferrer"&gt;how IHttpHandler works&lt;/a&gt; in Microsoft's documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;IHttpHandler&lt;/code&gt; gives us extremely low-level to an incoming HTTP Request and the outgoing HTTP Response.&lt;/p&gt;

&lt;p&gt;There's no WebForms code here, just the raw request and response 😮.&lt;/p&gt;

&lt;p&gt;This is perfect for our use-case since we don't want to render HTML through a complex web of page lifecycle events and user controls 👏.&lt;/p&gt;

&lt;p&gt;Let's take a look at some code for a concrete example of how all of this works.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I see the &lt;code&gt;IHttpHandler&lt;/code&gt; solution as ideal for small client-side integrations and also JSON-based server-to-server integrations with existing, in-production, Kentico CMS sites that can't risk big architectural changes.&lt;/p&gt;

&lt;p&gt;I've done both, but we'll look at the client-side solution today.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Example: An E-Commerce Store with Dynamic Prices
&lt;/h2&gt;

&lt;p&gt;Imagine we have a Business-to-Business (B2B) e-commerce application where the prices and inventory need to be pulled live from a back-end warehousing or &lt;a href="https://en.wikipedia.org/wiki/Enterprise_resource_planning" rel="noopener noreferrer"&gt;ERP&lt;/a&gt; system (not Kentico).&lt;/p&gt;

&lt;p&gt;We don't want to delay the loading of a product details page each time a visitor requests it because we need to fetch the pricing - that would hurt SEO and the User Experience ☹!&lt;/p&gt;

&lt;p&gt;Instead, we want to cache the product details page and then, via JavaScript, request the price independently 😁.&lt;/p&gt;

&lt;p&gt;So, we need a simple API endpoint that can forward this request on to the back-end system.&lt;/p&gt;




&lt;h3&gt;
  
  
  Creating the .ashx File
&lt;/h3&gt;

&lt;p&gt;Let's open our Kentico CMS solution, expand the project, and then the &lt;code&gt;CMSPages&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Right-click on the &lt;code&gt;CMSPages&lt;/code&gt; folder and select "Add" -&amp;gt; "Generic Handler".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnub8t8rcj8c821hftsmv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnub8t8rcj8c821hftsmv.jpg" alt="Visual Studio Project UI context menu for adding a Generic Handler"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're going to name this handler &lt;code&gt;ProductApi&lt;/code&gt; and Visual Studio will add the &lt;code&gt;.ashx&lt;/code&gt; extension for us.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IIS already knows how to process requests for &lt;code&gt;.ashx&lt;/code&gt; files, so even if you haven't worked with this file type before, there's no extra work necessary to use it in your application 🤓.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What we end up with is a class named &lt;code&gt;ProductApi&lt;/code&gt; that looks like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductApi&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHttpHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ProcessRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsReusable&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Handling the Request
&lt;/h3&gt;

&lt;p&gt;Now we need to handle the incoming request from the browser.&lt;/p&gt;

&lt;p&gt;We have the raw &lt;code&gt;HttpContext&lt;/code&gt; to work with here. This is good, because we don't want the typical Web Forms infrastructure to get in our way, but it also means a bit more work for us 😑.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ProcessRequest&lt;/code&gt; method is where we will do all of our work with the HTTP Request.&lt;/p&gt;

&lt;p&gt;Let's assume that the browser is going to send an XHR request, with a &lt;code&gt;skuid&lt;/code&gt; specified as a query string parameter, and it will expect a JSON response in return.&lt;/p&gt;

&lt;p&gt;Here's &lt;code&gt;ProcessRequest&lt;/code&gt; with some validation and error handling:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ProcessRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Set this first&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;skuIdParam&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"skuid"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;skuId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ValidationHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuIdParam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;SKUInfo&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKUInfoProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSKUInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// continue processing ...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating the Response
&lt;/h3&gt;

&lt;p&gt;Now that we have handled all the potential issues, we can get our back-end system identifier out of the &lt;code&gt;sku&lt;/code&gt; object and request the most up-to-date values.&lt;/p&gt;

&lt;p&gt;For our example we will pretend the response comes back in the following shape:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductStats&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Inventory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let's get hand-wavy 👋 here and assume we were successful in getting values back from our back-end system and we now want to send them back to the browser.&lt;/p&gt;

&lt;p&gt;These last few steps are pretty simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the &lt;code&gt;ProductStats&lt;/code&gt; response from the back-end system&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Newtonsoft.Json&lt;/code&gt; to serialize the C# object into a JSON &lt;code&gt;string&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Write the JSON &lt;code&gt;string&lt;/code&gt; as the HTTP response
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// continue processing ...&lt;/span&gt;

    &lt;span class="n"&gt;ProductStats&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="c1"&gt;// response from our back-end system&lt;/span&gt;

    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;responseText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;serializationSettings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You might have noticed the &lt;code&gt;serializationSettings&lt;/code&gt; parameter above. That can be customized to your preferences and use-case, but it allows you to define how &lt;code&gt;Newtonsoft.Json&lt;/code&gt; produces JSON from your C#.&lt;/p&gt;

&lt;p&gt;I typically store this in a &lt;code&gt;static readonly&lt;/code&gt; field in my &lt;code&gt;IHttpHandler&lt;/code&gt;, and these are the settings I tend to use 😎:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerSettings&lt;/span&gt; &lt;span class="n"&gt;serializationSettings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; 
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerSettings&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Formatting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Formatting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ContractResolver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CamelCasePropertyNamesContractResolver&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;

        &lt;span class="c1"&gt;// UTC Date serialization configuration&lt;/span&gt;
        &lt;span class="n"&gt;DateFormatHandling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateFormatHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsoDateFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;DateParseHandling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateParseHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;DateTimeZoneHandling&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTimeZoneHandling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;DateFormatString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"yyyy-MM-ddTHH:mm:ss.fffK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using Our API
&lt;/h3&gt;

&lt;p&gt;So what does using this "API" look like?&lt;/p&gt;

&lt;p&gt;Well, we can request the "API" in the browser like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fpgvzy5zyps9j5po7fhll.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fpgvzy5zyps9j5po7fhll.jpg" alt="Browser displaying fake data JSON response from HttpHandler"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But what about from JavaScript? Well, that's just as easy 😀!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm going to assume we're supporting modern browsers, so legacy fallbacks are up to the reader 😏:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;skuid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/CMSPages/ProductApi.ashx?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Who would have thought we could create a completely custom JSON based integration API in just a couple of minutes 🤗!?&lt;/p&gt;


&lt;h2&gt;
  
  
  Bonus: All the Context
&lt;/h2&gt;

&lt;p&gt;I would also like to note that since the HTTP request is going to the same domain that the JavaScript loaded under, there's no annoying cookie or CORS restrictions 🧐.&lt;/p&gt;

&lt;p&gt;All cookies for the current domain are sent back to the server with every HTTP request, even XHR requests to our &lt;code&gt;.ashx&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;This means that the normal Kentico &lt;code&gt;*Context&lt;/code&gt; classes that give us access to the ambient request data, like the current authenticated user (&lt;code&gt;MembershipContext&lt;/code&gt;) and the current shopping cart (&lt;code&gt;ShoppingCartContext&lt;/code&gt;) are all still available in our &lt;code&gt;ProductApi&lt;/code&gt; class ⚡.&lt;/p&gt;

&lt;p&gt;If we want to respond with additional discounts for Customers in different groups, or send the current user's ID with the SKU Id to our back-end system to get product recommendations, we can do that too 😄!&lt;/p&gt;

&lt;p&gt;How about displaying a shipping time estimate based on information gathered from the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API" rel="noopener noreferrer"&gt;Browser Geolocation API&lt;/a&gt; and the items in the shopping cart? Yup, we could do that 😃.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;While Kentico's Web API 2 integration approach and the built-in REST API provide a lot of functionality, they don't quite fit the requirements for a small, custom, minimalist endpoint exposed by the CMS for XHR requests from the browser.&lt;/p&gt;

&lt;p&gt;Fortunately, &lt;code&gt;IHttpHandler&lt;/code&gt;s and &lt;code&gt;.ashx&lt;/code&gt; files give us a quick and dirty way to stand up an endpoint using low-level ASP.NET features, without losing out on the functionality of the CMS 👍.&lt;/p&gt;

&lt;p&gt;If you try this approach out, let me know what you think!&lt;/p&gt;

&lt;p&gt;Thanks for reading 🙏!&lt;/p&gt;



&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Or my Kentico blog series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12: Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip"&gt;Kentico CMS Quick Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kentico</category>
      <category>javascript</category>
      <category>aspnet</category>
      <category>cms</category>
    </item>
    <item>
      <title>Kentico Connection: Denver 2019 - My Experiences</title>
      <dc:creator>Sean G. Wright</dc:creator>
      <pubDate>Thu, 10 Oct 2019 13:25:22 +0000</pubDate>
      <link>https://forem.com/wiredviews/kentico-connection-denver-2019-my-experiences-c82</link>
      <guid>https://forem.com/wiredviews/kentico-connection-denver-2019-my-experiences-c82</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fm2b3igyvr4hpx960chzn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fm2b3igyvr4hpx960chzn.jpg" alt="Downtown Denver at dusk"&gt;&lt;/a&gt;&lt;/p&gt;
All photos by &lt;a href="https://www.seangwright.me" rel="noopener noreferrer"&gt;Me&lt;/a&gt;  :)



&lt;p&gt;I previously wrote about my expectations for Kentico Connection: Denver 2019 conference:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/wiredviews" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F779%2F9e5935b0-1bee-4832-bbb5-72ba2f3c38c4.png" alt="WiredViews"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F152179%2F1a3ce219-703d-46cb-b473-dcadd80152f4.jpg" alt=""&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/wiredviews/kentico-connection-denver-2019-my-expectations-356m" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Kentico Connection: Denver 2019 - My Expectations&lt;/h2&gt;
      &lt;h3&gt;Sean G. Wright for WiredViews ・ Sep 30 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#kentico&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#mvc&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#conference&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#cms&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Having now attending the conference and spent nearly a week out in Denver, Colorado, I'd like to reflect on the event and whether or not my expectations were met 🤔.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Experienced
&lt;/h2&gt;

&lt;h3&gt;
  
  
  First-Time Attendee
&lt;/h3&gt;

&lt;p&gt;I definitely felt like a first-time attendee, which had its pros and cons.&lt;/p&gt;

&lt;p&gt;Everything about the conference was new to me - the structure, content, personality, and especially the people 😮.&lt;/p&gt;

&lt;p&gt;Having not been engaged with the Kentico community previously, this was the first time I ever spoke to nearly all the people there, and it was definitely the first time I met any face-to-face.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fltcqq31lylydnnil50v0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fltcqq31lylydnnil50v0.jpg" alt="Kentico Connection attendees"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While other attendees were catching up like old friends, I was being bombarded (in the best way possible!) by handshakes, new names and faces, and questions about my blog and presentation.&lt;/p&gt;

&lt;p&gt;I can't really compare the only other conference I know, &lt;a href="https://www.ng-conf.org/" rel="noopener noreferrer"&gt;NG-Conf&lt;/a&gt;, to Kentico Connection, because Angular has such a larger reach and community, whereas Kentico is a much more niche product.&lt;/p&gt;

&lt;p&gt;That said, the things I've always enjoyed about NG-Conf held true for Kentico Connection 😀:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Friendly people&lt;/li&gt;
&lt;li&gt;Great conversations&lt;/li&gt;
&lt;li&gt;Welcoming community&lt;/li&gt;
&lt;li&gt;Well organized conference&lt;/li&gt;
&lt;li&gt;Solid presenters and talks&lt;/li&gt;
&lt;li&gt;Exciting product announcements&lt;/li&gt;
&lt;li&gt;Leaving with a desire to return next year!&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Community
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fx02d2wym8uaalx793iv6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fx02d2wym8uaalx793iv6.jpg" alt="Dinner with Kentico partners"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I now feel, more than ever, connected and engaged with the larger Kentico community - and it's very reassuring to know that &lt;em&gt;there is&lt;/em&gt; a Kentico community out there, excited for new developers and new voices 🤗.&lt;/p&gt;

&lt;p&gt;I received lots of great feedback about my Kentico-related blog posts here on DEV, and my presentation.&lt;/p&gt;

&lt;p&gt;It's really nice to know that everything I've been writing about Kentico is helping others and, in some cases, inspiring others to start blogging and participating in the community 😎.&lt;/p&gt;

&lt;p&gt;So, I now want to report my findings clearly: The Kentico community is out there and wants &lt;em&gt;you&lt;/em&gt; to join it. If your company is looking for a CMS solution and you are unsure about which product will provide the development support you feel you need, check out Kentico.&lt;/p&gt;




&lt;h3&gt;
  
  
  The City
&lt;/h3&gt;

&lt;p&gt;I stayed in an AirBnb in Denver, which was the right decision because it gave me a nice 15 minute walk 🚶‍♂️, to and from the hotel where the conference was, each day.&lt;/p&gt;

&lt;p&gt;I stayed in one of my favorite parts of Denver, just a bit south of Five Points, near a bunch of great craft breweries and restaurants.&lt;/p&gt;

&lt;p&gt;The highlights for me were:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://goo.gl/maps/GvJjSHc3c69WzzSZ7" rel="noopener noreferrer"&gt;Zocalito Latin Bistro&lt;/a&gt; - great mole and Oaxacan cuisine 🍠
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fayvqnf8uqx11hsjvlhk9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fayvqnf8uqx11hsjvlhk9.jpg" alt="Oaxacan chicken"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://goo.gl/maps/evQqFTsbjndtausL7" rel="noopener noreferrer"&gt;Woods Boss Brewing Company&lt;/a&gt; - cool space, solid craft beer 🍻
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F0hq2jfu5vufsa54ldr6j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F0hq2jfu5vufsa54ldr6j.jpg" alt="Woods Bos Brewing"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;a href="https://goo.gl/maps/bkR4HsbVb3uitgQS6" rel="noopener noreferrer"&gt;Goed Zuur&lt;/a&gt; - eclectic beer (lots of sours) and charcuterie... mmm 🍺🥓🧀
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fos0h6uypnbnic3v9n2df.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fos0h6uypnbnic3v9n2df.jpg" alt="Charcuterie board"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Company
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fbyu8oliigqtzaybbqvsm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fbyu8oliigqtzaybbqvsm.jpg" alt="Keynote session"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had been looking forward to conversations and general brain-picking with the Kentico employees, and I didn't leave the conference disappointed.&lt;/p&gt;

&lt;p&gt;I met product owners, most of the developer relations (devrel) team, and the Kentico EMS technical lead.&lt;/p&gt;

&lt;p&gt;The devrel team did a great job of reaching out to me at the conference and making sure I knew they appreciated the content I've been putting out for the Kentico community here.&lt;/p&gt;

&lt;p&gt;And honestly, I think this - simple 'thank you's - is something that is easy to forget about 😊.&lt;/p&gt;

&lt;p&gt;Those of of us who blog and contribute to open source know how much time goes into these efforts and we don't expect or demand recognition, but when we get some, it instantly recharges our inspiration and motivation ⚡.&lt;/p&gt;

&lt;p&gt;Thanks guys 🙏!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fthhejcc3yl2gy9zc06l0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fthhejcc3yl2gy9zc06l0.jpg" alt="Downtown Denver"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was also really cool to talk with the Kentico Kontent and Kentico EMS product owners/representatives about their thoughts on the things they are in charge of.&lt;/p&gt;

&lt;p&gt;I feel I now have a much better idea of where Kentico, as an organization, sees its products, and also where and how it wants to grow them.&lt;/p&gt;

&lt;p&gt;I've tried to explain what I learned and my impressions to my co-workers at &lt;a href="https://dev.to/wiredviews"&gt;WiredViews&lt;/a&gt; so that we can position ourselves best to support businesses using &lt;a href="https://www.kentico.com/" rel="noopener noreferrer"&gt;Kentico EMS&lt;/a&gt; and &lt;a href="https://kontent.ai/" rel="noopener noreferrer"&gt;Kentico Kontent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, I had a fantastic conversation, at one of the post-conference events, with the technical lead for Kentico EMS.&lt;/p&gt;

&lt;p&gt;I was able to talk 'shop' and ask all the questions I had, from a technical perspective, about Kentico as a solution, its implementation details, the plans for bringing Kentico to .NET Core, and how some of its APIs and features might change in the near and longer-term future 🤓.&lt;/p&gt;

&lt;p&gt;These kind of conversations are why I value going to conferences, and why I'd recommend you attend a Kentico Connection event if you work with Kentico as a developer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ASP.NET Core
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F59d5apnqctqdkyl7u8d1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F59d5apnqctqdkyl7u8d1.jpg" alt=".NET Core session"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ASP.NET Core is coming to Kentico EMS, and it's coming faster than you might think ⌚.&lt;/p&gt;

&lt;p&gt;Kentico 2020 / Kentico 13 / Kentico Phoenix are all monikers of the same thing - the next generation of Kentico EMS being built on ASP.NET Core so that developers can leverage the latest and greatest in the .NET ecosystem.&lt;/p&gt;

&lt;p&gt;Planned for (if I remember correctly) September, 2020, the next version of Kentico is going to be a great thing for us devs 👍.&lt;/p&gt;

&lt;p&gt;That said, the move from .NET Framework and MVC 5 to .NET Core and ASP.NET Core might require some work for developers and businesses.&lt;/p&gt;

&lt;p&gt;All the internal (and 3rd party) .NET libraries, that have been leveraged repeatedly to speed up development time, need to be .NET Core compatible if they are going to be used in the next version of Kentico.&lt;/p&gt;

&lt;p&gt;Kentico has been preparing for their migration over the past several versions, but we need to prep our code as well.&lt;/p&gt;

&lt;p&gt;If you are looking to start migrating to .NET Core early, check out my post &lt;em&gt;Kentico 12 Class Libraries with Modern .NET Core Features&lt;/em&gt; 🧐:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/seangwright" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F152179%2F1a3ce219-703d-46cb-b473-dcadd80152f4.jpg" alt="seangwright"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/seangwright/kentico-12-class-libraries-with-modern-net-core-features-34n5" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Kentico 12 Class Libraries with Modern .NET Core Features&lt;/h2&gt;
      &lt;h3&gt;Sean G. Wright ・ Jun 1 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#csharp&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#dotnet&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#dotnetcore&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#kentico&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;I'm, to put it simply, super-hyped for the opportunity to build Kentico EMS applications with ASP.NET Core, and I'm counting down the days 😃!&lt;/p&gt;




&lt;h3&gt;
  
  
  The MVC 5 Transition
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Ffquy6horpakc4bbul45g.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Ffquy6horpakc4bbul45g.jpg" alt="View from the stage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the US, Kentico MVC adoption is sitting at 40% (which is apparently lower than Europe, but I don't have their stat). This means that teams are moving forward with the transition away from Web Forms and Kentico Portal Engine development and towards MVC 5 with Kentico 12.&lt;/p&gt;

&lt;p&gt;I find this super exciting!&lt;/p&gt;

&lt;p&gt;Yes, there's still 60% to go, but this change isn't a trivial one and for lots of teams, the Portal Engine development process has been refined to perfection over many years - the same can't be said for estimating project deadlines and complexity for Kentico + MVC 😑.&lt;/p&gt;

&lt;p&gt;Therefore, seeing 40% adoption is, I think, encouraging for the larger Kentico community. There are going to be resources for those who haven't made the switch yet, in blog posts, open source projects, and a general confirmation that "Yes, this is possible."&lt;/p&gt;

&lt;p&gt;I know my presentation raised lots of questions from other developers and business leaders at the conference, but the feeling I got was that these questions weren't coming from a place of apprehension, but rather a desire to be armed with the best tools when venturing into unfamiliar lands 🤠.&lt;/p&gt;

&lt;p&gt;Developers in the Kentico community are smart and will be able to adopt MVC as a technical solution, if they haven't already. The real question remaining is, how long will it take to be as comfortable with MVC as they are with Portal Engine?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For those interested, here you can find the link to the slides for my presentation &lt;a href="https://drive.google.com/file/d/0B6jxyeGSYH4pdlptTFZvVnpJdV9BbjF3QjFSTzdtN0p1VHFV/view?usp=sharing" rel="noopener noreferrer"&gt;From the Ground Up: Kentico 12 MVC Design Patterns&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Even if you saw my presentation, I had to 'hide' a large number of slides to fit within the time constraints, so there's some content in there I didn't get to share in Denver 😉.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Kentico Kontent
&lt;/h3&gt;

&lt;p&gt;I've only explored &lt;a href="https://kontent.ai/" rel="noopener noreferrer"&gt;Kentico Kontent&lt;/a&gt; briefly, working with the &lt;a href="https://gridsome.org/plugins/@meeg/gridsome-source-kentico-kontent" rel="noopener noreferrer"&gt;Gridsome plugin&lt;/a&gt;, but I find the idea of Content as a Service (Caas) very interesting.&lt;/p&gt;

&lt;p&gt;At Kentico Connection, Kentico Kontent was presented as a solution for enterprises wishing to have centralized content management, both for multi-channel delivery (think Web, Mobile, Smart-home devices, ect...) and organizational governance (who can edit what and ensuring content and messaging are consistent across an organization) 🤯.&lt;/p&gt;

&lt;p&gt;The price points for Kentico Kontent definitely reflect it being targeted at more mature businesses, but the free tier does look like it might be a great option for smaller companies that might not fit with a Kentico EMS solution, but still want a fully customized and fast CMS solution.&lt;/p&gt;

&lt;p&gt;I plan on blogging some about my explorations into Kentico Kontent, and some leaders in the Kentico community have already started to here on DEV:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/mattnield" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F23220%2Fa5b308f2-c767-4bbb-accf-1d227f70a591.jpg" alt="mattnield"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/mattnield/mmt-gatsbyjs-meetup-4ni6" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;MMT &amp;amp; GatsbyJS Meetup&lt;/h2&gt;
      &lt;h3&gt;Matt Nield ・ Oct 9 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#gatsby&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#kenticokontent&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#graphql&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;div class="ltag__link"&gt;
  &lt;a href="/ileshmistry" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F253%2Fdeed5ebd-1676-491f-b202-b5b31c66d04e.jpg" alt="ileshmistry"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/ileshmistry/adding-and-retrieving-localized-strings-from-kentico-cloud-to-gatsbyjs-and-graphql-3fed" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Adding and retrieving localized strings from Kentico Cloud to GatsbyJS and GraphQL&lt;/h2&gt;
      &lt;h3&gt;Ilesh Mistry ・ Aug 22 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#gatsby&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#graphql&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#kenticocloud&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#kentico&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  What I Brought Back
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fg0wn0dxejpxh8lqblko2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fg0wn0dxejpxh8lqblko2.jpg" alt="A beer on a patio"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After recovering from an exciting but exhausting week in Denver, and catching up on everything I missed back at the office, I had a great meeting with my co-workers, describing to them my impressions of the Kentico Connection conference and what my key takeaways were.&lt;/p&gt;

&lt;p&gt;I think, more than anything, what I've come back with is a desire to stay engaged with the larger Kentico community and do my small part to help it grow and mature 😁.&lt;/p&gt;

&lt;p&gt;Based on my conversations with other developers at the conference, I feel confident that WiredViews' technical approach to Kentico 12 MVC projects is very solid, but we are definitely novices when it comes to exploring the opportunities enabled by Kentico Kontent.&lt;/p&gt;

&lt;p&gt;Based on my conversations with business leaders, I feel confident that WiredViews is well positioned in the Kentico solutions marketplace to help businesses implement their Kentico projects with our unique skill-sets and deep pool of experience 😎.&lt;/p&gt;

&lt;p&gt;However, there's lots of changes always happening in the CMS, marketing, and custom web development space and hearing about the struggles and successes of other businesses was very enlightening.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fp76fyytr9l7139vhah89.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fp76fyytr9l7139vhah89.jpg" alt="Downtown Denver at dusk"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've made it this far, I hope you are either a Kentico Connection attendee already or a newly converted future attendee 😆.&lt;/p&gt;

&lt;p&gt;Kentico, as a company, is taking its products in exciting new directions.&lt;/p&gt;

&lt;p&gt;The businesses using these products have built relationships with each other over many years and continue to believe in Kentico as a great solution for content management, due to their repeated success stories.&lt;/p&gt;

&lt;p&gt;The developers responsible for implementing and building Kentico-based solutions have created a strong and growing community that I was warmly welcomed into.&lt;/p&gt;

&lt;p&gt;I think all these things make it clear why I'm definitely looking forward to the next Kentico Connection in 2020 👏👏.&lt;/p&gt;

&lt;p&gt;Time to start counting down the days!&lt;/p&gt;




&lt;p&gt;If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:&lt;/p&gt;


&lt;div class="ltag__tag ltag__tag__id__5339"&gt;
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="https://dev.to/t/kentico" class="ltag__tag__link"&gt;kentico&lt;/a&gt; Follow
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Or my Kentico blog series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns"&gt;Kentico 12: Design Patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip"&gt;Kentico CMS Quick Tips&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kentico</category>
      <category>mvc</category>
      <category>conference</category>
      <category>cms</category>
    </item>
  </channel>
</rss>
