<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Ethan Schofer</title> <link>https://world.optimizely.com/blogs/ethan-schofer/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Managing Your Graph Conventions</title>            <link>https://world.optimizely.com/blogs/ethan-schofer/dates/2024/12/managing-your-graph-conventions/</link>            <description>&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With the latest Graph NuGet packages (3.14.3) they have made a lot of the functionality of the ConventionRepository internal. Specifically, it contains some collections. There is currently no way to instantiate these, even with some sort of mock or substitute. So, for the time being, the unit test section of this article will not work.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Recently, Optimizely released a Conventions API for manging how various fields on your CMS content are indexed by the Graph. This is an extremely useful update as it allows us to much more easily manage what gets indexed and how it gets indexed: &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/conventions-api&quot;&gt;https://docs.developers.optimizely.com/content-management-system/docs/conventions-api&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Generally speaking, you manage this in the application Configure method in Startup.cs. According to the documentation, you create a service scope, get the conventions repository, and configure the fields on a given block or page.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;using (var serviceScope = app.ApplicationServices.CreateScope())&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; var services = serviceScope.ServiceProvider;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; var conventionRepo = services.GetRequiredService&amp;lt;ConventionRepository&amp;gt;();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; conventionRepo.ForInstancesOf&amp;lt;StandardPage&amp;gt;()&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .ExcludeField(p =&amp;gt; p.ContentAreaItem1) //exclude a field&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .IncludeField(p =&amp;gt; p.TermsAndConditions()) //add a dynamic field&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; //set indexing type for specific properties&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Price, IndexingType.Queryable)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Heading, IndexingType.Searchable)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Quantity, IndexingType.OnlyStored);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; conventionRepo.ExcludeContentType&amp;lt;StartPage&amp;gt;(); //exclude specific content types&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; conventionRepo.ExcludeAllContentTypes() //exclude all content types except a few&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Except&amp;lt;OrderBlock&amp;gt;();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; conventionRepo&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .IncludeAbstract&amp;lt;MyAbstractPage2&amp;gt;() //add interface and abstract types to schema&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .IncludeAbstract&amp;lt;MyAbstractPage&amp;gt;()&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .IncludeInterface&amp;lt;ISearchPage&amp;gt;();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt; background-color: #ffffff;&quot;&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;And this is excellent functionality. Fine grained control over my Graph indexing.&lt;/p&gt;
&lt;p&gt;The problem arises when we start to think of this in the context of a large site. Are we going to add hundreds of lines of code to our Startup.cs? Do we have to touch Startup.cs each time we want to adjust our conventions? This all seems like a maintenance issue. We want to make this whiole process much easier to maintain. Specifically:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Our code is organized in feature folders. Can we organize our conventions in this way as well?&lt;/li&gt;
&lt;li&gt;How can we add new pages or blocks, as well as the relevant conventions, without having to touch a whole bunch of code every time?&lt;/li&gt;
&lt;li&gt;How can we adjust existing pages or blocks and their relevant conventions and ensure our changes don&#39;t break anything?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Our initial idea was to manage these conventions similar to how Entity Framework manages conventions (&lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/modeling/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/modeling/&lt;/a&gt;) using IEntityTypeConfiguration. We want our convention classes to be pulled into the Graph conventions automatically, so that if we make a new file of conventions, it will automatically be included, and we dont have to worry about missing a step.&lt;/p&gt;
&lt;h2&gt;Create an Interface&lt;/h2&gt;
&lt;p&gt;The first step is to create an interface that all of my conventions will use. Then we can do some work on any class that implements this interface.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// Interface for conventions for each type&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;public interface IGraphTypeConventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// Method for adding the conventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;param name=&quot;conventionRepository&quot;&amp;gt;The graph conventions repository&amp;lt;/param&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; public void Configure(ConventionRepository conventionRepository);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Any class we create that contains conventions must implement this interface. An example implementaion might look like this:&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// Conventions for the accordion blocks&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;public class AccordionGraphConventions : IGraphTypeConventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// Adds conventions for the accordion blocks&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;param name=&quot;conventionRepository&quot;&amp;gt;The conventions repository&amp;lt;/param&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; public void Configure(ConventionRepository conventionRepository)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Ensure we have the repository&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ArgumentNullException.ThrowIfNull(conventionRepository);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Configure the accordion grouping block&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conventionRepository?.ForInstancesOf&amp;lt;AccordionGroupingBlock&amp;gt;()?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Title, IndexingType.Searchable)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Description, IndexingType.OnlyStored)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.IsFaq, IndexingType.Queryable)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Items, IndexingType.OnlyStored)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .ExcludeField(p =&amp;gt; p.UniqueId);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Configure the accordion item block&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conventionRepository?.ForInstancesOf&amp;lt;AccordionItemBlock&amp;gt;()?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Title, IndexingType.Searchable)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .Set(p =&amp;gt; p.Text, IndexingType.OnlyStored)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; .ExcludeField(p =&amp;gt; p.UniqueId);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Where we:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check that we have the repository&lt;/li&gt;
&lt;li&gt;Configure the AccordionGroupingBlock&lt;/li&gt;
&lt;li&gt;Configure the AccordionItemBlock&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In a large site, we would expect to have at least one implementation of IGraphTypeConventions for each feature.&lt;/p&gt;
&lt;h2&gt;Register Instances of the Interface&lt;/h2&gt;
&lt;p&gt;In Startup.cs, in ConfigureServices, we want to add all of our conventions to the Depdency Injection container. So, right after we configure the graph, we call a new ServiceCollection extension method called RegisterAllGraphConventions where we get the current assembly, get all implementations of IGraphConvention from the assembly, and add them to the DI container.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;*Note: This is predicated on our architecture, where everything is in a single DLL. If your solution is spread out over multiple DLLs youi may need to adjust this to ensure you pick up all the IGraphTypeConventions.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// Extension method to register all graph convention classes&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;public static class ServiceCollectionExtensions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// Register all instances of IGraphTypeConventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;param name=&quot;services&quot;&amp;gt;&amp;lt;/param&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; public static void RegisterAllGraphConventions(this IServiceCollection services)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Get the current assembly - everything is in a single assembly - this aassembly&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var currentAssembly = Assembly.GetExecutingAssembly();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Get all graph convention classes from the assembly&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var typesFromAssembly =&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; currentAssembly.DefinedTypes.Where(x =&amp;gt; x.GetInterfaces().Contains(typeof(IGraphTypeConventions)));&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Loop through the types&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; foreach (var type in typesFromAssembly)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Register them in the DI container&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; services.Add(new ServiceDescriptor(typeof(IGraphTypeConventions), type, ServiceLifetime.Transient));&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The advantage here is that if we add new conventions, they will automatically get registered. We can then easily call this from Startup.cs:&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;// Add graph conventions to the DI container&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;services.RegisterAllGraphConventions();&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Add Conventions to the Conventions Repository&lt;/h2&gt;
&lt;p&gt;Lastly, we want to actually add these conventions to the ConventionsRepository. We do this in the application Configure method in Startup.cs. We create an extention method on IApplicationBuilder that will actually perform the registeration. In that method, we get the service provider and then get the ConventionRepository, as described in their documentation. But then we get all instances of IGraphTypeConventions that we registered in the DI container, loop through all of them and call the Configure method on each instance.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// Application builder extensions to register all graph conventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;public static class GraphConventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// Add all graph conventions to the convention repository&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;/summary&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; /// &amp;lt;param name=&quot;app&quot;&amp;gt;&amp;lt;/param&amp;gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; public static void AddGraphConventions(this IApplicationBuilder app)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Get the scope&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; using var serviceScope = app.ApplicationServices.CreateScope();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Get the service provider&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var services = serviceScope.ServiceProvider;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Get the convention repository&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var conventionRepository = services.GetRequiredService&amp;lt;ConventionRepository&amp;gt;();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // If we don&#39;t have the repository&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (conventionRepository == null)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Just move on&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Get all the graph conventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var graphConventions = services.GetAllInstances&amp;lt;IGraphTypeConventions&amp;gt;();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Loop through the conventions&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; foreach (var graphConvention in graphConventions)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // Call the configure method on each graph convention&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; graphConvention.Configure(conventionRepository);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;This will ensure that all conventions are registered, and it makes adding new conventions pretty easy. This would be added in Configure like this:&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;// Configure graph sync&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;app.AddGraphConventions();&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;Lastly, I want to be able to test these conventions so that if they get changed in the future, the test will fail until the test is also updated, ensuring we are adding conventions correctly. For testing, I am using &lt;a href=&quot;https://xunit.net/&quot;&gt;XUnit&lt;/a&gt;, &lt;a href=&quot;https://nsubstitute.github.io/&quot;&gt;NSubstitute &lt;/a&gt;and &lt;a href=&quot;https://fluentassertions.com/&quot;&gt;FluentAssertions&lt;/a&gt;, so if you use different tools, the syntax of these tests may be different, but the concept should remain the same.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a Substitute of the ConventionsRepository&lt;/li&gt;
&lt;li&gt;Create an instance of you conventions class&lt;/li&gt;
&lt;li&gt;Call the configure method&lt;/li&gt;
&lt;li&gt;Get the field conventions for this particular content type&lt;/li&gt;
&lt;li&gt;Review the conventions&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;[Fact]&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;public void Configure_ConfigureAccordionGroupingBlock_ConfigurationIsSet()&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;{&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; // Arrange&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; var conventionRepository = Substitute.For&amp;lt;ConventionRepository&amp;gt;();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; var accordionGraphConventions = new AccordionGraphConventions();&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; // Act&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; accordionGraphConventions.Configure(conventionRepository);&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; // Assert&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; var fieldConventions = conventionRepository.GetFieldConventions(typeof(AccordionGroupingBlock));&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; foreach (var conventionType in fieldConventions)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; {&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var excludedFields = conventionType.GetExcludedFields();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var excludedFieldsArray = excludedFields as string[] ?? excludedFields.ToArray();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; excludedFieldsArray.Length.Should().Be(1);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; excludedFieldsArray.FirstOrDefault().Should().Be(nameof(AccordionGroupingBlock.UniqueId));&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var indexedFields = conventionType.GetFieldsWithIndexingSetting();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var indexedFieldsArray = indexedFields as string[] ?? indexedFields.ToArray();&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; indexedFieldsArray.Length.Should().Be(4);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.Title));&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.Description));&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.IsFaq));&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; indexedFieldsArray.Should().Contain(nameof(AccordionGroupingBlock.Items));&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conventionType.GetIndexingType(nameof(AccordionGroupingBlock.Title)).Should().Be(IndexingType.Searchable);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conventionType.GetIndexingType(nameof(AccordionGroupingBlock.Description)).Should().Be(IndexingType.OnlyStored);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conventionType.GetIndexingType(nameof(AccordionGroupingBlock.IsFaq)).Should().Be(IndexingType.Queryable);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; conventionType.GetIndexingType(nameof(AccordionGroupingBlock.Items)).Should().Be(IndexingType.OnlyStored);&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;&amp;nbsp; &amp;nbsp; }&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: &#39;andale mono&#39;, monospace; font-size: 10pt;&quot;&gt;}&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;So, we can bu sure that any of the following will break the test, requring you to examine your changes to ensure they are correct:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Changing the number and/or field that is excluded from convetions&lt;/li&gt;
&lt;li&gt;Change the number and/or field included in conventions&lt;/li&gt;
&lt;li&gt;Change the indexing type for a given field&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;All in all, the Conventions API is a nice feature from Optimizely to help us manage how the Graph indexes our content. Using the mthod outlined above will result in easy to maintain conventions that scale along with your solution.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/ethan-schofer/dates/2024/12/managing-your-graph-conventions/</guid>            <pubDate>Tue, 31 Dec 2024 16:58:12 GMT</pubDate>           <category>Blog post</category></item><item> <title>Roll Your Own Security Headers</title>            <link>https://world.optimizely.com/blogs/ethan-schofer/dates/2024/2/roll-your-own-security-headers/</link>            <description>&lt;p&gt;Proper security headers are a must for your Optimizely driven website. There are a variety of tools out there that will help with this, but when possible I prefer to roll my own solution, especially for such a low level feature as security headers. No need to keep a NuGet package up to date. No need to worry about constant version updates. Its all self contained in the code base that I control. I have done this with some custom middleware that can be set up in Startup.cs. The headers this could add include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;X-Content-Type-Options&lt;/li&gt;
&lt;li&gt;X-Frame-Options&lt;/li&gt;
&lt;li&gt;Server&lt;/li&gt;
&lt;li&gt;Strict-Transport-Security&lt;/li&gt;
&lt;li&gt;X-XSS-Protection&lt;/li&gt;
&lt;li&gt;Referrer-Policy&lt;/li&gt;
&lt;li&gt;Permissions-Policy&lt;/li&gt;
&lt;li&gt;Content-Security-Policy&lt;/li&gt;
&lt;li&gt;X-Download-Options&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first thing I need to do is create some constants classes that contain all the header names and values I will need. Each class contains a string of the header name and strings of any options. Content Security Policy and Permissions Policy, being a little more complex, also needs a enum to cover some options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;X-Content-Type-Options&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// X-Content-Type-Options-related constants.
/// &amp;lt;/summary&amp;gt;
public static class ContentTypeOptionsConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for X-Content-Type-Options
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;X-Content-Type-Options&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Disables content sniffing
    /// &amp;lt;/summary&amp;gt;
    public static readonly string NoSniff = &quot;nosniff&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case I&amp;nbsp;only have the nosniff options because that is what I will be using.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;X-Frame-Options&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// X-Frame-Options-related constants.
/// &amp;lt;/summary&amp;gt;
public static class FrameOptionsConstants
{
    /// &amp;lt;summary&amp;gt;
    /// The header value for X-Frame-Options
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;X-Frame-Options&quot;;

    /// &amp;lt;summary&amp;gt;
    /// The page cannot be displayed in a frame, regardless of the site attempting to do so.
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Deny = &quot;DENY&quot;;

    /// &amp;lt;summary&amp;gt;
    /// The page can only be displayed in a frame on the same origin as the page itself.
    /// &amp;lt;/summary&amp;gt;
    public static readonly string SameOrigin = &quot;SAMEORIGIN&quot;;

    /// &amp;lt;summary&amp;gt;
    /// The page can only be displayed in a frame on the specified origin. {0} specifies the format string
    /// &amp;lt;/summary&amp;gt;
    public static readonly string AllowFromUri = &quot;ALLOW-FROM {0}&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Server headery-related constants.
/// &amp;lt;/summary&amp;gt;
public static class ServerConstants
{
    /// &amp;lt;summary&amp;gt;
    /// The header value for X-Powered-By
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;Server&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Strict-Transportat-Security&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Strict-Transport-Security-related constants.
/// &amp;lt;/summary&amp;gt;
public static class StrictTransportSecurityConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for Strict-Transport-Security
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;Strict-Transport-Security&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Tells the user-agent to cache the domain in the STS list for the provided number of seconds {0} 
    /// &amp;lt;/summary&amp;gt;
    public static readonly string MaxAge = &quot;max-age={0}&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Tells the user-agent to cache the domain in the STS list for the provided number of seconds {0} and include any subdomains.
    /// &amp;lt;/summary&amp;gt;
    public static readonly string MaxAgeIncludeSubdomains = &quot;max-age={0}; includeSubDomains&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Tells the user-agent to remove, or not cache the host in the STS cache.
    /// &amp;lt;/summary&amp;gt;
    public static readonly string NoCache = &quot;max-age=0&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;X-XSS-Protection&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// X-XSS-Protection-related constants.
/// &amp;lt;/summary&amp;gt;
public static class XssProtectionConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for X-XSS-Protection
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;X-XSS-Protection&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Enables the XSS Protections
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Enabled = &quot;1&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Disables the XSS Protections offered by the user-agent.
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Disabled = &quot;0&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Enables XSS protections and instructs the user-agent to block the response in the event that script has been inserted from user input, instead of sanitizing.
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Block = &quot;1; mode=block&quot;;

    /// &amp;lt;summary&amp;gt;
    /// A partially supported directive that tells the user-agent to report potential XSS attacks to a single URL. Data will be POST&#39;d to the report URL in JSON format. 
    /// {0} specifies the report url, including protocol
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Report = &quot;1; report={0}&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Referrer-Policy&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Referrer Policy related constants
/// &amp;lt;/summary&amp;gt;
public static class ReferrerPolicyConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for Referrer-Policy
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;Referrer-Policy&quot;;

    public static readonly string NoReferrer = &quot;no-referrer&quot;;

    public static readonly string NoReferrerWhenDowngrade = &quot;no-referrer-when-downgrade&quot;;

    public static readonly string SameOrigin = &quot;same-origin&quot;;

    public static readonly string Origin = &quot;origin&quot;;

    public static readonly string StrictOrigin = &quot;strict-origin&quot;;

    public static readonly string OriginWhenCrossOrigin = &quot;origin-when-cross-origin&quot;;

    public static readonly string StrictOriginWhenCrossOrigin = &quot;strict-origin-when-cross-origin&quot;;

    public static readonly string UnsafeUrl = &quot;unsafe-url&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Permissions-Policy&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Permissions Policy related constants
/// &amp;lt;/summary&amp;gt;
public static class PermissionsPolicyConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for Permissions-Policy
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;Permissions-Policy&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When setting permissions policy, there are a variety of functions that you can set permissions for. The various functions I have put into an enum:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Enum of some permission policies
/// &amp;lt;/summary&amp;gt;
public enum PermissionPolicy
{
    [EnumSelectionDescription(Text = &quot;Accelerometer&quot;, Value = &quot;accelerometer&quot;)]
    Accelerometer = 1,

    [EnumSelectionDescription(Text = &quot;Camera&quot;, Value = &quot;camera&quot;)]
    Camera = 2,

    [EnumSelectionDescription(Text = &quot;Geolocation&quot;, Value = &quot;&quot;)]
    Geolocation = 3,

    [EnumSelectionDescription(Text = &quot;Gyroscope&quot;, Value = &quot;gyroscope&quot;)]
    Gyroscope = 4,

    [EnumSelectionDescription(Text = &quot;Magnetometer&quot;, Value = &quot;magnetometer&quot;)]
    Magnetometer = 5,

    [EnumSelectionDescription(Text = &quot;Microphone&quot;, Value = &quot;microphone&quot;)]
    Microphone = 6,

    [EnumSelectionDescription(Text = &quot;Payment&quot;, Value = &quot;payment&quot;)]
    Payment = 7,

    [EnumSelectionDescription(Text = &quot;Usb&quot;, Value = &quot;usb&quot;)]
    Usb = 8
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Content-Security-Policy&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Permissions Policy related constants
/// &amp;lt;/summary&amp;gt;
public static class ContentSecurityPolicyConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for Permissions-Policy
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;Content-Security-Policy&quot;;

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Content Security Policy needs to be set for a variety of types of content. I control these with an enum also:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;public enum ContentSecurityPolicy
{
    [EnumSelectionDescription(Text = &quot;DefaultSource&quot;, Value = &quot;default-src &#39;self&#39;&quot;)]
    DefaultSource = 1,

    [EnumSelectionDescription(Text = &quot;ConnectSource&quot;, Value = &quot; connect-src * &#39;self&#39; data: https:&quot;)]
    ConnectSource = 2,

    [EnumSelectionDescription(Text = &quot;FontSource&quot;, Value = &quot; font-src &#39;self&#39; data: https:&quot;)]
    FontSource = 3,

    [EnumSelectionDescription(Text = &quot;FrameSource&quot;, Value = &quot; frame-src &#39;self&#39; data: https:&quot;)]
    FrameSource = 4,

    [EnumSelectionDescription(Text = &quot;ImageSource&quot;, Value = &quot;  img-src * &#39;self&#39; data: https: blob:&quot;)]
    ImageSource = 5,

    [EnumSelectionDescription(Text = &quot;ScriptSource&quot;, Value = &quot; script-src &#39;self&#39; &#39;nonce-{nonceValue}&#39; &#39;strict-dynamic&#39; &quot;)]
    ScriptSource = 6,

    [EnumSelectionDescription(Text = &quot;StyleSource&quot;, Value = &quot; style-src &#39;self&#39; &#39;unsafe-inline&#39; *&quot;)]
    StyleSource = 7,

    [EnumSelectionDescription(Text = &quot;FormAction&quot;, Value = &quot; form-action &#39;self&#39; data: https:&quot;)]
    FormAction = 8,

    [EnumSelectionDescription(Text = &quot;MediaSource&quot;, Value = &quot; media-src &#39;self&#39; data: https: blob:&quot;)]
    MediaSource = 9
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;X-Download-Options&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Permissions Policy related constants
/// &amp;lt;/summary&amp;gt;
public static class XDownloadOptionsConstants
{
    /// &amp;lt;summary&amp;gt;
    /// Header value for Permissions-Policy
    /// &amp;lt;/summary&amp;gt;
    public static readonly string Header = &quot;X-Download-Options&quot;;

    public static readonly string NoOpen = &quot;noopen&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The real work I do is in a builder class, SecurityHeadersBuilder. This builder has a variety of methods for setting the different header values. I then create a method that builds up the security policy. You could conceptually have a variety of policies. I only need one, so I have a default method that creates all of my headers. It stores them in a policy class.&lt;/p&gt;
&lt;p&gt;First the policy class. Its pretty straight forward, it just has an add and remove method for setting headers.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// The security headers policy
/// &amp;lt;/summary&amp;gt;
public class SecurityHeadersPolicy
{
    /// &amp;lt;summary&amp;gt;
    /// Headers to add
    /// &amp;lt;/summary&amp;gt;
    public IDictionary&amp;lt;string, string&amp;gt; SetHeaders { get; }
        = new Dictionary&amp;lt;string, string&amp;gt;();

    /// &amp;lt;summary&amp;gt;
    /// Headers to remove
    /// &amp;lt;/summary&amp;gt;
    public ISet&amp;lt;string&amp;gt; RemoveHeaders { get; }
        = new HashSet&amp;lt;string&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It stores these values in a dictionary.&lt;/p&gt;
&lt;p&gt;The builder then adds the headers to this policy:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Middle ware to create the desired security headers
/// &amp;lt;/summary&amp;gt;
public class SecurityHeadersBuilder
{
    private readonly SecurityHeadersPolicy _policy = new();

    /// &amp;lt;summary&amp;gt;
    /// The number of seconds in one year
    /// &amp;lt;/summary&amp;gt;
    public const int OneYearInSeconds = 60 * 60 * 24 * 365;

    private CompositeFormat FrameOptionsAllowFromUri { get; set; } = CompositeFormat.Parse(FrameOptionsConstants.AllowFromUri);

    private CompositeFormat XssProtectionConstantsReport { get; set; } = CompositeFormat.Parse(XssProtectionConstants.Report);

    private CompositeFormat StrictTransportSecurityConstantsMaxAge { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAge);

    private CompositeFormat StrictTransportSecurityConstantsMaxAgeIncludeSubdomains { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAgeIncludeSubdomains);

    /// &amp;lt;summary&amp;gt;
    /// Add default headers in accordance with most secure approach
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddDefaultSecurePolicy()
    {
        this.AddXssProtectionBlock();
        this.AddStrictTransportSecurityMaxAge();
        this.AddPermissionsPolicy();
        this.AddXssProtectionBlock();
        this.AddContentTypeOptionsNoSniff();
        this.AddReferrerPolicyStrictOriginWhenCrossOrigin();
        this.AddXDownloadOptionsNoOpen();

        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-Frame-Options DENY to all requests.
    /// The page cannot be displayed in a frame, regardless of the site attempting to do so
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddFrameOptionsDeny()
    {
        this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-Frame-Options SAMEORIGIN to all requests.
    /// The page can only be displayed in a frame on the same origin as the page itself.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
    {
        this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-Frame-Options ALLOW-FROM {uri} to all requests, where the uri is provided
    /// The page can only be displayed in a frame on the specified origin.
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;uri&quot;&amp;gt;The uri of the origin in which the page may be displayed in a frame&amp;lt;/param&amp;gt;
    public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
    {
        this._policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(CultureInfo.InvariantCulture, this.FrameOptionsAllowFromUri, uri);
        return this;
    }


    /// &amp;lt;summary&amp;gt;
    /// Add X-XSS-Protection 1 to all requests.
    /// Enables the XSS Protections
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddXssProtectionEnabled()
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Enabled;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-XSS-Protection 0 to all requests.
    /// Disables the XSS Protections offered by the user-agent.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddXssProtectionDisabled()
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Disabled;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-XSS-Protection 1; mode=block to all requests.
    /// Enables XSS protections and instructs the user-agent to block the response in the event that script has been inserted from user input, instead of sanitizing.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddXssProtectionBlock()
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] = XssProtectionConstants.Block;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-XSS-Protection 1; report=http://site.com/report to all requests.
    /// A partially supported directive that tells the user-agent to report potential XSS attacks to a single URL. Data will be POST&#39;d to the report URL in JSON format.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddXssProtectionReport(string reportUrl)
    {
        this._policy.SetHeaders[XssProtectionConstants.Header] =
            string.Format(CultureInfo.InvariantCulture, this.XssProtectionConstantsReport, reportUrl);
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add Strict-Transport-Security max-age=&amp;lt;see cref=&quot;maxAge&quot;/&amp;gt; to all requests.
    /// Tells the user-agent to cache the domain in the STS list for the number of seconds provided.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddStrictTransportSecurityMaxAge(int maxAge = OneYearInSeconds)
    {
        this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
            string.Format(CultureInfo.InvariantCulture, this.StrictTransportSecurityConstantsMaxAge, maxAge);
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add Strict-Transport-Security max-age=&amp;lt;see cref=&quot;maxAge&quot;/&amp;gt;; includeSubDomains to all requests.
    /// Tells the user-agent to cache the domain in the STS list for the number of seconds provided and include any subdomains.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddStrictTransportSecurityMaxAgeIncludeSubDomains(int maxAge = OneYearInSeconds)
    {
        this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
            string.Format(CultureInfo.InvariantCulture, this.StrictTransportSecurityConstantsMaxAgeIncludeSubdomains, maxAge);
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add Strict-Transport-Security max-age=0 to all requests.
    /// Tells the user-agent to remove, or not cache the host in the STS cache
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddStrictTransportSecurityNoCache()
    {
        this._policy.SetHeaders[StrictTransportSecurityConstants.Header] =
            StrictTransportSecurityConstants.NoCache;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add X-Content-Type-Options nosniff to all requests.
    /// Can be set to protect against MIME type confusion attacks.
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder AddContentTypeOptionsNoSniff()
    {
        this._policy.SetHeaders[ContentTypeOptionsConstants.Header] = ContentTypeOptionsConstants.NoSniff;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Removes the Server header from all responses
    /// &amp;lt;/summary&amp;gt;
    public SecurityHeadersBuilder RemoveServerHeader()
    {
        this._policy.RemoveHeaders.Add(ServerConstants.Header);
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add the XDownload option headers
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public SecurityHeadersBuilder AddXDownloadOptionsNoOpen()
    {
        this._policy.SetHeaders[XDownloadOptionsConstants.Header] = XDownloadOptionsConstants.NoOpen;
        return this;

    }

    /// &amp;lt;summary&amp;gt;
    /// Adds a custom header to all requests
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;header&quot;&amp;gt;The header name&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;value&quot;&amp;gt;The value for the header&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public SecurityHeadersBuilder AddCustomHeader(string header, string value)
    {
        if (string.IsNullOrEmpty(header))
        {
            throw new ArgumentNullException(nameof(header));
        }

        this._policy.SetHeaders[header] = value;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Remove a header from all requests
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;header&quot;&amp;gt;The to remove&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public SecurityHeadersBuilder RemoveHeader(string header)
    {
        if (string.IsNullOrEmpty(header))
        {
            throw new ArgumentNullException(nameof(header));
        }

        this._policy.RemoveHeaders.Add(header);
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add referrer policy header of NoReferrer
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public SecurityHeadersBuilder AddReferrerPolicyNoReferrer()
    {
        this._policy.SetHeaders[ReferrerPolicyConstants.Header] = ReferrerPolicyConstants.NoReferrer;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add referrer policy header of NoReferrer
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public SecurityHeadersBuilder AddReferrerPolicyStrictOriginWhenCrossOrigin()
    {
        this._policy.SetHeaders[ReferrerPolicyConstants.Header] = ReferrerPolicyConstants.StrictOriginWhenCrossOrigin;
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add the permissions policy
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public SecurityHeadersBuilder AddPermissionsPolicy()
    {
        // Get all permissions policies
        var permissionPolicies = Enum.GetValues&amp;lt;PermissionPolicy&amp;gt;();

        // Loop through the policies
        var policy = (from permissionPolicy in permissionPolicies
                      select permissionPolicy.Value() into value
                      where !string.IsNullOrEmpty(value)
                      select $&quot;{value}=()&quot;).ToList();

        this._policy.SetHeaders[PermissionsPolicyConstants.Header] = string.Join(&#39;,&#39;, policy.ToArray());
        return this;
    }

    /// &amp;lt;summary&amp;gt;
    /// Builds a new &amp;lt;see cref=&quot;SecurityHeadersPolicy&quot;/&amp;gt; using the entries added.
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;The constructed &amp;lt;see cref=&quot;SecurityHeadersPolicy&quot;/&amp;gt;.&amp;lt;/returns&amp;gt;
    public SecurityHeadersPolicy Build() =&amp;gt; this._policy;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Breaking down whats happening here:&lt;/p&gt;
&lt;p&gt;First I create the policy:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;private readonly SecurityHeadersPolicy _policy = new();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I create a constant to store 1 year in seconds (For the Strinct Transport Security header, I set a max age, which should be a year)&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// The number of seconds in one year
/// &amp;lt;/summary&amp;gt;
public const int OneYearInSeconds = 60 * 60 * 24 * 365;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I have a few header values that need string replacement. For each of these I create a CompositeFormat object by parsing the header value. Creating the CompositeFormat caches the parsing of this string, adding a small performance boost on subsequent calls. The replacement will happen later:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;private CompositeFormat FrameOptionsAllowFromUri { get; set; } = CompositeFormat.Parse(FrameOptionsConstants.AllowFromUri);

private CompositeFormat XssProtectionConstantsReport { get; set; } = CompositeFormat.Parse(XssProtectionConstants.Report);

private CompositeFormat StrictTransportSecurityConstantsMaxAge { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAge);

private CompositeFormat StrictTransportSecurityConstantsMaxAgeIncludeSubdomains { get; set; } = CompositeFormat.Parse(StrictTransportSecurityConstants.MaxAgeIncludeSubdomains);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, if you look above, you can see that FrameOptionsConstants.AllowFromUri equals &quot;ALLOW-FROM {0}&quot;.&amp;nbsp; When I set this header, I will replace &#39;{0}&#39; with the url.&lt;/p&gt;
&lt;p&gt;Next I create a method for setting each header value. For example, for FrameOptions, I have three methods for setting the different values:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Add X-Frame-Options DENY to all requests.
/// The page cannot be displayed in a frame, regardless of the site attempting to do so
/// &amp;lt;/summary&amp;gt;
public SecurityHeadersBuilder AddFrameOptionsDeny()
{
    this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.Deny;
    return this;
}

/// &amp;lt;summary&amp;gt;
/// Add X-Frame-Options SAMEORIGIN to all requests.
/// The page can only be displayed in a frame on the same origin as the page itself.
/// &amp;lt;/summary&amp;gt;
public SecurityHeadersBuilder AddFrameOptionsSameOrigin()
{
    this._policy.SetHeaders[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
    return this;
}

/// &amp;lt;summary&amp;gt;
/// Add X-Frame-Options ALLOW-FROM {uri} to all requests, where the uri is provided
/// The page can only be displayed in a frame on the specified origin.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;uri&quot;&amp;gt;The uri of the origin in which the page may be displayed in a frame&amp;lt;/param&amp;gt;
public SecurityHeadersBuilder AddFrameOptionsSameOrigin(string uri)
{
    this._policy.SetHeaders[FrameOptionsConstants.Header] = string.Format(CultureInfo.InvariantCulture, this.FrameOptionsAllowFromUri, uri);
    return this;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each method sets the header to the desired value. Using the policy object I created, it adds the header using the constant of the header name from our constants classes, and then the constant of the desired value.&lt;/p&gt;
&lt;p&gt;Removing the server header is common, so I have a specific method for this:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Removes the Server header from all responses
/// &amp;lt;/summary&amp;gt;
public SecurityHeadersBuilder RemoveServerHeader()
{
    this._policy.RemoveHeaders.Add(ServerConstants.Header);
    return this;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For permission policy, I want to allow all possible permissions of features, so I just iterate over the enum and add all the values.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Add the permissions policy
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
public SecurityHeadersBuilder AddPermissionsPolicy()
{
    // Get all permissions policies
    var permissionPolicies = Enum.GetValues&amp;lt;PermissionPolicy&amp;gt;();

    // Loop through the policies
    var policy = (from permissionPolicy in permissionPolicies
                  select permissionPolicy.Value() into value
                  where !string.IsNullOrEmpty(value)
                  select $&quot;{value}=()&quot;).ToList();

    this._policy.SetHeaders[PermissionsPolicyConstants.Header] = string.Join(&#39;,&#39;, policy.ToArray());
    return this;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For your needs, you might need to create a different version of this method that only sets the permissions you need.&lt;/p&gt;
&lt;p&gt;I have a specific method that creates our default policy:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Add default headers in accordance with most secure approach
/// &amp;lt;/summary&amp;gt;
public SecurityHeadersBuilder AddDefaultSecurePolicy()
{
    this.AddXssProtectionBlock();
    this.AddStrictTransportSecurityMaxAge();
    this.AddPermissionsPolicy();    
    this.AddContentTypeOptionsNoSniff();
    this.AddReferrerPolicyStrictOriginWhenCrossOrigin();
    this.AddXDownloadOptionsNoOpen();

    return this;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here I am only calling the methods I want/need to make my policy. Notice Content Security Policy is missing here. I will come back to that.&lt;/p&gt;
&lt;p&gt;Lastly I have a &#39;build&#39; method that returns the policy:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Builds a new &amp;lt;see cref=&quot;SecurityHeadersPolicy&quot;/&amp;gt; using the entries added.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;returns&amp;gt;The constructed &amp;lt;see cref=&quot;SecurityHeadersPolicy&quot;/&amp;gt;.&amp;lt;/returns&amp;gt;
public SecurityHeadersPolicy Build() =&amp;gt; this._policy;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The next piece I need is the actual middleware to create and use these headers. Middleware requires an invoke method that does the work. That is, it takes the policies created in the builder and adds them as actual headers. Additionally, this is where I add the Content Security Policy.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;using System.Globalization;
using EPiServer.Framework.ClientResources;
using Hero.OptimizelyCMS.Foundation.Extensions;
using System.Web;
using Hero.OptimizelyCMS.Foundation.Utilities;

namespace Hero.OptimizelyCMS.Foundation.SecurityPolicy.SecurityMiddleware;

/// &amp;lt;summary&amp;gt;
/// Middleware for setting security headers
/// &amp;lt;/summary&amp;gt;
public class SecurityHeadersMiddleware
{
    private readonly RequestDelegate _next;
    private readonly SecurityHeadersPolicy _policy;
    private readonly ICspNonceService _cspNonceService;

    public SecurityHeadersMiddleware(RequestDelegate next, SecurityHeadersPolicy policy, ICspNonceService cspNonceService)
    {
        this._next = next;
        this._policy = policy;
        this._cspNonceService = cspNonceService;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add and remove desired headers
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;context&quot;&amp;gt;The current HttpContext&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public async Task Invoke(HttpContext context)
    {
        // Get existing headers
        var headersDictionary = context.Response.Headers;

        // If we should add Csp
        if (ShouldAddCsp(context))
        {
            // Add CSP
            headersDictionary[ContentSecurityPolicyConstants.Header] = this.GetContentSecurityPolicy();

            // Frame options
            headersDictionary[FrameOptionsConstants.Header] = FrameOptionsConstants.SameOrigin;
        }

        // Loop through headers to add
        foreach (var headerValuePair in this._policy.SetHeaders)
        {
            // Add each header
            headersDictionary[headerValuePair.Key] = headerValuePair.Value;
        }

        // Loop through headers to remove
        foreach (var header in this._policy.RemoveHeaders)
        {
            // Remove the header
            headersDictionary.Remove(header);
        }

        await this._next(context);
    }

    /// &amp;lt;summary&amp;gt;
    /// Determine if we should add Csp
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;context&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private static bool ShouldAddCsp(HttpContext context)
    {
        // Get the current url
        var currentUrl = context.Request.Url();
        var path = currentUrl.PathAndQuery;
        var segments = path.Split(&#39;/&#39;, StringSplitOptions.RemoveEmptyEntries);

        // Get the first segment
        var segmentZero = segments.ElementAtOrDefault(0);

        // If there is no segment zero, we are on the home page
        if (string.IsNullOrEmpty(segmentZero))
        {
            return true;
        }

        // If the segment is one of these, we should not add CSP
        return segmentZero.ToLower(CultureInfo.InvariantCulture) switch
        {
            &quot;error&quot; =&amp;gt; false,
            &quot;episerver&quot; =&amp;gt; false,
            &quot;util&quot; =&amp;gt; false,
            &quot;cleaner&quot; =&amp;gt; false,
            &quot;redirectmanager&quot; =&amp;gt; false,
            _ =&amp;gt; true,
        };
    }

    /// &amp;lt;summary&amp;gt;
    /// Create the Csp
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public string GetContentSecurityPolicy()
    {
        var policy = new List&amp;lt;string&amp;gt;();

        // Get Csp Enum values
        var contentSecurityPolicies = Enum.GetValues&amp;lt;ContentSecurityPolicy&amp;gt;();

        // Loop through each policy
        foreach (var contentSecurityPolicy in contentSecurityPolicies)
        {
            // Get the policy value
            var value = contentSecurityPolicy.Value();

            // Add the nonce to the policy
            if (string.IsNullOrEmpty(value))
            {
                continue;
            }

            var nonce = this._cspNonceService.GetNonce();
            nonce = HttpUtility.JavaScriptStringEncode(nonce);
            value = value.Replace(&quot;{nonceValue}&quot;, nonce);
            policy.Add(value);
        }

        // Return the policy
        return string.Join(&#39;;&#39;, policy.ToArray());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And lets break down whats happening here:&lt;/p&gt;
&lt;p&gt;I inject the RequestDelegate. This is how the middleware will move on to the next piece of middleware once done here. I add our header policy, and an instance of ICspNonceService, for adding a content security policy nonce (More on this in a bit) .&lt;/p&gt;
&lt;p&gt;When Invoke is called, I get the current headers dictionary, I determine if I should add the Content Security Policy, and then I loop through all the appropriate headers in our policy and add them to the response, and remove any headers that should be removed.&lt;/p&gt;
&lt;p&gt;I found that adding the Content Security Policy while inside the CMS broke a lot of CMS functionality. So I look at the path and I don&#39;t add the security policy if I am in the CMS. This includes the normal cms paths (episerver, util) as well as some custom urls for custom admin tools. You might need to include other paths, depending on any custom tools or third party add ons you have installed.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Determine if we should add Csp
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;context&quot;&amp;gt;&amp;lt;/param&amp;gt;
/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
private static bool ShouldAddCsp(HttpContext context)
{
    // Get the current url
    var currentUrl = context.Request.Url();
    var path = currentUrl.PathAndQuery;
    var segments = path.Split(&#39;/&#39;, StringSplitOptions.RemoveEmptyEntries);

    // Get the first segment
    var segmentZero = segments.ElementAtOrDefault(0);

    // If there is no segment zero, we are on the home page
    if (string.IsNullOrEmpty(segmentZero))
    {
        return true;
    }

    // If the segment is one of these, we should not add CSP
    return segmentZero.ToLower(CultureInfo.InvariantCulture) switch
    {
        &quot;error&quot; =&amp;gt; false,
        &quot;episerver&quot; =&amp;gt; false,
        &quot;util&quot; =&amp;gt; false,
        &quot;cleaner&quot; =&amp;gt; false,
        &quot;redirectmanager&quot; =&amp;gt; false,
        _ =&amp;gt; true,
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I need to actually build the policy, I loop through the Enum I set up and add these values.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Create the Csp
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
public string GetContentSecurityPolicy()
{
    var policy = new List&amp;lt;string&amp;gt;();

    // Get Csp Enum values
    var contentSecurityPolicies = Enum.GetValues&amp;lt;ContentSecurityPolicy&amp;gt;();

    // Loop through each policy
    foreach (var contentSecurityPolicy in contentSecurityPolicies)
    {
        // Get the policy value
        var value = contentSecurityPolicy.Value();

        // Add the nonce to the policy
        if (string.IsNullOrEmpty(value))
        {
            continue;
        }

        var nonce = this._cspNonceService.GetNonce();
        nonce = HttpUtility.JavaScriptStringEncode(nonce);
        value = value.Replace(&quot;{nonceValue}&quot;, nonce);
        policy.Add(value);
    }

    // Return the policy
    return string.Join(&#39;;&#39;, policy.ToArray());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is one small thing to call out here. I want to use a nonce with scripts. A nonce is a hash that is unique for each request. If a script tag doesnt have the nonce, the browser will treat is as suspicious and wont run the script. A script tag with a nonce might look like this:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;script src=&quot;/dist/main.min.js?t=638433482440000000&quot; nonce=&quot;wm+Z3Sws/IkrrJUnTulrX4M0Lez8VY49of/BSW8vyOw=&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I created a custom implementation of this service and I use this to generate the nonce and string replace it in the ScriptSource attribute of the Content Security Policy.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Concrete implementation of nonce service for generating the nonce
/// &amp;lt;/summary&amp;gt;
public class CspNonceService : ICspNonceService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CspNonceService(IHttpContextAccessor httpContextAccessor) =&amp;gt; this._httpContextAccessor = httpContextAccessor;

    // Cache key
    public const string Key = &quot;csp-nonce&quot;;

    /// &amp;lt;summary&amp;gt;
    /// Get the current nonce
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public string GetNonce()
    {
        // Get the cache
        var items = this._httpContextAccessor.HttpContext?.Items;

        // If we have no items and the key is not in the cache
        if (items != null &amp;amp;&amp;amp; !items.ContainsKey(Key))
        {
            // Generate the nonce and add it to cache
            items.Add(Key, GenerateNonce());
        }

        // Get the nonce from cache
        var nonce = items != null ? items[Key] : GenerateNonce();

        // Return the nonce
        return nonce?.ToString();
    }

    /// &amp;lt;summary&amp;gt;
    /// Generate a new nonce
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private static string GenerateNonce()
    {
        // Generate a new nonce
        var numArray = new byte[32];
        using (var randomNumberGenerator = RandomNumberGenerator.Create())
        {
            randomNumberGenerator.GetBytes(numArray);
        }

        // Convert it to base 64 string
        return Convert.ToBase64String(numArray);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Startup&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The last step is to register any resources I need, and add the middleware to the pipeline.&lt;/p&gt;
&lt;p&gt;In Startup.cs, I register the nonce service in the ConfigureServices method:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;// Add nonce service
services.AddScoped&amp;lt;ICspNonceService&amp;gt;(sp =&amp;gt; new CspNonceService(sp.GetRequiredService&amp;lt;IHttpContextAccessor&amp;gt;()));&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optimizely can add this to its own script blocks once this is registered (&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/content-security-policy&quot;&gt;https://docs.developers.optimizely.com/content-management-system/docs/content-security-policy&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;NOTE: Using the nonce is an all or nothing proposition. If you implement this, any script tags added to pages will need the nonce. This will require you to manually set it using the ICspNonceService implementation.&lt;/p&gt;
&lt;p&gt;Next I create an extension method to make this policy easier to set.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Method for implementing security header middleware
/// &amp;lt;/summary&amp;gt;
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseSecurityHeadersMiddleware(this IApplicationBuilder app, SecurityHeadersBuilder builder)
    {
        var policy = builder.Build();
        return app.UseMiddleware&amp;lt;SecurityHeadersMiddleware&amp;gt;(policy);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Lastly I add the middleware to the pipeline, also in Startup.cs in the configure method. I add it early in the pipeline so all requests get it:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;// Add security headers
app.UseSecurityHeadersMiddleware(new SecurityHeadersBuilder()
    .AddDefaultSecurePolicy()
    .RemoveHeader(&quot;X-Powered-By&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can be customized easily to your specific needs.&lt;/p&gt;
&lt;p&gt;NOTE: I am running locally and you can see that the server header is still there. The Server header for Kestrel has to be removed in a different way, when you configure Kestrel in your code.&lt;/p&gt;
&lt;p&gt;Now when I go to my site I can see all of my headers:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/6af98df6651649f5a3fb63a6e967d82d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And I can see if I log in to the CMS, the Content Security Policy is not being added:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/82b328e0b6394557bf30d057f6ff3644.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Lastly, if I scan a site using this configuration, I can see that my headers are set up appropriately:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/e5fbcfb81931409ab02aec8ec49bbf8d.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Potential Future Enhancement: With Content Security Policy, here I have constructed a fairly permissive policy. You can actually include specific urls for the various parts of the policy. This would probably best be maintained an appsettings, and then retrieved and added to the policy.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/ethan-schofer/dates/2024/2/roll-your-own-security-headers/</guid>            <pubDate>Wed, 21 Feb 2024 19:49:23 GMT</pubDate>           <category>Blog post</category></item><item> <title>Unit Testing Optimizely - How to deal with the ServiceLocator</title>            <link>https://world.optimizely.com/blogs/ethan-schofer/dates/2024/2/unit-testing-optimizely---how-to-deal-with-the-servicelocator/</link>            <description>&lt;p&gt;Unit testing with Optimizely can be challenging. One of those challenges is Optimizely&#39;s use of the ServiceLocator pattern. ServiceLocator is generally considered to be an anti-pattern (&lt;a href=&quot;https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/&quot;&gt;https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/&lt;/a&gt;). One of the reasons for this is the difficulty it causes with unit tests. We, as developers can avoid this pattern, but it is still baked into a lot of Optimizely&#39;s code. So, testing code that relies on Optimizely classes can run into a problem because the ServiceLocator used in those Optimizely classes doesnt have anything registered in the context of a unit test. Here is a way around this problem.&lt;/p&gt;
&lt;p&gt;For this blog post, Im using Xunit for tests, and NSubstitute for mocking things. The method described below should conceptually work with any testing and mocking framework, but the syntax may vary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Xunit, at the time of this post, does not have a built in way to run some code once prior to all tests. Luckily, someone named Daniel Cazzulino has created a NuGet package that will allow us to do exactly this: &lt;a href=&quot;https://www.cazzulino.com/assembly-fixtures.html&quot;&gt;https://www.cazzulino.com/assembly-fixtures.html&lt;/a&gt;. Xunit calls classes that maintain things accross tests in a single test a &#39;fixture&#39;. This package allows for the creation of an assembly fixture, a fixture that maintains a lifecycle accross all test. You can use it by having your test class implement IAssemblyFixture, a generic interface that requires an actual fixture class.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Fixture&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For this case, we need an fixture that mimics the functionalty of the service locator.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.Templating;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;

namespace MyProject.Tests.Infrastructure;

/// &amp;lt;summary&amp;gt;
/// Mock service locator for tests
/// &amp;lt;/summary&amp;gt;
public class ServiceLocatorFixture : IDisposable
{
    /// &amp;lt;summary&amp;gt;
    /// Add items to DI on constructor
    /// &amp;lt;/summary&amp;gt;
    public ServiceLocatorFixture()
    {
        this.Providing&amp;lt;IContentLoader&amp;gt;();        
        this.Providing&amp;lt;ICompositeViewEngine&amp;gt;();
        this.Providing&amp;lt;IModelTemplateTagResolver&amp;gt;();
        this.Providing&amp;lt;IHtmlHelper&amp;gt;();
        this.Providing&amp;lt;IContextModeResolver&amp;gt;();        
    }

    /// &amp;lt;summary&amp;gt;
    /// Encapsulated service provider
    /// &amp;lt;/summary&amp;gt;
    private IServiceProvider _serviceProvider;

    /// &amp;lt;summary&amp;gt;
    /// Public accessor for service locator
    /// &amp;lt;/summary&amp;gt;
    public IServiceProvider ServiceProvider
    {
        get
        {
            // If it already exists, just return it            
            if (this._serviceProvider != null)
            {
                return this._serviceProvider;
            }
            // Create new service provider
            this._serviceProvider = Substitute.For&amp;lt;IServiceProvider&amp;gt;();
            ServiceLocator.SetServiceProvider(this._serviceProvider);
            return this._serviceProvider;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// Add item to service locator
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;typeparam name=&quot;T&quot;&amp;gt;&amp;lt;/typeparam&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public T Providing&amp;lt;T&amp;gt;() where T : class
    {
        var obj = Substitute.For&amp;lt;T&amp;gt;();
        this.ServiceProvider.GetService&amp;lt;T&amp;gt;().Returns(obj);
        this.ServiceProvider.GetRequiredService&amp;lt;T&amp;gt;().Returns(obj);
        return obj;
    }

    /// &amp;lt;summary&amp;gt;
    /// Add item to service locator
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;typeparam name=&quot;T&quot;&amp;gt;&amp;lt;/typeparam&amp;gt;
    /// &amp;lt;param name=&quot;instance&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public T Providing&amp;lt;T&amp;gt;(T instance) where T : class
    {
        this.ServiceProvider.GetService&amp;lt;T&amp;gt;().Returns(instance);
        this.ServiceProvider.GetRequiredService&amp;lt;T&amp;gt;().Returns(instance);
        return instance;
    }    

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reviewing whats going on here:&lt;br /&gt;&lt;br /&gt;This fixture has a property of IServiceProvider. This is what underlies the ServiceLocator. The first time it is called, it checks if the underlying field _serviceProvider has value. If not, it creates a substitute for the service provider, and then assigns it to the ServiceLocator:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public IServiceProvider ServiceProvider
{
	get
	{
		// If it already exists, just return it            
		if (this._serviceProvider != null)
		{
			return this._serviceProvider;
		}
		// Create new service provider
		this._serviceProvider = Substitute.For&amp;lt;IServiceProvider&amp;gt;();
		ServiceLocator.SetServiceProvider(this._serviceProvider);
		return this._serviceProvider;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fixture offers two ways to add things to the service provider with the methods &#39;Providing&#39;. The first method just takes a type as generic attribute:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public T Providing&amp;lt;T&amp;gt;() where T : class
{
	var obj = Substitute.For&amp;lt;T&amp;gt;();
	this.ServiceProvider.GetService&amp;lt;T&amp;gt;().Returns(obj);
	this.ServiceProvider.GetRequiredService&amp;lt;T&amp;gt;().Returns(obj);
	return obj;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that it implements GetService and GetRequiredService. Items can be registered simply:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;this.Providing&amp;lt;IContentLoader&amp;gt;(); &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This specific content loader will be available in any test class implementing this fixture.&lt;/p&gt;
&lt;p&gt;The other version of providing allows you to register a specific implementation of a class or substitute:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public T Providing&amp;lt;T&amp;gt;(T instance) where T : class
{
	this.ServiceProvider.GetService&amp;lt;T&amp;gt;().Returns(instance);
	this.ServiceProvider.GetRequiredService&amp;lt;T&amp;gt;().Returns(instance);
	return instance;
}   &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that this also implements GetService and GetRequiredService. Also notice that using NSubstitute, we can make this return a specific implementation.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var contentTypeRepository = Substitute.For&amp;lt;IContentTypeRepository&amp;gt;();
contentTypeRepository.List().Returns(new List&amp;lt;ContentType&amp;gt; { testBlockType, new() { Name = &quot;TestPage&quot;, ID = 2 } });
contentTypeRepository.Load(1).Returns(testBlockType);
this.Providing(contentTypeRepository);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So using the ServiceLocator to get the content type repository will return this specific implementation. Lastly, in the constructor we register all the things we might need from the ServiceLocator in tests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Usage&lt;br /&gt;&lt;br /&gt;&lt;/strong&gt;To use this fixture, we need to have our test class implement IAssemblyFixture, and to inject the ServiceLocatorFixture in the test class constructor.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class MyClassTests : IAssemblyFixture&amp;lt;ServiceLocatorFixture&amp;gt;
{
    private readonly ServiceLocatorFixture _serviceLocatorFixture;

    public MyClassTests(ServiceLocatorFixture serviceLocatorFixture) =&amp;gt; this._serviceLocatorFixture = serviceLocatorFixture;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then we can start writing some tests. For a class that requires something from the DI container, we can get it from the fixture:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Fact]
public void Build_NormalDefaults_ViewModelPopulated()
{
    var contentLoader = this._fixture.ServiceProvider.GetRequiredService&amp;lt;IContentLoader&amp;gt;();
    var logger = Substitute.For&amp;lt;ILogger&amp;lt;TestBlockViewModelBuilder&amp;gt;&amp;gt;();

    var builder = new TestBlockViewModelBuilder(contentLoader, logger);
    var viewModel = builder.Build(new TestBlock());

    builder.CurrentContent.Should().NotBeNull();
    viewModel.Should().NotBeNull();
    viewModel.CurrentBlock.Should().NotBeNull();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But we can also make use of Optimizely classes that rely on the ServiceLocator, even if we dont need direct access to the ServiceLocator from the test:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Fact]
public void BuildStandardLoaderOptions_NoCulture_ReturnsOptions()
{
    var loaderOptionsService = new LoaderOptionsService();

    var options = loaderOptionsService.BuildStandardLoaderOptions();

    options.Should().NotBeNull();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Loader Options Service is a class we created to streamline creating LoaderOptions for calls to get content. If I look at the BuildStandardLoaderOptions method, if I do not provide a CultureInfo, it uses ContentLanguage.PreferredCulture, an Optimizely method for getting the current culture. This method relies on IContentLanguageAccessor. So for this test to work, I need to register IContentLanguageAccessor in the constructor of my fixture:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var contentLanguageAccessor = Substitute.For&amp;lt;IContentLanguageAccessor&amp;gt;();
contentLanguageAccessor.Language.Returns(new CultureInfo(&quot;en&quot;));
this.Providing(contentLanguageAccessor);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this project, we wanted the default language to be &quot;en&quot;, so this will always return that. So, this test will work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test Lifetime&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One thing to watch out for. When running unit tests in Visual Studio, running tests in Debug means each test is run in serial, one after the other. But if I just run tests all at once, they are on multiple threads. So, something done to something in the ServiceLocator could affect multiple tests. A symptom of this is that debugging all tests, the tests might pass, but if you just run all tests, they may fail because the ServiceLocator might return unexpected results.&lt;/p&gt;
&lt;p&gt;In order to deal with this, you might&amp;nbsp; need to combine this service locator fixture with specific Substitutes in the test. In particular, if you test returns specific content items that you need to test against, you might need to do this.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;p&gt;In my ServiceLocator I have registered a IContentLoader. But, for a specific test, I need that repository to return some content specific for this test. So, even though my test class implements IAssemblyFixture, in my test I create a specific substitute of IContentLoader just for this test.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class ContentLoaderExtensionsTests : TestBase, IAssemblyFixture&amp;lt;ServiceLocatorFixture&amp;gt;
{
    private readonly ServiceLocatorFixture _fixture;

    public ContentLoaderExtensionsTests(ServiceLocatorFixture fixture) =&amp;gt; this._fixture = fixture;

    [Fact]
    public void GetFirstChild_WhenCalled_ReturnsFirstChild()
    {
        var contentLoader = Substitute.For&amp;lt;IContentLoader&amp;gt;();

        var page1 = new TestPage();
        page1.Property.Add(&quot;PageLink&quot;, new PropertyContentReference(1));
        page1.Property.Add(&quot;PageTypeID&quot;, new PropertyNumber(100));
        page1.Property.Add(&quot;PageStartPublish&quot;, new PropertyDate(DateTime.Now.AddDays(-1)));
        page1.Property.Add(&quot;PageStopPublish&quot;, new PropertyDate(DateTime.Now.AddDays(1)));

        var page2 = new TestPage();
        page2.Property.Add(&quot;PageLink&quot;, new PropertyContentReference(2));
        page2.Property.Add(&quot;PageTypeID&quot;, new PropertyNumber(100));
        page2.Property.Add(&quot;PageStartPublish&quot;, new PropertyDate(DateTime.Now.AddDays(-1)));
        page2.Property.Add(&quot;PageStopPublish&quot;, new PropertyDate(DateTime.Now.AddDays(1)));
        page2.Property.Add(&quot;ParentLink&quot;, new PropertyContentReference(1));

        var page3 = new TestPage();
        page3.Property.Add(&quot;PageLink&quot;, new PropertyContentReference(3));
        page3.Property.Add(&quot;PageTypeID&quot;, new PropertyNumber(100));
        page3.Property.Add(&quot;PageStartPublish&quot;, new PropertyDate(DateTime.Now.AddDays(-1)));
        page3.Property.Add(&quot;PageStopPublish&quot;, new PropertyDate(DateTime.Now.AddDays(1)));
        page3.Property.Add(&quot;ParentLink&quot;, new PropertyContentReference(1));

        var securable = page1 as IContentSecurable;
        var securityDescriptor = securable?.GetContentSecurityDescriptor();
        securityDescriptor?.AddEntry(new AccessControlEntry(&quot;test&quot;, AccessLevel.FullAccess, SecurityEntityType.User));

        var securable2 = page2 as IContentSecurable;
        var securityDescriptor2 = securable2?.GetContentSecurityDescriptor();
        securityDescriptor2?.AddEntry(new AccessControlEntry(&quot;test&quot;, AccessLevel.FullAccess, SecurityEntityType.User));

        var securable3 = page3 as IContentSecurable;
        var securityDescriptor3 = securable3?.GetContentSecurityDescriptor();
        securityDescriptor3?.AddEntry(new AccessControlEntry(&quot;test&quot;, AccessLevel.FullAccess, SecurityEntityType.User));

        var principal = Substitute.For&amp;lt;IPrincipal&amp;gt;();
        principal?.Identity?.Name.Returns(&quot;test&quot;);
        PrincipalInfo.CurrentPrincipal.Returns(principal);

        var templateResolver = this._fixture.ServiceProvider.GetRequiredService&amp;lt;ITemplateResolver&amp;gt;();

        var type = page1.GetOriginalType();
        const TemplateTypeCategories categories = TemplateTypeCategories.Request;
        var list = Enumerable.Empty&amp;lt;string&amp;gt;();
        templateResolver.ResolveAll(page1, type, categories, list)
            .Returns(new List&amp;lt;TemplateModel&amp;gt; { new() });

        var type2 = page2.GetOriginalType();
        templateResolver.ResolveAll(page2, type2, categories, list)
            .Returns(new List&amp;lt;TemplateModel&amp;gt; { new() });

        var type3 = page3.GetOriginalType();
        templateResolver.ResolveAll(page3, type3, categories, list)
            .Returns(new List&amp;lt;TemplateModel&amp;gt; { new() });

        var publishedStateAssessor = this._fixture.ServiceProvider.GetRequiredService&amp;lt;IPublishedStateAssessor&amp;gt;();
        publishedStateAssessor.IsPublished(Arg.Any&amp;lt;IContent&amp;gt;()).Returns(true);

        contentLoader.GetChildren&amp;lt;TestPage&amp;gt;(Arg.Any&amp;lt;ContentReference&amp;gt;(), Arg.Any&amp;lt;LoaderOptions&amp;gt;()).Returns(new List&amp;lt;TestPage&amp;gt; { page2, page3 });

        var child = contentLoader.GetFirstChild&amp;lt;TestPage&amp;gt;(page1.ContentLink);
        child.Should().NotBeNull();
        child.Should().BeOfType&amp;lt;TestPage&amp;gt;();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see that the class implements the IAssemblyFixture, but in the test, we create a specific IContentLoader substitute:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var contentLoader = Substitute.For&amp;lt;IContentLoader&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then we explicitly set what it should return:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;contentLoader.GetChildren&amp;lt;TestPage&amp;gt;(Arg.Any&amp;lt;ContentReference&amp;gt;(), Arg.Any&amp;lt;LoaderOptions&amp;gt;()).Returns(new List&amp;lt;TestPage&amp;gt; { page2, page3 });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we are confident that GetChildren will only return these two pages in thuis specific test.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Writing unit tests for Optimizely is a challenge. Implementing a test replacement for ServiceLocator allows us to test a whole bunch of code that still relies on ServiceLocator in the Optimizely code base.&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/ethan-schofer/dates/2024/2/unit-testing-optimizely---how-to-deal-with-the-servicelocator/</guid>            <pubDate>Wed, 14 Feb 2024 18:54:09 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>