<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by HTTP 418</title> <link>https://world.optimizely.com/blogs/http-418/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Dialing-in Your AI with Optimizely Feature Experimentation</title>            <link>https://blog.http418.dev/2023/11/dialing-in-your-ai-with-optimizely.html</link>            <description>&lt;p&gt;If you&#39;re in technology, AIs are everywhere nowadays. At work, I use an AI to help me make proofs of concept, sometimes in communicating with other AIs. At home, I use Google&#39;s Bard to help with those day-to-day mental chores such as meal planning. Even at play, when indulging in some Gran Turismo there&#39;s an AI to &lt;a href=&quot;https://www.gran-turismo.com/us/gran-turismo-sophy/&quot;&gt;race against&lt;/a&gt;. I can&#39;t do anything about the AIs I use more casually (aside from being aware of them). At work though, I have some control! When interfacing with an AI through an API, you&#39;re given the opportunity to tweak parameters or try the same query with different contexts before getting a response from your AI. And what better to help judge your user engagement with an AI than Optimizely&#39;s Feature Experimentation!&lt;/p&gt;&lt;p&gt;Here I&#39;ll be working with OpenAI&#39;s API along with their client package from nuget in order to simplify some rather ugly setup. I&#39;ll include an HttpClient version of the call at the end of the post so you can see the details involved.&amp;nbsp;&lt;/p&gt;&lt;p&gt;In this demo, I&#39;ll be switching the AI model that my application uses, but this approach can be used on any of the parameters sent to the AI. Your basic OpenAI API call follows this format:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;&lt;code&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; client = new OpenAIAPI(apiKey);&lt;/p&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var parameters = new CompletionRequest&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Model = &quot;gpt-3_5-turbo&quot;,&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Prompt = promptText,&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Temperature = 0.7,&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; MaxTokens = 100&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; };&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var response = await client.Completions.CreateCompletionAsync(parameters);&lt;/p&gt;&lt;p&gt;&lt;br /&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; generatedText = response.Completions[0].Text;&lt;br /&gt;&lt;/p&gt;&lt;/code&gt;&lt;br /&gt;This should look familiar to any developer that&#39;s used a client wrapper for an API. The connection details are handled by the client, but the business-end of the of request is the CompletionRequest parameters. Here&#39;s we&#39;re able to select the AI model we want to query against, as well as supply the context/prompt, as well as other details relevant to the AI setup.&amp;nbsp;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;But what if we aren&#39;t sure that the model is the best fit for our purposes? Wouldn&#39;t it be nice to try out a couple different models and track metrics on them? Here&#39;s Feature Experimentation to help with that!&amp;nbsp;&lt;br /&gt;&lt;br /&gt;What I&#39;ve done is set up an experiment that either sends the user&#39;s query to ChatGPT 3.5 or the Davinci model:&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvN6A72RuMkx1_w_Bi5SyvyjYNdWO8Rbzty_RGhqiaz7IGnsiFW3ClRHBK5_mDRwMTTVQch8sgz9FCp7dDB3o_qnJlvNGoqZLwnV1LQW4rhDnf9lwxrOKrHge_hgONpRjqvdHdEokM3K5CNjaWqK2QegI-gRxF-XPIbj9nk358TmxDoOTrKkHVTaR8MrNu/s1101/Screenshot%202023-11-13%20095416.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1101&quot; data-original-width=&quot;673&quot; height=&quot;642&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvN6A72RuMkx1_w_Bi5SyvyjYNdWO8Rbzty_RGhqiaz7IGnsiFW3ClRHBK5_mDRwMTTVQch8sgz9FCp7dDB3o_qnJlvNGoqZLwnV1LQW4rhDnf9lwxrOKrHge_hgONpRjqvdHdEokM3K5CNjaWqK2QegI-gRxF-XPIbj9nk358TmxDoOTrKkHVTaR8MrNu/w393-h642/Screenshot%202023-11-13%20095416.png&quot; width=&quot;393&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;br /&gt;&lt;/div&gt;From there, we configure the Optimizely client back in our solution:&lt;div&gt;&lt;br /&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; optimizely = OptimizelyFactory.NewDefaultInstance(Configuration.GetValue&amp;lt;string&amp;gt;(&quot;Optimizely:SdkKey&quot;));&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; user = optimizely.CreateUserContext(numGen.Next().ToString());&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; user.TrackEvent(&quot;Engaged_AI&quot;);&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/code&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Now we can make our decisions on whether the user gets to interact with the chosen AI and which model they&#39;re conversing with, using Optimizely&#39;s Feature Experimentation.:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;var decision = user.Decide(&quot;ai_model&quot;);&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;if (decision.Enabled)&lt;/div&gt;&lt;div&gt;{&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; var ai_model = decision.VariationKey;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; var parameters = new CompletionRequest&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Model = ai_model,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Prompt = promptText,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Temperature = 0.7,&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; MaxTokens = 100&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; };&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; var response = await client.Completions.CreateCompletionAsync(parameters);&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; generatedText = response.Completions[0].Text;&lt;/div&gt;&lt;div&gt;}&lt;/div&gt;&lt;div&gt;else{&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; generatedText = &quot;AI is not enabled for this user&quot;;&lt;/div&gt;&lt;div&gt;}&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;According to the experiment I&#39;ve set up, 90% of my users will have the opportunity to interact with the AI, if they do their query will either be sent to ChatGPT 3.5 or Davinici and I&#39;ll be able to judge the level of engagement between my users and my AI.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Here is also where you can add more context to the prompt to give the AI a better understanding of the query. This could help your AI understand its starting point, such as &quot;you are a customer service agent for company A.&quot;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Now as you introduce AI into your applications, you can see how both Feature Flagging and Feature Experimentation can help you evaluate your approach to AI.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://blog.http418.dev/2023/11/dialing-in-your-ai-with-optimizely.html</guid>            <pubDate>Mon, 13 Nov 2023 17:17:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Databases from the Future: What is Database Version 954?</title>            <link>https://blog.http418.dev/2023/10/databases-from-future-what-is-database.html</link>            <description>&lt;p&gt;&amp;nbsp;Have you started up a new Optimizely site, and prepared your database only get to this error message?&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTkqte6WNmKYWmMHDdDPLkVmDiAVfU9AQY8RbJn6WqonzRI4FVdjGh3oaeXmLPglay0B5dKgOK2XOVvQwYok7kTNQC1RR7LyeJORDN-NAMHGwXM8sc-oF6NRW3HEhyJ63vFm8B13JmuwaSXCDNoEvdGc-myXuS4lNYNDF4DT5TVd7TsBLEC-LAnajrqWb/s2214/New%20Bitmap%20Image.bmp&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;336&quot; data-original-width=&quot;2214&quot; height=&quot;61&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwTkqte6WNmKYWmMHDdDPLkVmDiAVfU9AQY8RbJn6WqonzRI4FVdjGh3oaeXmLPglay0B5dKgOK2XOVvQwYok7kTNQC1RR7LyeJORDN-NAMHGwXM8sc-oF6NRW3HEhyJ63vFm8B13JmuwaSXCDNoEvdGc-myXuS4lNYNDF4DT5TVd7TsBLEC-LAnajrqWb/w619-h61/New%20Bitmap%20Image.bmp&quot; width=&quot;619&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;p&gt;&lt;br /&gt;Reading the error message itself, you&#39;ll see that the created DB version is at 957 where the current version of Optimizely, at the time of this writing, is 904.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Where did this come from?&lt;br /&gt;How do I put my database in a time machine and bring it back to the present?&lt;br /&gt;&lt;br /&gt;The good news on both parts of this manageable with a visit from Dr. Emmett Brown. The first question: &quot;Where did this come from?&quot; is a versioning issue. SQL Server 2022 (v16) will create future database if you simply run your database creation against a SQL 2022 instance. This will occur if your default instance is SQL 2022 regardless of how you go about generating your database.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;So, that&#39;s how it happens, now how do you create a database that is ready now, instead of a database from the far future? This is where Visual Studio and SQL Server Object Explorer come in handy. Visual Studio installs a database instance for itself, which you can see when you pull up the SQL Server Object Explorer:&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4XNCqrUn76DJ2Xusn_V5qD3gybqzsaE1hnT-5-ryM2O-OgALxUjfMLemzRKJ6aGNTb6lPSIPKkQbmQ3gpZ5seQiem4EzKff0TX5JvtBqA9pJWZyyMU-Ex2YDcO9H8_L0kIMisbVu650MqubwjNMgTnemhKnbzWlLSzTKwSLgqLOh4FJUqMMC_vNkhZtiZ/s997/VsInstance.jpg&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;428&quot; data-original-width=&quot;997&quot; height=&quot;211&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4XNCqrUn76DJ2Xusn_V5qD3gybqzsaE1hnT-5-ryM2O-OgALxUjfMLemzRKJ6aGNTb6lPSIPKkQbmQ3gpZ5seQiem4EzKff0TX5JvtBqA9pJWZyyMU-Ex2YDcO9H8_L0kIMisbVu650MqubwjNMgTnemhKnbzWlLSzTKwSLgqLOh4FJUqMMC_vNkhZtiZ/w493-h211/VsInstance.jpg&quot; width=&quot;493&quot; /&gt;&lt;/a&gt;&lt;/div&gt;You can already see from the screenshot that this is a SQL 2019 instance as it is v15. Right clicking this instance and selecting New Query will open up a new SQL command tab. Here it is a simple matter of running your creation script to create your blank database so Optimizely can configure it on startup.&lt;br /&gt;&lt;p&gt;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgG9G6db-UOaBHqkFZvW71OFo5gHthUMDEZxap7VsakP5jzq-s-pikYyVFMbzn8Lz6CJcehUGHDD7ws5Exn94jgsquJBelWKH2gfaDi1PdwJKh1s5I1p_NwEJAon9c59Jqbjq7AAkA0BR4JAeYTjygrumyBZtf0m2U1ThScRDbltqpw8JfJnLccVrrwD2Sn/s2920/SqlCommand.jpg&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;210&quot; data-original-width=&quot;2920&quot; height=&quot;47&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgG9G6db-UOaBHqkFZvW71OFo5gHthUMDEZxap7VsakP5jzq-s-pikYyVFMbzn8Lz6CJcehUGHDD7ws5Exn94jgsquJBelWKH2gfaDi1PdwJKh1s5I1p_NwEJAon9c59Jqbjq7AAkA0BR4JAeYTjygrumyBZtf0m2U1ThScRDbltqpw8JfJnLccVrrwD2Sn/w651-h47/SqlCommand.jpg&quot; width=&quot;651&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;</description>            <guid>https://blog.http418.dev/2023/10/databases-from-future-what-is-database.html</guid>            <pubDate>Mon, 16 Oct 2023 17:08:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Fixing Missing Token in For This {0}</title>            <link>https://blog.http418.dev/2023/10/fixing-missing-token-in-for-this-0.html</link>            <description>&lt;p&gt;&amp;nbsp;I recently encountered an issue where my non-content blocks (site settings, categories, etc.) were producing a &quot;For This&quot; folder with a missing token. This missing token appeared in custom content, as well as add-ons like Geta Categories.&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFYu11l60UEu0Q04Uf28gN7-vN6fk0E0nHkuS3tEW7Zi5FSL1e4GkagYvQ7m8I9Ra35QS0Z33bwooksCAlwc1oVBLcwnM6vauTVSvd_njpgccNtNyId1icTUB0JOuLvJrwb72HKoObjiVDkNJif1l4ChPECUCG2qjyD9e8sxk995YkG_lbemSOxRl0Umq1/s448/MissingToken.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;179&quot; data-original-width=&quot;448&quot; height=&quot;205&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFYu11l60UEu0Q04Uf28gN7-vN6fk0E0nHkuS3tEW7Zi5FSL1e4GkagYvQ7m8I9Ra35QS0Z33bwooksCAlwc1oVBLcwnM6vauTVSvd_njpgccNtNyId1icTUB0JOuLvJrwb72HKoObjiVDkNJif1l4ChPECUCG2qjyD9e8sxk995YkG_lbemSOxRl0Umq1/w514-h205/MissingToken.png&quot; width=&quot;514&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;br /&gt;&lt;/div&gt;For custom content, two things are required to get a value into that empty token: an XML file under the lang folder, and UIDescriptor. In order to fix an add-on like Geta Categories (which already has the UIDescriptor baked-in), all that is needed is an XML file under the lang folder. This is a pretty simple XML which simply denotes the content type in question and the name you&#39;d like to give it. Like so:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;div&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; standalone=&quot;yes&quot;?&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;lt;languages&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;lt;language name=&quot;English&quot; id=&quot;en&quot;&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;lt;contenttypes&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;CategoryData&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;name&amp;gt;Working Category! &amp;lt;/name&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;/CategoryData&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;lt;/contenttypes&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;lt;/language&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;lt;/languages&amp;gt;&lt;/div&gt;&lt;/code&gt;&lt;p&gt;Simply adding the file for Geta Categories will resolve the issue there, since they&#39;ve already included the required UIDescriptor in the package. The result will look like this:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinBrW5mX4-ZtIkSCM0eKhGA5D6dXBMPpqq2q7Hmg0HjtceV6-qUSRhOvFOVUWFlZV67SWt345XttvHbI3Kj1-z5s_6vt0DUt4_GSnXnjpobz6BusKxk6HCv82FACb6yQW-hEeYOQyntFOjfp4E7JA9ICefkGEcm-nj3mtZ9y4ke-hn8R9ht1z-qgBmAJ7E/s590/CategoryFix.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;216&quot; data-original-width=&quot;590&quot; height=&quot;194&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinBrW5mX4-ZtIkSCM0eKhGA5D6dXBMPpqq2q7Hmg0HjtceV6-qUSRhOvFOVUWFlZV67SWt345XttvHbI3Kj1-z5s_6vt0DUt4_GSnXnjpobz6BusKxk6HCv82FACb6yQW-hEeYOQyntFOjfp4E7JA9ICefkGEcm-nj3mtZ9y4ke-hn8R9ht1z-qgBmAJ7E/w531-h194/CategoryFix.png&quot; width=&quot;531&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;For your own custom types, the already-mentioned UIDescriptor is also required. This is a simple bit of code to notify Optimizely what to fill in for what tokens. The XML can be expanded to include multiple types, each with their own user-defined name node.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;div&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; standalone=&quot;yes&quot;?&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;lt;languages&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;lt;language name=&quot;English&quot; id=&quot;en&quot;&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;lt;contenttypes&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;SettingsBase&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;name&amp;gt;Setting&amp;lt;/name&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;/SettingsBase&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;CategoryData&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;name&amp;gt;Category&amp;lt;/name&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;lt;/CategoryData&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;lt;/contenttypes&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;lt;/language&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;lt;/languages&amp;gt;&lt;/div&gt;&lt;/code&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;The UIDescriptor is few lines of code that inherit from the generic UIDescriptor&amp;lt;T&amp;gt; where T is the content type we want to target. As you can see in the XML the parent node of name should be the content type name so that Optimizely can pair it up correctly.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; [UIDescriptorRegistration]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; public class SettingsBlockFolderDescriptor : UIDescriptor&amp;lt;SettingsBase&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public SettingsBlockFolderDescriptor()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; IsPrimaryType = true;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;With both pieces in place, we&#39;ll now see our XML-defined name appear instead of the empty token, {0}:&lt;br /&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUOmjmsW06fxxfN1WkSo51pDa2wNeA7orKoJTfHpzM1RxzCJbeJVmbljowlYEYtgne6026MCsdBc1JRNvyiKuHl7t6AlLRPJQ2O8c1FuAU-58OrAmfvWzE4U-YuAlY4JqgvQ2UhqrTRWriAkpBh9WZBQh1udX6Bwc_ssy9ZXekZjnVV5xaaPHWZxc2EQQR/s584/AllFixed.png&quot; imageanchor=&quot;1&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;181&quot; data-original-width=&quot;584&quot; height=&quot;166&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUOmjmsW06fxxfN1WkSo51pDa2wNeA7orKoJTfHpzM1RxzCJbeJVmbljowlYEYtgne6026MCsdBc1JRNvyiKuHl7t6AlLRPJQ2O8c1FuAU-58OrAmfvWzE4U-YuAlY4JqgvQ2UhqrTRWriAkpBh9WZBQh1udX6Bwc_ssy9ZXekZjnVV5xaaPHWZxc2EQQR/w539-h166/AllFixed.png&quot; width=&quot;539&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;</description>            <guid>https://blog.http418.dev/2023/10/fixing-missing-token-in-for-this-0.html</guid>            <pubDate>Fri, 13 Oct 2023 16:07:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Is Your Postman Delivering Too Much? Postman Alternatives</title>            <link>https://blog.http418.dev/2023/02/is-your-postman-delivering-too-much.html</link>            <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;I&#39;ve been a long-time user of Postman, and I&#39;ve seen it grow throughout the years. All I need from Postman is a REST client to check my API endpoints. Recently though, as I opened Postman and had to click through three offers for services I will never use, I started thinking that I should take a survey of alternatives with less bloat and take them for a test drive.&amp;nbsp;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5_xQHcc9upa6S8pa1crgHJysiXILwJjgtBuQG0taRumyeUzDL_Tc7ux3Xtap0joWbXAwEwQ3YyaoI0ZFtm-xXX3oN1QtwUxNzvc8cwXAfS8BMcBgv12jDWI-lv9O-9QV8xiG8ofRBc8cpC-cn5uSRyIxvMNz2vUIFQGazKJQKylxDsW5fk8STNZdpRA/s1440/postman.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1440&quot; data-original-width=&quot;960&quot; height=&quot;320&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5_xQHcc9upa6S8pa1crgHJysiXILwJjgtBuQG0taRumyeUzDL_Tc7ux3Xtap0joWbXAwEwQ3YyaoI0ZFtm-xXX3oN1QtwUxNzvc8cwXAfS8BMcBgv12jDWI-lv9O-9QV8xiG8ofRBc8cpC-cn5uSRyIxvMNz2vUIFQGazKJQKylxDsW5fk8STNZdpRA/s320/postman.png&quot; width=&quot;213&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;Below are the services I found that serve the purpose of simply sending requests to an API among a few the other features that went into consideration were:&amp;nbsp;&lt;/p&gt;&lt;ul style=&quot;text-align: left;&quot;&gt;&lt;li&gt;Request History&lt;/li&gt;&lt;ul&gt;&lt;li&gt;The ability to recall previous requests was second only to sending requests in my priority list while evaluating clients.&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Post Response Testing&lt;/li&gt;&lt;ul&gt;&lt;li&gt;If doing repeated testing against an API having the ability to test the responses via JavaScript in the client can save a lot of time.&amp;nbsp;&lt;/li&gt;&lt;/ul&gt;&lt;li&gt;Collections&lt;/li&gt;&lt;ul&gt;&lt;li&gt;To go hand-in-hand with post-response testing, collections are a great way to organize requests if you&#39;re working with multiple APIs, such as I do as a consultant. In previous lives, I worked with only API, so in those cases collections become less important.&lt;/li&gt;&lt;/ul&gt;&lt;/ul&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;https://hoppscotch.io/&quot; target=&quot;_blank&quot;&gt;Hoppscotch.io&lt;/a&gt;&lt;br /&gt;&lt;/h2&gt;&lt;div&gt;Hoppscotch is a simple web-based interface client, with an available Universal Windows app for download. Unfortunately, Hoppscotch does not inherently support local testing, but there is a browser extension that fills the gap. Once I installed that the extension, I found Hoppscotch quite easy to use. It has a minimal interface, which I appreciate opposed to Visual Studio&#39;s cockpit&#39;s-worth of buttons, dropdown menus, and tools that 80% of us only have a vague idea of its purpose. The desktop app even has a &quot;Zen Mode&quot; which removes the text labels from the side menu. (I do this with my phone apps too, my partner has no idea how I&#39;m able to find anything!) Testing in Hoppscotch is accomplished through their own assertion functions and runs only one test script per request. This can make for some difficult test development if you need to test several pieces of the response. Essentially, you&#39;ll need to AND all your test results together, meaning a failing test could leave you scratching your head for a bit. Overall, Hoppscotch fills the role of &quot;just an API client&quot; very well. If you&#39;re doing local testing, you&#39;ll have to deal with the additional setup, and having an extension attached to your browser which your IT security folks may not be a fan of. If you&#39;re willing and able to use the extension, Hoppscotch is a great overall experience. 8/10&lt;/div&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0MQ3DckSjfH-5ZkyZu3ZmPU09VJSej0XZQ9LcctPcZxzEegK0m2-ioL2f1kY_gQ_xQnavX6dGN7_8BWxfb1ls-L9S5DbWK6pA-ExNKYyYLV8V-peBjnCfufLKhb_1N0EkZMY3pKbhwr9FcaWEt-4KxaKipSEHT0GCvGg53hRlcYF9sdnHWjopuUbfAQ/s1691/hoppscotch1.PNG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1196&quot; data-original-width=&quot;1691&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj0MQ3DckSjfH-5ZkyZu3ZmPU09VJSej0XZQ9LcctPcZxzEegK0m2-ioL2f1kY_gQ_xQnavX6dGN7_8BWxfb1ls-L9S5DbWK6pA-ExNKYyYLV8V-peBjnCfufLKhb_1N0EkZMY3pKbhwr9FcaWEt-4KxaKipSEHT0GCvGg53hRlcYF9sdnHWjopuUbfAQ/s16000/hoppscotch1.PNG&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Hoppscotch.io as a UWP, clean &amp;amp; simple&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVEKL93b-0PePacm_Lph4fXRiTDbVk09U9UgJH2SEqXXBkbCkXUnvX_npb_ekijO_2MBnoC4eO_gO72u16NqrPmpOHg3s4qrNcyjK8pir8VJtU8LzTIqSBofBQpNhf5s7A5NZOd0rz2KKMfumKcKW3kWNBRdUTdT31nn-Fs20N7f1PE_MHXpVPqUc-dg/s1691/hoppscotchzen.PNG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1197&quot; data-original-width=&quot;1691&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVEKL93b-0PePacm_Lph4fXRiTDbVk09U9UgJH2SEqXXBkbCkXUnvX_npb_ekijO_2MBnoC4eO_gO72u16NqrPmpOHg3s4qrNcyjK8pir8VJtU8LzTIqSBofBQpNhf5s7A5NZOd0rz2KKMfumKcKW3kWNBRdUTdT31nn-Fs20N7f1PE_MHXpVPqUc-dg/s16000/hoppscotchzen.PNG&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Same as above, but Zen Mode Activate!&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;https://insomnia.rest/&quot; target=&quot;_blank&quot;&gt;Insomnia&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;I was turned onto Insomnia by a colleague and as an insomniac myself, I felt I had to give this client a try! Insomnia is one of the more robust clients I test drove. It sits at the cusp of usability and bloat, in my opinion. Insomnia offers a full-featured experience, included my top three priorities: history, collections, and testing. I was quite impressed with Insomnia&#39;s testing feature. Using the Chai API for assertions, I was able to assemble simple tests within seconds. Even given my zero previous experience with Chai, I was able confirm various pieces of the returned data without having to curse once! Any developer that works primarily in the backend, knows exactly what a blissful experience it is not having to curse at your JavaScipt to get it to work can be. I digress, Insomnia&#39;s testing capabilities extend beyond individual tests and allow you to run an entire suit of tests at once. This feature makes for fantastic regression testing, as you can fire off multiple requests to different endpoints, each getting their own response to run the test script against. If you&#39;re willing to put in the work, Insomnia can even chain requests for testing, taking values for the previous request and supplying them to the next. Insomnia, despite beginning to get into the territory of &quot;features I&#39;ll never use&quot;, is the best well-rounded Postman alternative I encountered during this venture. 9/10&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4y1sE0Lja1hBIEwi8zA34oaHoiDGAsS_vLqLuOCLnDrM9VSyUlniltUESzut1b2Em-cfpb0DBpZCuBrb2KJDep-xjZ4pKIS_4LZ0k0VMIFchkrpmUkXBjvdTi9JK-GNGGq8aUtI612Z8gNrOZHERAG__D9qGdXX63Aj2WFZSx08u1zhdq3ObSL7hOEQ/s2165/insomnia.PNG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1187&quot; data-original-width=&quot;2165&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4y1sE0Lja1hBIEwi8zA34oaHoiDGAsS_vLqLuOCLnDrM9VSyUlniltUESzut1b2Em-cfpb0DBpZCuBrb2KJDep-xjZ4pKIS_4LZ0k0VMIFchkrpmUkXBjvdTi9JK-GNGGq8aUtI612Z8gNrOZHERAG__D9qGdXX63Aj2WFZSx08u1zhdq3ObSL7hOEQ/s16000/insomnia.PNG&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Clean, simple, powerful&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6jK21vdKWDJ2OdasGRm-q7Z1u2VYE423oB2Aoxxr94vjn4sxKpGrn6uI7y9VUn79yk9rGXyHzflT-d9LWB-P1pnLYE3c4bp_v_aTPtNmGTSa46u9fzUpuqosnkS1H5kPTK63hR_w2IOl77ZxeaTGuSMUx7mAG9BHNs4hZiPmHg1x31LD0c9KTFWA6_g/s2165/insomniatest.PNG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1187&quot; data-original-width=&quot;2165&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi6jK21vdKWDJ2OdasGRm-q7Z1u2VYE423oB2Aoxxr94vjn4sxKpGrn6uI7y9VUn79yk9rGXyHzflT-d9LWB-P1pnLYE3c4bp_v_aTPtNmGTSa46u9fzUpuqosnkS1H5kPTK63hR_w2IOl77ZxeaTGuSMUx7mAG9BHNs4hZiPmHg1x31LD0c9KTFWA6_g/s16000/insomniatest.PNG&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Sweet testing suite!&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;https://www.thunderclient.com/&quot; target=&quot;_blank&quot;&gt;Thunder Client&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;Thunder Client is another tool in the Swiss Army knife of development that is VS Code. I love VS Code, I&#39;m always on the lookout for ways to get more use out of it. So, Thunder Client fits right into my daily use. Thunder Client lacks some of the features that I&#39;ve taken for granted in the previous two clients. It lacks the ability to bulk edit query parameters and does not provide easy GraphQL queries. If those are important to you, maybe Hoppscotch or Insomnia are a better fit for you. That out of the way, Thunder Client supports all my big three wants in a client. Thunder Client&#39;s testing is a simple dropdown query builder. With a few selections, given limited functions, you&#39;re able to quickly put together tests to examine components of the response. However, if you&#39;re looking to do more advanced testing, again you&#39;ll need to go elsewhere as Thunder Client doesn&#39;t provide access to the JavaScript generated from the dropdown selectors. Thunder Client is a great way to fire off requests if you&#39;ve already got VS Code open and just need to get to that breakpoint to see where that null reference is popping up. I wouldn&#39;t suggest it as a tool to cover all your documentable cases, but it&#39;s a handy debugging tool attached to an application you likely already have open. 7/10&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_wsTljZkXMExhRjaPZDJudcZ6zdgOw_HDzgLOcxeEgt3TpFINGlLKz9QjrEhAmZBA52Z0H3_vF9HUhvaAR_NI9QaBz4ys-L142w4ZYoJI9RiwlpqVwRuG64MVsvc0UciiHCeKFDW0VHod_vdlUnflLJuB27EQw5cT35g8nKjssAUysKCNvaLMyvUxSg/s2042/thunderclient.PNG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1223&quot; data-original-width=&quot;2042&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_wsTljZkXMExhRjaPZDJudcZ6zdgOw_HDzgLOcxeEgt3TpFINGlLKz9QjrEhAmZBA52Z0H3_vF9HUhvaAR_NI9QaBz4ys-L142w4ZYoJI9RiwlpqVwRuG64MVsvc0UciiHCeKFDW0VHod_vdlUnflLJuB27EQw5cT35g8nKjssAUysKCNvaLMyvUxSg/s16000/thunderclient.PNG&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Thunder Client in all its glory&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;br /&gt;&lt;/div&gt;&lt;br /&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjn3zBAwXiEvh8IHHpv6mm6DdIMVButTNz0A94kq-tl0sl6uwmlfNg4B2YQ4xYT4ylAazqJPch1TSgd9EV5mR6UGqG5mPh0tQVN1TETVefrAAZNh4jOy5t6bUkGEBA3FYws4uEJtiFFgPCQHZs4gumdn16T6iTa96htlcO1zm19dr79fRPHX45VQ0wZTA/s2042/thunderclienttest.PNG&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img border=&quot;0&quot; data-original-height=&quot;1223&quot; data-original-width=&quot;2042&quot; src=&quot;https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjn3zBAwXiEvh8IHHpv6mm6DdIMVButTNz0A94kq-tl0sl6uwmlfNg4B2YQ4xYT4ylAazqJPch1TSgd9EV5mR6UGqG5mPh0tQVN1TETVefrAAZNh4jOy5t6bUkGEBA3FYws4uEJtiFFgPCQHZs4gumdn16T6iTa96htlcO1zm19dr79fRPHX45VQ0wZTA/s16000/thunderclienttest.PNG&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Simple test generation!&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;Honorable Mention&lt;/h2&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;a href=&quot;https://github.com/EsperoTech/yaade&quot; target=&quot;_blank&quot;&gt;Yaade&lt;/a&gt;&lt;br /&gt;&lt;/h2&gt;&lt;div&gt;Yet Another API Development Environment (YAADE) is a container-based API client that you can simply pull the container, install a browser extension, and you&#39;re off and running. This brings the convenience of a web-based approach to your local environment. Unfortunately, Yaade does not appear to support testing JavaScript on return at this time. However, it does provide for collections and history. I don&#39;t use containers in my day to day, and I didn&#39;t want to get involved with dealing with containers at this time, so I gave this one a pass. If you use Yaade, or are set up to try it out, post a comment below and let us know how it is!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Disclaimer: The above views and opinions are the author&#39;s alone, and a not endorsed by the covered applications, the author&#39;s employer, or any other associated entity.&lt;/div&gt;</description>            <guid>https://blog.http418.dev/2023/02/is-your-postman-delivering-too-much.html</guid>            <pubDate>Tue, 28 Feb 2023 21:21:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>You Just Got Vectored! SVG Image Formats</title>            <link>https://blog.http418.dev/2023/02/you-just-got-vectored-svg-image-formats.html</link>            <description>&lt;p&gt;&amp;nbsp;If you&#39;re reading this, then you&#39;ve come across a need that nearly all Opti developers encounter in their careers; You need to display a vector image format (SVGs and the like) properly, the &amp;lt;IMG&amp;gt; tag just isn&#39;t going to cut it anymore. Post like this are a right-of-passage for Opti bloggers.&lt;br /&gt;&lt;br /&gt;&quot;So,&quot; you think &quot;if there are so many other blogs out there on the topic, why should I read yours?&quot; Firstly, you enjoy my familiar and conversational tone of writing. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;&lt;table align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; class=&quot;tr-caption-container&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td style=&quot;text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiZ95KS1aN_isbFl750DiUVWct5K2PZyIGC7zAzxhbGxfvh5FpR1j5t2A_VZzApbxiGbMdrjxZbiXbJC3yTARp8bZPbVVjjDb_Puq5rP4XW2Ul3bPcmxMSOIozH3LfPSdXgwyFxytJJuUdSYlNzZWOnBgu4UgfQWvYUqispI446FZI-_ucCldXMDF7kFw&quot; style=&quot;margin-left: auto; margin-right: auto;&quot;&gt;&lt;img alt=&quot;&quot; data-original-height=&quot;300&quot; data-original-width=&quot;500&quot; height=&quot;192&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEiZ95KS1aN_isbFl750DiUVWct5K2PZyIGC7zAzxhbGxfvh5FpR1j5t2A_VZzApbxiGbMdrjxZbiXbJC3yTARp8bZPbVVjjDb_Puq5rP4XW2Ul3bPcmxMSOIozH3LfPSdXgwyFxytJJuUdSYlNzZWOnBgu4UgfQWvYUqispI446FZI-_ucCldXMDF7kFw&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class=&quot;tr-caption&quot; style=&quot;text-align: center;&quot;&gt;Cary Elwes does the English accent, I do the nerd stuff.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Second, you&#39;ve already come this far, you might as well finish as this point, it&#39;s not long, I promise. To that point, and most importantly, I&#39;ve seen some complex solutions out there, this one is a quick and simple implementation. 100% Guarantee!&lt;br /&gt;&lt;br /&gt;The VectorFile class:&lt;br /&gt;&lt;code&gt;&lt;div&gt;[MediaDescriptor(ExtensionString = &quot;svg&quot;)]&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; public class VectorFile : ImageData&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public override Blob Thumbnail { get =&amp;gt; BinaryData; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public virtual string XML&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; get&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; try&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var blob = BinaryData;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var xmlDoc = new XmlDocument();&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; xmlDoc.Load(blob.OpenRead());&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return xmlDoc.InnerXml;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; catch&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return &quot;&quot;;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/code&gt;This delightful little class will extract the thumbnail for previews and place the image in an XML document that can be inserted into a view as an &amp;lt;SVG&amp;gt; tag. You can see from the MediaDescriptor attribute that we&#39;re only allowing .SVG files here. This list can be expanded as needed. This is where you can also supply further properties regarding your SVG in the class, it&#39;s just content after all!&amp;nbsp;&lt;div&gt;&lt;br /&gt;&lt;div&gt;The VectorFileController class:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; public class VectorFileController : AsyncPartialContentComponent&amp;lt;VectorFile&amp;gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; protected override async Task&amp;lt;IViewComponentResult&amp;gt; InvokeComponentAsync(VectorFile currentBlock)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return await Task.FromResult(View(&quot;~/Features/Media/_vectorFile.cshtml&quot;, currentBlock));&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/code&gt;The should look familiar to CMS12 block development, just your run-of-the-mill controller. No tricks, no fills.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;The VectorFile view:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;div&gt;@model VectorFile&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;@if (Model != null)&lt;/div&gt;&lt;div&gt;{&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; @((MarkupString)Model.XML)&lt;/div&gt;&lt;div&gt;}&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;br /&gt;&lt;div&gt;A lot of devs would have you use @Html.Raw to put the SVG code on the screen, but with the &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring?view=aspnetcore-6.0&quot; target=&quot;_blank&quot;&gt;MarkupString struct&lt;/a&gt;. We on less shaky ground here because MarkupString is parsed in an HTML or SVG format, rather than trusting that the silly SVG you grabbed off Pinterest for that company party doesn&#39;t have some nasty embedded code.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Now, on the next upload to your Media folder, any SVG file should appear as a VectorFile instead of an ImageFile! (You did take the .svg extension out the MediaDescriptor of your raster image class, right?)&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Told you it would be short and simple! Happy coding Opti-mists!&lt;/div&gt;</description>            <guid>https://blog.http418.dev/2023/02/you-just-got-vectored-svg-image-formats.html</guid>            <pubDate>Fri, 03 Feb 2023 13:44:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Config-Per-Site in Multi-Tenant Environments</title>            <link>https://blog.http418.dev/2023/01/config-per-site-in-multi-tenant.html</link>            <description>&lt;p&gt;Recently, a task was given to me at work where we needed a multisite configuration. We all know that multi-environment is as easy as appsettings.&amp;lt;environment_name&amp;gt;.config. What about in multi-tenant environments? You can&#39;t have appsettings.site1.config and appsettings.site2.config in your site! Well, not without a little extra work...&lt;/p&gt;&lt;p&gt;Allow me to introduce&amp;nbsp;&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.keyperfileconfigurationbuilderextensions.addkeyperfile?view=dotnet-plat-ext-7.0#microsoft-extensions-configuration-keyperfileconfigurationbuilderextensions-addkeyperfile(microsoft-extensions-configuration-iconfigurationbuilder-system-action((microsoft-extensions-configuration-keyperfile-keyperfileconfigurationsource)))&quot; target=&quot;_blank&quot;&gt;AddKeyPerFile&lt;/a&gt;; this handy little function that you set up in your Program.cs will enumerate the files in a directory and add them to your Configuration.&amp;nbsp;&lt;/p&gt;&lt;p&gt;&quot;Alright!&quot; you must be thinking &quot;Show me how this miracle function works...&quot; Well, let&#39;s get to it! First, you&#39;ll want to identify the config sections that will be unique to each site and put them in their own folder. I do this by site name because it makes the most sense by our conventions, if something else works for your practice, these names aren&#39;t set in stone.&amp;nbsp;&lt;br /&gt;&lt;/p&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;&lt;a href=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjDAlfcr0Iz7l7It7dxvo0UFeDJo71rJTDdbexEETTZ-W0oD639QFgiC7bH6ed4T0lilnBPbr4b8dHUMBY68TJHAK9LQmfWxr8WFmbgX-1VqGV8erlU21ae7mSYrThXfQhkxCHyxyeAk8ltc2njN-S36moe7G_SB_Kp9JkrfRvOcDLysk1xHiT8GIbMQQ&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;&lt;img alt=&quot;&quot; data-original-height=&quot;66&quot; data-original-width=&quot;142&quot; height=&quot;149&quot; src=&quot;https://blogger.googleusercontent.com/img/a/AVvXsEjDAlfcr0Iz7l7It7dxvo0UFeDJo71rJTDdbexEETTZ-W0oD639QFgiC7bH6ed4T0lilnBPbr4b8dHUMBY68TJHAK9LQmfWxr8WFmbgX-1VqGV8erlU21ae7mSYrThXfQhkxCHyxyeAk8ltc2njN-S36moe7G_SB_Kp9JkrfRvOcDLysk1xHiT8GIbMQQ&quot; width=&quot;320&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br /&gt;&lt;br /&gt;&lt;p&gt;Now, in order to suck those values into your config, there&#39;s one simple line to add to Program.cs:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;&lt;code&gt;&lt;p&gt;&amp;nbsp;var configBuilder = new ConfigurationBuilder()&lt;/p&gt;&lt;p&gt;&amp;nbsp;.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))&lt;/p&gt;&lt;p&gt;&amp;nbsp;.AddJsonFile(&quot;appsettings.json&quot;, false, true)&lt;/p&gt;&lt;p&gt;&amp;nbsp;.AddJsonFile($&quot;appsettings.Environment.GetEnvironmentVariable(&quot;ASPNETCORE_ENVIRONMENT&quot;)}.json&quot;, true, true)&lt;/p&gt;&lt;p&gt;&amp;nbsp;.AddJsonFile(&quot;appsettings.local.json&quot;, true, true)&lt;/p&gt;&lt;p&gt;&amp;nbsp;.AddJsonFile($&quot;appsettings.{Environment.MachineName}.json&quot;, true, true)&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;span style=&quot;background-color: #fcff01;&quot;&gt;.AddKeyPerFile(Path.Combine(Directory.GetCurrentDirectory(), &quot;SiteConfigs&quot;), true, true)&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&amp;nbsp;.AddEnvironmentVariables();&lt;/p&gt;&lt;br /&gt;&lt;/code&gt;&lt;p style=&quot;text-align: left;&quot;&gt;AddKeyPerFile expects at least a fully qualified path to the config folder, no short cutting this. The following two bools indicate if the file is optional, and to reload the file if it changes, respectively and these are optional.&lt;/p&gt;&lt;p style=&quot;text-align: left;&quot;&gt;So, now that we&#39;ve got them in the Configuration, how do we get them out? Simple:&lt;/p&gt;&lt;code&gt;&lt;p style=&quot;text-align: left;&quot;&gt;_configuration[$&quot;{siteName}.json&quot;];&lt;/p&gt;&lt;/code&gt;&lt;p&gt;This is where your file naming convention comes into play, you need to be able to look them up by name in Configuration, so make sure it&#39;s something you can work with. However, there&#39;s a snag with this... We get the entire contents of the file as a value, so we&#39;ll need to deserialize that into something a bit more usable:&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;&lt;code&gt;&lt;p&gt;var jsonSettings = JsonConvert.DeserializeObject&amp;lt;StyleSettings&amp;gt;(_configuration[$&quot;{siteName}.json&quot;] ?? string.Empty);&lt;/p&gt;&lt;/code&gt;&lt;p&gt;And there we go; we&#39;ve got access to Configuration values on a per-site basis.&amp;nbsp;&lt;/p&gt;</description>            <guid>https://blog.http418.dev/2023/01/config-per-site-in-multi-tenant.html</guid>            <pubDate>Wed, 11 Jan 2023 23:41:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>The 1001st Piece in your 1000 Piece Puzzle: .NET Default Interface Functions</title>            <link>https://blog.http418.dev/2022/11/the-1001st-piece-in-your-1000-piece.html</link>            <description>I was recently working with a client who wanted a reasonably large subsystem added to Optimizely that would add automated management to their content. While cutting the code for this, I found myself writing similar code across multiple classes. The reason why I had to write it that way was: 1) The client was currently on CMS11 and didn&#39;t have access to newer language features; 2) The hierarchy of the classes prevented me from inserting a common ancestor. Thankfully, .NET has expanded the functionality of interfaces, so we can take advantage of those within Optimizely.&lt;div&gt;&lt;br /&gt;&lt;div&gt;With .NET 5, Microsoft introduced default implementations on interfaces. What this means is that function bodies can be included in the interface definition itself. Resulting in all classes that implement the interface can use the default implementation, or override it with custom logic. Enough text, lets code!&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;b&gt;Original Interface&lt;/b&gt;&lt;/h2&gt;&lt;div&gt;The following code is something that we&#39;d create for an Optimizely experiment:&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;code style=&quot;background-color: white;&quot;&gt;&lt;div&gt;using OptimizelySDK;&lt;/div&gt;&lt;div&gt;using OptimizelySDK.Entity;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;namespace Teapot.Interfaces.Services&lt;/div&gt;&lt;div&gt;{&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; public interface IExperimentation&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public OptimizelyUserContext CreateUserContext(UserAttributes userAttributes = null, EventTags eventTags = null);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public string GetUserId();&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public void TrackEvent(string eventKey);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;}&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Not much to see here, it&#39;s just like every other interface you&#39;ve ever written.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;b&gt;Interface with default implementation&lt;/b&gt;&lt;/h2&gt;&lt;div&gt;With this update to the interface, I&#39;ve added default code to the GetUserId function. This will do a couple things: Centralize repeated code (remember to keep it DRY), and this function does not have to be implemented separately when a class utilizes the interface.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;code&gt;&lt;div&gt;using OptimizelySDK;&lt;/div&gt;&lt;div&gt;using OptimizelySDK.Entity;&lt;/div&gt;&lt;div&gt;using Perficient.Infrastructure.Interfaces.Services;&lt;/div&gt;&lt;div&gt;using System;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;namespace Teapot.Interfaces.Services&lt;/div&gt;&lt;div&gt;{&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; public interface IExperimentation&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public OptimizelyUserContext CreateUserContext(UserAttributes userAttributes = null, EventTags eventTags = null);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public string GetUserId(ICookieService cookieService)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; var userId = cookieService.Get(&quot;opti-experiment-testA&quot;);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; if (userId == null)&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; userId = Guid.NewGuid().ToString();&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cookieService.Set(&quot;opti-experiment-testA&quot;, userId);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; return userId;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public void TrackEvent(string eventKey);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;}&lt;/div&gt;&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;As with all interface functions, the default implementation can be overridden as needed if there is an exception to your standard usage.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;b&gt;Using default implementation&lt;/b&gt;&lt;/h2&gt;&lt;div&gt;To call the default implementation in the interface, the object needs to be cast to the interface type, otherwise the runtime will look for an overridden version of the function on the implementing object.&amp;nbsp;&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;code&gt;&lt;div&gt;return _featureExpermentation.CreateUserContext((this as IExperimentation).GetUserId(_cookieService));&lt;/div&gt;&lt;/code&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;Since this can create some ugly code if the default implementation is often called. The can be remedied with a little syntactic sugar in the class leveraging the interface:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;code&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; public string GetUserId()&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; {&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;return (this as IExperimentation).GetUserId(_cookieService);&lt;/div&gt;&lt;div&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/code&gt;&lt;h2 style=&quot;text-align: left;&quot;&gt;&lt;b&gt;Thoughts&lt;/b&gt;&lt;/h2&gt;&lt;/div&gt;&lt;div&gt;I wanted to post this earlier, but I spent some time considering where this paradigm shift sits in my current Opti architecture. &quot;This doesn&#39;t fit the traditional view of Domain-Driven Design!&quot; was my first thought. As much as I was excited for this new language feature, I thought it was going to be the stork-beak pliers in my coding toolbox. As I tried a few things out and tinkered around with default functions, I thought back to all the times I wrote functions in services that stood alone, no arguments or services required to do the job. These are the little places that I think a default interface implementation can fill in, alleviating some of the bloat (we&#39;ll never get all of it) from services.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;My rule of thumb from here has been that if a method is acting on the object with no outside dependencies, maybe it should be a default function on the appropriate interface.&amp;nbsp;&lt;/div&gt;</description>            <guid>https://blog.http418.dev/2022/11/the-1001st-piece-in-your-1000-piece.html</guid>            <pubDate>Mon, 28 Nov 2022 18:55:00 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Gets More (Case-)Sensitive</title>            <link>https://blog.http418.dev/2022/08/optimizely-gets-more-case-sensitive.html</link>            <description>&lt;div&gt;As Windows developers, we don&#39;t always have to pay attention to capitalization when dealing with paths and URIs. With CMS12, Optimizely has started deploying to a Linux container for hosting sites. This means that deployed sites (and developers!) will have to pay attention to capitals in references. Inconsistent capitalization can cause 404 errors in unexpected places. Thankfully there&#39;s a few ways to handle that!&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;The worst part of this is that developers won&#39;t be able to find these issues until deployed to Azure, with Windows, being case in-sensitive and all. Once named, files and folders can be difficult to change in git. Below are some ways to help rename your files so that they&#39;ll play nicely in Linux.&lt;br /&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;b&gt;Rename via Git mv Command&lt;/b&gt;&amp;nbsp;&lt;/div&gt;&lt;div&gt;One way to update capitalization is by the git mv command. In you git command shell:&amp;nbsp;&lt;/div&gt;&lt;div class=&quot;codesnippet&quot;&gt;&lt;br /&gt;git mv &amp;lt;source&amp;gt; &amp;lt;target&amp;gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;This command will rename a file or folder, however it still runs in the context of Windows. If you want to simply change change capitalization you&#39;ll have to use an intermediate name due to the previously mention case-insensitivity of Windows, otherwise you&#39;ll get an error message from git. For example using the Scripts folder, in order to get the name to simply lower-case the following command sequence has to be run:&lt;br /&gt;&lt;br /&gt;&lt;div class=&quot;codesnippet&quot;&gt;git mv Scripts scripts-lowercase &lt;br /&gt;git mv scripts-lowercase scripts&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Changing name directly in repo&lt;/b&gt;&lt;/div&gt;&lt;div&gt;This is perhaps the most straight-forward way to fix capitalization. If available, you can simply rename the files that need to match capitalization. I work in Azure DevOps, where this changes is as simple as hitting the kebab menu and select &quot;rename&quot;.&lt;br /&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;  &lt;a href=&quot;https://lh3.googleusercontent.com/-Zwm0oTACNik/YwU7leZ-iJI/AAAAAAAACmU/KDVNpvLe0i04TDR9XvyNRKiE6kBMtkI0gCNcBGAsYHQ/s1600/1661287316943441-0.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;    &lt;img border=&quot;0&quot; src=&quot;https://lh3.googleusercontent.com/-Zwm0oTACNik/YwU7leZ-iJI/AAAAAAAACmU/KDVNpvLe0i04TDR9XvyNRKiE6kBMtkI0gCNcBGAsYHQ/s1600/1661287316943441-0.png&quot; width=&quot;400&quot; /&gt;  &lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;Updated file names will carry-through a pull request, giving you proper naming on your master branch when completed.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;Update your git configuration&lt;/b&gt;&lt;/div&gt;&lt;div&gt;Perhaps the best way to enforce case-sensitivity is to update your git configuration to be case-sensitive in the first place. To do this, run this command in your git shell:&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div class=&quot;codesnippet&quot;&gt;git config core.ignorecase false&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;This command will force git to pay attention capitalization on commits. If you want to only enforce this restriction do your local dev environment, you can add the --local argumment to the command. Doing this will cause only your commits to enforce capitalization, so other devs are not impacted.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;Unfortunately, if trying to update an existing repo this method can leave artifacts behind in your repository that will need to be cleaned up. These artifacts will not show up in your local file system, and will have to be removed via git command with the rm function, or removing the folders directly from your git repo.&amp;nbsp;&lt;/div&gt;&lt;div&gt;&lt;div class=&quot;separator&quot; style=&quot;clear: both; text-align: center;&quot;&gt;  &lt;a href=&quot;https://lh3.googleusercontent.com/-04pbW0rO5NA/YweUDV9nZXI/AAAAAAAACoU/ecvP-27Ffr4RdQTT61G9Xe4NAEMC20L7wCNcBGAsYHQ/s1600/1661441036427634-0.png&quot; style=&quot;margin-left: 1em; margin-right: 1em;&quot;&gt;    &lt;img border=&quot;0&quot; src=&quot;https://lh3.googleusercontent.com/-04pbW0rO5NA/YweUDV9nZXI/AAAAAAAACoU/ecvP-27Ffr4RdQTT61G9Xe4NAEMC20L7wCNcBGAsYHQ/s1600/1661441036427634-0.png&quot; width=&quot;400&quot; /&gt;  &lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;&lt;b&gt;And another thing!&lt;/b&gt;&lt;/div&gt;&lt;div&gt;If a PR already exists and you&#39;re trying to merge capitalization changes into it, this will not update the names on merge. If you cancel/abandon the PR and create a new one, the capitalization changes will then be merged in properly.&lt;/div&gt;&lt;div&gt;&lt;br /&gt;&lt;/div&gt;&lt;div&gt;This is a prime time to examine your file names and if they aren&#39;t inline with your current standards, now is the time to reduce some tech debt rather than create more by carrying bad practice past this point. A future dev (possibly even a future you) will thank you.&amp;nbsp;&lt;/div&gt;&lt;/div&gt;</description>            <guid>https://blog.http418.dev/2022/08/optimizely-gets-more-case-sensitive.html</guid>            <pubDate>Thu, 25 Aug 2022 15:25:00 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>