<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by David Drouin-Prince</title><link href="http://world.optimizely.com" /><updated>2025-11-15T18:30:30.0000000Z</updated><id>https://world.optimizely.com/blogs/david-drouin-prince/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Optimizely CMS RSS Feed Integration Library — Version 2 Release</title><link href="https://www.davidhome.net/blog/optimizely-rss-feed-integration-v2/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h2 data-start=&quot;325&quot; data-end=&quot;392&quot;&gt;&lt;!--StartFragment--&gt;&lt;/h2&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Optimizely CMS Easy RSS Feed Integration Library &amp;mdash; Now in v2&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;A while ago I launched a NuGet-package called &lt;strong&gt;DavidHome.RssFeed&lt;/strong&gt; to make RSS feed generation from Optimizely CMS &lt;em&gt;really easy&lt;/em&gt;. Back then, I focused on a simple, opinionated design: container and item types marked by interfaces, Azure Blob Storage as the backend, and a scheduled generation job. (If you missed that, you can still read the &lt;a href=&quot;https://www.davidhome.net/blog/optimizely-cms-easy-rss-feed-integration-library/&quot;&gt;original post&lt;/a&gt;.)&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Today, I&amp;rsquo;m excited to announce that the library has matured significantly &amp;mdash; welcome &lt;strong&gt;version 2.0.0&lt;/strong&gt;. This isn&amp;rsquo;t a small patch: it&amp;rsquo;s a major rewrite and refactor, and I think it will serve more complex scenarios much better, especially for multi-language, multi-site Optimizely projects.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Here&amp;rsquo;s what&amp;rsquo;s changed, why I made these changes, and how to upgrade.&lt;/p&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;What&amp;rsquo;s Changed Since v1&lt;/strong&gt;&lt;/p&gt;
&lt;ol style=&quot;margin-top: 0cm;&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Improved Processor Pipeline&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The processor architecture has been clarified and formalized. Rather than ad-hoc processors, there are well-defined builder and processor interfaces.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Better extension points: it&amp;rsquo;s now much easier to write custom processing logic (filtering, transformation, enrichment).&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Multi-Host and Multi-Language Awareness&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Feeds are now scoped per host (domain) and language. Each container type can generate distinct feeds depending on the hostname and language.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;This is a big deal if your Optimizely setup supports multiple sites or localized content &amp;mdash; no more &amp;ldquo;one feed for everything&amp;rdquo; workaround.&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Storage Provider Contracts&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The storage abstraction (IRssFeedStorageProvider) is stronger and more flexible.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The Azure Blob provider now handles host- and language-based folder structures, e.g. /mydomain.com/en/MyFeed.xml.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;This means the library can be extended more reliably if you want to implement a different storage backend.&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Breaking API Changes&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Many core interfaces have changed (builders, processor callbacks, storage signatures).&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;If you wrote custom processors in v1, they may need to be updated to match the new generic types and discovery model.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;Registration style in Startup.cs is slightly different: you must pass assemblies for discovery, and you register things in a more modular way.&lt;/li&gt;
&lt;/ul&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level1 lfo1; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Better Error Handling &amp;amp; Logging&lt;/strong&gt;&lt;/li&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;circle&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;The new pipeline makes it easier to surface problems during feed generation, so malformed items or storage errors are more diagnosable.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l2 level2 lfo1; tab-stops: list 72.0pt;&quot;&gt;This should make production issues less mysterious (especially in complex, multi-language sites).&lt;/li&gt;
&lt;/ul&gt;
&lt;/ol&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Why These Changes Matter&lt;/strong&gt;&lt;/p&gt;
&lt;ul style=&quot;margin-top: 0cm;&quot; type=&quot;disc&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: As my own blog (and potentially other sites) grew, I needed more control and modularity, not a one-size-fits-all approach.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Maintainability&lt;/strong&gt;: The cleaner separation of concerns makes the code easier to test, extend, and maintain.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l1 level1 lfo2; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Real-world Use Cases&lt;/strong&gt;: Multi-host and multi-language support is no longer an afterthought &amp;mdash; it&#39;s built in.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;How to Upgrade to v2&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;If you&amp;rsquo;re already using v1, here&amp;rsquo;s a rough migration path:&lt;/p&gt;
&lt;ol style=&quot;margin-top: 0cm;&quot; start=&quot;1&quot; type=&quot;1&quot;&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Update your NuGet references&lt;/strong&gt; to the v2 packages (core, Optimizely, storage)&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Review your DI setup&lt;/strong&gt;: switch to the new registration style, pass assemblies to discovery&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Refactor custom processors&lt;/strong&gt;: align them with the new builder/processor interfaces&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Update feed models&lt;/strong&gt;: you need to change them with the new marker interfaces&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Validate behavior&lt;/strong&gt;: regenerate your feeds, make sure routing works correctly, deploy in a test or staging environment&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot; style=&quot;mso-list: l0 level1 lfo3; tab-stops: list 36.0pt;&quot;&gt;&lt;strong&gt;Monitor and tune&lt;/strong&gt;: watch for any error logs during feed generation, especially for multilingual or multi-host feeds&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;MsoNormal&quot; style=&quot;text-align: center;&quot; align=&quot;center&quot;&gt;&lt;hr align=&quot;center&quot; size=&quot;2&quot; width=&quot;100%&quot; /&gt;&lt;/div&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Version 2 of &lt;strong&gt;DavidHome.RssFeed&lt;/strong&gt; is a big leap: it&amp;rsquo;s not just an &amp;ldquo;improvement&amp;rdquo;, it&amp;rsquo;s a &lt;strong&gt;re-architecture&lt;/strong&gt;. I believe this makes the library much more powerful for serious Optimizely use cases &amp;mdash; but also more flexible than ever for hobby or solo projects like my blog.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;If you&amp;rsquo;re using it today, now&amp;rsquo;s a good time to upgrade. If you&amp;rsquo;re just discovering it, hopefully v2 feels more robust and future-ready than the early days. You can find it on &lt;a href=&quot;https://www.nuget.org/packages?q=DavidHome.RssFeed&quot;&gt;NuGet.org&lt;/a&gt;. If you&#39;re interested, you can also view my &lt;a href=&quot;https://www.davidhome.net/nuget-packages/&quot;&gt;NuGet library&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Thanks to everyone who has been using, testing, or contributing &amp;mdash; I appreciate every bit of feedback. If you run into issues or want to help shape the future of this library, let me know or open an issue in the repo.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-11-15T18:30:30.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely CMS platform bug in ErrorsController (EPiServer.CMS.Core 12.22.9 fix)</title><link href="https://www.davidhome.net/blog/optimizely-cms-platform-bug-in-errorscontroller/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p data-start=&quot;225&quot; data-end=&quot;536&quot;&gt;While checking&amp;nbsp;&lt;strong data-start=&quot;240&quot; data-end=&quot;264&quot;&gt;Application Insights&lt;/strong&gt; earlier this year, I stumbled upon a strange exception in my Optimizely site. At first, I thought it might be a misconfigured redirect or some leftover test code &amp;mdash; but after digging in, it turned out to be an&amp;nbsp;&lt;strong data-start=&quot;470&quot; data-end=&quot;493&quot;&gt;actual platform bug&lt;/strong&gt;, one I ended up reporting to Optimizely.&lt;/p&gt;
&lt;p data-start=&quot;538&quot; data-end=&quot;700&quot;&gt;It&amp;rsquo;s a subtle one. If you&amp;rsquo;re using the &lt;strong data-start=&quot;577&quot; data-end=&quot;623&quot;&gt;custom error pages provided out of the box&lt;/strong&gt;, you could be impacted &amp;mdash; unless you&amp;rsquo;re already running the latest version.&lt;/p&gt;
&lt;hr data-start=&quot;702&quot; data-end=&quot;705&quot; /&gt;
&lt;h3 data-start=&quot;707&quot; data-end=&quot;724&quot;&gt;The Symptom&lt;/h3&gt;
&lt;p data-start=&quot;726&quot; data-end=&quot;916&quot;&gt;My site doesn&amp;rsquo;t generate a lot of exceptions, so this one stood out right away. A few errors were being thrown, all with the same root cause, and all triggered by &lt;strong data-start=&quot;889&quot; data-end=&quot;897&quot;&gt;bots&lt;/strong&gt;, not real users.&lt;/p&gt;
&lt;p data-start=&quot;918&quot; data-end=&quot;966&quot;&gt;Here&amp;rsquo;s what showed up in Application Insights:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;System.UriFormatException:
   at System.Uri.CreateThis (System.Private.Uri undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=b03f5f7f11d50a3a undefined)
   at System.Uri..ctor (System.Private.Uri undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=b03f5f7f11d50a3a undefined)
   at System.UriBuilder..ctor (System.Private.Uri undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=b03f5f7f11d50a3a undefined)
   at EPiServer.UrlBuilder.Init (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Core.Routing.Pipeline.Internal.SimpleAddressResolverPipelineStep.ResolveSimpleAddress (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Core.Routing.Pipeline.Internal.SimpleAddressResolverPipelineStep.Resolve (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Core.Routing.Internal.DefaultContentUrlResolver.Resolve (EPiServer undefined, Version=12.22.6.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Cms.Shell.UI.Controllers.Internal.ErrorsController.ResolveUICulture (EPiServer.Cms.Shell.UI undefined, Version=12.32.5.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at EPiServer.Cms.Shell.UI.Controllers.Internal.ErrorsController.Error500 (EPiServer.Cms.Shell.UI undefined, Version=12.32.5.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at lambda_method58068 .lambda_method58068 (Anonymously Hosted, Version=0.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=null undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+SyncActionResultExecutor.Execute (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker+&amp;lt;&amp;lt;InvokeActionMethodAsync&amp;gt;g__Logged|12_1&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker+&amp;lt;&amp;lt;InvokeNextActionFilterAsync&amp;gt;g__Awaited|10_0&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker+&amp;lt;&amp;lt;InvokeNextResourceFilter&amp;gt;g__Awaited|25_0&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker+&amp;lt;&amp;lt;InvokeAsync&amp;gt;g__Logged|17_1&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker+&amp;lt;&amp;lt;InvokeAsync&amp;gt;g__Logged|17_1&amp;gt;d.MoveNext (Microsoft.AspNetCore.Mvc.Core undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Stott.Optimizely.RobotsHandler.Environments.RobotsHeaderMiddleware+&amp;lt;Invoke&amp;gt;d__2.MoveNext (Stott.Optimizely.RobotsHandler undefined, Version=4.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=null undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware+&amp;lt;Invoke&amp;gt;d__11.MoveNext (Microsoft.AspNetCore.Authorization.Policy undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware+&amp;lt;Invoke&amp;gt;d__6.MoveNext (Microsoft.AspNetCore.Authentication undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Geta.NotFoundHandler.Infrastructure.Initialization.NotFoundHandlerMiddleware+&amp;lt;InvokeAsync&amp;gt;d__2.MoveNext (Geta.NotFoundHandler undefined, Version=5.0.13.0 undefined, Culture=neutral undefined, PublicKeyToken=null undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at EPiServer.Cms.Shell.UI.Internal.RegisterAdminUserMiddleware+&amp;lt;InvokeAsync&amp;gt;d__5.MoveNext (EPiServer.Cms.Shell.UI undefined, Version=12.32.5.0 undefined, Culture=neutral undefined, PublicKeyToken=8fe83dea738b45b7 undefined)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=7cec85d7bea7798e undefined)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl+&amp;lt;HandleException&amp;gt;d__11.MoveNext (Microsoft.AspNetCore.Diagnostics undefined, Version=8.0.0.0 undefined, Culture=neutral undefined, PublicKeyToken=adb9793829ddae60 undefined)&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary&quot;&gt;
&lt;div class=&quot;sticky top-9&quot;&gt;
&lt;div class=&quot;absolute end-0 bottom-0 flex h-9 items-center pe-2&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-start=&quot;1260&quot; data-end=&quot;1263&quot; /&gt;
&lt;h3 data-start=&quot;1265&quot; data-end=&quot;1285&quot;&gt;The Root Cause&lt;/h3&gt;
&lt;p data-start=&quot;1287&quot; data-end=&quot;1473&quot;&gt;After a bit of debugging, I found that certain requests contained &lt;strong data-start=&quot;1353&quot; data-end=&quot;1386&quot;&gt;invalid characters in the URL&lt;/strong&gt;, which caused Optimizely&amp;rsquo;s &lt;code data-start=&quot;1414&quot; data-end=&quot;1432&quot;&gt;ErrorsController&lt;/code&gt; to fail while trying to build a &lt;code data-start=&quot;1465&quot; data-end=&quot;1470&quot;&gt;Uri&lt;/code&gt;.&lt;/p&gt;
&lt;p data-start=&quot;1475&quot; data-end=&quot;1533&quot;&gt;Here&amp;rsquo;s an example of a request that triggered the issue:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;https://www.davidhome.net/h%20ttps:%20oast.me&lt;/code&gt;&lt;/pre&gt;
&lt;p data-start=&quot;1590&quot; data-end=&quot;1837&quot;&gt;That tiny bit of malformed input was enough to cause a &lt;code data-start=&quot;1645&quot; data-end=&quot;1672&quot;&gt;System.UriFormatException&lt;/code&gt; deep inside the platform. Essentially, the controller attempts to construct a &lt;code data-start=&quot;1751&quot; data-end=&quot;1756&quot;&gt;Uri&lt;/code&gt; from the incoming request &amp;mdash; but when the value isn&amp;rsquo;t valid, everything breaks.&lt;/p&gt;
&lt;hr data-start=&quot;1839&quot; data-end=&quot;1842&quot; /&gt;
&lt;h3 data-start=&quot;1844&quot; data-end=&quot;1857&quot;&gt;The Fix&lt;/h3&gt;
&lt;p data-start=&quot;1859&quot; data-end=&quot;2057&quot;&gt;The issue has already been fixed in &lt;strong data-start=&quot;1895&quot; data-end=&quot;1925&quot;&gt;EPiServer.CMS.Core 12.22.9&lt;/strong&gt;.&lt;br data-start=&quot;1926&quot; data-end=&quot;1929&quot; /&gt;If you&amp;rsquo;re on an earlier version and you&amp;rsquo;re seeing similar exceptions, simply &lt;strong data-start=&quot;2006&quot; data-end=&quot;2029&quot;&gt;update your package&lt;/strong&gt; and you&amp;rsquo;ll be good to go.&lt;/p&gt;
&lt;hr data-start=&quot;2059&quot; data-end=&quot;2062&quot; /&gt;
&lt;h3 data-start=&quot;2064&quot; data-end=&quot;2084&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p data-start=&quot;2086&quot; data-end=&quot;2243&quot;&gt;This isn&amp;rsquo;t something an end user would normally trigger, but it&amp;rsquo;s still worth cleaning up to keep your telemetry free of noise and your logs easy to trust.&lt;/p&gt;
&lt;p data-start=&quot;2245&quot; data-end=&quot;2408&quot;&gt;Small bugs like this remind me why it&amp;rsquo;s always worth checking Application Insights from time to time &amp;mdash; even when you think nothing&amp;rsquo;s happening behind the scenes.&lt;/p&gt;
&lt;p data-start=&quot;2410&quot; data-end=&quot;2432&quot;&gt;Happy programming &#128075;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-11-09T18:52:25.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Avoid Using OnStatusChanged in Optimizely CMS – It Can Impact Database Performance</title><link href="https://www.davidhome.net/blog/avoid-using-onstatuschanged-it-bothers-the-database/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h3 data-start=&quot;455&quot; data-end=&quot;529&quot;&gt;Beware of Overusing &lt;code data-start=&quot;479&quot; data-end=&quot;496&quot;&gt;OnStatusChanged&lt;/code&gt; in Optimizely CMS Scheduled Jobs&lt;/h3&gt;
&lt;p data-start=&quot;531&quot; data-end=&quot;750&quot;&gt;Optimizely CMS allows you to create scheduled jobs &amp;mdash; a powerful feature often used to automate repetitive tasks such as product imports. Sometimes, we even use them for one-off data migrations and delete them afterward.&lt;/p&gt;
&lt;p data-start=&quot;752&quot; data-end=&quot;978&quot;&gt;This post assumes you&amp;rsquo;re already familiar with the basics, but if not, you can catch up here:&amp;nbsp;&lt;a class=&quot;decorated-link&quot; href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/scheduled-jobs&quot; target=&quot;_new&quot; rel=&quot;noopener&quot; data-start=&quot;851&quot; data-end=&quot;978&quot;&gt;Optimizely Scheduled Jobs Documentation&lt;/a&gt;&lt;/p&gt;
&lt;p data-start=&quot;980&quot; data-end=&quot;1330&quot;&gt;Most developers are aware of the &lt;code data-start=&quot;1013&quot; data-end=&quot;1030&quot;&gt;OnStatusChanged&lt;/code&gt; method. It&amp;rsquo;s handy for updating the administrative interface with progress messages while a scheduled job runs. For example, you might use it to display periodic updates like &amp;ldquo;Importing products&amp;hellip;&amp;rdquo; or &amp;ldquo;Processing batch 2 of 5&amp;hellip;&amp;rdquo; so that anyone monitoring the job can see what&amp;rsquo;s happening in real time.&lt;/p&gt;
&lt;p data-start=&quot;1332&quot; data-end=&quot;1363&quot;&gt;There&amp;rsquo;s a small hiccup, though.&lt;/p&gt;
&lt;hr data-start=&quot;1365&quot; data-end=&quot;1368&quot; /&gt;
&lt;h4 data-start=&quot;1370&quot; data-end=&quot;1386&quot;&gt;The Problem&lt;/h4&gt;
&lt;p data-start=&quot;1388&quot; data-end=&quot;1619&quot;&gt;While investigating a performance issue for one of our clients, we noticed the database was under heavy load during certain scheduled jobs. After some digging, we realized that our frequent use of &lt;code data-start=&quot;1585&quot; data-end=&quot;1602&quot;&gt;OnStatusChanged&lt;/code&gt; was the culprit.&lt;/p&gt;
&lt;p data-start=&quot;1621&quot; data-end=&quot;1737&quot;&gt;Even worse, some jobs began failing with an unexpected error &amp;mdash; one that had nothing to do with the job logic itself:&lt;/p&gt;
&lt;div class=&quot;contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary&quot;&gt;
&lt;div class=&quot;overflow-y-auto p-4&quot; dir=&quot;ltr&quot;&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;An unhandled error occured while running the job &#39;MyJob&#39;.
Microsoft.Data.SqlClient.SqlException (0x80131904): The server failed to resume the transaction. Desc:3e00000334.
The transaction active in this session has been committed or aborted by another session.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean&amp;amp; dataReady)
   at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at Microsoft.Data.SqlClient.TdsParser.TdsExecuteTransactionManagerRequest(Byte[] buffer, TransactionManagerRequestType request, String transactionName, TransactionManagerIsolationLevel isoLevel, Int32 timeout, SqlInternalTransaction transaction, TdsParserStateObject stateObj, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction2005(TransactionRequest transactionRequest, String transactionName, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ExecuteTransaction(TransactionRequest transactionRequest, String name, IsolationLevel iso, SqlInternalTransaction internalTransaction, Boolean isDelegateControlRequest)
   at Microsoft.Data.SqlClient.SqlInternalConnection.BeginSqlTransaction(IsolationLevel iso, String transactionName, Boolean shouldReconnect)
   at Microsoft.Data.SqlClient.SqlConnection.BeginTransaction(IsolationLevel iso, String transactionName)
   at Microsoft.Data.SqlClient.SqlConnection.BeginTransaction(IsolationLevel iso)
   at Microsoft.Data.SqlClient.SqlConnection.BeginDbTransaction(IsolationLevel isolationLevel)
   at EPiServer.Data.Providers.Internal.ConnectionContext.BeginTransaction()
   at EPiServer.Data.Internal.DefaultConnectionContextHandler.CreateConnectionScope(Boolean requireTransaction, Action completeAction)
   at EPiServer.Data.Internal.ConnectionScopeResolver.GetConnectionScope(Boolean requireTransaction)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.GetConnection(Boolean requireTransaction)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.&amp;lt;&amp;gt;c__DisplayClass28_0`1.&amp;lt;ExecuteTransaction&amp;gt;b__0()
   at EPiServer.Data.Providers.SqlTransientErrorsRetryPolicy.Execute[TResult](Func`1 method)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.ExecuteTransaction[TResult](Func`1 action)
   at EPiServer.Data.Providers.Internal.SqlDatabaseExecutor.ExecuteTransaction(Action action)
   at EPiServer.DataAccess.Internal.SchedulerDB.UpdateCurrentStatusMessage(Guid id, String statusMessage)
   at EPiServer.Scheduler.Internal.DefaultScheduledJobExecutor.JobInstance_StatusChanged(Object sender, JobStatusChangedEventArgs e)
   at EPiServer.Scheduler.ScheduledJobBase.OnStatusChanged(String statusMessage)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-start=&quot;2337&quot; data-end=&quot;2340&quot; /&gt;
&lt;h4 data-start=&quot;2342&quot; data-end=&quot;2362&quot;&gt;What&amp;rsquo;s Going On&lt;/h4&gt;
&lt;p data-start=&quot;2364&quot; data-end=&quot;2674&quot;&gt;Digging through the stack trace reveals that each call to &lt;code data-start=&quot;2422&quot; data-end=&quot;2439&quot;&gt;OnStatusChanged&lt;/code&gt; triggers a database write. Every single status update results in a new SQL transaction. If your job updates its status too frequently &amp;mdash; say, inside a loop &amp;mdash; you can easily overwhelm the database with hundreds or thousands of writes.&lt;/p&gt;
&lt;p data-start=&quot;2676&quot; data-end=&quot;2778&quot;&gt;In our case, this not only degraded performance but also caused transaction errors like the one above.&lt;/p&gt;
&lt;hr data-start=&quot;2780&quot; data-end=&quot;2783&quot; /&gt;
&lt;h4 data-start=&quot;2785&quot; data-end=&quot;2797&quot;&gt;The Fix&lt;/h4&gt;
&lt;p data-start=&quot;2799&quot; data-end=&quot;2978&quot;&gt;Once we understood what was happening, we simply removed (or drastically reduced) our calls to &lt;code data-start=&quot;2894&quot; data-end=&quot;2911&quot;&gt;OnStatusChanged&lt;/code&gt;. That immediately stabilized the system and reduced database load.&lt;/p&gt;
&lt;p data-start=&quot;2980&quot; data-end=&quot;3305&quot;&gt;If your scheduled jobs make frequent calls to &lt;code data-start=&quot;3026&quot; data-end=&quot;3043&quot;&gt;OnStatusChanged&lt;/code&gt;, we strongly suggest you review and limit them. Consider logging detailed progress elsewhere (e.g., a file, Application Insights, or custom monitoring) and only update the UI when it truly matters &amp;mdash; such as at the start, at major milestones, or upon completion.&lt;/p&gt;
&lt;hr data-start=&quot;3307&quot; data-end=&quot;3310&quot; /&gt;
&lt;h4 data-start=&quot;3312&quot; data-end=&quot;3325&quot;&gt;Takeaway&lt;/h4&gt;
&lt;p data-start=&quot;3327&quot; data-end=&quot;3476&quot;&gt;&lt;code data-start=&quot;3327&quot; data-end=&quot;3344&quot;&gt;OnStatusChanged&lt;/code&gt; is a useful feature, but it&amp;rsquo;s not free. Every update hits the database. Use it sparingly to keep your jobs reliable and performant.&lt;/p&gt;
&lt;p data-start=&quot;3478&quot; data-end=&quot;3533&quot;&gt;Hope this helps someone avoid the same headache we had!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-11-09T18:19:02.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Disable Optimizely CMS Default Error Handling in ASP.NET Core</title><link href="https://www.davidhome.net/blog/how-to-override-optimizely-cms-custom-error-handling-in-asp.net-core" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When building a .NET web application, it&#39;s common to use the combination of &lt;code&gt;UseExceptionHandler&lt;/code&gt; and &lt;code&gt;UseStatusCodePagesWithReExecute&lt;/code&gt; in your &lt;code&gt;Startup.cs&lt;/code&gt; to serve user-friendly error pages:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseExceptionHandler(&quot;/errorhandler/500&quot;);
app.UseStatusCodePagesWithReExecute(&quot;/errorhandler/{0}&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works seamlessly&amp;mdash;even for Optimizely CMS solutions. However, we recently uncovered a caveat when working on a project involving custom API endpoints. It turns out Optimizely has its own hidden behavior regarding error handling, and it can silently interfere with your carefully configured pipeline.&lt;/p&gt;
&lt;pre&gt;&amp;nbsp;&lt;/pre&gt;
&lt;h3&gt;The Problem: Custom Errors for an API Segment&lt;/h3&gt;
&lt;p&gt;We had an API route that &lt;em&gt;intentionally&lt;/em&gt; returns raw HTTP status codes (e.g., 401 Unauthorized) depending on business logic. The expected behavior was to return the actual HTTP response code, not a friendly HTML error page.&lt;/p&gt;
&lt;p&gt;However, once &lt;code&gt;UseStatusCodePagesWithReExecute&lt;/code&gt; is enabled, even API endpoints like &lt;code&gt;/my/custom/api&lt;/code&gt; get intercepted, and you&amp;rsquo;ll end up with an HTML error response when your API returns a 401.&lt;/p&gt;
&lt;p&gt;So, we added conditional logic:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseWhen(
    context =&amp;gt; !context.Request.Path.StartsWithSegments(&quot;/my/custom/api&quot;),
    appBuilder =&amp;gt;
    {
        appBuilder.UseExceptionHandler(&quot;/errorhandler/500&quot;);
        appBuilder.UseStatusCodePagesWithReExecute(&quot;/errorhandler/{0}&quot;);
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works&amp;mdash;&lt;strong&gt;until you go to production&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;The Surprise: Optimizely Hooks in Its Own Middleware&lt;/h3&gt;
&lt;p&gt;To our surprise, our API was still returning Optimizely&#39;s custom error page in production. After some digging, we discovered that &lt;strong&gt;Optimizely CMS automatically registers its own exception and status code handling&lt;/strong&gt;, regardless of what you configure.&lt;/p&gt;
&lt;p&gt;This happens when you call &lt;code&gt;.AddCms()&lt;/code&gt;&amp;mdash;specifically, &lt;code&gt;.AddCmsUI()&lt;/code&gt; internally registers the following service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.AddStartupFilter&amp;lt;ErrorsStartupFilter&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This filter looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public Action&amp;lt;IApplicationBuilder&amp;gt; Configure(Action&amp;lt;IApplicationBuilder&amp;gt; nextAction)
{
    return app =&amp;gt;
    {
        if (app.ApplicationServices.GetRequiredService&amp;lt;IWebHostEnvironment&amp;gt;().IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseStatusCodePagesWithReExecute(&quot;/Util/Errors/Error{0}&quot;);
            app.UseExceptionHandler(&quot;/Util/Errors/Error500&quot;);
        }

        nextAction(app);
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, Optimizely&amp;nbsp;&lt;em&gt;always&lt;/em&gt; wires in its own error handlers&amp;mdash;after yours.&lt;/p&gt;
&lt;h3&gt;Our Solution: Remove the Startup Filter&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s no public configuration to disable this behavior. The class is &lt;code&gt;internal&lt;/code&gt;, so you can&amp;rsquo;t override or configure it. We had to remove it from the service collection ourselves:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var errorStartupFilterServiceDescriptor = services
    .FirstOrDefault(descriptor =&amp;gt; descriptor.ImplementationType?.Name == &quot;ErrorsStartupFilter&quot;);

if (errorStartupFilterServiceDescriptor != null)
{
    services.Remove(errorStartupFilterServiceDescriptor);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing this early in &lt;code&gt;ConfigureServices&lt;/code&gt;, you prevent Optimizely&amp;rsquo;s error handlers from being injected, giving you full control over the pipeline again.&lt;/p&gt;
&lt;h3&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This is one of those &lt;em&gt;&quot;framework magic vs. developer control&quot;&lt;/em&gt; situations. It&amp;rsquo;s easy to miss because everything seems to work locally&amp;mdash;until production exposes the conflict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hopefully, this saves someone a few hours of debugging!&lt;/strong&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-07-25T06:19:24.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>UrlResolver Bug in Optimizely DXP: Troubleshooting Inconsistent URLs in Scaled-Out Apps</title><link href="https://www.davidhome.net/blog/optimizely-cms-content-not-the-same-between-different-nodes/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p data-start=&quot;356&quot; data-end=&quot;588&quot;&gt;A while back, our team encountered a puzzling production bug: URLs generated by the &lt;code data-start=&quot;440&quot; data-end=&quot;453&quot;&gt;UrlResolver&lt;/code&gt; would &lt;strong data-start=&quot;460&quot; data-end=&quot;479&quot;&gt;randomly differ&lt;/strong&gt; depending on who accessed them. The bug has since &lt;a href=&quot;https://world.optimizely.com/documentation/Release-Notes/ReleaseNote/?releaseNoteId=CMS-26486&quot;&gt;been fixed&lt;/a&gt;, and a patch is now available for installation.&lt;/p&gt;
&lt;p data-start=&quot;593&quot; data-end=&quot;840&quot;&gt;After some initial investigation and discussion with the team, we confirmed this was &lt;strong data-start=&quot;678&quot; data-end=&quot;709&quot;&gt;one of those elusive issues&lt;/strong&gt;&amp;mdash;reproducible only in certain browsers or for specific users. We couldn&amp;rsquo;t reproduce it locally, nor in our integration environment.&lt;/p&gt;
&lt;p data-start=&quot;845&quot; data-end=&quot;866&quot;&gt;So what was going on?&lt;/p&gt;
&lt;p data-start=&quot;871&quot; data-end=&quot;1152&quot;&gt;If you&#39;re familiar with Optimizely DXP, you know it runs on &lt;a href=&quot;https://azure.microsoft.com/en-us/products/app-service&quot;&gt;Azure App Services&lt;/a&gt;, with your app &lt;strong data-start=&quot;965&quot; data-end=&quot;1005&quot;&gt;scaled out across multiple instances&lt;/strong&gt;. For those new to the concept: scaling out means your application code runs on several servers in parallel to handle high web traffic efficiently.&lt;/p&gt;
&lt;p data-start=&quot;1157&quot; data-end=&quot;1260&quot;&gt;But there&#39;s a caveat&amp;mdash;when running code across multiple nodes,&amp;nbsp;&lt;strong data-start=&quot;1219&quot; data-end=&quot;1259&quot;&gt;synchronizing state becomes critical&lt;/strong&gt;.&lt;/p&gt;
&lt;p data-start=&quot;1265&quot; data-end=&quot;1497&quot;&gt;Optimizely CMS handles this using Azure Service Bus to propagate key events and updates across all nodes. Whether you&#39;re publishing content or stopping a scheduled job, those actions are broadcast so that all instances stay in sync.&lt;/p&gt;
&lt;p data-start=&quot;1502&quot; data-end=&quot;1809&quot;&gt;In our case, however, the problem was a &lt;strong data-start=&quot;1542&quot; data-end=&quot;1570&quot;&gt;cache invalidation issue&lt;/strong&gt; across nodes. One node properly refreshed its cache and generated the updated URL, while others continued using stale data. This led to inconsistent URL generation depending on which server a user hit&amp;mdash;hence the randomness in user reports.&lt;/p&gt;
&lt;p data-start=&quot;1502&quot; data-end=&quot;1809&quot;&gt;Here&#39;s a visual to illustrate the scaled-out app structure with multiple nodes, one Azure Service Bus, and a single point of entry.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;                +----------------------+
                |      Load Balancer   |
                +----------+-----------+
                           |
      +--------------------+--------------------+
      |                    |                    |
+-----v-----+        +-----v-----+        +-----v-----+
|  Node A   |        |  Node B   |        |  Node C   |
| (Updated) |        | (Stale)   |        | (Stale)   |
+-----------+        +-----------+        +-----------+
      \                    |                    /
       \                   |                   /
        \         +--------v--------+         /
         +--------&amp;gt; Azure Service   &amp;lt;--------+
                  |      Bus        |
                  +-----------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-start=&quot;1814&quot; data-end=&quot;2109&quot;&gt;You can test this behavior yourself (in production or preproduction) if scale-out is enabled. Just open your browser&amp;rsquo;s developer tools, inspect your cookies, and delete &lt;code data-start=&quot;1983&quot; data-end=&quot;1996&quot;&gt;ARRAffinity&lt;/code&gt; and &lt;code data-start=&quot;2001&quot; data-end=&quot;2022&quot;&gt;ARRAffinitySameSite&lt;/code&gt;. Reload the page. If a different GUID appears, you&amp;rsquo;ve been routed to a different node.&lt;/p&gt;
&lt;p data-start=&quot;2114&quot; data-end=&quot;2347&quot;&gt;The takeaway? &lt;strong data-start=&quot;2128&quot; data-end=&quot;2244&quot;&gt;If a bug appears non-deterministically across users or browsers, consider your application&amp;rsquo;s distributed nature.&lt;/strong&gt; Multi-node environments can introduce quirks that don&#39;t show up locally or in single-instance testing.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-06-18T00:34:05.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Automating Optimizely DXP Deployments with GitLab CI/CD</title><link href="https://www.davidhome.net/blog/optimizely-dxp-gitlab-pipeline/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;h3&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This blog post will explore the GitLab CI/CD pipeline configuration for deploying to Optimizely DXP. I wanted to be able to automate as much as possible every deployment steps to DXP, including the build stages, so that almost none of the operations were manual. The end result looks as followed:&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/gitlab-cicd-dxp-template&quot;&gt;GitLab CI/CD DXP Template repository&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;I must say, this is clearly easier that I initially thought. In general, a full run, starting from the build to the latest stage where it deploys to production can take approximatively 30 minutes.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;PowerShell and GitLab CI&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;GitLab uses bash as its default shell when you configure a runner. In my own setup, I have decided to use the &lt;a href=&quot;https://hub.docker.com/r/gitlab/gitlab-runner&quot;&gt;docker image&lt;/a&gt; and configure two different runners in the same docker instance (yes you can do this). One for running bash based scripts and the other that needs to run PowerShell based scripts. Consequently, the pipeline heavily utilizes PowerShell (&lt;code&gt;pwsh&lt;/code&gt; tag) for deployment tasks as they are used in my configuration to stipulate to use my PowerShell based runner instead of the default one. Optimizely DXP&#39;s APIs and deployment commands are PowerShell-based, making it a prerequisite for certain stages. You&#39;ll find PowerShell scripts handling package uploads and triggering deployments.&lt;/p&gt;
&lt;p&gt;Here&#39;s an example where the tag &lt;code&gt;pwsh&lt;/code&gt; has been specified:&amp;nbsp;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;send-package-dxp:
  stage: send-package
  image: mcr.microsoft.com/powershell:lts
  tags:
    - pwsh
  script:
    - Install-Module -Name EpiCloud -Force
    - Connect-EpiCloud -ClientKey $DXP_CLIENT_KEY -ClientSecret $DXP_CLIENT_SECRET -ProjectId $DXP_PROJECT_ID
    - $packageLocation = Get-EpiDeploymentPackageLocation
    - $foundPackageLocations = Get-ChildItem -Path $ARTIFACTS_LOCATION -Filter &quot;*.nupkg&quot; | Sort-Object -Property Name -Descending
    - $resolvedPackagePath = $foundPackageLocations | Select-Object -First 1
    - &quot;Write-Host \&quot;The following package will be deployed: $resolvedPackagePath\&quot;&quot;
    - Add-EpiDeploymentPackage -SasUrl $packageLocation -Path $resolvedPackagePath.FullName&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the &lt;code&gt;pwsh&lt;/code&gt; tag, these PowerShell commands would fail to execute properly in GitLab CI/CD, as the default shell does not support them.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;Pipeline highlights&lt;/strong&gt;&lt;/h3&gt;
&lt;ul data-spread=&quot;false&quot;&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Handling Failed Deployments&lt;/strong&gt;: The pipeline includes rollback mechanisms to reset or complete failed deployments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manual Triggers for Preproduction and Production&lt;/strong&gt;: To add a safety net, the preproduction and production deployment stages are set to manual, allowing for final verification before releasing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&#39;s a visual example:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/image.png&quot;&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/image.png?1740336480023#1740336546073&quot; alt=&quot;example 1&quot; width=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And of the job dependencies:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/screenshot-2025-02-23-134850.png&quot;&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/f1f5bc62e3bf4917a5eb0c117f14fb39/screenshot-2025-02-23-134850.png&quot; alt=&quot;example 2&quot; width=&quot;300&quot; height=&quot;71&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;As a reminder, you can refer to the complete files available in the repository:&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/gitlab-cicd-dxp-template&quot;&gt;GitLab CI/CD DXP Template&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By using a well-defined CI/CD pipeline for Optimizely DXP, you can:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce manual deployment work.&lt;/li&gt;
&lt;li&gt;Ensure consistency across environments.&lt;/li&gt;
&lt;li&gt;Minimize deployment risks.&lt;/li&gt;
&lt;li&gt;Enable easy rollbacks if needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This GitLab CI template brings structure to your DXP deployments and makes life easier for your development and operations teams.&lt;/p&gt;
&lt;p&gt;Happy deploying! &#128640;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-02-23T18:56:37.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Enhancing Optimizely CMS Multi-Site Architecture with Structured Isolation</title><link href="https://www.davidhome.net/blog/optimizely-cms-multi-site-setup/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;The main challenge of building an Optimizely CMS website is to think about its multi site capabilities up front. Making adjustment after the fact can be a difficult task and often requires a lot of refactoring.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In this blog post, I&#39;ll talk about a bit on how I have found a way to easily isolate a single website, structurally speaking, under a C# solution.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Of course, this implies a couple of modifications and is far from perfect. But I thought sharing my discovery to the community could help a lot.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Of course building the architecture of&amp;nbsp;a solution to something that will easily accommodate&amp;nbsp;future development is always a question of determining whether it&#39;s worth the upfront efforts and cost that it entails. If your client is sure to have more than a single site in your Optimizely CMS project, well then the question of correctly structuring your solution up front is the best you can do to reduce the development complexity with a bigger and bigger code base that does a lot in a single place.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This is something that we&#39;ve come across&amp;nbsp;quite often when clients were requesting us to make a new site under the same umbrella. What to do with the existing code base and avoid any regression while implementing the new website code base?&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Well then, here&#39;s my solution to it: routing based on the site name configured under Optimizely CMS. This essentially allow you to create a completely separated C# library project and isolate every business logic into that single bucket for that single CMS site.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;In-dept details&lt;/h2&gt;
&lt;p&gt;Obviously there is not only &lt;em&gt;routing&lt;/em&gt; magic down there, but all in all, the goal was to modify every core elements of ASP.NET Core and Optimizely CMS that drives where and how certain elements are rendered.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Optimizely Content Types&lt;/h3&gt;
&lt;p&gt;By default, there is no controller for your content, you have to create one for your pages and your blocks. You are not forced to create a controller for your blocks, this is optional and also &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/content-templates#block-components-and-views&quot;&gt;recommended by Optimizely&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Avoiding to use a controller just to change the location of the partial view requires an adjustment&amp;nbsp;on the ASP.NET razor view engine. This is something that you can personalize by creating a class that implements a &lt;code&gt;IViewLocationExpander&lt;/code&gt;. That location expander needs to do a couple of things for us.&lt;/p&gt;
&lt;p&gt;First making sure the order of the locations are to the most precise to the less precised one. In the logic then goes like that:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Look for a view that is inside a folder with the name of the site and the name of the content type.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Look for a view that is inside a folder with the name of the site.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Look for a view that is inside a folder with the name of the content type currently being rendered.&lt;/li&gt;
&lt;li&gt;Similarly to the previous steps, but inside the &quot;Shared&quot; folder. The expander will now look inside a &quot;Shared&quot; location for the site.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the way, more information on the implementation will be shared later on. But you get the idea. This will essentially ask the view engine to look for locations that is only applicable for the site being requested. To give an example, this blog has been designed with that in mind. Here&#39;s a visual example of how the view structure looks:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/8ed44ddf74c8406794985b4df2808242/image.png&quot; alt=&quot;Razor view structure&quot; width=&quot;300&quot; height=&quot;640&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, every views are inside a folder with the name of the site. None of them are outside of it. This means two things:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If I want to share the same content types between sites, it&#39;s entirely possible. They can:&amp;nbsp;
&lt;ul&gt;
&lt;li&gt;Use the same shared view.&lt;/li&gt;
&lt;li&gt;Have distinctive views for specific sites.&lt;/li&gt;
&lt;li&gt;Work in &lt;em&gt;hybrid&lt;/em&gt; mode, meaning that you could have both to work together.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Have a very specific &lt;code&gt;_ViewImports&lt;/code&gt; and &lt;code&gt;_ViewStart&lt;/code&gt; file for a site.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this approach, the only thing needed to be done is to create a folder with the name of the site that will be created under Optimizely CMS and voila, you&#39;re good to go. The project you see in my previous screenshot doesn&#39;t contain a &lt;code&gt;Startup.cs&lt;/code&gt;. This is something only the starting assembly project have. This here is&amp;nbsp;&lt;em&gt;only&lt;/em&gt; a C# library project that is completely separated from the rest.&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;But how does it know under which site and content type the request is being made?&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;This is a good question. The short answer rely then again on an alteration to do on the ASP.NET Core routing engine.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Optimizely CMS uses what is known as the endpoint middleware. This is the reason why under your &lt;code&gt;Startup.cs&lt;/code&gt; file, you have to do something similar as that:&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;        app.UseEndpoints(endpoints =&amp;gt;
        {
            endpoints.MapContent();
        });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MapContent()&lt;/code&gt;&amp;nbsp;is essentially the method that hooks the entire CMS routing system on Microsoft&#39;s. Of course this is an oversimplification, but you get the picture. When the endpoint middleware is called, the matcher policies are called, and eventually the one from Optimizely is too. This consequently is used to load the correct page controller of the current http request.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;So, knowing a matcher policy is checking if the content from the CMS can be loaded, I&#39;ve hooked myself on that same system to add two variables in the route values:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One for knowing on which site this request is being made.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;To other to know which content type is loaded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;MultiSiteMatcher&lt;/code&gt; is adding those routing values only after the &lt;code&gt;ContentMatcherPolicy&lt;/code&gt; from Optimizely is running, making sure it is able to grab the correct data while adding them in the routing values.&lt;/p&gt;
&lt;h2&gt;Static assets&lt;/h2&gt;
&lt;p&gt;The other challenge with multi sites are the static assets. The classic setup with Optimizely CMS is to create a folder for a specific site under &lt;code&gt;wwwroot&lt;/code&gt; and then store all your assets in it for that site. All in all, you might want different scripts, styles, fonts, etc. and you don&#39;t necessarily want to share them with other sites.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;With this setup though, two things are happening:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The browser load assets in a subfolder instead of&amp;nbsp;&lt;em&gt;looking&lt;/em&gt; like it&#39;s being loaded from the root of the website.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;You could technically load other assets that are not designed or meant for the site in the page.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course these problems are mainly cosmetic, but I felt like I needed to close the loop with this whole multi site thing correctly.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;There is a way to customize that and still keep a good separation between sites. By implementing a new &lt;code&gt;IFileProvider&lt;/code&gt;. Essentially there it&#39;s quite simple, this file provider is trying to find a file directly from a subpath of the requested site and if it cannot be found, fallback to the out of the box file provider.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;NuGet package &amp;amp; Sources&lt;/h2&gt;
&lt;p&gt;All these concepts were implemented during the development of my blog. I have recently separated this logic into a NuGet package if you are interested.&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NuGet: https://www.nuget.org/packages?q=DavidHome.Optimizely.MultiSite&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Sources: &lt;a href=&quot;https://git.davidhome.net/web/davidhome-optimizely-multisite&quot;&gt;https://git.davidhome.net/web/davidhome-optimizely-multisite&lt;/a&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please let me know what you think of! Of course if you&#39;d like to participate and contribute to the project, please let me know via LinkedIn!&amp;nbsp;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-02-09T20:47:34.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely CMS easy RSS feed integration library</title><link href="https://www.davidhome.net/blog/optimizely-cms-easy-rss-feed-integration-library/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;As I&#39;ve mentioned in my&amp;nbsp;&lt;a href=&quot;https://www.davidhome.net/blog/my-blog-is-now-running-using-optimizely-cms/&quot;&gt;previous blog post&lt;/a&gt;, while I was developing the Optimizely version of my blog, I tried to look for a library that could accommodate&amp;nbsp;my RSS feeds needs with the CMS. I&#39;ve found &lt;a href=&quot;https://www.hiddenfoundry.com/thoughts/rss-feed-nuget-package-for-optimizely-cms/&quot;&gt;one&lt;/a&gt; made by another fellow OMVP peer, but unfortunately it wasn&#39;t quite covering my own requirements and I thought why not developing it from scratch altogether.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Some might thing it&#39;s overkill, but it doesn&#39;t matter, I had fun designing &amp;amp; building it. &#128516;&lt;/p&gt;
&lt;h3&gt;Introducing DavidHome.RssFeed&lt;/h3&gt;
&lt;p&gt;This NuGet package integrates seamlessly into your Optimizely project and uses Azure Blob Storage for hosting your feeds. Whether you&#39;re building a single feed or managing multiple content streams, this tool is ready to meet your needs, and even better, its&amp;nbsp;extensible!&lt;/p&gt;
&lt;p&gt;I know there is probably stuff that has been totally overlooked, but any contribution will be greatly welcomed. If you&#39;d like to, simply contact me via LinkedIn and I&#39;ll give you the required access to my git repository.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Getting Started&lt;/h3&gt;
&lt;p&gt;First, install the required NuGet packages:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;dotnet add package DavidHome.RssFeed.Optimizely
dotnet add package DavidHome.RssFeed.Storage.AzureBlob&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then configure the plugin in your &lt;code&gt;Startup.cs&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configure Azure Blob Storage (inside &lt;code&gt;ConfigureServices&lt;/code&gt;)&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;services.AddDavidHomeRssFeed(configuration)
    .AddOptimizelyFeedIntegration(configuration)
    .AddDefaultOptimizelyProcessors()
    .AddContentPageFeed&amp;lt;MyContainer, MyItem&amp;gt;()
    .AddAzureBlobStorage(_configuration.GetSection(&quot;ConnectionStrings&quot;).GetSection(&quot;EPiServerAzureBlobs&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialize Blob Storage (inside &lt;code&gt;Configure&lt;/code&gt;)&lt;/strong&gt;&lt;br /&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;app.UseAzureBlobRssFeed();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure Blob Storage is &lt;strong&gt;mandatory&lt;/strong&gt; for this plugin. That said, if you&#39;re&amp;nbsp;feeling adventurous, you can fork the project and add your own storage implementation or create your own NuGet package to extend the functionality.&lt;/li&gt;
&lt;li&gt;You need to update &quot;Microsoft.Extensions.Azure&quot; to the latest version, or the further you can, so that support to use the direct IConfiguration section having the connection string information works as demonstrated there.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Fine-Tuning Your Feeds&lt;/h3&gt;
&lt;p&gt;The plugin follows a configuration-less design, meaning you&#39;re&amp;nbsp;good to go with just the defaults. But for those who want to tweak things, you can use &lt;code&gt;appsettings.json&lt;/code&gt; to configure your feeds:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code&gt;{
  &quot;DavidHome&quot;: {
    &quot;RssFeed&quot;: {
      &quot;ContentMaxLength&quot;: 25000000,
      &quot;MyCustomContainerPageTypeName&quot;: {
        &quot;ContentMaxLength&quot;: 15000000,
        &quot;ContentAreaPropertyName&quot;: &quot;MainContentArea&quot;,
        &quot;FeedTitlePropertyName&quot;: &quot;HeadTitle&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Key Optimizely-Specific Options&lt;/h3&gt;
&lt;p&gt;For &lt;a href=&quot;https://git.davidhome.net/web/davidhome-rssfeed#configuration&quot;&gt;Optimizely specific options&lt;/a&gt;, the following are available:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FeedRelativeUrl: Allows customization of the relative URL of your feed(s) based on the location of your container page(s) in your CMS tree.&lt;/li&gt;
&lt;li&gt;ContentAreaPropertyName: When provided, it will automatically extract the content of the content area as HTML and use it as the description field in your syndication item.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;FeedTitlePropertyName: This optional property is useful if you want to change the title of the link meta tag in your DOM head tag that can be generated when calling &lt;code&gt;@Html.SyndicationLink()&lt;/code&gt;&amp;nbsp;in your layout page.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can personalize every feed, and yes, you can have multiple feeds targeting different sections of your CMS tree!&lt;/p&gt;
&lt;h3&gt;Setting Up Your Content Types&lt;/h3&gt;
&lt;p&gt;To integrate with Optimizely&amp;rsquo;s data structure, you&#39;ll&amp;nbsp;need to apply these marker interfaces to your content types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IRssFeedContainer&amp;lt;TFeedItem&amp;gt;&lt;/code&gt; for container types.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRssFeedItem&amp;lt;TFeedContainer&amp;gt;&lt;/code&gt; for feed items.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Add the &lt;code&gt;IgnoreAttribute&lt;/code&gt; to unsupported properties.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Azure Blob Storage&lt;/h3&gt;
&lt;p&gt;Azure Blob Storage powers the backend for your RSS feeds. It stores generated feeds over there so that when it&#39;s being requested, it doesn&#39;t go back to the database and try to build it again on the fly. This allows a fast and seamless delivery of the file and avoids to use your precious CPU/database computing resources. A scheduled job is automatically running in the background to generate and store the file representing the syndication feed of your content. It&#39;s scheduled by default to run each hours, but can be easily customized under the&amp;nbsp;Optimizely CMS administrative interface.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Room for Innovation&lt;/h3&gt;
&lt;p&gt;The library is built with extensibility in mind. While Azure Blob Storage is the default (and required for now), you&#39;re welcome to dive into the source code and add your own flair. Whether that&#39;s&amp;nbsp;another storage provider or custom processors, the door is wide open for contributions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ready to get started?&lt;/strong&gt; &lt;a href=&quot;https://git.davidhome.net/web/davidhome-rssfeed&quot; target=&quot;_new&quot; rel=&quot;noopener&quot;&gt;Check out the source code here&lt;/a&gt; and give it a try.&amp;nbsp;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-01-25T01:17:04.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>My blog is now running using Optimizely CMS!</title><link href="https://www.davidhome.net/blog/my-blog-is-now-running-using-optimizely-cms/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;It&#39;s official! You are currently reading this post on my shiny new Optimizely CMS website.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In the past weeks, I have been quite busy crunching every items to transfer my originally developed website from Orchard CMS to Optimizely CMS. It was quite the experience and also, a lot of new opportunities are now on the horizon.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Now what?&lt;/h2&gt;
&lt;p&gt;During this experience, I was able to take my time to tackle certain architectural challenges that we&#39;re often facing when creating a new Optimizely CMS website. So in the upcoming days/weeks, I will be able to talk about these, but here&#39;s a sneak&amp;nbsp;peek on the topics I&#39;d like to talk about:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What&#39;s the right project structure, or at least, best&amp;nbsp;&lt;em&gt;suggested&lt;/em&gt;&amp;nbsp;structure,&amp;nbsp;when building the project with a multi site setup in mind.&lt;/li&gt;
&lt;li&gt;Creation &amp;amp; announcement of a new NuGet feed that will ease the multi site setup for developers.&lt;/li&gt;
&lt;li&gt;For those interested, I have also developed a custom RSS feed integration for Optimizely CMS that I plan on publishing. You can see it in action on this blog:&amp;nbsp;&lt;a href=&quot;https://www.davidhome.net/rss/&quot;&gt;https://www.davidhome.net/rss/&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;GitLab pipelines. I have fully automated builds &amp;amp; deployments of the website using this technology. I&#39;m normally accustomed to use Azure DevOps, but for this personal project, it was a great opportunity to learn something different using my own GitLab instance.&lt;/li&gt;
&lt;li&gt;Maybe an open topic on the mediator pattern? I&#39;ve used the library MediatR for my blog.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stay tuned &amp;amp; happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2025-01-12T21:36:56.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Content Graph - Sync Computed Getter Properties like with Search &amp; Navigation</title><link href="https://www.davidhome.net/blog/optimizely-content-graph-sync-computed-getter-properties-like-with-search-navigation" /><id>&lt;p&gt;Recently, I have been re-writing my blog using Optimizely CMS. During this process, I wanted to try new released technologies offered by Optimizely. Today, I&#39;m going to talk a bit about my experience with Content Graph.&lt;/p&gt;
&lt;p&gt;Of course, I thought about using Search &amp;amp; Navigation, but I think it was beneficial that I learned something that will eventually, to my opinion, replace it entirely.&lt;/p&gt;
&lt;p&gt;With Search &amp;amp; Navigation, the majority of Optimizely developers are well aware that you can synchronize not only properties meant for the content type itself, but also the ones that are computed, meaning only a getter has been used. The following example demonstrate that (see FirstPublishedDate):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-10-18%20153816.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-10-18%20153816.png?width=160&quot; alt=&quot;Figure 1&quot; title=&quot;Figure 2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you use Search &amp;amp; Navigation, this property is getting indexed and can be used on any query. It also means you can order your result with that computed field and do a bunch of cool tricks, such as including different ways to represent a certain element from the content type in a fashion consumable by the Search &amp;amp; Navigation C# client. Complex types aren’t really its force, so we often add getter only properties to return a more simplified, but useful, data format.&lt;/p&gt;
&lt;p&gt;So, under Content Graph, I was under the impression that I could do the same thing, but alas, no, you can&#39;t for the moment. But Optimizely did confirmed to myself via a support request they have plans to add something that would allow developers to customize and add the ability to extend the data synchronization of content types. In the meantime, I have concocted a very... but very hackish workaround that allow you to synchronize getter properties. Before moving forward with the solution, a little explanation is in order to understand a bit more what you can/can&#39;t do:&lt;/p&gt;
&lt;p&gt;The schedule job &quot;Optimizely Graph content synchronization job&quot; is the main responsible to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scan for all existing content types and synchronize them to Content Graph. It&#39;s using their definitions to know the structure your content should have under its own engine.&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;By the way, you can also use their endpoints to synchronize content types completely unrelated to the CMS: &lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&quot;&gt;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&lt;/a&gt;. Very useful in scenarios where you need content from different external sources, but all available in the same place. Super powerful and allow the leverage of the engine to do extremely useful searches.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then scans for all existing content in the CMS and synchronize all of its data under Content Graph. &lt;strong&gt;This is the step that interests us&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While digging for a solution to extend my content type and allow the synchronization of getter only properties, I have realized the following must be respected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your property &lt;strong&gt;cannot&lt;/strong&gt; be a getter only, you still have to put a setter. It&#39;s because otherwise the property is not considered a &lt;em&gt;real&lt;/em&gt; CMS property and thus is getting ignored during the indexation. A &lt;em&gt;real&lt;/em&gt; property will appear under &quot;CMS -&amp;gt; Settings -&amp;gt; Content Type&quot; menu.&lt;/li&gt;
&lt;li&gt;So I simply added a setter using &lt;code&gt;this.SetPropertyValue()&lt;/code&gt; to get over this limitation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you get your property synchronized to Optimizely database comes the part where the indexation job of Content Graph is still ignoring it. While reading the decompiled code of Optimizely, I learned that the current business logic is &lt;strong&gt;only&lt;/strong&gt; relying on the data stored within the CMS database, which means that in the end &lt;strong&gt;all&lt;/strong&gt; defined elements in the code is entirely ignored, since the job is only directly pulling the data inside the internal content type tables.&lt;/p&gt;
&lt;p&gt;So, well, this is where I come with the idea to build a dynamic type. I had to create a dynamic assembly which was allowed to interact with &quot;internal&quot; types of the &quot;Optimizely.ContentGraph.Cms.NetCore&quot; assembly. In the end, I had to create IL code so that my desired behavior could be added to the synchronization job. To keep that brief, if you want the solution, here&#39;s my &lt;a href=&quot;https://github.com/ddprince17/optimizely-content-graph&quot;&gt;GitHub repository&lt;/a&gt; with a small example. The dynamic type is inheriting from &quot;ContentGraphContentConverter&quot; and is replacing the original type under the IoC container. Under the &quot;Convert&quot; method, instead of directly returning the model, I&#39;m calling the extension AddReadonlyProperties, which then allow me to customize the return value.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
</id><updated>2024-10-18T21:13:50.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Content Graph - Sync Computed Getter Properties like with Search &amp; Navigation</title><link href="https://www.davidhome.net/blog/optimizely-content-graph-sync-computed-getter-properties-like-with-search-navigation/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;Recently, I have been re-writing my blog using Optimizely CMS. During this process, I wanted to try new released technologies offered by Optimizely. Today, I&#39;m going to talk a bit about my experience with Content Graph.&lt;/p&gt;
&lt;p&gt;Of course, I thought about using Search &amp;amp; Navigation, but I think it was beneficial that I learned something that will eventually, to my opinion, replace it entirely.&lt;/p&gt;
&lt;p&gt;With Search &amp;amp; Navigation, the majority of Optimizely developers are well aware that you can synchronize not only properties meant for the content type itself, but also the ones that are computed, meaning only a getter has been used. The following example demonstrate that (see FirstPublishedDate):&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/22ee54e089604fc4b4e70b23b51da9c7/screenshot202024-10-18201538161.png&quot;&gt;&lt;img src=&quot;https://www.davidhome.net/contentassets/22ee54e089604fc4b4e70b23b51da9c7/screenshot202024-10-18201538161.png&quot; alt=&quot;Figure 1&quot; width=&quot;160&quot; height=&quot;67&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you use Search &amp;amp; Navigation, this property is getting indexed and can be used on any query. It also means you can order your result with that computed field and do a bunch of cool tricks, such as including different ways to represent a certain element from the content type in a fashion consumable by the Search &amp;amp; Navigation C# client. Complex types aren&amp;rsquo;t really its force, so we often add getter only properties to return a more simplified, but useful, data format.&lt;/p&gt;
&lt;p&gt;So, under Content Graph, I was under the impression that I could do the same thing, but alas, no, you can&#39;t for the moment. But Optimizely did confirmed to myself via a support request they have plans to add something that would allow developers to customize and add the ability to extend the data synchronization of content types. In the meantime, I have concocted a very... but very hackish workaround that allow you to synchronize getter properties. Before moving forward with the solution, a little explanation is in order to understand a bit more what you can/can&#39;t do:&lt;/p&gt;
&lt;p&gt;The schedule job &quot;Optimizely Graph content synchronization job&quot; is the main responsible to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scan for all existing content types and synchronize them to Content Graph. It&#39;s using their definitions to know the structure your content should have under its own engine.&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;By the way, you can also use their endpoints to synchronize content types completely unrelated to the CMS:&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&quot;&gt;https://docs.developers.optimizely.com/platform-optimizely/v1.4.0-optimizely-graph/docs/synchronize-content-types&lt;/a&gt;. Very useful in scenarios where you need content from different external sources, but all available in the same place. Super powerful and allow the leverage of the engine to do extremely useful searches.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then scans for all existing content in the CMS and synchronize all of its data under Content Graph.&amp;nbsp;&lt;strong&gt;This is the step that interests us&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While digging for a solution to extend my content type and allow the synchronization of getter only properties, I have realized the following must be respected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your property&amp;nbsp;&lt;strong&gt;cannot&lt;/strong&gt;&amp;nbsp;be a getter only, you still have to put a setter. It&#39;s because otherwise the property is not considered a&amp;nbsp;&lt;em&gt;real&lt;/em&gt;&amp;nbsp;CMS property and thus is getting ignored during the indexation. A&amp;nbsp;&lt;em&gt;real&lt;/em&gt;&amp;nbsp;property will appear under &quot;CMS -&amp;gt; Settings -&amp;gt; Content Type&quot; menu.&lt;/li&gt;
&lt;li&gt;So I simply added a setter using&amp;nbsp;&lt;code&gt;this.SetPropertyValue()&lt;/code&gt;&amp;nbsp;to get over this limitation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you get your property synchronized to Optimizely database comes the part where the indexation job of Content Graph is still ignoring it. While reading the decompiled code of Optimizely, I learned that the current business logic is&amp;nbsp;&lt;strong&gt;only&lt;/strong&gt;&amp;nbsp;relying on the data stored within the CMS database, which means that in the end&amp;nbsp;&lt;strong&gt;all&lt;/strong&gt;&amp;nbsp;defined elements in the code is entirely ignored, since the job is only directly pulling the data inside the internal content type tables.&lt;/p&gt;
&lt;p&gt;So, well, this is where I come with the idea to build a dynamic type. I had to create a dynamic assembly which was allowed to interact with &quot;internal&quot; types of the &quot;Optimizely.ContentGraph.Cms.NetCore&quot; assembly. In the end, I had to create IL code so that my desired behavior could be added to the synchronization job. To keep that brief, if you want the solution, here&#39;s my&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/optimizely-content-graph&quot;&gt;GitHub repository&lt;/a&gt;&amp;nbsp;with a small example. The dynamic type is inheriting from &quot;ContentGraphContentConverter&quot; and is replacing the original type under the IoC container. Under the &quot;Convert&quot; method, instead of directly returning the model, I&#39;m calling the extension AddReadonlyProperties, which then allow me to customize the return value.&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2024-10-18T04:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>GetNextSegment with empty Remaining causing fuzzes</title><link href="https://www.davidhome.net/blog/getnextsegment-with-empty-remaining-causing-fuzzes" /><id>&lt;p&gt;Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL. Of course, a more in-dept explanation can be found at the following URL: &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&lt;/a&gt;. All in all, this concept also existed in the previous version of the CMS, but it differed a bit than it is currently.&lt;/p&gt;
&lt;p&gt;See, when we migrated a customer from .NET Framework to .NET Core, AKA CMS 11 to 12, we encountered a memory leak that we couldn&#39;t really put our hand on at first sight. The solution heavily used those partial routers. Our issue also often occurred on DXP and not on our development machine, which was even weirder. Until we remembered that the application is automatically &lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites&quot;&gt;warmed up&lt;/a&gt; when running under DXP. This led us to find the source of the problem, the partial routers, but more specifically on the part when we get the next value of the URL segment.&lt;/p&gt;
&lt;p&gt;As explained in the &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;CMS 12 documentation&lt;/a&gt;, you can use from an &lt;code&gt;UrlResolverContext&lt;/code&gt; the method &lt;code&gt;GetNextSegment&lt;/code&gt; to get that information. It returns a &lt;code&gt;Segment&lt;/code&gt;, which contains what&#39;s &quot;next&quot; and the &quot;remaining path&quot;. Our code, since it was originally developed for CMS 11, used at the time the old API, a method &quot;GetNextValue&quot;, probably from a similar service which I don&#39;t reminder the name and was looping around it until Optimizely code returned an empty segment. It all makes sense right? Here&#39;s the decompiled code from CMS 11:
&lt;img src=&quot;/media/image001.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, CMS 11 was simply returning an empty &quot;next&quot; and &quot;remaining&quot; &lt;code&gt;SegmentPair&lt;/code&gt; if the remaining path was empty. Our code was relying on this information, well, specifically the property &quot;Remaining&quot; of the segment pair. I&#39;m sure at this point you&#39;re probably realizing why we were having memory leaks. Indeed, under CMS 12, this piece of code changed and caused infinite loops in ours, concatenating string &lt;em&gt;to infinity and beyond&lt;/em&gt;. The change itself is very inexplicable I would say, even myself don&#39;t understand why Optimizely decided to make it that way, it feels more like an unwanted error than anything else. Here&#39;s a snapshot of the decompiled code, this will speak for itself:
&lt;img src=&quot;/media/Screenshot 2024-07-07 232757.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It, hum, well, reassign the remaining path if it&#39;s empty, but also have &lt;strong&gt;the same check just after&lt;/strong&gt;, a condition, which at this point, will never be met, that simply does &lt;strong&gt;what we would expect&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We informed Optimizely of this quirk, though unfortunately didn&#39;t ended up being fixed. Our code now has different checks and also a failsafe to avoid looping indefinitely. I hope that with this post can save you time diagnosing similar problems on your end.&lt;/p&gt;
</id><updated>2024-07-08T03:50:26.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>GetNextSegment with empty Remaining causing fuzzes</title><link href="https://www.davidhome.net/blog/getnextsegment-with-empty-remaining-causing-fuzzes/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL. Of course, a more in-dept explanation can be found at the following URL:&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&lt;/a&gt;. All in all, this concept also existed in the previous version of the CMS, but it differed a bit than it is currently.&lt;/p&gt;
&lt;p&gt;See, when we migrated a customer from .NET Framework to .NET Core, AKA CMS 11 to 12, we encountered a memory leak that we couldn&#39;t really put our hand on at first sight. The solution heavily used those partial routers. Our issue also often occurred on DXP and not on our development machine, which was even weirder. Until we remembered that the application is automatically&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites&quot;&gt;warmed up&lt;/a&gt;&amp;nbsp;when running under DXP. This led us to find the source of the problem, the partial routers, but more specifically on the part when we get the next value of the URL segment.&lt;/p&gt;
&lt;p&gt;As explained in the&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/example-of-news-partial-routing&quot;&gt;CMS 12 documentation&lt;/a&gt;, you can use from an&amp;nbsp;&lt;code&gt;UrlResolverContext&lt;/code&gt;&amp;nbsp;the method&amp;nbsp;&lt;code&gt;GetNextSegment&lt;/code&gt;&amp;nbsp;to get that information. It returns a&amp;nbsp;&lt;code&gt;Segment&lt;/code&gt;, which contains what&#39;s &quot;next&quot; and the &quot;remaining path&quot;. Our code, since it was originally developed for CMS 11, used at the time the old API, a method &quot;GetNextValue&quot;, probably from a similar service which I don&#39;t reminder the name and was looping around it until Optimizely code returned an empty segment. It all makes sense right? Here&#39;s the decompiled code from CMS 11:&amp;nbsp;&lt;img src=&quot;https://www.davidhome.net/contentassets/d4ddd2d2f5c440eca0ded2148cedf170/image0011.png&quot; alt=&quot;Image 1&quot; width=&quot;1278&quot; height=&quot;760&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, CMS 11 was simply returning an empty &quot;next&quot; and &quot;remaining&quot;&amp;nbsp;&lt;code&gt;SegmentPair&lt;/code&gt;&amp;nbsp;if the remaining path was empty. Our code was relying on this information, well, specifically the property &quot;Remaining&quot; of the segment pair. I&#39;m sure at this point you&#39;re probably realizing why we were having memory leaks. Indeed, under CMS 12, this piece of code changed and caused infinite loops in ours, concatenating string&amp;nbsp;&lt;em&gt;to infinity and beyond&lt;/em&gt;. The change itself is very inexplicable I would say, even myself don&#39;t understand why Optimizely decided to make it that way, it feels more like an unwanted error than anything else. Here&#39;s a snapshot of the decompiled code, this will speak for itself:&amp;nbsp;&lt;img src=&quot;https://www.davidhome.net/contentassets/d4ddd2d2f5c440eca0ded2148cedf170/screenshot202024-07-07202327571.png&quot; alt=&quot;Image 2&quot; width=&quot;1252&quot; height=&quot;956&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It, hum, well, reassign the remaining path if it&#39;s empty, but also have&amp;nbsp;&lt;strong&gt;the same check just after&lt;/strong&gt;, a condition, which at this point, will never be met, that simply does&amp;nbsp;&lt;strong&gt;what we would expect&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We informed Optimizely of this quirk, though unfortunately didn&#39;t ended up being fixed. Our code now has different checks and also a failsafe to avoid looping indefinitely. I hope that with this post can save you time diagnosing similar problems on your end.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2024-07-07T04:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How NDepend can quickly help you find code quality issues and resolve them</title><link href="https://www.davidhome.net/blog/how-ndepend-can-quickly-help-you-find-code-quality-issues-and-resolve-them/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;NDepend has been around&amp;nbsp;&lt;a href=&quot;https://web.archive.org/web/20060518122151/https://www.ndepend.com/&quot;&gt;for quite a long time&lt;/a&gt;. If you are unaware of what it does, you should be checking it. It&#39;s an analysis tools which helps you find inconsistencies and discrepancies in your C# source code. I&#39;ve been trying it out recently and helped me found certain issue which I wasn&#39;t aware of.&lt;/p&gt;
&lt;p&gt;To be clear, there is indeed other available solution and it&#39;s up to your team to decide which fits best with you. In this case, NDepend runs independently from any IDE, but you can install an integration plugin inside Visual Studio. This can give the leverage/advantage to avoid loading any IDE to obtain a report. Unfortunately, there is no plugin/extension support under JetBrains Rider, my personal IDE of choice, like there is under Visual Studio. I hope their team can implement one, that would be greatly valuable.&lt;/p&gt;
&lt;h3&gt;Dashboard&lt;/h3&gt;
&lt;p&gt;Once a solution analysis is completed, you will be greeted with the dashboard tab:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-22201216421.png&quot;&gt;&lt;img title=&quot;Figure 1&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-22201216421.png&quot; alt=&quot;Figure 1&quot; width=&quot;160&quot; height=&quot;87&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here lies a general overview of the quality of the whole solution that you have analyzed. This gives a great quick shot of the elements you could get your hand onto if your goal is to refactor the code to make it more maintainable. You can personalize the rules and change them to your likings. For example, in my previous screenshot, there is one critical rule that flags I&#39;m using a type within another type which have both different namespaces.&amp;nbsp;&lt;a href=&quot;https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1400#!&quot;&gt;The debt explanation is interesting&lt;/a&gt;. Of course, you can always decide to ignore it, but it gives an interesting point of view regarding the fact that some developers are using folders within a project to &quot;organize&quot; classes, which can lead to this kind of rule alert. Of course, everything is to take with a grain of salt, as I agree with someone from the comments of&amp;nbsp;&lt;a href=&quot;https://stackoverflow.com/questions/59519084/how-to-avoid-namespaces-dependency-cycles-between-my-entities&quot;&gt;this Stack Overflow thread&lt;/a&gt;, these kind of alerts can be opinion based. An interesting thing that NDepend&#39;s team could be adding inside their analysis program is rule categorization; Opiniated ones versus the others by example. It could help to quickly address undiscussable items and leave any development team to discuss the other opiniated points and see whether they need to be addressed.&lt;/p&gt;
&lt;h3&gt;Dependency Graph&lt;/h3&gt;
&lt;p&gt;I think the most interesting feature of NDepend is the dependency graph. It&#39;s so incredibly useful to quickly identify areas of your code that aren&#39;t supposed to inherit another project within your solution. This can otherwise, to my opinion, often lead to solutions having classes/elements heavily coupled/entangled together. Based on the same project that I have used to create my first screenshot, here is an example of the graph:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23200946081.png&quot;&gt;&lt;img title=&quot;Figure 2&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23200946081.png&quot; alt=&quot;Figure 2&quot; width=&quot;160&quot; height=&quot;82&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As you can realize in the previous screenshot, there is a design pattern here that I often follow in projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The website is the main &quot;referrer&quot; of projects, meaning that nothing should be referencing the website.
&lt;ul&gt;
&lt;li&gt;The exception are the unit tests. You can also realize the only additional reference of the test projects are the &quot;Models&quot; projects. Nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &quot;Bootstrappers&quot; projects serve only the purpose to add inside the IoC container the necessary services that are consumable by all &quot;Contracts&quot; projects.&lt;/li&gt;
&lt;li&gt;Each consumable &quot;Contracts&quot; projects comes in pair with a minimum of an additional &quot;concrete&quot; implementation project. These projects, as previously mentioned, only contain the implementation your contracts, which will then be consumed by the website.&lt;/li&gt;
&lt;li&gt;Almost everything consumes the &quot;Models&quot;. This is again, by design. Models are the basically &quot;the end of the road&quot;, it should generally refer nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;I will be doing another blog post about the design pattern I personally follow in projects.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;NDepend quickly identified a discrepancy in my project: There is a direct reference from the website to the database library and this is not normal. Double clicking on the arrow gives me the following:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201034241.png&quot;&gt;&lt;img title=&quot;Figure 3&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201034241.png&quot; alt=&quot;Figure 3&quot; width=&quot;160&quot; height=&quot;81&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With that, I can easily remove the three direct references from my code and refactor to use my repository pattern implementation. It&#39;s one of many example that can clearly help you and your team to identify elements such as this one.&lt;/p&gt;
&lt;h3&gt;Metrics&lt;/h3&gt;
&lt;p&gt;The &quot;Metrics&quot; tab is again another great example of the tool usefulness; From there you can view &quot;heated&quot; areas of your code which has strong cyclomatic complexity. Here in my example, the following method is considered a bit more complex:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201114511.png&quot;&gt;&lt;img title=&quot;Figure 4&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201114511.png&quot; alt=&quot;Figure 4&quot; width=&quot;160&quot; height=&quot;62&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Double clicking it opens the file in your preferred IDE and points your cursor directly to the method. If you&#39;re interesting to have different insights, you can also change the metric data type to something else:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201107411.png&quot;&gt;&lt;img title=&quot;Figure 5&quot; src=&quot;https://www.davidhome.net/contentassets/35299a47b8d442dfb348d60119617eb6/screenshot202024-01-23201107411.png&quot; alt=&quot;Figure 5&quot; width=&quot;160&quot; height=&quot;205&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Closure&lt;/h3&gt;
&lt;p&gt;There is also a CI/CD integration that you can install. Obviously, the goal is to obtain those analysis directly from your build pipelines so that you can act preventively on any code quality depreciation. If you&#39;re interested, you can also view certain sample reports at the following URL:&amp;nbsp;&lt;a href=&quot;https://www.ndepend.com/sample-reports/&quot;&gt;https://www.ndepend.com/sample-reports/&lt;/a&gt;. For Optimizely solutions, CD/CI can come super handy because such projects can quickly become a burden maintainability wise.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2024-01-26T05:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>How NDepend can quickly help you find code quality issues and resolve them</title><link href="https://www.davidhome.net/blog/how-ndepend-can-quickly-help-you-find-code-quality-issues-and-resolve-them" /><id>&lt;p&gt;NDepend has been around &lt;a href=&quot;https://web.archive.org/web/20060518122151/https://www.ndepend.com/&quot;&gt;for quite a long time&lt;/a&gt;. If you are unaware of what it does, you should be checking it. It&#39;s an analysis tools which helps you find inconsistencies and discrepancies in your C# source code. I&#39;ve been trying it out recently and helped me found certain issue which I wasn&#39;t aware of.&lt;/p&gt;
&lt;p&gt;To be clear, there is indeed other available solution and it&#39;s up to your team to decide which fits best with you. In this case, NDepend runs independently from any IDE, but you can install an integration plugin inside Visual Studio. This can give the leverage/advantage to avoid loading any IDE to obtain a report. Unfortunately, there is no plugin/extension support under JetBrains Rider, my personal IDE of choice, like there is under Visual Studio. I hope their team can implement one, that would be greatly valuable.&lt;/p&gt;
&lt;h3&gt;Dashboard&lt;/h3&gt;
&lt;p&gt;Once a solution analysis is completed, you will be greeted with the dashboard tab:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-22%20121642.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-22%20121642.png?width=160&quot; alt=&quot;Figure 1&quot; title=&quot;Figure 1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here lies a general overview of the quality of the whole solution that you have analyzed. This gives a great quick shot of the elements you could get your hand onto if your goal is to refactor the code to make it more maintainable. You can personalize the rules and change them to your likings. For example, in my previous screenshot, there is one critical rule that flags I&#39;m using a type within another type which have both different namespaces. &lt;a href=&quot;https://www.ndepend.com/default-rules/NDepend-Rules-Explorer.html?ruleid=ND1400#!&quot;&gt;The debt explanation is interesting&lt;/a&gt;. Of course, you can always decide to ignore it, but it gives an interesting point of view regarding the fact that some developers are using folders within a project to &quot;organize&quot; classes, which can lead to this kind of rule alert. Of course, everything is to take with a grain of salt, as I agree with someone from the comments of &lt;a href=&quot;https://stackoverflow.com/questions/59519084/how-to-avoid-namespaces-dependency-cycles-between-my-entities&quot;&gt;this Stack Overflow thread&lt;/a&gt;, these kind of alerts can be opinion based. An interesting thing that NDepend&#39;s team could be adding inside their analysis program is rule categorization; Opiniated ones versus the others by example. It could help to quickly address undiscussable items and leave any development team to discuss the other opiniated points and see whether they need to be addressed.&lt;/p&gt;
&lt;h3&gt;Dependency Graph&lt;/h3&gt;
&lt;p&gt;I think the most interesting feature of NDepend is the dependency graph. It&#39;s so incredibly useful to quickly identify areas of your code that aren&#39;t supposed to inherit another project within your solution. This can otherwise, to my opinion, often lead to solutions having classes/elements heavily coupled/entangled together. Based on the same project that I have used to create my first screenshot, here is an example of the graph:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20094608.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20094608.png?width=160&quot; alt=&quot;Figure 2&quot; title=&quot;Figure 2&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As you can realize in the previous screenshot, there is a design pattern here that I often follow in projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The website is the main &quot;referrer&quot; of projects, meaning that nothing should be referencing the website.
&lt;ul&gt;
&lt;li&gt;The exception are the unit tests. You can also realize the only additional reference of the test projects are the &quot;Models&quot; projects. Nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &quot;Bootstrappers&quot; projects serve only the purpose to add inside the IoC container the necessary services that are consumable by all &quot;Contracts&quot; projects.&lt;/li&gt;
&lt;li&gt;Each consumable &quot;Contracts&quot; projects comes in pair with a minimum of an additional &quot;concrete&quot; implementation project. These projects, as previously mentioned, only contain the implementation your contracts, which will then be consumed by the website.&lt;/li&gt;
&lt;li&gt;Almost everything consumes the &quot;Models&quot;. This is again, by design. Models are the basically &quot;the end of the road&quot;, it should generally refer nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;I will be doing another blog post about the design pattern I personally follow in projects.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;NDepend quickly identified a discrepancy in my project: There is a direct reference from the website to the database library and this is not normal. Double clicking on the arrow gives me the following:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20103424.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20103424.png?width=160&quot; alt=&quot;Figure 3&quot; title=&quot;Figure 3&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With that, I can easily remove the three direct references from my code and refactor to use my repository pattern implementation. It&#39;s one of many example that can clearly help you and your team to identify elements such as this one.&lt;/p&gt;
&lt;h3&gt;Metrics&lt;/h3&gt;
&lt;p&gt;The &quot;Metrics&quot; tab is again another great example of the tool usefulness; From there you can view &quot;heated&quot; areas of your code which has strong cyclomatic complexity.
Here in my example, the following method is considered a bit more complex:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20111451.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20111451.png?width=160&quot; alt=&quot;Figure 4&quot; title=&quot;Figure 4&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Double clicking it opens the file in your preferred IDE and points your cursor directly to the method. If you&#39;re interesting to have different insights, you can also change the metric data type to something else:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/media/Screenshot%202024-01-23%20110741.png&quot;&gt;&lt;img src=&quot;/media/Screenshot%202024-01-23%20110741.png?width=160&quot; alt=&quot;Figure 5&quot; title=&quot;Figure 5&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Closure&lt;/h3&gt;
&lt;p&gt;There is also a CI/CD integration that you can install. Obviously, the goal is to obtain those analysis directly from your build pipelines so that you can act preventively on any code quality depreciation. Within Optimizely solutions, this can come super handy because such projects can quickly become a burden maintainability wise.&lt;/p&gt;
</id><updated>2024-01-23T16:34:37.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Content Cloud CMS Apps (Add-ons) - Tips &amp; Tricks</title><link href="https://www.davidhome.net/blog/optimizely-content-cloud-cms-apps-add-ons-tips-tricks/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;Developing an Optimizely CMS Add-on can be a bit tricky. There is a lot of advantages of doing it, but building it right can be a bit tedious. You have probably already seen the following&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/developing-add-ons&quot;&gt;documentation page&lt;/a&gt;&amp;nbsp;about the subject itself, but it feels like a lot of details are missing to make it right. You will also quickly realize there is a lot of historical elements which makes the process a bit more complicated/confusing. In this blog, we will unravel everything so that you can successfully build yours!&lt;/p&gt;
&lt;p&gt;I will be assuming that your project is currently using .NET 6.0, but the process is the same if the target framework changes. You can also add conditional dependencies based on the target framework, which then the command&amp;nbsp;&lt;code&gt;dotnet pack&lt;/code&gt;&amp;nbsp;will automatically handle when bundling your library. I will also assume that you are already mastering how packing your library to .nupkg file(s) works.&lt;/p&gt;
&lt;h2&gt;1. Adjustements inside the project file&lt;/h2&gt;
&lt;p&gt;First of all, create a new solution with a library project using your preferred IDE, then edit the .csproj file. Your file should look like the following in the end:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Sdk&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Microsoft.NET.Sdk.Razor&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;TargetFramework&lt;/span&gt;&amp;gt;&lt;/span&gt;net6.0&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;TargetFramework&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Nullable&lt;/span&gt;&amp;gt;&lt;/span&gt;enable&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Nullable&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ImplicitUsings&lt;/span&gt;&amp;gt;&lt;/span&gt;enable&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ImplicitUsings&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;AddRazorSupportForMvc&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;AddRazorSupportForMvc&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;FrameworkReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Microsoft.AspNetCore.App&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
  
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
		&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!--    These are package example. Install those you need. --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.AspNetCore.Mvc&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.Core&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageReference&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.UI.Core&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Remove&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;module.config&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;module.config&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;Never&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Remove&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;packages.lock.json&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;packages.lock.json&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;Never&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;CopyToOutputDirectory&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;None&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can also realize, I&#39;m using&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management&quot;&gt;CPM&lt;/a&gt;. This will greatly simplify the dependency management along the way.&lt;/p&gt;
&lt;p&gt;Couple of things to highlight here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Super important to change the SDK attribute on the &quot;Project&quot; node to &quot;Microsoft.NET.Sdk.Razor&quot;.&lt;/li&gt;
&lt;li&gt;Inside the first PropertyGroup node, add AddRazorSupportForMvc and set it &quot;true&quot;.&lt;/li&gt;
&lt;li&gt;Make sure to add a FrameworkReference node which will be including the Microsoft.AspNetCore.App reference. Necessary for having the ASP.NET Core web references, which normally the SDK Microsoft.NET.Sdk.Web includes by default.&lt;/li&gt;
&lt;li&gt;The last, but not least, make sure to remove &amp;amp; ignore any files you want to exclude from your .nupkg file. In our example, and you will probably need it too, we want to prevent both &quot;module.config&quot; and &quot;packages.lock.json&quot; file to be inside the file package.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Custom MSBuild instructions&lt;/h2&gt;
&lt;p&gt;There is a couple of adjustments to make so that MSBuild is helping up ease the bundling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Assuming your projects are hierarchically structured in the following directory pattern: [root]/src/projectname/projectname.csproj, create a Directory.Build.props file in the &quot;src&quot; folder with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;NU1507&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;True&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Assuming the same structure, create under the &quot;src&quot; folder the file Directory.Packages.props with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ManagePackageVersionsCentrally&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ManagePackageVersionsCentrally&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CentralPackageTransitivePinningEnabled&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;CentralPackageTransitivePinningEnabled&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
		&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!--    These are package example. Install those you need. --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageVersion&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.AspNetCore.Mvc&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Version&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;[12.4.0, 13.0.0)&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageVersion&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.Core&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Version&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;[12.4.0, 13.0.0)&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageVersion&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServer.CMS.UI.Core&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Version&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;[12.4.0, 13.0.0)&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Inside the project folder, where the .csproj file resides, create a new file entitled Directory.Build.props with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt; ?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ClientResources&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(ProjectDir)ClientResources\**\*&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;TmpOutDir&lt;/span&gt;&amp;gt;&lt;/span&gt;$([System.IO.Path]::Combine($(ProjectDir), &#39;tmp&#39;))&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;TmpOutDir&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;NU1507&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;NoWarn&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;True&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;RestorePackagesWithLockFile&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PropertyGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(MSBuildProjectName).zip&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;contentFiles\any\any\modules\_protected\$(MSBuildProjectName)&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;BuildAction&lt;/span&gt;&amp;gt;&lt;/span&gt;None&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;BuildAction&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackageCopyToOutput&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PackageCopyToOutput&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;msbuild\CopyZipFiles.targets&quot;&lt;/span&gt; &amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;true&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Pack&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;build\$(MSBuildProjectName).targets&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;PackagePath&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Content&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Still in the project folder, create the file Directory.Build.targets and add the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt; ?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CreateCmsAddOnZip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(ProjectDir)module.config&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFolder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)\content&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;@(ClientResources)&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;@(ClientResources -&amp;gt; &#39;$(TmpOutDir)\content\$(PackageVersion)\ClientResources\%(RecursiveDir)%(Filename)%(Extension)&#39;)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!-- Update the module config with the version information --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;XmlPoke&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;XmlInputPath&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)\content\module.config&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Query&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/module/@clientResourceRelativePath&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(PackageVersion)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ZipClientResources&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;AfterTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CreateCmsAddOnZip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DependsOnTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CreateCmsAddOnZip&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ZipDirectory&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceDirectory&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)\content&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFile&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(ProjectDir)$(MSBuildProjectName).zip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Overwrite&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;true&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CleanupTmpOutDir&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;AfterTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ZipClientResources&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DependsOnTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;ZipClientResources&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;RemoveDir&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Directories&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(TmpOutDir)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;You will also have to add the file CopyZipFiles.targets to a new &quot;msbuild&quot; folder under the root of the project directory:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;xmlns&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;ToolsVersion&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;4.0&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CmsAddOnZips&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Include&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(MSBuildThisFileDirectory)..\contentFiles\any\any\modules\_protected\**\*.zip&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ItemGroup&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CopyCmsAddOnZip&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;BeforeTargets&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Build&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Copy&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;SourceFiles&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;@(CmsAddOnZips)&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DestinationFolder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)&quot;&lt;/span&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Target&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Project&lt;/span&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To summarize, these customizations will do the following to your project each time you will be compiling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adds CPM, as previously mentioned.&lt;/li&gt;
&lt;li&gt;Adds NuGet lock files, super useful for your DevOps pipeline.&lt;/li&gt;
&lt;li&gt;Automatically compile views.&lt;/li&gt;
&lt;li&gt;Automatically generates module the zip file with all necessary elements in it that will be packaged within your .nupkg file.
&lt;ul&gt;
&lt;li&gt;This file contains the recommended structure &amp;amp; content that you can find in the&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/developing-add-ons&quot;&gt;Optimizely CMS addon documentation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Minimum required configuration&lt;/h2&gt;
&lt;p&gt;See the &quot;module.config&quot; file like the definition of your addon. Without it, Optimizely will use the default values from the class ShellModuleManifest, which unfortunately omit a very important detail; The assembly&amp;rsquo;s name of your addon. Without it, Optimizely won&#39;t be able to load yours at boot. Create it where the .csproj file resides with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;?xml version=&lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0&quot;&lt;/span&gt; encoding=&lt;span class=&quot;hljs-string&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;loadFromBin&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;false&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Your.Assembly.Name&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;viewEngine&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Razor&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;clientResourceRelativePath&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;$version$&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;tags&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;EPiServerModulePackage&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;assemblies&lt;/span&gt;&amp;gt;&lt;/span&gt;
		&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!-- Change the assembly name with yours --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;assembly&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Your.Assembly.Name&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;assemblies&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;clientModule&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;moduleDependencies&lt;/span&gt;&amp;gt;&lt;/span&gt;
			&lt;span class=&quot;hljs-comment&quot;&gt;&amp;lt;!-- Adjust accordingly --&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;dependency&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;CMS&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;RunAfter&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;moduleDependencies&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;clientModule&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;module&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add an extension method on the interface &quot;IServiceCollection&quot; and add at least the following piece of code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;static&lt;/span&gt; IServiceCollection &lt;span class=&quot;hljs-title&quot;&gt;AddMyAddon&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;this&lt;/span&gt; IServiceCollection services&lt;/span&gt;)&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-comment&quot;&gt;// Add services here.&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; services
                &lt;span class=&quot;hljs-comment&quot;&gt;// Super required, otherwise your addon won&#39;t load when the site loads.&lt;/span&gt;
            .Configure&amp;lt;ProtectedModuleOptions&amp;gt;(
            pm =&amp;gt;
            {
                &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!pm.Items.Any(i =&amp;gt; i.Name.Equals(ModuleName, StringComparison.OrdinalIgnoreCase)))
                {
                    pm.Items.Add(&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; ModuleDetails { Name = ModuleName });
                }
            });
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Controllers &amp;amp; Views&lt;/h2&gt;
&lt;p&gt;Probably the most undocumented/unclear part for Optimizely CMS addons. The instructions around views doesn&#39;t exactly explain how they can be used or how the routing works within your library. If you&#39;re looking at existing addons, e.g.,&amp;nbsp;&lt;a href=&quot;https://github.com/Geta/geta-notfoundhandler&quot;&gt;Geta.NotFoundHandler&lt;/a&gt;, the majority of developers are handling it slightly differently.&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/migrating-add-ons-to-net-5&quot;&gt;This page&lt;/a&gt;&amp;nbsp;explains how to structure your files in a manner that the module will automatically include them for you, but I was unable to make it work. Maybe because it should be exclusively a&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-8.0&amp;amp;tabs=visual-studio&quot;&gt;razor page&lt;/a&gt;&amp;nbsp;and not a simple razor view dependent on a Controller. Fortunately, you can make it work with a controller, but with a little additional tweaking:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-8.0#custom-route-attributes-using-iroutetemplateprovider&quot;&gt;Route Attribute&lt;/a&gt;. We will use it to customize the routes to our controllers. Here&#39;s an example:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Shell;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Microsoft.AspNetCore.Mvc.Routing;

&lt;span class=&quot;hljs-keyword&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Playground.Mvc&lt;/span&gt;;

[&lt;span class=&quot;hljs-meta&quot;&gt;AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)&lt;/span&gt;]
&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;ModuleRoute&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;Attribute&lt;/span&gt;, &lt;span class=&quot;hljs-title&quot;&gt;IRouteTemplateProvider&lt;/span&gt;
{
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; _controllerName;
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; _actionName;

    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; Template =&amp;gt; Paths.ToResource(&lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt;(ModuleRoute), &lt;span class=&quot;hljs-string&quot;&gt;$&quot;&lt;span class=&quot;hljs-subst&quot;&gt;{_controllerName}&lt;/span&gt;/&lt;span class=&quot;hljs-subst&quot;&gt;{_actionName}&lt;/span&gt;&quot;&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;int&lt;/span&gt;? Order { &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt;; } = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; Name { &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt;; &lt;span class=&quot;hljs-keyword&quot;&gt;set&lt;/span&gt;; }

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;ModuleRoute&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; controllerName, &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; actionName&lt;/span&gt;)&lt;/span&gt;
    {
        _controllerName = controllerName;
        _actionName = actionName;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Decorate your controller actions with your newly created route attribute. e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;    [&lt;span class=&quot;hljs-meta&quot;&gt;HttpGet&lt;/span&gt;]
    [&lt;span class=&quot;hljs-meta&quot;&gt;ModuleRoute(&lt;span class=&quot;hljs-string&quot;&gt;&quot;Default&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;Index&quot;&lt;/span&gt;)&lt;/span&gt;]
    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; IActionResult &lt;span class=&quot;hljs-title&quot;&gt;Index&lt;/span&gt;()&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; View();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Create a new menu provider class including all paths to your actions in your addons. e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Framework.Localization;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Shell;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; EPiServer.Shell.Navigation;
&lt;span class=&quot;hljs-keyword&quot;&gt;using&lt;/span&gt; Playground.Controllers;

&lt;span class=&quot;hljs-keyword&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Playground.Optimizely&lt;/span&gt;;

[&lt;span class=&quot;hljs-meta&quot;&gt;MenuProvider&lt;/span&gt;]
&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;AddonMenuProvider&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;IMenuProvider&lt;/span&gt;
{
    &lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;readonly&lt;/span&gt; LocalizationService _localizationService;

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;AddonMenuProvider&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;LocalizationService localizationService&lt;/span&gt;)&lt;/span&gt;
    {
        _localizationService = localizationService;
    }

    &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; IEnumerable&amp;lt;MenuItem&amp;gt; &lt;span class=&quot;hljs-title&quot;&gt;GetMenuItems&lt;/span&gt;()&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;UrlMenuItem&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;_localizationService.GetString(&lt;span class=&quot;hljs-string&quot;&gt;&quot;/myaddon/gadget/title&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;My Addon&quot;&lt;/span&gt;&lt;/span&gt;), &quot;/&lt;span class=&quot;hljs-keyword&quot;&gt;global&lt;/span&gt;/cms/myaddon&quot;,
            Paths.&lt;span class=&quot;hljs-title&quot;&gt;ToResource&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;GetType(&lt;/span&gt;), $&quot;Default/&lt;/span&gt;{&lt;span class=&quot;hljs-keyword&quot;&gt;nameof&lt;/span&gt;(DefaultController.Index)}&lt;span class=&quot;hljs-string&quot;&gt;&quot;))
        {
            SortIndex = 0,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };

        yield return new UrlMenuItem(_localizationService.GetString(&quot;&lt;/span&gt;/myaddon/index/menu&lt;span class=&quot;hljs-string&quot;&gt;&quot;, &quot;&lt;/span&gt;Home&lt;span class=&quot;hljs-string&quot;&gt;&quot;), &quot;&lt;/span&gt;/&lt;span class=&quot;hljs-keyword&quot;&gt;global&lt;/span&gt;/cms/myaddon/index&lt;span class=&quot;hljs-string&quot;&gt;&quot;,
            Paths.ToResource(GetType(), $&quot;&lt;/span&gt;Default/{&lt;span class=&quot;hljs-keyword&quot;&gt;nameof&lt;/span&gt;(DefaultController.Index)}&lt;span class=&quot;hljs-string&quot;&gt;&quot;))
        {
            SortIndex = 10,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };

        yield return new UrlMenuItem(_localizationService.GetString(&quot;&lt;/span&gt;/myaddon/secondaction/menu&lt;span class=&quot;hljs-string&quot;&gt;&quot;, &quot;&lt;/span&gt;Second Action&lt;span class=&quot;hljs-string&quot;&gt;&quot;), &quot;&lt;/span&gt;/&lt;span class=&quot;hljs-keyword&quot;&gt;global&lt;/span&gt;/cms/myaddon/secondaction&lt;span class=&quot;hljs-string&quot;&gt;&quot;,
            Paths.ToResource(GetType(), $&quot;&lt;/span&gt;Default/{&lt;span class=&quot;hljs-keyword&quot;&gt;nameof&lt;/span&gt;(DefaultController.SecondAction)}&lt;span class=&quot;hljs-string&quot;&gt;&quot;))
        {
            SortIndex = 20,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };
    }
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing so, you are generating routes which will point directly on your addon controller. Say by example you have the &quot;DefaultController&quot; class with the &quot;Index&quot; action, well then, the action under your browser should look as the following: ~/EPiServer/MyAddon/Default/Index. This also convenientely allow you to use razor helpers such as Html.BeginForm, since the MVC routing system knows these belongs to your addon with a custom template.&lt;/p&gt;
&lt;p&gt;It is very important to respect the actions defined within your IMenuProvider implementation, otherwise the custom routing attribute won&#39;t be working just right. As you can see, a certain minimum level of structure has been established in this file, which makes actions from the controller to work correctly. The third parameter of the constructor of UrlMenuItem is exactly where the magic happens. The recommendation is indeed to keep using the helper, Paths.ToResource and add the additional segment manually. This consequently creates the same path as previously described in the last paragraph and will match it with the available controller action. A menu item is essentially an element that will appear under the Backoffice, when navigating under ~/EPiServer.&lt;/p&gt;
&lt;p&gt;I hope this will be super helpful to people reading this blog! You can view a starter code example over there:&amp;nbsp;&lt;a href=&quot;https://github.com/ddprince17/Optimizely-CMS-Addon-Playground&quot;&gt;https://github.com/ddprince17/Optimizely-CMS-Addon-Playground&lt;/a&gt;.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2024-01-22T05:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Secure your SMTP configuration - Configuration Builders</title><link href="https://www.davidhome.net/blog/secure-your-smtp-configuration-configuration-builders/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;In .NET Framework, we are all aware of the famous&amp;nbsp;&lt;code&gt;&amp;lt;network&amp;gt;&lt;/code&gt;&amp;nbsp;node under the Web.config file. This has been the facto way to configure your SMTP settings since, well, a long time. In .NET, the story is quite different, as the approach is way cleaner as it previously was, there is no enforced structure or way to configure your settings, you decide, by yourself, how you would like these settings to exist within, either your appsetting.json file or any configuration provider you might have added to your solution.&lt;/p&gt;
&lt;p&gt;For example, under a Optimizely CMS 12 and higher solution, you would have to add configuration&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/digital-experience-platform/v1.2.0-dxp-cloud-services/docs/configuring-the-email-server&quot;&gt;under the &quot;EPiServer.Cms.Smtp.Network&quot; section&lt;/a&gt;. But in the end, this structure is something established by Optimizely, nothing prevents you to add them elsewhere (even though I would not recommend to duplicate configuration under multiple different nodes).&lt;/p&gt;
&lt;p&gt;In previous version of Optimizely (lower than version 12), since we&#39;re using .NET Framework, we must use the Web.config structure to be able to have our SMTP settings properly setup. The thing though is that we had to put the password cleartext right inside this section. There was also no way to use configuration transformation as it usually works with the appSettings and connectionStrings sections. Until we found a way to move those settings inside the appSettings section.&lt;/p&gt;
&lt;p&gt;We deploy our solutions to DXP. So naturally speaking, we have no control over the infrastructure where the application is hosted, but we have a way, just before sending the package to Optimizely, to transform the configuration. Under Azure DevOps, there&#39;s a task,&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/file-transform-v2?view=azure-pipelines&quot;&gt;File Transform v2&lt;/a&gt;, which does exactly what we want. We store our secrests inside&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops&amp;amp;tabs=yaml&quot;&gt;variable groups&lt;/a&gt;&amp;nbsp;and let the configuration transformation task to do its magic (variable substitution). So, this step covers like 90% of our cases, we can safely store secrets, but unfortunately, like I said, this only works for the appSettings section. What about the smtp section then? I&#39;d tell you &quot;&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/config-builder&quot;&gt;Configuration builders&lt;/a&gt;&quot;. There are already existing config builders in the wild, the one we use the most is the AzureKeyVaultConfigBuilder, but mainly for development purpose, as we still don&#39;t want to expose secrets, even for that.&lt;/p&gt;
&lt;p&gt;But I can hear ya, like at a 100 miles, with your question: &quot;But Dave, what does this change for the SMTP configuration?&quot; I&#39;m glad you&#39;re asking! See, you can add&amp;nbsp;&lt;em&gt;existing&lt;/em&gt;&amp;nbsp;config builder, but you can also&amp;nbsp;&lt;strong&gt;create&lt;/strong&gt;&amp;nbsp;a new one. So, since I want the SMTP configuration to be loaded from the appSettings, I&#39;ll create a class that will do the following:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs hljs language-csharp&quot; data-highlighted=&quot;yes&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;SmtpConfigurationBuilder&lt;/span&gt; : &lt;span class=&quot;hljs-title&quot;&gt;ConfigurationBuilder&lt;/span&gt;
    {
        &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;override&lt;/span&gt; ConfigurationSection &lt;span class=&quot;hljs-title&quot;&gt;ProcessConfigurationSection&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;ConfigurationSection configSection&lt;/span&gt;)&lt;/span&gt;
        {
            &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!(configSection &lt;span class=&quot;hljs-keyword&quot;&gt;is&lt;/span&gt; SmtpSection smtpSection) || smtpSection.Network == &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;) 
                &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;base&lt;/span&gt;.ProcessConfigurationSection(configSection);

            smtpSection.Network.Host = ConfigurationManager.AppSettings[&lt;span class=&quot;hljs-string&quot;&gt;&quot;smtp-host&quot;&lt;/span&gt;];
            smtpSection.Network.UserName = ConfigurationManager.AppSettings[&lt;span class=&quot;hljs-string&quot;&gt;&quot;smtp-userName&quot;&lt;/span&gt;];
            smtpSection.Network.Password = ConfigurationManager.AppSettings[&lt;span class=&quot;hljs-string&quot;&gt;&quot;smtp-password&quot;&lt;/span&gt;];
            smtpSection.Network.Port = ConfigurationManager.AppSettings[&lt;span class=&quot;hljs-string&quot;&gt;&quot;smtp-port&quot;&lt;/span&gt;].ToInt(defaultValue: &lt;span class=&quot;hljs-number&quot;&gt;587&lt;/span&gt;);
            smtpSection.Network.EnableSsl = ConfigurationManager.AppSettings[&lt;span class=&quot;hljs-string&quot;&gt;&quot;smtp-enableSsl&quot;&lt;/span&gt;].ToBoolean();

            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;base&lt;/span&gt;.ProcessConfigurationSection(configSection);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will instruct whatever that uses my config builder to execute only when the current section is a SMTP section and that the Network node isn&#39;t null. To have that work under your Web.config, you&#39;ll have to add three additional items:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Look for the &quot;configSections&quot; node and add the following at the end:
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;section&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;configBuilders&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;restartOnExternalChanges&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;false&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;requirePermission&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;false&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Just after the &quot;configSections&quot; node, add the following:
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;configBuilders&lt;/span&gt;&amp;gt;&lt;/span&gt;
  	&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;builders&lt;/span&gt;&amp;gt;&lt;/span&gt;
  		&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;SmtpConfigurationBuilder&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;Full.Namespace.Of.My.Class.SmtpConfigurationBuilder, Assembly.Name&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  	&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;builders&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;configBuilders&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
If you have multiple config builder, take a good attention of the ordering, as I think it has importance when they are loaded.&lt;/li&gt;
&lt;li&gt;And finally, replace your SMTP node with something like so:
&lt;pre&gt;&lt;code class=&quot;language-xml hljs&quot; data-highlighted=&quot;yes&quot;&gt;  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;system.net&lt;/span&gt;&amp;gt;&lt;/span&gt;
  	&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;mailSettings&lt;/span&gt;&amp;gt;&lt;/span&gt;
  		&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;smtp&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;from&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;donotreply@example.com&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;configBuilders&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;SmtpConfigurationBuilder&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  	&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;mailSettings&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;system.net&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, with that, you have a way to load all you SMTP configuration directly from your appSettings. Isn&#39;t that great? Assuming you&#39;re using Azure DevOps to build and deploy your solutions, you can now easily leverage the variable substitution to change your SMTP settings just before sending the package to Optimizely for a deployment.&lt;/p&gt;
&lt;p&gt;Another thing to point out, as you can see, you can add multiple config builders. We do that on our side, only for our development environments, to protect any sensitive information, by using the AzureKeyVaultConfigBuilder. This setup can work in pair, meaning that, say, you have both AzureKeyVaultConfigBuilder and the SmtpConfigurationBuilder, you can use the first config builder to load the secrets from your key vault, then ask your SMTP builder to load these for your SMTP configuration. It can be extremely useful for a lot of different applications.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2024-01-21T05:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Content Cloud CMS Apps (Add-ons) - Tips &amp; Tricks</title><link href="https://www.davidhome.net/blog/optimizely-content-cloud-cms-apps-add-ons-tips-tricks" /><id>&lt;p&gt;Developing an Optimizely CMS Add-on can be a bit tricky. There is a lot of advantages of doing it, but building it right can be a bit tedious. You have probably already seen the following &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/developing-add-ons&quot;&gt;documentation page&lt;/a&gt; about the subject itself, but it feels like a lot of details are missing to make it right. You will also quickly realize there is a lot of historical elements which makes the process a bit more complicated/confusing. In this blog, we will unravel everything so that you can successfully build yours!&lt;/p&gt;
&lt;p&gt;I will be assuming that your project is currently using .NET 6.0, but the process is the same if the target framework changes. You can also add conditional dependencies based on the target framework, which then the command &lt;code&gt;dotnet pack&lt;/code&gt; will automatically handle when bundling your library. I will also assume that you are already mastering how packing your library to .nupkg file(s) works.&lt;/p&gt;
&lt;h2&gt;1. Adjustements inside the project file&lt;/h2&gt;
&lt;p&gt;First of all, create a new solution with a library project using your preferred IDE, then edit the .csproj file. Your file should look like the following in the end:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;Project Sdk=&quot;Microsoft.NET.Sdk.Razor&quot;&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;TargetFramework&amp;gt;net6.0&amp;lt;/TargetFramework&amp;gt;
    &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
    &amp;lt;ImplicitUsings&amp;gt;enable&amp;lt;/ImplicitUsings&amp;gt;
    &amp;lt;AddRazorSupportForMvc&amp;gt;true&amp;lt;/AddRazorSupportForMvc&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;

  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;FrameworkReference Include=&quot;Microsoft.AspNetCore.App&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
  
  &amp;lt;ItemGroup&amp;gt;
		&amp;lt;!--    These are package example. Install those you need. --&amp;gt;
    &amp;lt;PackageReference Include=&quot;EPiServer.CMS.AspNetCore.Mvc&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;EPiServer.CMS.Core&quot; /&amp;gt;
    &amp;lt;PackageReference Include=&quot;EPiServer.CMS.UI.Core&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;

  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;Content Remove=&quot;module.config&quot; /&amp;gt;
    &amp;lt;None Include=&quot;module.config&quot;&amp;gt;
      &amp;lt;CopyToOutputDirectory&amp;gt;Never&amp;lt;/CopyToOutputDirectory&amp;gt;
    &amp;lt;/None&amp;gt;
    &amp;lt;Content Remove=&quot;packages.lock.json&quot; /&amp;gt;
    &amp;lt;None Include=&quot;packages.lock.json&quot;&amp;gt;
      &amp;lt;CopyToOutputDirectory&amp;gt;Never&amp;lt;/CopyToOutputDirectory&amp;gt;
    &amp;lt;/None&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can also realize, I&#39;m using &lt;a href=&quot;https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management&quot;&gt;CPM&lt;/a&gt;. This will greatly simplify the dependency management along the way.&lt;/p&gt;
&lt;p&gt;Couple of things to highlight here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Super important to change the SDK attribute on the &quot;Project&quot; node to &quot;Microsoft.NET.Sdk.Razor&quot;.&lt;/li&gt;
&lt;li&gt;Inside the first PropertyGroup node, add AddRazorSupportForMvc and set it &quot;true&quot;.&lt;/li&gt;
&lt;li&gt;Make sure to add a FrameworkReference node which will be including the Microsoft.AspNetCore.App reference. Necessary for having the ASP.NET Core web references, which normally the SDK Microsoft.NET.Sdk.Web includes by default.&lt;/li&gt;
&lt;li&gt;The last, but not least, make sure to remove &amp;amp; ignore any files you want to exclude from your .nupkg file. In our example, and you will probably need it too, we want to prevent both &quot;module.config&quot; and &quot;packages.lock.json&quot; file to be inside the file package.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. Custom MSBuild instructions&lt;/h2&gt;
&lt;p&gt;There is a couple of adjustments to make so that MSBuild is helping up ease the bundling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Assuming your projects are hierarchically structured in the following directory pattern: [root]/src/projectname/projectname.csproj, create a Directory.Build.props file in the &quot;src&quot; folder with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;Project&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;NoWarn&amp;gt;NU1507&amp;lt;/NoWarn&amp;gt;
    &amp;lt;RestorePackagesWithLockFile&amp;gt;True&amp;lt;/RestorePackagesWithLockFile&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Assuming the same structure, create under the &quot;src&quot; folder the file Directory.Packages.props with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;Project&amp;gt;
  &amp;lt;PropertyGroup&amp;gt;
    &amp;lt;ManagePackageVersionsCentrally&amp;gt;true&amp;lt;/ManagePackageVersionsCentrally&amp;gt;
    &amp;lt;CentralPackageTransitivePinningEnabled&amp;gt;true&amp;lt;/CentralPackageTransitivePinningEnabled&amp;gt;
  &amp;lt;/PropertyGroup&amp;gt;
  &amp;lt;ItemGroup&amp;gt;
		&amp;lt;!--    These are package example. Install those you need. --&amp;gt;
    &amp;lt;PackageVersion Include=&quot;EPiServer.CMS.AspNetCore.Mvc&quot; Version=&quot;[12.4.0, 13.0.0)&quot; /&amp;gt;
    &amp;lt;PackageVersion Include=&quot;EPiServer.CMS.Core&quot; Version=&quot;[12.4.0, 13.0.0)&quot; /&amp;gt;
    &amp;lt;PackageVersion Include=&quot;EPiServer.CMS.UI.Core&quot; Version=&quot;[12.4.0, 13.0.0)&quot; /&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Inside the project folder, where the .csproj file resides, create a new file entitled Directory.Build.props with the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&amp;gt;
&amp;lt;Project xmlns=&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&amp;gt;
    &amp;lt;ItemGroup&amp;gt;
        &amp;lt;ClientResources Include=&quot;$(ProjectDir)ClientResources\**\*&quot;/&amp;gt;
    &amp;lt;/ItemGroup&amp;gt;
    &amp;lt;PropertyGroup&amp;gt;
        &amp;lt;TmpOutDir&amp;gt;$([System.IO.Path]::Combine($(ProjectDir), &#39;tmp&#39;))&amp;lt;/TmpOutDir&amp;gt;
        &amp;lt;NoWarn&amp;gt;NU1507&amp;lt;/NoWarn&amp;gt;
        &amp;lt;RestorePackagesWithLockFile&amp;gt;True&amp;lt;/RestorePackagesWithLockFile&amp;gt;
    &amp;lt;/PropertyGroup&amp;gt;
    &amp;lt;ItemGroup&amp;gt;
        &amp;lt;Content Include=&quot;$(MSBuildProjectName).zip&quot;&amp;gt;
            &amp;lt;Pack&amp;gt;true&amp;lt;/Pack&amp;gt;
            &amp;lt;PackagePath&amp;gt;contentFiles\any\any\modules\_protected\$(MSBuildProjectName)&amp;lt;/PackagePath&amp;gt;
            &amp;lt;BuildAction&amp;gt;None&amp;lt;/BuildAction&amp;gt;
            &amp;lt;PackageCopyToOutput&amp;gt;true&amp;lt;/PackageCopyToOutput&amp;gt;
        &amp;lt;/Content&amp;gt;
        &amp;lt;Content Include=&quot;msbuild\CopyZipFiles.targets&quot; &amp;gt;
            &amp;lt;Pack&amp;gt;true&amp;lt;/Pack&amp;gt;
            &amp;lt;PackagePath&amp;gt;build\$(MSBuildProjectName).targets&amp;lt;/PackagePath&amp;gt;
        &amp;lt;/Content&amp;gt;
    &amp;lt;/ItemGroup&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Still in the project folder, create the file Directory.Build.targets and add the following content:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&amp;gt;
&amp;lt;Project xmlns=&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot;&amp;gt;
  &amp;lt;Target Name=&quot;CreateCmsAddOnZip&quot; BeforeTargets=&quot;Build&quot;&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;$(ProjectDir)module.config&quot; DestinationFolder=&quot;$(TmpOutDir)\content&quot;/&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;@(ClientResources)&quot; DestinationFiles=&quot;@(ClientResources -&amp;gt; &#39;$(TmpOutDir)\content\$(PackageVersion)\ClientResources\%(RecursiveDir)%(Filename)%(Extension)&#39;)&quot;/&amp;gt;

    &amp;lt;!-- Update the module config with the version information --&amp;gt;
    &amp;lt;XmlPoke XmlInputPath=&quot;$(TmpOutDir)\content\module.config&quot; Query=&quot;/module/@clientResourceRelativePath&quot; Value=&quot;$(PackageVersion)&quot;/&amp;gt;
  &amp;lt;/Target&amp;gt;
  &amp;lt;Target Name=&quot;ZipClientResources&quot; BeforeTargets=&quot;Build&quot; AfterTargets=&quot;CreateCmsAddOnZip&quot; DependsOnTargets=&quot;CreateCmsAddOnZip&quot;&amp;gt;
    &amp;lt;ZipDirectory SourceDirectory=&quot;$(TmpOutDir)\content&quot; DestinationFile=&quot;$(ProjectDir)$(MSBuildProjectName).zip&quot; Overwrite=&quot;true&quot;/&amp;gt;
  &amp;lt;/Target&amp;gt;
  &amp;lt;Target Name=&quot;CleanupTmpOutDir&quot; BeforeTargets=&quot;Build&quot; AfterTargets=&quot;ZipClientResources&quot; DependsOnTargets=&quot;ZipClientResources&quot;&amp;gt;
    &amp;lt;RemoveDir Directories=&quot;$(TmpOutDir)&quot;/&amp;gt;
  &amp;lt;/Target&amp;gt;
&amp;lt;/Project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;You will also have to add the file CopyZipFiles.targets to a new &quot;msbuild&quot; folder under the root of the project directory:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;Project xmlns=&quot;http://schemas.microsoft.com/developer/msbuild/2003&quot; ToolsVersion=&quot;4.0&quot;&amp;gt;
  &amp;lt;ItemGroup&amp;gt;
    &amp;lt;CmsAddOnZips Include=&quot;$(MSBuildThisFileDirectory)..\contentFiles\any\any\modules\_protected\**\*.zip&quot;/&amp;gt;
  &amp;lt;/ItemGroup&amp;gt;

  &amp;lt;Target Name=&quot;CopyCmsAddOnZip&quot; BeforeTargets=&quot;Build&quot;&amp;gt;
    &amp;lt;Copy SourceFiles=&quot;@(CmsAddOnZips)&quot; DestinationFolder=&quot;$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)&quot;/&amp;gt;
  &amp;lt;/Target&amp;gt;
&amp;lt;/Project&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To summarize, these customizations will do the following to your project each time you will be compiling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adds CPM, as previously mentioned.&lt;/li&gt;
&lt;li&gt;Adds NuGet lock files, super useful for your DevOps pipeline.&lt;/li&gt;
&lt;li&gt;Automatically compile views.&lt;/li&gt;
&lt;li&gt;Automatically generates module the zip file with all necessary elements in it that will be packaged within your .nupkg file.
&lt;ul&gt;
&lt;li&gt;This file contains the recommended structure &amp;amp; content that you can find in the &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/developing-add-ons&quot;&gt;Optimizely CMS addon documentation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Minimum required configuration&lt;/h2&gt;
&lt;p&gt;See the &quot;module.config&quot; file like the definition of your addon. Without it, Optimizely will use the default values from the class ShellModuleManifest, which unfortunately omit a very important detail; The assembly’s name of your addon. Without it, Optimizely won&#39;t be able to load yours at boot.
Create it where the .csproj file resides with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;module loadFromBin=&quot;false&quot; name=&quot;Your.Assembly.Name&quot; viewEngine=&quot;Razor&quot; clientResourceRelativePath=&quot;$version$&quot; tags=&quot;EPiServerModulePackage&quot;&amp;gt;
  &amp;lt;assemblies&amp;gt;
		&amp;lt;!-- Change the assembly name with yours --&amp;gt;
    &amp;lt;add assembly=&quot;Your.Assembly.Name&quot; /&amp;gt;
  &amp;lt;/assemblies&amp;gt;
  &amp;lt;clientModule&amp;gt;
    &amp;lt;moduleDependencies&amp;gt;
			&amp;lt;!-- Adjust accordingly --&amp;gt;
      &amp;lt;add dependency=&quot;CMS&quot; type=&quot;RunAfter&quot; /&amp;gt;
    &amp;lt;/moduleDependencies&amp;gt;
  &amp;lt;/clientModule&amp;gt;
&amp;lt;/module&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add an extension method on the interface &quot;IServiceCollection&quot; and add at least the following piece of code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs&quot;&gt;public static IServiceCollection AddMyAddon(this IServiceCollection services)
    {
        // Add services here.
        return services
                // Super required, otherwise your addon won&#39;t load when the site loads.
            .Configure&amp;lt;ProtectedModuleOptions&amp;gt;(
            pm =&amp;gt;
            {
                if (!pm.Items.Any(i =&amp;gt; i.Name.Equals(ModuleName, StringComparison.OrdinalIgnoreCase)))
                {
                    pm.Items.Add(new ModuleDetails { Name = ModuleName });
                }
            });
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Controllers &amp;amp; Views&lt;/h2&gt;
&lt;p&gt;Probably the most undocumented/unclear part for Optimizely CMS addons. The instructions around views doesn&#39;t exactly explain how they can be used or how the routing works within your library. If you&#39;re looking at existing addons, e.g., &lt;a href=&quot;https://github.com/Geta/geta-notfoundhandler&quot;&gt;Geta.NotFoundHandler&lt;/a&gt;, the majority of developers are handling it slightly differently. &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/migrating-add-ons-to-net-5&quot;&gt;This page&lt;/a&gt; explains how to structure your files in a manner that the module will automatically include them for you, but I was unable to make it work. Maybe because it should be exclusively a &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-8.0&amp;amp;tabs=visual-studio&quot;&gt;razor page&lt;/a&gt; and not a simple razor view dependent on a Controller. Fortunately, you can make it work with a controller, but with a little additional tweaking:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-8.0#custom-route-attributes-using-iroutetemplateprovider&quot;&gt;Route Attribute&lt;/a&gt;. We will use it to customize the routes to our controllers. Here&#39;s an example:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs&quot;&gt;using EPiServer.Shell;
using Microsoft.AspNetCore.Mvc.Routing;

namespace Playground.Mvc;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class ModuleRoute : Attribute, IRouteTemplateProvider
{
    private readonly string _controllerName;
    private readonly string _actionName;

    public string Template =&amp;gt; Paths.ToResource(typeof(ModuleRoute), $&quot;{_controllerName}/{_actionName}&quot;);
    public int? Order { get; set; } = 0;
    public string Name { get; set; }

    public ModuleRoute(string controllerName, string actionName)
    {
        _controllerName = controllerName;
        _actionName = actionName;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Decorate your controller actions with your newly created route attribute. e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs&quot;&gt;    [HttpGet]
    [ModuleRoute(&quot;Default&quot;, &quot;Index&quot;)]
    public IActionResult Index()
    {
        return View();
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Create a new menu provider class including all paths to your actions in your addons. e.g.:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cs&quot;&gt;using EPiServer.Framework.Localization;
using EPiServer.Shell;
using EPiServer.Shell.Navigation;
using Playground.Controllers;

namespace Playground.Optimizely;

[MenuProvider]
public class AddonMenuProvider : IMenuProvider
{
    private readonly LocalizationService _localizationService;

    public AddonMenuProvider(LocalizationService localizationService)
    {
        _localizationService = localizationService;
    }

    public IEnumerable&amp;lt;MenuItem&amp;gt; GetMenuItems()
    {
        yield return new UrlMenuItem(_localizationService.GetString(&quot;/myaddon/gadget/title&quot;, &quot;My Addon&quot;), &quot;/global/cms/myaddon&quot;,
            Paths.ToResource(GetType(), $&quot;Default/{nameof(DefaultController.Index)}&quot;))
        {
            SortIndex = 0,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };

        yield return new UrlMenuItem(_localizationService.GetString(&quot;/myaddon/index/menu&quot;, &quot;Home&quot;), &quot;/global/cms/myaddon/index&quot;,
            Paths.ToResource(GetType(), $&quot;Default/{nameof(DefaultController.Index)}&quot;))
        {
            SortIndex = 10,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };

        yield return new UrlMenuItem(_localizationService.GetString(&quot;/myaddon/secondaction/menu&quot;, &quot;Second Action&quot;), &quot;/global/cms/myaddon/secondaction&quot;,
            Paths.ToResource(GetType(), $&quot;Default/{nameof(DefaultController.SecondAction)}&quot;))
        {
            SortIndex = 20,
            Alignment = 0,
            IsAvailable = _ =&amp;gt; true
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By doing so, you are generating routes which will point directly on your addon controller. Say by example you have the &quot;DefaultController&quot; class with the &quot;Index&quot; action, well then, the action under your browser should look as the following: ~/EPiServer/MyAddon/Default/Index. This also convenientely allow you to use razor helpers such as Html.BeginForm, since the MVC routing system knows these belongs to your addon with a custom template.&lt;/p&gt;
&lt;p&gt;It is very important to respect the actions defined within your IMenuProvider implementation, otherwise the custom routing attribute won&#39;t be working just right. As you can see, a certain minimum level of structure has been established in this file, which makes actions from the controller to work correctly. The third parameter of the constructor of UrlMenuItem is exactly where the magic happens. The recommendation is indeed to keep using the helper, Paths.ToResource and add the additional segment manually. This consequently creates the same path as previously described in the last paragraph and will match it with the available controller action. A menu item is essentially an element that will appear under the Backoffice, when navigating under ~/EPiServer.&lt;/p&gt;
&lt;p&gt;I hope this will be super helpful to people reading this blog! I will try to publish an example starter code that will basically cover every minimal aspect of an Optimizely CMS addon and edit this post when it will be ready.&lt;/p&gt;
</id><updated>2024-01-18T03:07:42.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>IDX21323 - RequireNonce is true, Nonce was null</title><link href="https://www.davidhome.net/blog/idx21323-requirenonce-is-true-nonce-was-null" /><id>&lt;p&gt;We have multiple clients configured with Azure Active Directory (Microsoft Entra) for requiring authentication when accessing their website. The majority of them is only for protecting the backoffice (CMS admin), but we have certain of them that uses it for protecting the whole site.&lt;/p&gt;
&lt;p&gt;There is &lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/integrate-azure-ad-using-openid-connect&quot;&gt;existing Optimizely documentation&lt;/a&gt; that allow you to setup your site to use AAD authentication, but it doesn&#39;t talks about the &lt;em&gt;famous&lt;/em&gt; issue certain people are having when signing in to the website; &quot;IDX21323 - RequireNonce is true, Nonce was null&quot;. I might come with a surprise, but this issue will be occurring systematically under all DXP environments if you do not ask Optimizely to change a setting under Cloudflare for you. The problem is described there: &lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/7492181419789-Nonce-Error-Using-OpenIdConnect-for-Authentication-with-DXC&quot;&gt;https://support.optimizely.com/hc/en-us/articles/7492181419789-Nonce-Error-Using-OpenIdConnect-for-Authentication-with-DXC&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The solution though is something that doesn&#39;t seems to ring a bell for certain people. As of the time of writing, it says &quot;to bypass the origin cache control&quot;, but in contrary, the mentioned page rule is &lt;strong&gt;enabling&lt;/strong&gt; Cloudflare to respect the origin cache control header. You see, it is &lt;a href=&quot;https://community.cloudflare.com/t/cloudflare-dont-respect-origin-cache-control-header/244056&quot;&gt;not entirely respecting cache-control directive from the origin server, &lt;strong&gt;by default&lt;/strong&gt;&lt;/a&gt;. Organizations with an enterprise plan with Cloudflare are all having the rule disabled by default. Something which is hard to understand as Cloudflare&#39;s documentation doesn&#39;t really give up insights on that matter except in the previous thread. It means that even if you&#39;ve decided to put any custom cache-control directives within your application, Cloudflare &lt;a href=&quot;https://developers.cloudflare.com/cache/about/cache-control/#origin-cache-control-behavior&quot;&gt;will ignore some of them or will behave differently&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This default setup can cause a couple of different issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Signing in under certain URLs/paths, the Nonce cookie isn&#39;t setup during the authentication, thus causing the IDX21323 issue.&lt;/li&gt;
&lt;li&gt;Leaving certain assets &quot;publicly&quot; available after someone has loaded it once, even though it should require authentication, because it isn&#39;t validating it once the Edge has cached the asset.&lt;/li&gt;
&lt;li&gt;Breaking images because the Edge has cached the authentication redirection action, a http 302 status code.&lt;/li&gt;
&lt;li&gt;Preventing your application to set request cookies. Cloudflare will sometimes, under specific scenarios, completely discard the cookie before sending the response to the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don&#39;t get me wrong, caching is important, but you need to adapt certain rules depending on what your website is. For intranet sites by example, it&#39;s important that the caching doesn&#39;t break the experience nor expose certain privately accessible assets. There&#39;s a couple of tricks you can apply right up front, by example, when using the extension method &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect?view=aspnetcore-6.0#microsoft-extensions-dependencyinjection-openidconnectextensions-addopenidconnect(microsoft-aspnetcore-authentication-authenticationbuilder-system-action((microsoft-aspnetcore-authentication-openidconnect-openidconnectoptions)))&quot;&gt;AddOpenIdConnect&lt;/a&gt;, you can control the Cache-Control header &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onredirecttoidentityprovider?view=aspnetcore-6.0#microsoft-aspnetcore-authentication-openidconnect-openidconnectevents-onredirecttoidentityprovider&quot;&gt;only when redirecting to the authentication provider&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All in all, though, take a good attention of how your website is behaving when hosted under Optimizely DXP, especially when you are protecting certain areas, more than the CMS backoffice itself. Normally any Optimizely CMS solution will include right out of the box optimized cache control headers, especially for assets, but keep in mind that Cloudflare will react differently with them since the page rule &quot;Origin Cache-Control&quot; is not enabled by default (for enterprise customers only).&lt;/p&gt;
</id><updated>2023-10-01T16:07:53.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>IDX21323 - RequireNonce is true, Nonce was null</title><link href="https://www.davidhome.net/blog/idx21323-requirenonce-is-true-nonce-was-null/" /><id>&lt;div&gt;&lt;div&gt;
&lt;div&gt;&lt;p&gt;We have multiple clients configured with Azure Active Directory (Microsoft Entra) for requiring authentication when accessing their website. The majority of them is only for protecting the backoffice (CMS admin), but we have certain of them that uses it for protecting the whole site.&lt;/p&gt;
&lt;p&gt;There is&amp;nbsp;&lt;a href=&quot;https://docs.developers.optimizely.com/content-management-system/docs/integrate-azure-ad-using-openid-connect&quot;&gt;existing Optimizely documentation&lt;/a&gt;&amp;nbsp;that allow you to setup your site to use AAD authentication, but it doesn&#39;t talks about the&amp;nbsp;&lt;em&gt;famous&lt;/em&gt;&amp;nbsp;issue certain people are having when signing in to the website; &quot;IDX21323 - RequireNonce is true, Nonce was null&quot;. I might come with a surprise, but this issue will be occurring systematically under all DXP environments if you do not ask Optimizely to change a setting under Cloudflare for you. The problem is described there:&amp;nbsp;&lt;a href=&quot;https://support.optimizely.com/hc/en-us/articles/7492181419789-Nonce-Error-Using-OpenIdConnect-for-Authentication-with-DXC&quot;&gt;https://support.optimizely.com/hc/en-us/articles/7492181419789-Nonce-Error-Using-OpenIdConnect-for-Authentication-with-DXC&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The solution though is something that doesn&#39;t seems to ring a bell for certain people. As of the time of writing, it says &quot;to bypass the origin cache control&quot;, but in contrary, the mentioned page rule is&amp;nbsp;&lt;strong&gt;enabling&lt;/strong&gt;&amp;nbsp;Cloudflare to respect the origin cache control header. You see, it is&amp;nbsp;&lt;a href=&quot;https://community.cloudflare.com/t/cloudflare-dont-respect-origin-cache-control-header/244056&quot;&gt;not entirely respecting cache-control directive from the origin server,&amp;nbsp;&lt;strong&gt;by default&lt;/strong&gt;&lt;/a&gt;. Organizations with an enterprise plan with Cloudflare are all having the rule disabled by default. Something which is hard to understand as Cloudflare&#39;s documentation doesn&#39;t really give up insights on that matter except in the previous thread. It means that even if you&#39;ve decided to put any custom cache-control directives within your application, Cloudflare&amp;nbsp;&lt;a href=&quot;https://developers.cloudflare.com/cache/about/cache-control/#origin-cache-control-behavior&quot;&gt;will ignore some of them or will behave differently&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This default setup can cause a couple of different issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Signing in under certain URLs/paths, the Nonce cookie isn&#39;t setup during the authentication, thus causing the IDX21323 issue.&lt;/li&gt;
&lt;li&gt;Leaving certain assets &quot;publicly&quot; available after someone has loaded it once, even though it should require authentication, because it isn&#39;t validating it once the Edge has cached the asset.&lt;/li&gt;
&lt;li&gt;Breaking images because the Edge has cached the authentication redirection action, a http 302 status code.&lt;/li&gt;
&lt;li&gt;Preventing your application to set request cookies. Cloudflare will sometimes, under specific scenarios, completely discard the cookie before sending the response to the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don&#39;t get me wrong, caching is important, but you need to adapt certain rules depending on what your website is. For intranet sites by example, it&#39;s important that the caching doesn&#39;t break the experience nor expose certain privately accessible assets. There&#39;s a couple of tricks you can apply right up front, by example, when using the extension method&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.openidconnectextensions.addopenidconnect?view=aspnetcore-6.0#microsoft-extensions-dependencyinjection-openidconnectextensions-addopenidconnect(microsoft-aspnetcore-authentication-authenticationbuilder-system-action((microsoft-aspnetcore-authentication-openidconnect-openidconnectoptions)))&quot;&gt;AddOpenIdConnect&lt;/a&gt;, you can control the Cache-Control header&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents.onredirecttoidentityprovider?view=aspnetcore-6.0#microsoft-aspnetcore-authentication-openidconnect-openidconnectevents-onredirecttoidentityprovider&quot;&gt;only when redirecting to the authentication provider&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All in all, though, take a good attention of how your website is behaving when hosted under Optimizely DXP, especially when you are protecting certain areas, more than the CMS backoffice itself. Normally any Optimizely CMS solution will include right out of the box optimized cache control headers, especially for assets, but keep in mind that Cloudflare will react differently with them since the page rule &quot;Origin Cache-Control&quot; is not enabled by default (for enterprise customers only).&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</id><updated>2023-10-01T04:00:00.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>