<rss version="2.0">
  <channel>
    <title>Blog</title>
    <link>https://www.scottarbeit.com/</link>
    <description><![CDATA[GitHub, .NET, Azure, F# and other random stuff]]></description>
    <item>
      <title>LLM's and Smaller, Less Popular Programming Languages</title>
      <link>https://www.scottarbeit.com/blog/llm-s-and-smaller-less-popular-programming-languages</link>
      <description><![CDATA[<p>Photo by <a href="https://unsplash.com/@anniespratt">Annie Spratt</a> on <a href="https://unsplash.com/photos/white-and-brown-labeled-pack-Rv-O5fmUKbU">Unsplash</a></p>
<h1 id="llms-and-smaller-less-popular-programming-languages">LLM's and Smaller, Less Popular Programming Languages</h1>
<p>I work for GitHub, and so I've been a happy Copilot user since it was first created. It's been amazing to watch the pace of improvements in all of the foundation models that we use, and all of the models out there. (For programming, I <em>love</em> o1-mini, and I'm sure I'll love o3-mini, too.)</p>
<p>I'm also an F# programmer. My version control system, <a href="https://github.com/ScottArbeit/Grace">Grace</a>, is written almost entirely in F#.</p>
<p>That means that when I'm asking LLM's to generate code for me, I'm usually asking them for F#. In fact, my custom instructions for Copilot and ChatGPT include this:</p>
<pre><code class="language-text">For questions about programming, assume the programming language is F#, unless told otherwise.
Code samples should be provided in F#, unless told otherwise.
</code></pre>
<p>And here's the thing: <em>none of the models are very good at generating modern, idiomatic F#</em>.</p>
<p>This led me to two big questions:</p>
<ol>
<li>What does this mean for the future of less popular programming languages?</li>
<li>What should we do to help LLM's have a better understanding of less popular programming languages?</li>
</ol>
<h2 id="llms-are-great-with-popular-languages">LLM's are Great with Popular Languages</h2>
<p>Well, &quot;duh&quot;, right?</p>
<p>Popular languages, by definition, have more lines of code written for LLM's to train on. They have more examples of more ways to do stuff, so the LLM's have more of an opportunity to build a rich, internal model of the language they can use when generating code for us.</p>
<p>Which languages am I talking about? I don't know,  &lt;waves hands&gt; something like:</p>
<ul>
<li>C#</li>
<li>Java</li>
<li>JavaScript</li>
<li>TypeScript</li>
<li>Python</li>
<li>C++</li>
<li>Ruby</li>
</ul>
<p>You know... popular ones.</p>
<p>Same thing for popular vs. not-as-popular frameworks, like React, Angular, Rails, etc.</p>
<p>The models have gotten particularly good at creating new projects in these languages (and frameworks) with nothing more than a natural-language requirements list.</p>
<p>They're also pretty good at refactoring and suggesting edits in those languages.</p>
<p>If you're using one of those languages, you're one of the lucky ones in these still-early days of AI coding assistants.</p>
<h2 id="llms-are-not-great-with-less-popular-languages">LLM's are Not Great with Less Popular Languages</h2>
<p>So... I write a lot of F#. F# is not one of the big languages - even though it should be because it's <em>fucking awesome</em> - and so the Generative AI results I get when asking for F# are... &lt;sigh&gt;...</p>
<p>They're just not good. I mean, sometimes, for easy things, they're fine and I can use them and go. But for anything of any complexity, I need to specify a lot of detail to get good code, and sometimes, even then, I don't get usable, correct F#.</p>
<p>I don't spend any time in Julia, Crystal, OCaml, or Zig, or any of the other dozens of languages that fall into the &quot;smaller, less popular&quot; category, so I can't confirm they all get the same lower-fidelity treatment that F# does, but, it can't be good today to have fewer examples of code in your favorite language available for training the models.</p>
<h3 id="problem-1-old-syntax">Problem #1: Old syntax</h3>
<p>The custom instructions I shared above isn't the whole section I have about F#. What I actually have is:</p>
<pre><code class="language-text">For questions about programming, assume the programming language is F#, unless told otherwise.
Code samples should be provided in F#, unless told otherwise.
All F# code uses the task { } computation expression to handle asynchronous operations.
All F# code should be written in a functional style, using immutability and pure functions where possible.
All F# code should the new syntax when referring to arrays, lists, and sequences: use myArray[0] instead of myArray.[0].
</code></pre>
<p>Why do I have all of that? Because, without going into details about F#, to get even close to good output, I'm forced to explicitly say to the model: &quot;Use modern, correct syntax, not syntax from older language versions and older examples.&quot;</p>
<p>And this isn't just a problem for small languages. I haven't written any C++ lately, but I follow developments in the language. I imagine that a lot of the code examples you'd get using LLM's right now are going to show out-of-date examples of C++98-style, less safe stuff, instead of the much safer, <em>declaratively</em> safe, C++23.</p>
<p>(Seriously, check out the <a href="https://www.youtube.com/@CppCon">videos from CPPCon</a>. Anything with Herb Sutter, Andrei Alexandrescu, and Chandler Carruth is highly recommended.)</p>
<h3 id="problem-2-invalid-structure-and-syntax">Problem #2: Invalid structure and syntax</h3>
<p>Here's an example I dealt with recently.</p>
<p>In C#, or any object-oriented-ish language, doing early exits from a method is normal. For instance:</p>
<pre><code class="language-csharp">public async Task&lt;int&gt; DoSomethingWithStorageAsync(string writeToStorage, string readFromStorage, string clearState)
{
    if(String.IsNullOrEmpty(writeToStorage))
    {
        return -1;
    }

    // Keep doing whatever the method is supposed to do...
    // ...
    // ...
    // Cool, we're done. Everything worked.
    return 0;
}
</code></pre>
<p>Getting out early with <code>return -1</code> is how you avoid using an <code>else</code> and indenting everything after it, and, for better or worse, it's standard style these days.</p>
<hr />
<p>Doing that in F#, though, is not allowed. In F#, the <code>if</code> expression is exactly that: an <em>expression</em>. It returns a value.</p>
<p>(There is no <code>if</code> <em>statement</em> in F#, because functional programming.)</p>
<p>You just can't use that early-exit construct without raising an exception, and that's not good functional practice.</p>
<p>I was asking the models to do something that I would describe as a medium-complexity function. I should start by saying: I expect that the use of the GitHub REST API, and, specifically, use of the GitHub .NET SDK, are well-enough-known to the LLM's that that shouldn't have been a problem. The prompt starts:</p>
<pre><code>I am developing an F# command-line application that retrieves issues and pull requests from a specific GitHub repository using the GitHub API, and stores this data in a SQLite database...
</code></pre>
<p>and goes on with some detail, as I worked with GPT-4o first to craft the prompt before submitting it to o1-mini. And then to Claude Sonnet 3.5. And then to Google Gemini 2.0 Experimental. And even locally on LM Studio running Phi 3.5 and Llama 3.1.</p>
<p>None of them generated useful, working code. To get any of it working, I had to do way-too-much syntax fixing, and then way-too-much debugging.</p>
<p>There were problems with understanding how to use the F# Task Computation Expression - i.e. how F# does modern .NET async/await.</p>
<p>There were problems with understanding the output of the GitHub API and how to process it.</p>
<p>But, most worryingly... there were early exits.</p>
<p>These are obvious syntax errors, they're structurally wrong, they make no sense in F#, and I cannot imagine there are many examples of them in open-source F# code.</p>
<p>I guess the models are doing something like &quot;F# is .NET; .NET is C#; C# patterns therefore work in F#...&quot;.</p>
<p>Even after I updated the prompt to include &quot;do not use early exits&quot; and then even &quot;do not use early exits that look like <code>bad example it generated</code>&quot; I couldn't get proper code for it.</p>
<h2 id="wrong-solution-that-kind-of-worked">Wrong solution (that kind-of worked)</h2>
<p>Well, that was two hours of my life I can't get back.</p>
<p>I went to bed, and woke up with an idea: what if I just generate it in C#, and then have the LLM translate it to F#? I know they're better at code generation in C# than F#, and translating between languages is something they all do pretty well already.</p>
<p>So, I did. And, well, it kind-of worked. Still took more hand-holding than I'd like, but, I got it done.</p>
<p>But, that's not the right solution for this problem. I'll get object-oriented-ish F# out of that, not idiomatic, functional code that's beautiful.</p>
<h2 id="right-solution-help-train-the-llms">Right solution: Help train the LLM's</h2>
<p>I was thinking it might be good to use some pipeline of LLM's + compilers + thoughtful prompting to generate entire repositories of code that demonstrate the latest syntax and are published only after they successfully compile and are post-processed by a style-checker that validates that older syntax isn't present. With some human-in-the-loop, of course.</p>
<p>I wish there were a standard mechanism that the LLM's could recognize during training where we tell them &quot;here are examples of correct, modern, up-to-date code in this language; when generating new code, translate older syntax into this new syntax&quot;, or something like that.</p>
<p>But even if there is... is a repository full of LLM-generated code just spam? I think it doesn't have to be, but I see the point.</p>
<h2 id="language-model-optimization">Language Model Optimization</h2>
<p>For over 20 years, we've all been aware of how important it is to do Search Engine Optimization (SEO) if you want your site to appear near the top of the search rankings.</p>
<p>Maybe, with LLM's, we need to do Language Model Optimization (LMO).</p>
<p>I would suggest that, if you want your programming language to be generated well by the models, that you'll need to take ownership of the LMO for your language.</p>
<p>This presents an entirely new workstream for language owners to handle. Hopefully, the LLM's can help us with it.</p>
<h2 id="i-have-no-idea-how-this-would-work">I Have No Idea How This Would Work</h2>
<p>One idea is to work together to identify some set of tokens to tell the models: &quot;this is the good stuff&quot;, and maybe also to give each language one owner who is allowed to create such repositories, so that the models have a trusted source during training.</p>
<p>I'm happy to hear other ideas... seriously, I have no idea how this would work, but I think we need it, and I think the programming business, in particular, deserves some special treatment during training.</p>
<h2 id="if-we-dont-help-well-lose-the-smaller-languages">If we don't help, we'll lose the smaller languages</h2>
<p>If we don't do something to help the smaller languages, as code generation quality continues to improve for the more popular languages, the gap in quality between large and small languages will just continue to grow. Soon, the upside of trying a new language will be massively outweighed by that quality gap, and we'll become locked into the popular languages in an ever-tightening spiral of more popular = more quality.</p>
<p>Try talking your manager into letting the team learn that cool, new language when you can't use Copilot to get great results with it.</p>
<p>Anyway, I really love F#. I know people who love other, small languages as well. There's a good reason to have a healthy ecosystem of languages and ideas floating around, influencing each other. It's a Good Thing.</p>
<p>I hope Generative AI gets as good with idiomatic F# / Julia / Scheme / whatever as it already is with Java and TypeScript. I think it might need some help getting there.</p>
]]></description>
      <pubDate>Tue, 28 Jan 2025 05:46:20 GMT</pubDate>
      <guid isPermaLink="true">https://www.scottarbeit.com/blog/llm-s-and-smaller-less-popular-programming-languages</guid>
    </item>
    <item>
      <title>F# Advent Calendar 2023: A random walk in the direction of functional enlightenment</title>
      <link>https://www.scottarbeit.com/blog/advent-of-code-2023-a-random-walk-in-the-direction-of-functional-enlightenment</link>
      <description><![CDATA[<blockquote>
<p>(image by <a href="https://unsplash.com/@8moments">Simon Berger</a>, shared on <a href="https://unsplash.com/photos/leafless-trees-hxMdsEaiSyE">Unsplash</a>)</p>
</blockquote>
<p><em>I'm pleased to have the Friday-of-Christmas-weekend slot for F# Advent Calendar 2023. That means I know a lot of you will have some time to unwind with a longer read. I hope you enjoy.</em> 😉</p>
<hr />
<blockquote>
<p><em>Je n'ai fait celle-ci plus longue que parce que je n'ai pas eu le loisir de la faire plus courte.</em></p>
<p>I have made this [letter] longer than usual because I have not had the time to make it shorter.</p>
<p>- Blaise Pascal, <em>Provincial Letters</em>, 1656 <a id="fnref:1" href="#fn:1" class="footnote-ref"><sup>1</sup></a></p>
</blockquote>
<hr />
<p>Hi! I'm Scott. (Hi Scott.) And I've been struggling to think of something interesting to write about for F# Advent of Code 2023.</p>
<p>I mean, I love F#. It's my favorite programming language, ever. It's been the vehicle for me to learn how to think functionally about programming, after two decades of object-oriented[ish] programming. And I've never seen a language like this where <em>almost everyone</em> who starts using it regularly, loves it, too.</p>
<p>I know I could do some <em>Using F# with &lt;insert technology here&gt;!</em> posts. And I probably should. I've written around 24,000 lines of F# code for my version control system, <a href="https://github.com/scottarbeit/grace">Grace</a>, for almost three years now, and it uses things like Dapr, Dapr Actors, CosmosDB, Azure Storage, Spectre.Console, and more. I've got some interesting, functional code in there that I could talk about. (I think.) But none of that felt like the right thing to write about.</p>
<p>Anyway, one of the things I've learned about getting unstuck creatively is that sometimes it's best to start just by espressing something about what's present for you, what's going on in your life, right now.<a id="fnref:2" href="#fn:2" class="footnote-ref"><sup>2</sup></a></p>
<p>What's going on for me is: I'm working through some of the early code from Grace, figuring out how to rewrite it so it doesn't suck, and stumbling through the bugs I've found because of it.</p>
<p>So, that's what I'll share with you today: some not-great code I wrote a while back, why it's not great, how I'm fixing it, and what that might say about my development with F# and functional programming.</p>
<hr />
<blockquote>
<p><em>Anything worth doing is worth doing poorly, at first.</em></p>
<p>– Brian Tracy, many others</p>
</blockquote>
<hr />
<hr />
<blockquote>
<p><em>You don’t have to be great to start. But you have to start to be great.</em></p>
<p>– Zig Ziglar</p>
</blockquote>
<hr />
<p>My hope is that by sharing this part of my journey, I can free you to get started in your own journey with a new programming language (especially F#!).</p>
<p>I started writing an entire version control system when I didn't quite know what I was doing. You can start, too.</p>
<h3 id="a-little-bit-about-grace">A little bit about Grace</h3>
<p>This post isn't meant to be about Grace, but just to give the tiniest bit of context for what's coming:</p>
<p>Grace is a new, modern, cloud-native version control system. It is multi-tenant, centralized, and relies on PaaS products like Azure Blob Storage and AWS S3 for storing versions of files. Because it's centralized, Grace needs to ensure that when commands like <code>grace switch</code> or <code>grace rebase</code> are run, that the existing state of the branch is saved and uploaded before changes are made to the working directory.<a id="fnref:3" href="#fn:3" class="footnote-ref"><sup>3</sup></a></p>
<p>And, yes, it's really fast.</p>
<h3 id="what-im-working-on-right-now">What I'm working on right now</h3>
<hr />
<blockquote>
<p><em>What the hell?!?</em></p>
<p>– Me, looking at code I wrote a couple of years ago</p>
</blockquote>
<hr />
<p>Like you'd expect in a version control system, Grace has a command - <code>grace switch</code> - that lets you switch to another branch in your repo, and will update your working directory with the current contents of that other branch.</p>
<p>Grace also has a background agent mode - <code>grace watch</code> - that maintains a live connection to the server, enabling 2-way communication and local and server-side event processing, and that watches your working directory for changes and automatically uploads after every save-on-disk, allowing most of the more common Grace commands to run incredibly fast.</p>
<p>There's plenty of overlap between those two use cases. Both <code>grace switch</code> and <code>grace watch</code> have to:</p>
<ul>
<li>understand the current contents of the working directory,</li>
<li>make sure that the current contents are uploaded to the server,</li>
<li>update the local object cache that holds recent versions of the files in the repo, and</li>
<li>update the local Grace Status file that holds the state of the branch.</li>
</ul>
<p>That means that they share some function calls, of course.</p>
<p>Also, if you run <code>grace switch</code> while <code>grace watch</code> is running, <code>grace watch</code> has to be aware of it and ignore the changes in the working directory until the switch is complete.</p>
<p>The code for <code>grace watch</code> is about two years old. <code>grace switch</code> came right around the same time, maybe a month or two later. At the time, I wasn't very good at some important things. To make it worse, I was in a hurry writing it. 🤦🏼‍♂️</p>
<h3 id="the-problem-its-me.hi">The problem (It's me. Hi.)</h3>
<hr />
<blockquote>
<p><em>&lt;stares at monitor, blinking&gt;</em></p>
<p>– Me, looking at code I wrote a couple of years ago</p>
</blockquote>
<hr />
<p>There's a lot that I hadn't yet learned about thinking functionally. When I look at it now, some of the code is clearly, &quot;yeah, had no clue how to do that.&quot; Some of it is, &quot;Awww... that's cute. At least I tried.&quot; Some of it is, &quot;I need to come up with better names for those functions.&quot;</p>
<p>In the &quot;had no clue&quot; department, you'll find some of my code for Grace's command-line interface (CLI), specifically, the way I'm trying to interleave work being done, like calls to the server, or writing new local files, with UI updates (updating progress bars) that reflect the progress of completing the command.</p>
<p>As I was writing it, I <em>knew</em> it wasn't great. It all looked like the <a href="https://fsharpforfunandprofit.com/posts/computation-expressions-intro/">Pyramid of Doom</a> [towards the middle of the article] that our friend Scott Wlaschin warned us about, that I knew I should avoid, but couldn't figure out how to, and didn't have the time to deeply work through. It <em>worked</em>, but it wasn't anything I was proud of. I haven't deeply dived into this part of Grace in a long time, and I knew it would be a challenge to get back into it.</p>
<h3 id="lets-fix.something">Let's fix... something</h3>
<p>I decided some weeks ago to start by rewriting <code>grace switch</code> to be more idiomatically functional, to clean it up as the first example of how to do it for the rest of the CLI commands. This was kind-of stupid, because it's the longest bit of code in all of the CLI commands, it's complex, and would be the most difficult to do. But it was deliberate: if I could make it work for <code>grace switch</code> I could make it work for the rest.</p>
<h4 id="before">Before</h4>
<p>The &quot;before&quot; code starts <a href="https://github.com/ScottArbeit/Grace/blob/c5657f56b56cd67b865f2abe33991b04efa91e62/src/Grace.CLI/Command/Branch.CLI.fs#L1200">here</a>, and runs for 342 lines. I won't quote it here, and I don't expect anyone to actually read it or comprehend the logic, but if you glance at it, you'll notice how deeply indented the code gets. At its longest, the indentation is -------------------------------------------------------------------- <em>this</em> long. Ugh.</p>
<p>What's worse is that because Grace has multiple output modes – Normal, Json, Verbose, Minimal, and Silent – I duplicated most of the code in this function: once for when I'm showing output, and once for when I'm not. (DRY? Pfft.) That means that if I find a bug, I have to remember to fix it twice. Making a long story short, the logic is:</p>
<pre><code>if showOutput then
    // Imagine lots of code with all of the steps to switch branches,
    //   with crazy levels of indentation, 
    //   and then show some output
else
    // Imagine almost exactly the same code,
    //   with crazy levels of indentation, 
    //   and then show little-to-no output
</code></pre>
<p>It's not my finest work.</p>
<h4 id="after">After</h4>
<p>After extracting the work steps into separate functions, it's much easier to understand and deal with.</p>
<p>Let's start with the main part of the new <code>grace switch</code> handler function. In the local function <code>generateResult</code>, look! monadic bind! With that defined, we check if we should show output, and either create those progress bars, or not:</p>
<pre><code class="language-fsharp">let generateResult (progressTasks: ProgressTask array) =
    task {
        let! result = 
            (showOutput, parseResult, switchParameters)
            |&gt; validateIncomingParameters
            &gt;&gt;=! getCurrentBranch progressTasks[0]
            &gt;&gt;=! readGraceStatusFile progressTasks[1]
            &gt;&gt;=! scanForDifferences progressTasks[2]
            &gt;&gt;=! getNewGraceStatusAndDirectoryVersions progressTasks[3]
            &gt;&gt;=! uploadChangedFilesToObjectStorage progressTasks[4]
            &gt;&gt;=! uploadNewDirectoryVersions progressTasks[5]
            &gt;&gt;=! createSaveReference progressTasks[6]
            &gt;&gt;=! getLatestVersionOfNewBranch progressTasks[7]
            &gt;&gt;=! UpdateWorkingDirectory progressTasks[8]

        match result with
            | Ok _ -&gt; return 0
            | Error error -&gt;
                logToAnsiConsole Colors.Error $&quot;{error}&quot;
                return -1
    }

if showOutput then
    return! progress.Columns(progressColumns)
            .StartAsync(fun progressContext -&gt;
            task {
                let t0 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString GettingCurrentBranch}[/]&quot;, autoStart = false)
                let t1 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString ReadingGraceStatus}[/]&quot;, autoStart = false)
                let t2 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString ScanningWorkingDirectory}[/]&quot;, autoStart = false)
                let t3 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString CreatingNewDirectoryVersions}[/]&quot;, autoStart = false)
                let t4 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString UploadingFiles}[/]&quot;, autoStart = false)
                let t5 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString SavingDirectoryVersions}[/]&quot;, autoStart = false)
                let t6 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString CreatingSaveReference}[/]&quot;, autoStart = false)
                let t7 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString GettingLatestVersion}[/]&quot;, autoStart = false)
                let t8 = progressContext.AddTask($&quot;[{Color.DodgerBlue1}]{UIString.getString UpdatingWorkingDirectory}[/]&quot;, autoStart = false)

                return! generateResult [| t0; t1; t2; t3; t4; t5; t6; t7; t8 |]
            })
else
    // If we're not showing output, we don't need to create the progress tasks.
    return! generateResult [| emptyTask; emptyTask; emptyTask; emptyTask; emptyTask; emptyTask; emptyTask; emptyTask; emptyTask |]

</code></pre>
<p>I even created a custom monadic bind function and operator for it!</p>
<pre><code class="language-fsharp">let bindTaskResult (result: Task&lt;Result&lt;'T, 'TError&gt;&gt;) (f: 'T -&gt; Task&lt;Result&lt;'U, 'TError&gt;&gt;) =
		(task {
				match! result with
				| Ok returnValue -&gt; return (f returnValue)
				| Error error -&gt; return Error error |&gt; returnTask
		}).Unwrap()

/// Custom monadic bind operator for the nested monad Task&lt;Result&lt;'T, 'TError&gt;&gt;.
let inline (&gt;&gt;=!) (result: Task&lt;Result&lt;'T, 'TError&gt;&gt;) (f: 'T -&gt; Task&lt;Result&lt;'U, 'TError&gt;&gt;) =
		bindTaskResult result f
</code></pre>
<p>And instead of all of it in one huge function, here's a sample of some of that logic broken down into a smaller function that's easy to understand, with fewer places for bugs to hide, that handles its own output. This is the part that checks for changes in your working directory:</p>
<pre><code class="language-fsharp">// 2. Scan the working directory for differences.
let scanForDifferences (t: ProgressTask) (showOutput, parseResult: ParseResult, parameters: CommonParameters, currentBranch: BranchDto) =
    task {
        t |&gt; startProgressTask showOutput
        let! differences = 
            if currentBranch.SaveEnabled then
                scanForDifferences newGraceStatus
            else
                List&lt;FileSystemDifference&gt;() |&gt; returnTask
        t |&gt; setProgressTaskValue showOutput 100.0
        return Ok (showOutput, parseResult, parameters, currentBranch, differences)
    }
</code></pre>
<h3 id="mission-accomplished">Mission Accomplished!</h3>
<p>So, yay! Cleaner code, easier to maintain, less duplication. Winning!</p>
<h3 id="uhh">uhh...</h3>
<p>But... not exactly. After I refactored the code and started testing it, I found some bugs, and those bugs go all the way back to when I wrote <code>grace watch</code> and <code>grace switch</code> in the first place. Some of those bugs have now been fixed. A few remain that I'm tracking down.</p>
<p>All of them have the same root causes: an early, less-developed sense of what good functional programming looks like, and a lack of careful thinking, reflected in a lack of carefully-written code.</p>
<h3 id="for-instance">For instance...</h3>
<blockquote>
<p><em>Why is this happening? Better add some</em></p>
<p><code>logToConsole $&quot;...stuff...&quot;</code>.</p>
<p>– Me, debugging code I wrote a couple of years ago</p>
</blockquote>
<p>In Services.CLI.fs, I have a function called <code>getNewGraceStatusAndDirectoryVersions</code>. You can see it <a href="https://github.com/ScottArbeit/Grace/blob/c5657f56b56cd67b865f2abe33991b04efa91e62/src/Grace.CLI/Command/Services.CLI.fs#L536">here</a>.</p>
<p>It's almost 200 lines long. That may or may not sound like a lot to you, but, to me, now used to F# and thinking in terms of small, single-purpose, composable functions, it's a lot, it's probably too much, and it's a code smell.</p>
<p>In my first job out of college, in 1991, as a mainframe COBOL programmer for AT&amp;T, I remember the senior programmer on the team telling me, &quot;Most people can only hold about a screenful of code in mind at any given time,&quot; and that was back when we used 24 x 80 character green-screen monitors!</p>
<p>That same senior programmer had worked on that code for its entire six-year existence, and had previously worked on the system it replaced for many years. When I asked him questions about the code, like why a certain module was written the way it was, he would look up, scratch his chin, and start into a story like &quot;well, about four or five years ago, there was this requirement...&quot; or &quot;in the old system, about ten years ago, &lt;something&gt;... and we never cleaned that up.&quot; It was <em>always</em> a story, and almost always something that, if you didn't have the context from that story, would be much more difficult to understand.</p>
<p>That experience led me to one of my most important rules for programming:</p>
<p><strong>Never write code that you have to tell a story about.</strong></p>
<p>200-line functions are not good for comprehensibility or maintainability. They invite subtle bugs. They're hard to debug when you've never seen them before. (And, apparently, even when I wrote it myself.) If you get asked about why they are they way they are, you probably have to tell a story, and that's not good.</p>
<p>My story for <code>getNewGraceStatusAndDirectoryVersions</code>: <em>I wasn't very good at this when I wrote it.</em> 🤷🏼‍♂️</p>
<p>Also, the function name itself is a code smell. If your function name has &quot;and&quot; in it, that's a good sign that it's doing too many things and should be broken down into smaller pieces.</p>
<p>That's exactly what I'm working on: refactoring <code>getNewGraceStatusAndDirectoryVersions</code> and some of its related functions that are called both by <code>grace watch</code> and <code>grace switch</code>. It's a slog. I was trying to think in functions back then, but they're not shaped well, they do too much, and they're just hard to understand. I don't want to tell that story. I don't want to leave that for the next maintainer, who could be Future Me.</p>
<p>(You may have noticed that I still have that name for a smaller function in my refactored <code>grace switch</code> code, and I'll be looking at that, too.)</p>
<h3 id="my-development-in-f">My development in F#</h3>
<p>So where am I in my F# journey?</p>
<p>Well, I've gotten a much better sense of &quot;how big is too big?&quot; and &quot;how complex is too complex?&quot; when creating and refactoring functions. I might write a first-draft of a new function that's a bit long, but I immediately start breaking it down into smaller parts that are easier to understand, so I don't have to tell any stories for someone else to maintain the code.</p>
<p>As I showed above, I've figured out how to use monads in interesting ways to help write concise, easy-to-understand code.</p>
<p>I'm comfortable thinking about functions as first-class language constructs that can be passed as parameters, and I've gone through all of the phases of &quot;I still use classes because I'm still embedded in OO thinking&quot; to &quot;I never use classes because I'm cool like that&quot; to &quot;I use classes where it makes sense&quot;.</p>
<p>And I'm 100% willing to dive in and rewrite some of the less elegant, older code that gets in the way of moving forward quickly.</p>
<p>Some of Grace is really clever and well-written, if I do say so myself. For instance, the way I do parameter validations in Server API endpoints is kind-of neat. I create an array of function calls that check the values passed in, return a custom error if they fail, and I execute them before I run the &quot;real&quot; action of the endpoint. It took a bit to figure out the right pattern, but once I did, repeating if for all of the API endpoints has been easy. Here's an example, from the endpoint to <a href="https://github.com/ScottArbeit/Grace/blob/c5657f56b56cd67b865f2abe33991b04efa91e62/src/Grace.Server/Organization.Server.fs#L144-L165">set the name of an organization</a>:</p>
<pre><code class="language-fsharp">let validations (parameters: SetOrganizationNameParameters) (context: HttpContext) =
    [| Guid.isValidAndNotEmpty parameters.OwnerId InvalidOwnerId
       String.isValidGraceName parameters.OwnerName InvalidOwnerName
       Input.eitherIdOrNameMustBeProvided parameters.OwnerId parameters.OwnerName EitherOwnerIdOrOwnerNameRequired
       Guid.isValidAndNotEmpty parameters.OrganizationId InvalidOrganizationId
       String.isValidGraceName parameters.OrganizationName InvalidOrganizationName
       Input.eitherIdOrNameMustBeProvided parameters.OrganizationId parameters.OrganizationName EitherOrganizationIdOrOrganizationNameRequired
       String.isNotEmpty parameters.NewName OrganizationNameIsRequired
       String.isValidGraceName parameters.NewName InvalidOrganizationName
       Organization.organizationExists parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationDoesNotExist
       Organization.organizationIsNotDeleted parameters.OwnerId parameters.OwnerName parameters.OrganizationId parameters.OrganizationName OrganizationIsDeleted |]
</code></pre>
<p><code>validations</code> here is a function that takes in a parameter object, and the HttpContext, and returns <code>ValueTask&lt;Result&lt;unit, OrganizationError&gt;&gt; array</code>. With that in place, when I run the validations, I can use a utility function <code>allPass</code> to say:</p>
<pre><code class="language-fsharp">let validationResults = validations parameters context
let! validationsPassed = validationResults |&gt; allPass
</code></pre>
<hr />
<p>I have some small but nice utility functions to serialize and deserialize objects, using Grace's custom <a href="https://github.com/ScottArbeit/Grace/blob/c5657f56b56cd67b865f2abe33991b04efa91e62/src/Grace.Shared/Constants.Shared.fs#L18-L48">JsonSerializerOptions</a>:</p>
<pre><code>/// Serializes an object to JSON, using Grace's custom JsonSerializerOptions.
let serialize&lt;'T&gt; item =
    JsonSerializer.Serialize&lt;'T&gt;(item, Constants.JsonSerializerOptions)

/// Serializes an object to JSON and writes it to a stream, using Grace's custom JsonSerializerOptions.
let serializeAsync&lt;'T&gt; stream item =
    task {
        return! JsonSerializer.SerializeAsync&lt;'T&gt;(stream, item, Constants.JsonSerializerOptions)
    }

/// Deserializes a JSON string to a provided type, using Grace's custom JsonSerializerOptions.
let deserialize&lt;'T&gt; (s: string) =
    JsonSerializer.Deserialize&lt;'T&gt;(s, Constants.JsonSerializerOptions)

/// Deserializes a stream of JSON to a provided type, using Grace's custom JsonSerializerOptions.
let deserializeAsync&lt;'T&gt; stream =
    task {
        return! JsonSerializer.DeserializeAsync&lt;'T&gt;(stream, Constants.JsonSerializerOptions)
    }
</code></pre>
<p>These let me say things like</p>
<pre><code>    let json = serialize myThing
    let myThing = deserialize&lt;MyThing&gt; json
		
    logToConsole $&quot;DirectoryVersion: {serialize directoryVersion}&quot;

    override this.ToString() = serialize this
</code></pre>
<p>etc. really easily wherever I need to.</p>
<hr />
<p>There's much more in Grace's code that I'd show you, but, the details aren't important for this.</p>
<p>I still have lots more to learn about F# and about functional programming.</p>
<p>I've never used <a href="https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters">statically-resolved type parameters (SRTP)</a>. I've never used or created a <a href="https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/">type provider</a>. I've never created a <a href="https://chat.openai.com/share/983151de-64ac-4ded-8b4d-220606495559">custom computation expression (CE)</a>, and I still don't have an intuitive grasp of when creating and using one would improve my code. I have a basic understanding of <a href="https://www.youtube.com/playlist?list=PLbgaMIhjbmEnaH_LTkxLI7FMa2HsnawM_">category theory</a>, but I'd love to deepen that and use more of the patterns from category theory where they make sense in my code. I've never written Haskell, and knowing Haskell, and being able to compare it to F#, would, no doubt, be helpful.</p>
<p>I say all of that to say: I'm not at the beginning of my journey in F# anymore. And I'm far from the end of it. I'm solidly in the middle, and that means that I'm good enough to be creative and concise, and to feel good about the code I'm writing now.</p>
<p>And, probably, one day, I'll look back on the code I write in 2024 and think, &quot;I can do that better now.&quot;</p>
<h3 id="weve-all-come-a-long-way">We've all come a long way</h3>
<p>Debugging and refactoring that older code over the last few weeks has been a trip. It's like visiting a younger version of myself.</p>
<p>Even though that code isn't as elegant as I'd like it to be, I'm still proud that I wrote it.</p>
<p>And you should be proud of where you are in your journey. If you're a programmer (and if you've read this far, you probably are) then you've had your experience of starting knowing nothing, writing and shipping code even though you weren't very good at it yet, and consistently improving, maybe even approaching mastery with the languages and tools you use.</p>
<p>I'm proud that I stuck with F#.</p>
<p>I'm proud that I'm still working on Grace.</p>
<p>I'm proud that I can see the progress I've made, and it gives me proof that I can still learn completely new things, move from beginner to experienced to mastery, and continue to develop both my art and craft.</p>
<p>So can you. I hope you will.</p>
<hr />
<div class="footnotes">
<hr />
<ol>
<li id="fn:1">
<p>See <a href="https://en.wikiquote.org/wiki/Blaise_Pascal#Quotes">https://en.wikiquote.org/wiki/Blaise_Pascal#Quotes</a>.<a href="#fnref:1" class="footnote-back-ref">&#8617;</a></p>
</li>
<li id="fn:2">
<p>When I say &quot;right now,&quot; I mean: I'm interrupting my programming to write the first draft of this blog post on December 20th, 10:00 PM, two days before publishing it.<a href="#fnref:2" class="footnote-back-ref">&#8617;</a></p>
</li>
<li id="fn:3">
<p>Yes, this means there's no <code>grace stash</code>, you don't need it.<a href="#fnref:3" class="footnote-back-ref">&#8617;</a></p>
</li>
</ol>
</div>
]]></description>
      <pubDate>Sat, 23 Dec 2023 08:50:28 GMT</pubDate>
      <guid isPermaLink="true">https://www.scottarbeit.com/blog/advent-of-code-2023-a-random-walk-in-the-direction-of-functional-enlightenment</guid>
    </item>
    <item>
      <title>Trying (and failing) to get the CosmosDB Emulator container working with Dapr</title>
      <link>https://www.scottarbeit.com/cosmosdb-emulator-dapr</link>
      <description><![CDATA[<p><em>Photo by <a href="https://unsplash.com/@bryangoffphoto">Bryan Goff</a> on <a href="https://unsplash.com/s/photos/cosmos">Unsplash</a>.</em></p>
<blockquote>
<p>TL;DR: I've tried everything I can to get Dapr to talk to the CosmosDB Emulator container, which ships with a self-signed certificate and which therefore fails making an https connection, and I'm giving up for now.</p>
</blockquote>
<p>If I find a solution to this, I'll write another blog post about it and link to it here. If there's no link, 🤷‍♂️.</p>
<h2 id="introduction">Introduction</h2>
<p>So... I'm working on my source control system, <a href="https://github.com/scottarbeit/grace">Grace</a>, and the next thing I'd like to do is to enable contributors (and myself) to work on it using a Docker Compose configuration.</p>
<p>Grace is built using <a href="https://dapr.io">Dapr</a>, which allows users to write code once, and then plug in over 100 different pieces of infrastructure, including cloud PaaS services, so that the platform can be chosen at runtime. I love lots of things about this, but one of the big ones is that you can imagine writing your code locally running on containers for state store, observability, secrets, pub/sub, etc. and then run that same exact code in production on completely different, larger, scalable infrastructure.</p>
<p>Right now, Grace is sort-of hard-coded to expect Azure CosmosDB as the state store under Dapr. There was a reason in earlier versions of Dapr that I needed to do this, but Dapr has since added the features I need to remove that specific code and replace it with higher-level Dapr queries.</p>
<p>As part of the process of getting a Docker Compose setup going, I figured, hey, CosmosDB has an <a href="https://learn.microsoft.com/en-us/azure/cosmos-db/docker-emulator-linux">emulator container</a> available. Might as well add that to Compose so it's there while I spend the time to rewrite the CosmosDB-specific parts into Dapr code.</p>
<h2 id="the-problem">The problem</h2>
<p>The CosmosDB emulator container ships expecting to be connected to over https, which is a reasonable thing these days, but it ships with a self-signed certificate, which makes connecting over https a bit challenging. The certificate it has is not sourced from a <a href="https://www.quora.com/What-is-a-root-certificate">root CA</a>, and, by default, every browser and HTTP stack will refuse to connect to a site with a broken chain-of-trust, and self-signed certificates do not have a chain-of-trust. This is same problem we run into when, for instance, we spin up a new web project, configure it to run on https, and then get a warning message in the browser when we try to connect to it.</p>
<p>In my F# code, I can bypass this by configuring my CosmosClient with a custom HttpClientFactory that provides HttpClient instances that bypass TLS checking. Because this is something that you never want to do in production, I've configured it to only happen in debug builds.</p>
<pre><code class="language-fsharp">
    let CosmosClient() = 
        if not &lt;| isNull cosmosClient then
            cosmosClient
        else
            let cosmosDbConnectionString = Environment.GetEnvironmentVariable(Constants.EnvironmentVariables.AzureCosmosDBConnectionString)
            let cosmosClientOptions = CosmosClientOptions(
                ApplicationName = Constants.GraceServerAppId, 
                EnableContentResponseOnWrite = false, 
                LimitToEndpoint = true, 
                Serializer = new CosmosJsonSerializer(Constants.JsonSerializerOptions))
#if DEBUG
            let httpClientFactory = fun () -&gt;
                let httpMessageHandler: HttpMessageHandler = new HttpClientHandler(
                    ServerCertificateCustomValidationCallback = (fun _ _ _ _ -&gt; true))
                new HttpClient(httpMessageHandler)
            cosmosClientOptions.HttpClientFactory &lt;- httpClientFactory
            cosmosClientOptions.ConnectionMode &lt;- ConnectionMode.Direct
#endif
            cosmosClient &lt;- new CosmosClient(cosmosDbConnectionString, cosmosClientOptions)
            cosmosClient

</code></pre>
<p>And in my Dapr config for a state store using Azure CosmosDB, I can specify the values to connect to the emulator.</p>
<pre><code class="language-yaml">apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: actorStorage
spec:
  type: state.azure.cosmosdb
  version: v1
  initTimeout: 1m
  metadata:
  - name: url
    value: https://localhost:8081/
	# This key is a well-known default value for the emulator.	
  - name: masterKey
    value: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
  - name: database
    value: gracevcs-development-db
  - name: collection
    value: grace-development
  - name: partitionKey
    value: &quot;/partitionKey&quot;
  - name: actorStateStore
    value: &quot;true&quot;
</code></pre>
<p>Cool so far. The problem comes when Dapr starts and tries to connect to the emulator.</p>
<h2 id="it-just-doesnt-work">It just doesn't work</h2>
<pre><code>{&quot;app_id&quot;:&quot;grace&quot;,&quot;instance&quot;:&quot;dd6c98246b75&quot;,&quot;level&quot;:&quot;fatal&quot;,&quot;msg&quot;:&quot;process component actorStorage error: [INIT_COMPONENT_FAILURE]: initialization error occurred for actorStorage (state.azure.cosmosdb/v1): Get \&quot;https://localhost:8081/dbs/gracevcs-development-db/colls/grace-development\&quot;: dial tcp 127.0.0.1:8081: connect: connection refused&quot;,&quot;scope&quot;:&quot;dapr.runtime&quot;,&quot;time&quot;:&quot;2022-12-03T01:08:37.581552869Z&quot;,&quot;type&quot;:&quot;log&quot;,&quot;ver&quot;:&quot;1.9.4&quot;}
</code></pre>
<p>And that <code>connection refused</code> error is happening because of the self-signed cert on the emulator. Sigh.</p>
<p>I'm running on Windows 11, using WSL 2 and Docker Desktop in Linux mode. I've tried to do the things listed in <a href="https://learn.microsoft.com/en-us/azure/cosmos-db/docker-emulator-linux?tabs=sql-api%2Cssl-netstd21#run-the-linux-emulator-on-linux-os">Microsoft's documentation</a> for the emulator container, including exporting the cert from the emulator and installing it into Linux. But, still, the connection is failing.</p>
<p>I don't have any way to tell Dapr to bypass TLS checking on connections, and I don't think they should have a flag in their code to even enable that. It's bad practice, I get that, and I don't want to give anyone a footgun they'll regret using.</p>
<h2 id="lets-call-it">Let's call it</h2>
<p>Usually you write a blog post announcing how awesome you are for having figured something out.</p>
<p>In this case, I'm just admitting defeat for now. Just wanted this up here in case anyone else is searching for information on this. I might revisit this in a few weeks, but, for now, I'm going to try using the installed CosmosDB Emulator, and if that doesn't work quickly, I'll just keep using actual CosmosDB until I can replace the code that needs it and just use Dapr's functionality.</p>
]]></description>
      <pubDate>Sat, 03 Dec 2022 06:15:35 GMT</pubDate>
      <guid isPermaLink="true">https://www.scottarbeit.com/cosmosdb-emulator-dapr</guid>
    </item>
    <item>
      <title>Configuring Azure Blob Storage and Azure CDN with Orchard CMS</title>
      <link>https://www.scottarbeit.com/blog/configuring-azure-blob-storage-and-azure-cdn-with-orchard-cms</link>
      <description><![CDATA[<blockquote>
<p>Photo by <a href="https://unsplash.com/@cbpsc1?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Clint Patterson</a></p>
</blockquote>
<p>I was setting up this very blog you're seeing, and wanted to try Orchard CMS. So far, I like it a lot.</p>
<p>For the media library - the part where I upload the images that go into the posts - I wanted to use an Azure Storage account, and it was a bit of a journey to get it set up.</p>
<p>The Orchard docs show how to configure everything in <code>appsettings.json</code>, but I wanted to set my configuration values in environment variables. to make sure I didn't check in any secrets to my source code.</p>
<p>And I wanted to put Azure CDN in front of all of the static files I serve. Why? Because I can. 🙂</p>
<p>Here's what I did.</p>
<h2 id="basic-steps">Basic steps</h2>
<ul>
<li>Enable the Media features in Orchard's admin site</li>
<li>Set up Azure CDN endpoints for both your Azure Storage account, and the web site itself</li>
<li>Configure the environment variables to tell Orchard to use Azure Storage for the Media Library</li>
<li>Configure the Site CDN in Orchard's admin</li>
<li>Restart the site so it picks up the new configuration</li>
</ul>
<h2 id="enable-media-features">Enable Media features</h2>
<p>First, in order to enable storing the Media Library in Azure Blob Storage, you need to make sure three features are enabled in Orchard.</p>
<ul>
<li>Media</li>
<li>Media Cache</li>
<li>Azure Media Storage</li>
</ul>
<p>To find them, in the <code>/admin</code> section of your site, go to <code>Configuration / Features</code>, and search for <code>Media</code>.</p>
<p><img src="https://cdn.scottarbeit.com/static/Features-Media.jpg"></p>
<h2 id="set-up-azure-cdn">Set up Azure CDN</h2>
<p>I have a Microsoft CDN (classic) already running, so that's what I'm using. Azure Front Door seems like a great service for web sites with significant traffic, but this blog isn't one of them, so I'm sticking with a &quot;regular&quot; CDN service.</p>
<p>I wanted to put Azure CDN in front of two things:</p>
<ul>
<li>the Azure Storage account that's holding my Media Library, i.e. all of the images on the site</li>
<li>the static files from the web server, i.e. CSS and JS and other stuff like that</li>
</ul>
<p>That meant that I needed to create two separate endpoints:</p>
<ul>
<li>scottarbeitstorage
<ul>
<li>Origin hostname of my Azure Storage account, scottarbeit.blob.core.windows.net</li>
<li>mapped to scottarbeitstorage.azureedge.net</li>
</ul>
</li>
<li>scottarbeitweb
<ul>
<li>Origin hostname of <a href="http://www.scottarbeit.com">www.scottarbeit.com</a></li>
<li>mapped to scottarbeitweb.azureedge.net</li>
</ul>
</li>
</ul>
<p>I also figured I'd create custom domain names for them. (Why not, right?)</p>
<p><img src="https://cdn.scottarbeit.com/static/AzureCdnConfig.jpg"></p>
<p>First, I needed to create two CNAME's in my domain's DNS entries:</p>
<ul>
<li>cdn.scottarbeit.com, mapped to scottarbeitstorage.azureedge.net</li>
<li>static.scottarbeit.com, mapped to scottarbeitweb.azureedge.net</li>
</ul>
<p>Then I needed to add them as custom domains in the configuration of each endpoint. It's simple enough to add them if you have the CNAME's you're going to use ready to go.</p>
<p>I strongly recommend, of course, that you enable HTTPS, allow Azure to create and manage the certificates you need - which is really, really nice - and set a minimum TLS version of 1.2.</p>
<p>Here's the results for the first custom domain I set up:</p>
<p><img src="https://cdn.scottarbeit.com/static/AzureCdnCustomDomain.jpg"></p>
<h2 id="configuring-environment-variables-for-the-media-library">Configuring environment variables for the Media Library</h2>
<p>I need to provide the connection string for Azure Storage, the name of the container that I'm using for the Media Library, the URL of the storage CDN endpoint we set up above, and a few other values. I want to use environment variables to keep secrets out of my code.</p>
<p>By setting the <code>CdnBaseUrl</code> value, Orchard will emit requests for files in the Media Library - images in my case - to come from the CDN instead of directly from Azure Storage.</p>
<p>In <code>appsettings.json</code> (or <code>appsettings.{Environment}.json</code>), we'd have:</p>
<pre><code class="language-json">{
  &quot;Logging&quot;: {
    ...
  },
  &quot;OrchardCore&quot;: {
    &quot;OrchardCore.Media&quot;: {
      &quot;CdnBaseUrl&quot;: &quot;https://cdn.mydomain.com&quot;,
      &quot;AssetsRequestPath&quot;: &quot;/static&quot;  // Default value is &quot;/media&quot;
    },
    &quot;OrchardCore.Media.Azure&quot;: {
      &quot;ConnectionString&quot;: &quot;DefaultEndpointsProtocol=https;AccountName=(your storage account name);AccountKey=(your storage account key);EndpointSuffix=core.windows.net&quot;,
      &quot;ContainerName&quot;: &quot;static&quot;,  // Default value is &quot;media&quot;
      &quot;BasePath&quot;: &quot;&quot;,
      &quot;CreateContainer&quot;: true
    }
  }
}
</code></pre>
<p>When translating a configuration hierarchy to environment variable names, substitute a double underscore <code>__</code> to denote a level, and replace periods in configuration key names with a single underscore <code>_</code>. That becomes:</p>
<ul>
<li><code>OrchardCore__OrchardCore_Media__CdnBaseUrl</code></li>
<li><code>OrchardCore__OrchardCore_Media__AssetsRequestPath</code></li>
<li><code>OrchardCore__OrchardCore_Media_Azure__ConnectionString</code></li>
<li><code>OrchardCore__OrchardCore_Media_Azure__ContainerName</code></li>
<li><code>OrchardCore__OrchardCore_Media_Azure__BasePath</code></li>
<li><code>OrchardCore__OrchardCore_Media_Azure__CreateContainer</code></li>
</ul>
<p>How you set your environment variables depends on how you deploy, of course. I'm in Azure App Service, so I set them in the Configuration page there.</p>
<p>With those values set, Orchard will read them at startup, you'll be able to keep them out of <code>appsettings.json</code>, and keep secrets out of your source code.</p>
<h2 id="configure-orchards-site-cdn">Configure Orchard's Site CDN</h2>
<p>The environment variables hold the CDN endpoint for the Media Library.</p>
<p>Now, I need to configure the CDN for the static files that are served from the web site itself, using the other CDN endpoint I created.</p>
<p>In the <code>/admin</code> site, go to <code>Configuration / Settings / General</code>, and click on the <code>Resources</code> tab. Then set the <code>Site CDN (Content Delivery Network) base url</code> to the static file CDN custom domain.</p>
<p><img src="https://cdn.scottarbeit.com/static/OrchardSiteCDNConfig.jpg"></p>
<h2 id="success">Success</h2>
<p>With all of that done, I have this very blog that you're reading accelerated through the use of Azure Storage and CDN's.</p>
<p>You should be able to get your instance of Orchard configured like this with a little patience and a little bit of time.</p>
]]></description>
      <pubDate>Wed, 16 Nov 2022 08:56:29 GMT</pubDate>
      <guid isPermaLink="true">https://www.scottarbeit.com/blog/configuring-azure-blob-storage-and-azure-cdn-with-orchard-cms</guid>
    </item>
  </channel>
</rss>