<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>stage.mikaberglund.com</title>
    <description>RSS feed for stage.mikaberglund.com</description>
    <link>https://stage.mikaberglund.com/feed</link>
    <item>
      <title>Add Static Page Generation to Blazor WebAssembly</title>
      <description>Add static page generation to your Blazor WebAssembly application with Blazorade Static Pages. Generate crawler-visible HTML, metadata, sitemaps, and Azure Static Web Apps routing while keeping your Blazor application as the source of truth.</description>
      <link>https://stage.mikaberglund.com/add-static-page-generation-to-blazor-webassembly</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/add-static-page-generation-to-blazor-webassembly</guid>
      <pubDate>Wed, 26 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="blazorade-staticpages" alt="Blazorade Static Pages" />
                
<figcaption>Blazorade Static Pages
</figcaption>
            
            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/use-microsoft-foundry-models-with-github-copilot-in-vs-code" role="button" aria-label="Previous article: Use Microsoft Foundry Models With GitHub Copilot in VS Code"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
            
</nav>

</figure>
            

            
<h1 id="add-static-page-generation-to-blazor-webassembly">Add Static Page Generation to Blazor WebAssembly
</h1>
            
<p class="article-meta">August 26, 2026
</p>

            
<h2 id="introduction">Introduction
</h2>
            
<p>Blazor WebAssembly is an interesting technology for building web applications that run .NET directly in the client browser without requiring a server-side application component. It is also a Single Page Application (SPA) technology, which means that it shares many of the same characteristics as applications built with frameworks such as React, Angular, and Vue.js.
</p>
            
<p>One of those characteristics is how content is rendered. The HTML returned by the server typically contains the application shell, while the actual page content is created by the application after it has started in the browser.
</p>
            
<p>That can be a problem when some pages contain public content that should be discoverable by search engines, social media crawlers, AI agents, and other bots. Crawlers that only download and inspect the static HTML never see content that exists only after the SPA has rendered it. Some crawlers can execute JavaScript and render the application, but many do not.
</p>
            
<p>This is where adding static page generation to Blazor WebAssembly can help.
</p>
            
<p><a href="https://www.nuget.org/packages/Blazorade.StaticPages">Blazorade Static Pages
</a> adds build-time static page generation to an existing Blazor WebAssembly application. Your application remains the source of truth, and you decide which pages and which parts of those pages should also be generated as static HTML.
</p>
            
<p>There are several ways to solve the discoverability problem, but I wanted an approach that would work with an ordinary Blazor WebAssembly application. I did not want to build a separate static site, duplicate routes, or make static content the foundation of the application.
</p>
            
<p>In this article, I will show you how Blazorade Static Pages works, how you can mix static and interactive content, and how to start using it in your own Blazor WebAssembly application.
</p>
            
<h2 id="what-about-blazorade-scraibe-then">What About Blazorade Scraibe Then?
</h2>
            
<p>I previously tried to solve the same problem with 
<a href="/blazorade-scraibe-making-blazor-content-discoverable/">Blazorade Scraibe
</a>.
</p>
            
<p>The main difference is the direction. Blazorade Scraibe is content-first: you start with static content and build a Blazor WebAssembly application around it. Blazorade Static Pages is application-first: you start with an existing Blazor WebAssembly application and add static page generation only where you need it.
</p>
            
<p>I believe the second approach fits Blazor WebAssembly better. These are primarily interactive applications, so it feels more natural to add discoverable static content to an existing application than to build the application around static content.
</p>
            
<p>For now, I have therefore abandoned the Blazorade Scraibe approach in favour of Blazorade Static Pages.
</p>
            
<h2 id="getting-started-with-blazorade-static-pages">Getting Started With Blazorade Static Pages
</h2>
            
<p>Getting started is quite simple. First, add the 
<a href="https://www.nuget.org/packages/Blazorade.StaticPages">Blazorade Static Pages NuGet package
</a> to your Blazor WebAssembly application.
</p>
            
<pre class="code-block" data-language="text"><code>
            
<span class="code-line">dotnet add package Blazorade.StaticPages
</span>
            
</code></pre>
            
<p>Then pick a routable page that you want to make available as static HTML and mark it with the 
<code>StaticPage
</code> attribute.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&#64;page &quot;/products&quot;
</span>
            
<span class="code-line">&#64;attribute [StaticPage]
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;StaticMetadata
</span>
            
<span class="code-line">    Title=&quot;Products&quot;
</span>
            
<span class="code-line">    Description=&quot;Explore our products.&quot; /&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;StaticContent&gt;
</span>
            
<span class="code-line">    &lt;h1&gt;Products&lt;/h1&gt;
</span>
            
<span class="code-line">    &lt;p&gt;Browse our product catalogue.&lt;/p&gt;
</span>
            
<span class="code-line">&lt;/StaticContent&gt;
</span>
            
</code></pre>
            
<p>There are three important parts here. The normal 
<code>&#64;page
</code> directive still defines the route, just like in any other Blazor application. The 
<code>StaticPage
</code> attribute tells Blazorade Static Pages that this route should participate in static generation. Finally, 
<code>StaticMetadata
</code> defines the metadata for the page, while 
<code>StaticContent
</code> defines the content that should be included in the generated HTML.
</p>
            
<p>When you build the application, Blazorade Static Pages scans the Razor source files and finds routable components marked with the 
<code>StaticPage
</code> attribute. It then analyzes their static metadata and content, generates HTML files, and copies those files to the application’s build output.
</p>
            
<p>Pages marked with 
<code>StaticPage
</code> are also included in the generated sitemap by default. If you have a static page that you do not want to include in the sitemap, you can disable that directly on the attribute.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&#64;attribute [StaticPage(IncludeInSitemap = false)]
</span>
            
</code></pre>
            
<p>The sitemap itself is generated when you have configured the public site URL for the application. I will come back to that later when we look at metadata and configuration in more detail.
</p>
            
<p>Pages that are not marked with 
<code>StaticPage
</code> are simply ignored by the generator and continue to behave just like any other page in your Blazor WebAssembly application. This means that you can add static generation gradually, only to the pages where you actually need it.
</p>
            
<h2 id="static-and-interactive-content">Static and Interactive Content
</h2>
            
<p>A Blazor WebAssembly page does not have to be either static or interactive. With Blazorade Static Pages, you can decide which parts of a page should be included in the generated static HTML and which parts should only exist when the Blazor application is actually running in the browser.
</p>
            
<p>The 
<code>StaticContent
</code> component marks content that should be included in the generated static page.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&lt;StaticContent&gt;
</span>
            
<span class="code-line">    &lt;h1&gt;Products&lt;/h1&gt;
</span>
            
<span class="code-line">    &lt;p&gt;Browse our product catalogue.&lt;/p&gt;
</span>
            
<span class="code-line">&lt;/StaticContent&gt;
</span>
            
</code></pre>
            
<p>The 
<code>StaticContent
</code> component itself is transparent. It does not add any extra HTML markup at runtime, and its wrapper is not emitted into the generated HTML either. Only the content inside it is included in the generated page.
</p>
            
<p>Sometimes you have content inside an otherwise static section that only makes sense when the Blazor application is running. This is where 
<code>InteractiveContent
</code> comes in.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&lt;StaticContent&gt;
</span>
            
<span class="code-line">    &lt;h1&gt;Products&lt;/h1&gt;
</span>
            
<span class="code-line">    &lt;p&gt;Browse our product catalogue.&lt;/p&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    &lt;InteractiveContent&gt;
</span>
            
<span class="code-line">        &lt;ProductConfigurator /&gt;
</span>
            
<span class="code-line">    &lt;/InteractiveContent&gt;
</span>
            
<span class="code-line">&lt;/StaticContent&gt;
</span>
            
</code></pre>
            
<p>The complete subtree inside 
<code>InteractiveContent
</code> is excluded from the generated static HTML, but it renders normally when the Blazor application runs in the browser. This makes it possible to have crawler-visible content and normal Blazor interactivity on the same page without maintaining two separate implementations.
</p>
            
<p>Reusable components can also participate in static generation. If a reusable component contains a 
<code>StaticContent
</code> section, that section becomes the component’s static representation.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&lt;StaticContent&gt;
</span>
            
<span class="code-line">    &lt;section class=&quot;product-summary&quot;&gt;
</span>
            
<span class="code-line">        &lt;h2&gt;&#64;Name&lt;/h2&gt;
</span>
            
<span class="code-line">        &lt;p&gt;Additional details are available interactively.&lt;/p&gt;
</span>
            
<span class="code-line">    &lt;/section&gt;
</span>
            
<span class="code-line">&lt;/StaticContent&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;InteractiveContent&gt;
</span>
            
<span class="code-line">    &lt;ProductEditor /&gt;
</span>
            
<span class="code-line">&lt;/InteractiveContent&gt;
</span>
            
</code></pre>
            
<p>When a page uses this component, Blazorade Static Pages includes only the component’s 
<code>StaticContent
</code> in the generated HTML. Ordinary markup outside 
<code>StaticContent
</code> is not included automatically, and a reusable component without a static representation contributes no static content.
</p>
            
<p>This explicit separation is intentional. Blazorade Static Pages does not try to execute arbitrary components and hope that their output can be turned into static HTML. Instead, you decide what is safe and meaningful to publish statically, while everything that depends on runtime behavior stays in the interactive application.
</p>
            
<h2 id="how-static-generation-works">How Static Generation Works
</h2>
            
<p>Blazorade Static Pages generates static HTML during the normal build of your Blazor WebAssembly application. The generator runs after the application has been built, scans the Razor source files, and looks for routable components marked with the 
<code>StaticPage
</code> attribute.
</p>
            
<p>The important thing to understand is that Blazorade Static Pages does not run your application. It does not execute components, invoke lifecycle methods, resolve services, call JavaScript, or fetch runtime data. Instead, it analyzes the source code and builds the static output from values and markup that can be determined at build time.
</p>
            
<p>This makes the generation process deterministic, but there is also a more fundamental reason for this restriction: we are talking about static content. Everything that is emitted into the generated HTML needs to be static in nature too.
</p>
            
<h3 id="reusing-compile-time-values">Reusing Compile-Time Values
</h3>
            
<p>You do not have to duplicate the same text in several places just because the generator works from source. Blazorade Static Pages can resolve supported compile-time string values and reuse them both in metadata and in static content.
</p>
            
<p>For instance, you can define the page title and description once and reference them from both 
<code>StaticMetadata
</code> and 
<code>StaticContent
</code>.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&#64;page &quot;/products&quot;
</span>
            
<span class="code-line">&#64;attribute [StaticPage]
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&#64;code {
</span>
            
<span class="code-line">    private const string PageTitle = &quot;Products&quot;;
</span>
            
<span class="code-line">    private const string PageDescription = &quot;Explore our products.&quot;;
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;StaticMetadata
</span>
            
<span class="code-line">    Title=&quot;&#64;PageTitle&quot;
</span>
            
<span class="code-line">    Description=&quot;&#64;PageDescription&quot; /&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;StaticContent&gt;
</span>
            
<span class="code-line">    &lt;h1&gt;&#64;PageTitle&lt;/h1&gt;
</span>
            
<span class="code-line">    &lt;p&gt;&#64;PageDescription&lt;/p&gt;
</span>
            
<span class="code-line">&lt;/StaticContent&gt;
</span>
            
</code></pre>
            
<p>The analyzer can also resolve supported constants from matching 
<code>.razor.cs
</code> code-behind files and from standard project C# files, including qualified references such as 
<code>&#64;Constants.Author
</code>.
</p>
            
<p>The key point is that these values must be constants. A 
<code>const
</code> value cannot change after compilation, which makes it safe to use as part of generated static HTML. A normal field, even a 
<code>static
</code> field, is still mutable and therefore not truly static in this sense.
</p>
            
<p>That distinction fits the purpose of the library quite well. If the generated HTML is supposed to be a deterministic static artifact, the values used to produce it must also be deterministic and unchangeable at build time.
</p>
            
<h3 id="what-cannot-be-resolved-at-build-time">What Cannot Be Resolved at Build Time
</h3>
            
<p>Because the application is not executed, runtime values cannot be used for static metadata or static content.
</p>
            
<p>That includes values that depend on things such as:
</p>
            
<ul><li>Dependency-injected services.
</li><li>Lifecycle methods.
</li><li>Property getters.
</li><li>Authentication or user-specific state.
</li><li>Browser APIs.
</li><li>External data fetched at runtime.
</li><li>Mutable fields or variables whose value can change.
</li></ul>
            
<p>Unsupported expressions cause a build error instead of silently producing incomplete static output.
</p>
            
<p>If some part of a page depends on runtime behavior, that content should stay in the normal Blazor application and be placed inside 
<code>InteractiveContent
</code> where appropriate.
</p>
            
<h3 id="generated-html">Generated HTML
</h3>
            
<p>Each generated page uses the application’s existing 
<code>wwwroot/index.html
</code> as its template. Blazorade Static Pages replaces the contents of the normal Blazor application root with the extracted static content, updates the page title and metadata, and keeps the rest of the application shell intact.
</p>
            
<p>This means that the generated document already contains meaningful HTML when it is downloaded, but it can still start the normal Blazor WebAssembly application afterwards. The static page is the baseline, and Blazor can enhance it with interactive functionality once the application is running in the browser.
</p>
            
<h2 id="metadata-and-configuration">Metadata and Configuration
</h2>
            
<p>Static HTML is only useful if the page also contains the metadata that search engines, social media platforms, and other crawlers expect. Blazorade Static Pages therefore generates metadata alongside the static page content.
</p>
            
<p>Page-specific metadata is defined with the 
<code>StaticMetadata
</code> component.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&#64;page &quot;/products&quot;
</span>
            
<span class="code-line">&#64;attribute [StaticPage]
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;StaticMetadata
</span>
            
<span class="code-line">    Title=&quot;Products&quot;
</span>
            
<span class="code-line">    Description=&quot;Explore our products.&quot;
</span>
            
<span class="code-line">    Author=&quot;Mika Berglund&quot;
</span>
            
<span class="code-line">    Image=&quot;images/products.jpg&quot;
</span>
            
<span class="code-line">    Locale=&quot;en-US&quot;
</span>
            
<span class="code-line">    Date=&quot;2026-08-26&quot; /&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;StaticContent&gt;
</span>
            
<span class="code-line">    &lt;h1&gt;Products&lt;/h1&gt;
</span>
            
<span class="code-line">    &lt;p&gt;Browse our product catalogue.&lt;/p&gt;
</span>
            
<span class="code-line">&lt;/StaticContent&gt;
</span>
            
</code></pre>
            
<p>The 
<code>Title
</code> parameter is required. The other metadata values are optional, but when supplied they also need to be compile-time-resolvable values, just like the static page content.
</p>
            
<p>From these values, Blazorade Static Pages generates the normal page title together with metadata such as description, author, Open Graph values, Twitter card metadata, locale, publication date, and image information.
</p>
            
<h3 id="configuring-the-public-site-url">Configuring the Public Site URL
</h3>
            
<p>Some metadata cannot be generated from the page itself. Canonical URLs and sitemap entries, for instance, need to know the public address where the application will eventually be hosted.
</p>
            
<p>You configure that in a 
<code>blazorade.config.json
</code> file next to your Blazor WebAssembly project file.
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">{
</span>
            
<span class="code-line">  &quot;staticPages&quot;: {
</span>
            
<span class="code-line">    &quot;siteUrl&quot;: &quot;https://www.example.com&quot;
</span>
            
<span class="code-line">  }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>The configured 
<code>siteUrl
</code> is combined with the normal Blazor route when generating canonical URLs.
</p>
            
<p>For example:
</p>
            
<pre class="code-block" data-language="text"><code>
            
<span class="code-line">https://www.example.com + /products
</span>
            
<span class="code-line">= https://www.example.com/products
</span>
            
</code></pre>
            
<p>That URL is then used for metadata such as the canonical link and 
<code>og:url
</code>.
</p>
            
<p>Blazorade Static Pages does not try to derive this URL from the host where the application happens to run. That would not be reliable, because the same build could be served locally, in a preview environment, or from the final production site.
</p>
            
<h3 id="sitemap-generation">Sitemap Generation
</h3>
            
<p>A sitemap is particularly important when you generate static pages, because it gives crawlers a clear list of the routes you want them to discover.
</p>
            
<p>When 
<code>staticPages.siteUrl
</code> is configured, Blazorade Static Pages generates a 
<code>sitemap.xml
</code> file containing the static pages in the application. Each sitemap URL is created from the configured site URL and the normal 
<code>&#64;page
</code> route.
</p>
            
<p>Static pages are included in the sitemap by default. If you have a page that should still be generated as static HTML but should not be advertised through the sitemap, you can exclude it with the 
<code>StaticPage
</code> attribute.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&#64;attribute [StaticPage(IncludeInSitemap = false)]
</span>
            
</code></pre>
            
<p>This lets static generation and sitemap inclusion remain separate decisions. A page can be available as static HTML without necessarily being listed in 
<code>sitemap.xml
</code>.
</p>
            
<p>If 
<code>siteUrl
</code> is not configured, the static pages are still generated, but canonical URLs and the sitemap are omitted.
</p>
            
<h3 id="static-web-apps-route-configuration">Static Web Apps Route Configuration
</h3>
            
<p>Blazorade Static Pages is primarily designed to be hosted with 
<a href="https://azure.microsoft.com/products/app-service/static">Azure Static Web Apps
</a>. That is also why the library generates a 
<code>staticwebapp.config.json
</code> file as part of the static output.
</p>
            
<p>The generated configuration contains an explicit rewrite for each static route, mapping the normal Blazor route to the corresponding generated HTML file. For example, a request to:
</p>
            
<pre class="code-block" data-language="text"><code>
            
<span class="code-line">/products
</span>
            
</code></pre>
            
<p>can be rewritten to:
</p>
            
<pre class="code-block" data-language="text"><code>
            
<span class="code-line">/products.html
</span>
            
</code></pre>
            
<p>This means that a crawler requesting 
<code>/products
</code> receives the generated static HTML directly instead of having to start and render the Blazor WebAssembly application first.
</p>
            
<p>The generated configuration also adds a navigation fallback to 
<code>/index.html
</code> for routes that are not backed by generated static pages. This preserves the normal SPA routing behaviour for the rest of the Blazor WebAssembly application. Static assets, framework files, the sitemap, and other known files are excluded from that fallback.
</p>
            
<p>In other words, Azure Static Web Apps is not just an arbitrary hosting option that happens to work with Blazorade Static Pages. It is the primary hosting model the library is designed around. Static routes are served directly as generated HTML, while the rest of the application continues to behave like a normal Blazor WebAssembly SPA.
</p>
            
<h3 id="configuration-specific-settings">Configuration-Specific Settings
</h3>
            
<p>You can also override configuration based on the active MSBuild configuration.
</p>
            
<p>For example:
</p>
            
<pre class="code-block" data-language="text"><code>
            
<span class="code-line">blazorade.config.json
</span>
            
<span class="code-line">blazorade.config.Debug.json
</span>
            
<span class="code-line">blazorade.config.Release.json
</span>
            
<span class="code-line">blazorade.config.Pre-Prod.json
</span>
            
</code></pre>
            
<p>Blazorade Static Pages first reads the default 
<code>blazorade.config.json
</code> file and then merges values from the configuration-specific file on top of it. This means that you can use normal Visual Studio or MSBuild build configurations without introducing a separate environment mechanism just for Static Pages.
</p>
            
<p>This is useful when different builds need different static generation settings while keeping the application itself unchanged.
</p>
            
<h2 id="migrating-my-wordpress-blog-to-blazorade-static-pages">Migrating My WordPress Blog to Blazorade Static Pages
</h2>
            
<p>My current blog is running on WordPress. It has worked well for me, but I am planning to migrate the site to a Blazor WebAssembly application that uses Blazorade Static Pages.
</p>
            
<p>There are a couple of reasons for this. First, I will move the site into a technology stack where I feel much more at home. I spend a lot of my time working with .NET, Blazor, Azure, and related technologies, so maintaining the blog as a Blazor application feels like a natural fit.
</p>
            
<p>The second reason is hosting. Blazor WebAssembly applications can be hosted as static files, which makes 
<a href="https://azure.microsoft.com/products/app-service/static">Azure Static Web Apps
</a> a very attractive option. Azure Static Web Apps starts with a free tier, so I expect the hosting costs for this blog to become significantly lower than they are today.
</p>
            
<p>Blazorade Static Pages is an important part of that plan. The blog will still be a Blazor WebAssembly application, but the public article pages can be generated as static HTML during the build. That means search engines, crawlers, and other bots can access the article content without having to execute and render the Blazor application first.
</p>
            
<p>I will write a separate article about the actual WordPress-to-Blazor migration once I have completed it. That article will cover the practical details of moving the content, routing, hosting, and everything else that turns out to be involved. When that article is available, I will update this article with a link to it.
</p>
            
<h2 id="common-questions-and-answers">Common Questions and Answers
</h2>
            
<h3 id="is-blazorade-static-pages-a-prerendering-solution">Is Blazorade Static Pages a prerendering solution?
</h3>
            
<p>No. Blazorade Static Pages does not execute the Blazor application or render components at build time. It analyzes the Razor source files and generates static HTML from content that can be resolved directly from the source.
</p>
            
<h3 id="does-blazorade-static-pages-replace-my-blazor-webassembly-application">Does Blazorade Static Pages replace my Blazor WebAssembly application?
</h3>
            
<p>No. Your Blazor WebAssembly application remains the source of truth. Static Pages only adds generated HTML for the routes you explicitly mark with 
<code>StaticPage
</code>.
</p>
            
<h3 id="can-a-page-contain-both-static-and-interactive-content">Can a page contain both static and interactive content?
</h3>
            
<p>Yes. Use 
<code>StaticContent
</code> for content that should be included in the generated HTML, and 
<code>InteractiveContent
</code> for parts that should only be available when the Blazor application runs in the browser.
</p>
            
<h3 id="can-reusable-components-contribute-static-content">Can reusable components contribute static content?
</h3>
            
<p>Yes. A reusable component can expose a static representation by placing that content inside 
<code>StaticContent
</code>. Only that explicitly declared static content is included in the generated page.
</p>
            
<h3 id="can-i-use-variables-and-runtime-values-in-static-content">Can I use variables and runtime values in static content?
</h3>
            
<p>Only compile-time-resolvable constants and supported string expressions can be used. Runtime values, services, lifecycle state, property getters, and other mutable or dynamic values are not evaluated during static generation.
</p>
            
<h3 id="are-all-blazor-pages-automatically-generated-as-static-pages">Are all Blazor pages automatically generated as static pages?
</h3>
            
<p>No. Only routable components marked with 
<code>&#64;attribute [StaticPage]
</code> are included in static generation. Other pages remain normal Blazor WebAssembly pages.
</p>
            
<h3 id="does-blazorade-static-pages-generate-a-sitemap">Does Blazorade Static Pages generate a sitemap?
</h3>
            
<p>Yes, when 
<code>staticPages.siteUrl
</code> is configured. Static pages are included in 
<code>sitemap.xml
</code> by default, but individual pages can be excluded with 
<code>IncludeInSitemap = false
</code>.
</p>
            
<h3 id="is-blazorade-static-pages-designed-for-azure-static-web-apps">Is Blazorade Static Pages designed for Azure Static Web Apps?
</h3>
            
<p>Primarily, yes. Blazorade Static Pages is designed with Azure Static Web Apps as its main hosting target, which is why it generates a 
<code>staticwebapp.config.json
</code> file as part of the build output. That file contains route rewrites for generated static pages and a navigation fallback for the rest of the Blazor WebAssembly application.
</p>
            
<h3 id="can-i-fetch-data-from-an-api-during-static-generation">Can I fetch data from an API during static generation?
</h3>
            
<p>No. The current generator does not execute application code or fetch runtime data. Content that depends on external data needs to remain runtime-only, or be represented through deterministic static content that is available at build time.
</p>
            
<h2 id="summary">Summary
</h2>
            
<p>Blazorade Static Pages takes an application-first approach to static page generation. You keep building your Blazor WebAssembly application the way you normally would, and add static generation only to the pages and content that need to be discoverable outside the running application.
</p>
            
<p>The generated HTML is created during the build without executing the application. Static content, metadata, sitemap entries, and Azure Static Web Apps routing configuration are all produced from information that can be resolved deterministically from the source.
</p>
            
<p>For me, this approach fits Blazor WebAssembly much better than starting from static content and building an application around it. Blazor WebAssembly is primarily an application technology, and Blazorade Static Pages adds static content capabilities without changing that model.
</p>
            
<p>I am also planning to use Blazorade Static Pages when I migrate this blog from WordPress to Blazor WebAssembly. I will cover that migration in a separate article once it is complete and update this article with a link to it.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Use Microsoft Foundry Models With GitHub Copilot in VS Code</title>
      <description>Use Microsoft Foundry models with GitHub Copilot in VS Code for flexible model choice, Azure billing, and pay-as-you-go AI development.</description>
      <link>https://stage.mikaberglund.com/use-microsoft-foundry-models-with-github-copilot-in-vs-code</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/use-microsoft-foundry-models-with-github-copilot-in-vs-code</guid>
      <pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="use-foundry-models-with-github-copilot-in-vs-code" alt="Use Microsoft Foundry Models With GitHub Copilot in VS Code" />
                
<figcaption>Use Microsoft Foundry Models With GitHub Copilot in VS Code
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/blazorade-scraibe-making-blazor-content-discoverable" role="button" aria-label="Previous article: Blazorade Scraibe: Making Blazor Content Discoverable"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/add-static-page-generation-to-blazor-webassembly" role="button" aria-label="Next article: Add Static Page Generation to Blazor WebAssembly"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="use-microsoft-foundry-models-with-github-copilot-in-vs-code">Use Microsoft Foundry Models With GitHub Copilot in VS Code
</h1>
            
<p class="article-meta">August 16, 2026
</p>

            
<p><a href="https://github.com/features/copilot">GitHub Copilot
</a> is a great way to add AI-assisted development to 
<a href="https://code.visualstudio.com/">VS Code
</a>. But what if you don’t want to decide up front which Copilot subscription gives you the models, features and capacity you need?
</p>
            
<p>By using your own models deployed in 
<a href="https://azure.microsoft.com/products/ai-foundry">Microsoft Foundry
</a>, you can use a more pay-as-you-go approach, where your costs are based on the models and resources you actually consume. You can also choose from models available in Microsoft Foundry that might not be available through your GitHub Copilot plan. In this article, I’ll show you how to connect these models to VS Code and use them in the same chat and agent workflows you are already familiar with from GitHub Copilot.
</p>
            
<h2 id="why-use-microsoft-foundry-models">Why Use Microsoft Foundry Models?
</h2>
            
<p>Many developers and architects use AI as a 
<strong>power tool
</strong> rather than as an autonomous developer. You know what you want to build and how you want to build it, but use AI to explain unfamiliar code, generate or modify code, write tests, refactor something, troubleshoot a problem, or simply get things done faster.
</p>
            
<p>If most of your AI usage happens by asking GitHub Copilot to do something from the VS Code Chat interface, models deployed in Microsoft Foundry can be a very interesting alternative.
</p>
            
<p>I have been using GitHub Copilot actively for around a year now, and models deployed in Microsoft Foundry with GitHub Copilot in VS Code for several months. During that time, I have tried different AI tools and integrations for VS Code, but I keep coming back to GitHub Copilot. I simply like how naturally it integrates with VS Code and fits into my everyday development workflow.
</p>
            
<p>Because I have been very pleased with how well Foundry models work with this setup, I thought it was worth sharing how I use them.
</p>
            
<p>By connecting VS Code to multiple Microsoft Foundry resources, you can also separate your AI usage costs between different Azure subscriptions, customers, or internal cost centers. The model usage is then billed to the corresponding Azure resources instead of everything being tied to one GitHub Copilot subscription.
</p>
            
<blockquote>I am not suggesting that models deployed in Microsoft Foundry can replace everything you get with a GitHub Copilot subscription. There are still GitHub Copilot features that require a proper Copilot plan. VS Code’s BYOK support applies to Chat and utility tasks, while features such as standard inline code completions, semantic search, and other features that rely on embeddings still depend on GitHub Copilot.
</blockquote>
            
<h2 id="models-i-use">Models I Use
</h2>
            
<p>At the time of writing, I mainly use two models from the GPT-5.6 family. 
<strong>GPT-5.6 Luna
</strong> has become my default workhorse, while I typically switch to 
<strong>GPT-5.6 Terra
</strong> for trickier problems. I have been very pleased with both, and Luna in particular has turned out to be a very capable, cost-effective, and fast model.
</p>
            
<p>The pricing makes Luna especially interesting for the kind of everyday AI-assisted development I describe in this article.
</p>
            
<p>To put that into perspective, here are my 
<strong>actual usage figures
</strong> after roughly two weeks of daily use. According to the monitoring information in Microsoft Foundry, I have consumed more than 
<strong>80 million input tokens
</strong> and over 
<strong>600,000 output tokens
</strong> with GPT-5.6 Luna. Looking at 
<strong>Cost Analysis
</strong> in the Azure portal, my actual total cost for that usage has been around 
<strong>€5.50
</strong>.
</p>
            
<p>These are not theoretical estimates or example calculations. They are the actual figures from my own Azure environment and my own daily usage.
</p>
            
<p>That is a lot of AI usage for the price of a couple of cups of coffee.
</p>
            
<p>Of course, your costs may be different. Pricing depends on factors such as the model, deployment type, region, how much context VS Code sends to the model, and your Azure pricing agreement. Still, my own numbers give a useful idea of why I find the pay-as-you-go approach so interesting for everyday development work.
</p>
            
<h2 id="setting-up-vs-code">Setting Up VS Code
</h2>
            
<blockquote><strong>Note!
</strong> The examples in this article were tested with 
<strong>Visual Studio Code 1.133
</strong>. The functionality described here was introduced in earlier versions of VS Code, but the user interface and available configuration options may differ between versions.
</blockquote>
            
<p>The setup described in this article uses the built-in language model support in VS Code, so you don’t need to install any extensions just to connect to your Foundry models. Azure and Microsoft Foundry is one of the model providers supported directly by VS Code.
</p>
            
<p>Before configuring VS Code, you need at least one suitable language model deployed in 
<a href="https://azure.microsoft.com/products/ai-foundry">Microsoft Foundry
</a>. In my case, I have a couple of different LLMs deployed so that I can switch between them depending on what I am working on.
</p>
            
<p>I will not go through the model deployment process in this article. I assume that you already know how to deploy models in Microsoft Foundry. What we need from each deployment is the endpoint, deployment name, and authentication details.
</p>
            
<p>For the setup in this article, I use an API key from my Foundry project. VS Code stores the key separately from the language model configuration, so you don’t have to add the actual key to the JSON configuration.
</p>
            
<p>For improved security, you can also restrict network access to your Microsoft Foundry resource. Foundry supports selected networks and private endpoints. If you restrict access this way, the computer running VS Code must naturally be able to reach the Foundry endpoint, which may mean connecting through your corporate network or VPN.
</p>
            
<h3 id="add-a-foundry-model-to-vs-code">Add a Foundry Model to VS Code
</h3>
            
<p>The first thing you need to do is open the VS Code Command Palette by pressing 
<strong>Ctrl + Shift + P
</strong>. Start typing 
<code>Chat: Manage Language Models
</code>, and select the 
<strong>Chat: Manage Language Models
</strong> command from the list.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-1" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-1" aria-label="Open image"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image.png" alt="image" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-1" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-1-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-1-label">image
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image.png" alt="image" /></div></div></div></div>
            
<p>This opens the 
<strong>Language Models
</strong> dialog shown below. This is where you can see the models currently available in VS Code and add new models from supported providers.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-2" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-2" aria-label="Open image-1-1024x523"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-1-1024x523.png" alt="image-1-1024x523" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-2" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-2-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-2-label">image-1-1024x523
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-1-1024x523.png" alt="image-1-1024x523" /></div></div></div></div>
            
<p>Click 
<strong>+ Add Models
</strong>, and select 
<strong>Azure
</strong> from the list of available model providers.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-3" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-3" aria-label="Open image-2"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-2.png" alt="image-2" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-3" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-3-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-3-label">image-2
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-2.png" alt="image-2" /></div></div></div></div>
            
<p>For every Microsoft Foundry resource you plan to use with GitHub Copilot in VS Code, you can define a separate group for its models. If you only plan to use one Foundry resource, you can simply leave the default group name as shown below.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-4" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-4" aria-label="Open image-3"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-3.png" alt="image-3" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-4" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-4-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-4-label">image-3
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-3.png" alt="image-3" /></div></div></div></div>
            
<p>After defining the group name, VS Code asks for the API key that it should use to authenticate with Microsoft Foundry.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-5" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-5" aria-label="Open image-4"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-4.png" alt="image-4" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-5" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-5-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-5-label">image-4
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-4.png" alt="image-4" /></div></div></div></div>
            
<p>You can find the API key on the 
<strong>Microsoft Foundry project home page
</strong>. Copy the key from there and paste it into VS Code when prompted.
</p>
            
<p>After you enter the key, VS Code opens the 
<code>chatLanguageModels.json
</code> configuration where you can finish configuring your models. The API key itself is stored separately and referenced from this configuration rather than being written directly into the JSON.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-6" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-6" aria-label="Open image-5-1024x522"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-5-1024x522.png" alt="image-5-1024x522" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-6" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-6-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-6-label">image-5-1024x522
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-5-1024x522.png" alt="image-5-1024x522" /></div></div></div></div>
            
<p>The configuration allows you to define one or more LLMs for the Foundry resource you are connecting to. If some of the attributes below are not included in the empty template, you can simply add them yourself.
</p>
            
<p>For each model, configure the following attributes:
</p>
            
<ul><li><code>id
</code>: The deployment name you used when you deployed the model to Microsoft Foundry.
</li><li><code>name
</code>: The name you want to see in the model drop-down in the VS Code Chat view.
</li><li><code>url
</code>: The endpoint that VS Code uses to communicate with the model. I will show you below how to get the correct URL.
</li><li><code>toolCalling
</code>: Set this to 
<code>true
</code> if the model supports tool calling. Tool calling is required if you want to use the model with agents in Chat.
</li><li><code>vision
</code>: Set this to 
<code>true
</code> if the model supports image input.
</li><li><code>thinking
</code>: Set this to 
<code>true
</code> if the model supports reasoning.
</li><li><code>maxInputTokens
</code>: The maximum number of input tokens available to the model.
</li><li><code>maxOutputTokens
</code>: The maximum number of output tokens the model can generate.
</li><li><code>editTools
</code>: Specifies which code editing tools VS Code can use with the model. I explicitly enable 
<code>apply-patch
</code>, 
<code>code-rewrite
</code>, 
<code>multi-find-replace
</code>, and 
<code>find-replace
</code>. The value for this attribute is a JSON array of strings.
</li></ul>
            
<p>The sum of 
<code>maxInputTokens
</code> and 
<code>maxOutputTokens
</code> must not exceed the context window supported by the model. The values therefore need to match the capabilities of the model you are configuring. The 
<code>editTools
</code> property is optional. If you leave it out, VS Code can try the available editing tools and choose one automatically.
</p>
            
<h3 id="getting-the-correct-model-url">Getting the Correct Model URL
</h3>
            
<p>The 
<code>url
</code> property is easy to get once you know where to look. Open the model deployment in Microsoft Foundry and go to the 
<strong>Details
</strong> tab. There you will find the 
<strong>Endpoint URL
</strong> for the model.
</p>
            
<p>For instance, the endpoint for my GPT-5.6 Luna deployment looks like this:
</p>
            
<p><code>https://&lt;foundry-instance-name&gt;.services.ai.azure.com/openai/v1/responses
</code></p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-7" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-7" aria-label="Open image-6-1024x340"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-6-1024x340.png" alt="image-6-1024x340" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-7" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-7-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-7-label">image-6-1024x340
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-6-1024x340.png" alt="image-6-1024x340" /></div></div></div></div>
            
<p>Copy that URL and use it as the value of the 
<code>url
</code> property in your VS Code model configuration.
</p>
            
<h3 id="complete-configuration-example">Complete Configuration Example
</h3>
            
<p>With the model details filled in, the configuration looks something like this:
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">[
</span>
            
<span class="code-line">  {
</span>
            
<span class="code-line">    &quot;name&quot;: &quot;Azure&quot;,
</span>
            
<span class="code-line">    &quot;vendor&quot;: &quot;azure&quot;,
</span>
            
<span class="code-line">    &quot;apiKey&quot;: &quot;${input:chat.lm.secret.-15291caa}&quot;,
</span>
            
<span class="code-line">    &quot;models&quot;: [
</span>
            
<span class="code-line">      {
</span>
            
<span class="code-line">        &quot;id&quot;: &quot;gpt-5.6-luna&quot;,
</span>
            
<span class="code-line">        &quot;name&quot;: &quot;gpt-5.6-luna&quot;,
</span>
            
<span class="code-line">        &quot;url&quot;: &quot;https://&lt;foundry-instance-name&gt;.services.ai.azure.com/openai/v1/responses&quot;,
</span>
            
<span class="code-line">        &quot;toolCalling&quot;: true,
</span>
            
<span class="code-line">        &quot;vision&quot;: true,
</span>
            
<span class="code-line">        &quot;thinking&quot;: true,
</span>
            
<span class="code-line">        &quot;maxInputTokens&quot;: 128000,
</span>
            
<span class="code-line">        &quot;maxOutputTokens&quot;: 16000,
</span>
            
<span class="code-line">        &quot;editTools&quot;: [
</span>
            
<span class="code-line">          &quot;apply-patch&quot;,
</span>
            
<span class="code-line">          &quot;code-rewrite&quot;,
</span>
            
<span class="code-line">          &quot;multi-find-replace&quot;,
</span>
            
<span class="code-line">          &quot;find-replace&quot;
</span>
            
<span class="code-line">        ]
</span>
            
<span class="code-line">      }
</span>
            
<span class="code-line">    ]
</span>
            
<span class="code-line">  }
</span>
            
<span class="code-line">]
</span>
            
</code></pre>
            
<p>The 
<code>apiKey
</code> value is a reference generated by VS Code. Your value will therefore be different from the one shown above.
</p>
            
<p>The token limits in this example are also just configuration values for this particular model. Make sure you use values that match the model you have deployed.
</p>
            
<p>If you have several models deployed in the same Microsoft Foundry resource, simply add more model objects to the 
<code>models
</code> array.
</p>
            
<p>When you are done, press 
<strong>Ctrl + S
</strong> to save the configuration. Your Foundry model should now be available in the model picker in the VS Code Chat view.
</p>
            
<h3 id="configure-the-utility-models">Configure the Utility Models
</h3>
            
<p>VS Code uses separate utility models for some background AI tasks instead of the model you have selected in the Chat view. These tasks include generating titles, commit messages, branch names, and other lightweight operations.
</p>
            
<p>To configure these models, open the Command Palette with 
<strong>Ctrl + Shift + P
</strong> and run:
</p>
            
<p><code>Preferences: Open User Settings
</code></p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-8" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-8" aria-label="Open image-7"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-7.png" alt="image-7" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-8" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-8-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-8-label">image-7
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-7.png" alt="image-7" /></div></div></div></div>
            
<p>In the Settings dialog, search for 
<strong>utility model
</strong>.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-9" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-9" aria-label="Open image-8-1024x451"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-8-1024x451.png" alt="image-8-1024x451" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-9" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-9-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-9-label">image-8-1024x451
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-8-1024x451.png" alt="image-8-1024x451" /></div></div></div></div>
            
<p>You should see the following two settings:
</p>
            
<ul><li><strong>Chat: Utility Model
</strong></li><li><strong>Chat: Utility Small Model
</strong></li></ul>
            
<p>Select the Microsoft Foundry model you configured earlier for both settings.
</p>
            
<p>For my setup, I use 
<strong>GPT-5.6 Luna
</strong> for both. It is fast, capable, and well suited for smaller tasks such as generating commit messages.
</p>
            
<p>The two settings actually serve slightly different purposes. 
<strong>Chat: Utility Model
</strong> is used for general utility tasks such as summaries, settings search, and Git review. 
<strong>Chat: Utility Small Model
</strong> handles faster and lighter tasks such as commit messages, branch names, rename suggestions, and intent detection.
</p>
            
<h3 id="configure-default-models-for-different-tasks">Configure Default Models for Different Tasks
</h3>
            
<p>You can also configure which model VS Code should use by default for different AI tasks. For instance, you might want to use one model for planning and another for everyday utility tasks.
</p>
            
<p>Open 
<strong>User Settings
</strong> again and search for 
<strong>default model
</strong>:
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-10" data-bs-toggle="modal" data-bs-target="#use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-10" aria-label="Open image-9-1024x656"><img class="article-content-image" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-9-1024x656.png" alt="image-9-1024x656" /></a></figure><div class="modal fade" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-10" tabindex="-1" aria-labelledby="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-10-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="use-microsoft-foundry-models-with-github-copilot-in-vs-code-image-10-label">image-9-1024x656
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/use-microsoft-foundry-models-with-github-copilot-in-vs-code/image-9-1024x656.png" alt="image-9-1024x656" /></div></div></div></div>
            
<p>VS Code will show settings where you can select default models for different tasks and agents. For example, the Plan agent has its own default model setting.
</p>
            
<p>For my setup, I can use 
<strong>GPT-5.6 Terra
</strong> as the default model for planning while keeping 
<strong>GPT-5.6 Luna
</strong> as the faster and more cost-effective model for everyday utility tasks.
</p>
            
<p>This gives you quite a bit of flexibility. You don’t have to use the same model everywhere. You can use a fast and inexpensive model for routine work and reserve a more capable model for tasks where the additional reasoning is actually useful.
</p>
            
<h2 id="common-questions">Common Questions
</h2>
            
<h3 id="do-i-still-need-a-github-copilot-plan">Do I Still Need a GitHub Copilot Plan?
</h3>
            
<p>Not necessarily.
</p>
            
<p>VS Code supports BYOK models for Chat and utility tasks without signing in to GitHub and without a GitHub Copilot plan. However, BYOK does not replace every Copilot feature. Standard inline code completions, semantic search, and features that depend on embeddings still require GitHub Copilot.
</p>
            
<p>If your main use case is interacting with AI through the VS Code Chat interface, your own Microsoft Foundry models can therefore cover a significant part of your everyday AI usage.
</p>
            
<h3 id="can-i-configure-more-than-one-microsoft-foundry-resource">Can I Configure More Than One Microsoft Foundry Resource?
</h3>
            
<p>Yes. You can create separate Azure model groups in VS Code for different Microsoft Foundry resources.
</p>
            
<p>This can be useful if you want to separate usage between different Azure subscriptions, customers, or internal cost centers.
</p>
            
<h3 id="can-i-configure-several-models-from-the-same-foundry-resource">Can I Configure Several Models From the Same Foundry Resource?
</h3>
            
<p>Yes. Simply add multiple model definitions to the 
<code>models
</code> array for the same Azure group.
</p>
            
<p>This is how I configure different models for different purposes. For instance, I use GPT-5.6 Luna for most everyday tasks and GPT-5.6 Terra when I need a bit more reasoning power.
</p>
            
<h3 id="do-i-have-to-use-the-same-model-everywhere-in-vs-code">Do I Have to Use the Same Model Everywhere in VS Code?
</h3>
            
<p>No. You can configure different models for different tasks.
</p>
            
<p>You can select a model directly from the VS Code Chat model picker, configure separate utility models, and define default models for specific agents and tasks.
</p>
            
<h2 id="summary">Summary
</h2>
            
<p>Using your own models from Microsoft Foundry with GitHub Copilot in VS Code gives you a lot of flexibility without changing the development environment you are already familiar with.
</p>
            
<p>You can choose the models you want to use, pay for the model usage through Azure, configure different models for different tasks, and even separate usage between different Foundry resources and Azure subscriptions.
</p>
            
<p>For me, this has turned out to be a very practical setup. I get to keep the GitHub Copilot experience in VS Code that I already like, while having much more control over which models I use and how that usage is billed.
</p>
            
<p>If you mainly use AI as a power tool to help you work faster, rather than relying on autonomous cloud-based development features, using your own Microsoft Foundry models with GitHub Copilot in VS Code is definitely worth trying.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>About Me</title>
      <description>Learn more about Mika Berglund, a Lead Cloud Architect specializing in Microsoft Azure and Microsoft 365.</description>
      <link>https://stage.mikaberglund.com/about</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/about</guid>
      <pubDate>Sun, 10 May 2026 17:30:00 GMT</pubDate>
      <content:encoded><![CDATA[
        
<div class="content-page">
        
<article class="content-article">
            
<div class="eyebrow"><span></span> A little context
</div>
            
<h1>About Me
</h1>

            
<figure class="about-photo">
                
<img src="@Image" alt="Mika Berglund" width="300" />
                
<figcaption>Mika Berglund
</figcaption>
            
</figure>

            
            
<p>I am a Lead Cloud Architect based in Helsinki, Finland, specializing in Microsoft Azure, Microsoft 365, enterprise cloud architecture, automation, identity, and modern Microsoft technologies. I have worked professionally with Microsoft technologies since the mid-1990s and started working with Azure and Office 365 before they reached general availability.
</p>

            
<p>My work focuses on designing secure, scalable, and maintainable systems based on Microsoft cloud technologies. I work extensively with Azure services, Microsoft 365, automation, identity and access management, C#, and .NET, helping organizations apply these technologies in ways that are practical, maintainable, and valuable in everyday operations. My approach combines long-term architectural thinking with hands-on implementation experience.
</p>

            
<p>At 
<a href="https://integrata.fi">Integrata Oy
</a>, I design and develop Azure-based systems and Microsoft 365 integrations that support payroll and HR services. My role combines cloud architecture, software engineering, automation, and system integration to create practical solutions for business-critical processes.
</p>

            
<p>In 2021, I founded 
<strong>Denomica Oy
</strong>, through which I provide consulting, architecture, and development services focused on Microsoft cloud technologies and modern Azure-based solutions. Through Denomica, I also explore emerging Microsoft cloud capabilities, particularly around automation, identity, and AI, while contributing to open-source and experimental projects.
</p>

            
<p>I am particularly interested in how modern cloud platforms, automation, and AI can improve the way organizations use and manage information across systems and processes.
</p>

            
<p>My professional background spans several decades across cloud architecture, enterprise systems, software development, and Microsoft technologies. During my career, I have worked at companies including 
<em>Visual Systems
</em>, 
<em>Tieto
</em>, 
<em>Sininen Meteoriitti
</em>, 
<em>Develore
</em>, 
<em>Valtti Kumppanit
</em>, and 
<em>Integrata
</em>. Earlier in my career, I also gained experience in the retail, energy production, and industrial construction industries, which gave me valuable insight into how technology supports real-world operations.
</p>

            
<p>Outside work, I contribute to open-source projects on GitHub and write about cloud architecture, Microsoft technologies, AI, automation, semantic technologies, and modern software development on this blog.
</p>

            
<p>If you would like to discuss Azure, Microsoft 365, cloud architecture, integrations, automation, or system design, feel free to reach out and network with me on 
<a href="https://github.com/mikaberglund" target="_blank">GitHub
</a> or 
<a href="https://www.linkedin.com/in/mikaberglund/" target="_blank">LinkedIn
</a>.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Blazorade Scraibe: Making Blazor Content Discoverable</title>
      <description>Blazor WASM apps often hide content from search and AI tools. Learn how Blazorade Scraibe makes your content discoverable without losing interactivity.</description>
      <link>https://stage.mikaberglund.com/blazorade-scraibe-making-blazor-content-discoverable</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/blazorade-scraibe-making-blazor-content-discoverable</guid>
      <pubDate>Sat, 02 May 2026 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="blazorade-scraibe" alt="Blazorade Scraibe: Making Blazor Content Discoverable" />
                
<figcaption>Blazorade Scraibe: Making Blazor Content Discoverable
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/protecting-apis-with-microsoft-entra-id" role="button" aria-label="Previous article: Protecting APIs with Microsoft Entra ID"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/use-microsoft-foundry-models-with-github-copilot-in-vs-code" role="button" aria-label="Next article: Use Microsoft Foundry Models With GitHub Copilot in VS Code"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="blazorade-scraibe-making-blazor-content-discoverable">Blazorade Scraibe: Making Blazor Content Discoverable
</h1>
            
<p class="article-meta">May 2, 2026
</p>

            
<p>If you’ve ever built a 
<a href="https://learn.microsoft.com/aspnet/core/blazor/hosting-models#blazor-webassembly">Blazor WebAssembly
</a> application with real content, you may have run into a frustrating problem. Everything works in the browser, but search engines and AI tools struggle to see your content.
</p>
            
<p>The reason is simple. Your content does not exist until the application runs in the browser.
</p>
            
<p>This is not specific to Blazor. The same issue affects all 
<a href="https://en.wikipedia.org/wiki/Single-page_application">Single Page Application (SPA)
</a> frameworks, including 
<a href="https://react.dev/">React
</a>, 
<a href="https://angular.dev/">Angular
</a>, and 
<a href="https://vuejs.org/">Vue
</a>. The initial HTTP response only returns bootstrap logic, and JavaScript renders the actual content later. Many bots and AI agents never execute that step. They fetch the HTML, see an empty shell, and move on.
</p>
            
<p>At the same time, something else has changed. With tools like 
<a href="https://github.com/features/copilot">GitHub Copilot
</a>, you are no longer building alone. You can treat Copilot as a relentless site builder that helps you write content, structure pages, and build components as you go.
</p>
            
<p>Blazorade Scraibe is built around that idea. It combines static content generation with a set of conventions and guidance that work well with Copilot, so you can generate content, structure your site, and build interactive components with its help.
</p>
            
<p>Blazorade Scraibe is an ongoing project. Features evolve based on real-world use, community contributions, and my own work on improving the project.
</p>
            
<h2 id="the-problem-with-spa-applications-and-content-visibility">The Problem With SPA Applications and Content Visibility
</h2>
            
<h3 id="how-spa-applications-work">How SPA Applications Work
</h3>
            
<p><a href="https://learn.microsoft.com/aspnet/core/blazor/hosting-models#blazor-webassembly">Blazor WebAssembly
</a>, 
<a href="https://react.dev/">React
</a>, 
<a href="https://angular.dev/">Angular
</a>, and other SPA frameworks all follow the same basic model. When a browser requests a page, the server returns a minimal HTML document along with JavaScript that bootstraps the application. The application then runs in the browser and renders the actual content.
</p>
            
<p>This approach works well for users. It gives you fast navigation, rich interactivity, and a smooth experience overall. But it also shifts where the content exists. Instead of being part of the initial response, the content only appears after the application has started running in the browser.
</p>
            
<h3 id="what-bots-and-ai-agents-actually-see">What Bots and AI Agents Actually See
</h3>
            
<p>This difference becomes obvious when something other than a browser accesses your site. When a bot sends an HTTP request, it typically only reads the initial HTML response and does not execute any JavaScript.
</p>
            
<p>In that situation, the bot never sees the rendered content. It only sees the application shell. From its point of view, the page is mostly empty, even though a real user would see a fully rendered view a moment later.
</p>
            
<p>You can test this yourself. Try loading any page from your SPA application using tools like 
<a href="https://www.postman.com/">Postman
</a> or 
<a href="https://www.usebruno.com/">Bruno
</a> and look at the response. It will not match what you see in the browser. Then try the same with the 
<a href="https://blazorade.com/scraibe-docs">Blazorade Scraibe documentation site
</a>. That site runs as a 
<a href="https://learn.microsoft.com/aspnet/core/blazor/hosting-models#blazor-webassembly">Blazor WebAssembly
</a> application on 
<a href="https://learn.microsoft.com/azure/static-web-apps/overview">Azure Static Web Apps
</a>, even on the free tier, but still returns fully readable content in the initial response.
</p>
            
<h3 id="why-this-matters">Why This Matters
</h3>
            
<p>For applications that focus purely on functionality, this may not be a big issue. But as soon as your application contains content that you want others to find, things change.
</p>
            
<p>Search engines rely on readable HTML to index your pages. AI tools rely on content they can directly read and process. If your content is not present in the initial response, it becomes much harder for these systems to understand and use it. The result is reduced visibility, weaker indexing, and less reliable previews.
</p>
            
<h3 id="this-is-not-a-blazor-problem">This Is Not a Blazor Problem
</h3>
            
<p>It is worth being clear about one thing. This is not a limitation of Blazor. It is a direct consequence of client-side rendering.
</p>
            
<p>You will see the same behavior in 
<a href="https://react.dev/">React
</a>, 
<a href="https://angular.dev/">Angular
</a>, 
<a href="https://vuejs.org/">Vue
</a>, and any other framework that renders content in the browser. If the content is not part of the initial HTML response, it remains invisible until something executes the code.
</p>
            
<h2 id="typical-solutions-and-their-trade-offs">Typical Solutions and Their Trade-offs
</h2>
            
<p>Once you run into this problem, there are a few ways to address it. Each of them solves the visibility issue, but they come with different trade-offs.
</p>
            
<h3 id="server-side-rendering-ssr">Server-Side Rendering (SSR)
</h3>
            
<p>One common solution is to move rendering to the server. With 
<a href="https://learn.microsoft.com/aspnet/core/blazor/fundamentals/#render-modes">server-side rendering in Blazor
</a>, the server generates HTML before sending the response. This ensures that both users and bots receive fully rendered content.
</p>
            
<p>This approach solves the visibility problem, but it changes the nature of the application. You now depend on a server to handle every request, which adds complexity and introduces a different cost model. As traffic grows, so does the need to scale your backend.
</p>
            
<p>For many applications, this is a perfectly valid trade-off. But if your goal is to keep the simplicity of static hosting, it may not be the ideal fit.
</p>
            
<h3 id="static-site-generators">Static Site Generators
</h3>
            
<p>Another option is to move away from SPA rendering entirely and generate static HTML using tools like 
<a href="https://gohugo.io/">Hugo
</a> or 
<a href="https://docusaurus.io/">Docusaurus
</a>. These tools produce fully rendered pages at build time, which makes the content easy to index and process.
</p>
            
<p>This solves the discoverability problem without requiring a server at runtime. The trade-off is that you lose the native interactivity of a Blazor application. Adding dynamic behavior becomes harder, and you often end up mixing different technologies to get the same level of functionality.
</p>
            
<h2 id="the-blazorade-scraibe-approach">The Blazorade Scraibe Approach
</h2>
            
<p>Blazorade Scraibe takes a different approach to the problem. Instead of rendering content on every request, or relying entirely on client-side rendering, it generates the content ahead of time and serves it as static HTML.
</p>
            
<p>The idea is simple. Generate the content at build time, and use Blazor at runtime to enhance the experience.
</p>
            
<p>During the build process, Blazorade Scraibe converts your content into static HTML pages. That content becomes part of the initial HTTP response, which means search engines and AI tools can read it directly without executing any JavaScript.
</p>
            
<p>Each generated page also acts as a bootstrapper for the 
<a href="https://learn.microsoft.com/aspnet/core/blazor/hosting-models#blazor-webassembly">Blazor WebAssembly
</a> application. No matter which page you load, it already contains both the rendered content and the logic required to start the application.
</p>
            
<p>You can observe this in practice on the 
<a href="https://blazorade.com/">Blazorade website
</a>. When you reload the front page, you briefly see the static content. It then clears and shows a “loading…” message while the Blazor app initializes, before rendering the final interactive view.
</p>
            
<p>What makes this approach especially interesting today is how it fits with tools like 
<a href="https://github.com/features/copilot">GitHub Copilot
</a>. Instead of manually wiring everything together, you can use Copilot to help you write content, structure pages, and build components as you go.
</p>
            
<p>Blazorade Scraibe leans into that workflow. It provides a structure and a set of conventions that work well with Copilot, so you can treat it as a kind of site builder that helps you move faster without losing control.
</p>
            
<p>In practice, this means you can focus more on the content and the experience, and let the tooling help you with the repetitive parts.
</p>
            
<h2 id="core-features-of-blazorade-scraibe">Core Features of Blazorade Scraibe
</h2>
            
<p>Blazorade Scraibe is built around a few core ideas that work together to solve the discoverability problem while keeping the flexibility of a Blazor application.
</p>
            
<h3 id="github-copilot-as-a-site-builder">GitHub Copilot as a Site Builder
</h3>
            
<p>Blazorade Scraibe is designed specifically for 
<a href="https://github.com/features/copilot">GitHub Copilot
</a>. In this context, Copilot becomes more than a coding assistant. It acts as a site builder that helps you create content, structure your site, and build components as you go.
</p>
            
<p>Instead of manually wiring everything together, you describe what you want. Copilot helps you generate pages, organize content, and implement components that fit into the Scraibe structure. You are not starting from a blank page, and you are not working alone.
</p>
            
<p>This changes how you approach building content-driven sites. You focus on the content and the experience, while the tooling helps you move faster and stay consistent.
</p>
            
<h3 id="static-content-generation">Static Content Generation
</h3>
            
<p>At the core of Scraibe is static content generation. During the build process, your content is converted into HTML and included directly in each page.
</p>
            
<p>This ensures that the full content is available in the initial response. Search engines and AI tools can read it without executing any JavaScript, which makes the content discoverable and usable outside of the browser.
</p>
            
<h3 id="content-first-structure">Content-First Structure
</h3>
            
<p>Scraibe uses a content-first approach based on 
<a href="https://en.wikipedia.org/wiki/Markdown">Markdown
</a> and a simple folder structure. Pages are defined as files, and sections are defined by folders.
</p>
            
<p>This keeps the content easy to organize and maintain, while also making it straightforward to understand how the site is structured.
</p>
            
<h3 id="designed-for-azure-static-web-apps">Designed for Azure Static Web Apps
</h3>
            
<p>Blazorade Scraibe is primarily designed to run as a static site in 
<a href="https://learn.microsoft.com/azure/static-web-apps/overview">Azure Static Web Apps
</a>. This gives you a simple deployment model with global distribution and low hosting costs.
</p>
            
<p>You can host it elsewhere, but Azure Static Web Apps is a natural fit for this type of application.
</p>
            
<h3 id="shortcodes-for-blazor-components">Shortcodes for Blazor Components
</h3>
            
<p>You add Blazor components to your Markdown content through shortcodes.
</p>
            
<p>A shortcode is a placeholder in your content that represents a full-featured Blazor component. When the application runs, Scraibe replaces that shortcode with the component it points to. That component can be something simple, like a button or carousel, or something much more advanced, like a form or an interactive tool that connects to a backend.
</p>
            
<p>This approach keeps your content clean and easy to work with, while still giving you the full power of Blazor wherever you need it.
</p>
            
<h3 id="an-evolving-project">An Evolving Project
</h3>
            
<p>Blazorade Scraibe is an ongoing project. Features are added as they are needed, driven by real-world usage, community contributions, and continuous development.
</p>
            
<h2 id="key-questions-about-blazorade-scraibe">Key Questions About Blazorade Scraibe
</h2>
            
<h3 id="when-is-blazorade-scraibe-a-good-fit">When is Blazorade Scraibe a good fit?
</h3>
            
<p>Blazorade Scraibe works well when your application contains content that you want others to find and read.
</p>
            
<p>Typical use cases include documentation sites, developer portals, blogs, campaign sites, and company websites, especially for smaller companies. It is a strong fit when you want to combine discoverable content with the flexibility and interactivity of a Blazor application.
</p>
            
<h3 id="when-is-it-not-a-good-fit">When is it not a good fit?
</h3>
            
<p>If your application is primarily focused on functionality and contains little or no static content, Scraibe may not add much value.
</p>
            
<p>The same applies to highly dynamic dashboards or applications that depend heavily on real-time data. In those cases, rendering content ahead of time does not provide much benefit.
</p>
            
<p>It is also not a strong fit for applications where content is only available to authenticated users. Since that content is not accessible to search engines or AI agents anyway, generating static versions of it does not add much value.
</p>
            
<p>It is also worth noting that SPA applications do not protect content simply by hiding it behind client-side logic. If the content is part of the application bundle, it is already available in the browser, even if it is not rendered. Content that should only be available to authenticated users must be retrieved from a backend, for example through a REST API that requires a valid access token.
</p>
            
<h3 id="why-not-just-use-blazor-ssr">Why not just use Blazor SSR?
</h3>
            
<p><a href="https://learn.microsoft.com/aspnet/core/blazor/fundamentals/#render-modes">Server-side rendering in Blazor
</a> solves the visibility problem by rendering HTML on the server before sending it to the client.
</p>
            
<p>That approach works well, but it introduces a server into the architecture. This increases complexity and changes the cost model as your traffic grows. If you want to keep the simplicity of static hosting, it may not be the best fit.
</p>
            
<h3 id="how-does-this-compare-to-static-site-generators">How does this compare to static site generators?
</h3>
            
<p>Tools like 
<a href="https://gohugo.io/">Hugo
</a> and 
<a href="https://docusaurus.io/">Docusaurus
</a> generate static HTML at build time, which makes content easy to index and distribute.
</p>
            
<p>Blazorade Scraibe takes a similar approach to content generation, but keeps the application in Blazor and is designed specifically for use with 
<a href="https://github.com/features/copilot">GitHub Copilot
</a>.
</p>
            
<p>With traditional static site generators, you typically write content and templates manually, and the tooling focuses on transforming that into static output. With Scraibe, you can use Copilot as a site builder that helps you create content, structure your site, and build interactive components directly within the same workflow.
</p>
            
<p>This means you are not just generating static pages. You are building a Blazor application where content and interactivity evolve together, with Copilot assisting you throughout the process.
</p>
            
<h3 id="can-you-mix-static-and-interactive-content">Can you mix static and interactive content?
</h3>
            
<p>Yes.
</p>
            
<p>Static content forms the foundation, and you add interactivity where needed using shortcodes that map to Blazor components. This lets you keep content simple while still building more advanced features when required.
</p>
            
<h3 id="is-blazorade-scraibe-tied-to-azure">Is Blazorade Scraibe tied to Azure?
</h3>
            
<p>In practice, yes.
</p>
            
<p>While 
<a href="https://learn.microsoft.com/aspnet/core/blazor/hosting-models#blazor-webassembly">Blazor WebAssembly
</a> itself is not tied to any specific platform, Blazorade Scraibe is designed with 
<a href="https://learn.microsoft.com/azure/static-web-apps/overview">Azure Static Web Apps
</a> in mind. For example, it generates the 
<code>staticwebapp.config.json
</code> configuration file, which is specific to that platform.
</p>
            
<p>It is possible to adapt it to other hosting environments, but Azure Static Web Apps provides the most natural fit and the least friction.
</p>
            
<h3 id="is-blazorade-scraibe-tied-to-github-copilot">Is Blazorade Scraibe tied to GitHub Copilot?
</h3>
            
<p>In practice, yes.
</p>
            
<p>Blazorade Scraibe is designed specifically for use with 
<a href="https://github.com/features/copilot">GitHub Copilot
</a>. The structure, conventions, and workflows are built to work well with Copilot as a site builder.
</p>
            
<p>You can use it without Copilot or with another coding AI agent, but you will need to review the structure, conventions, and workflows to make sure they align with the tool you choose. Much of the value comes from how Copilot helps you create content, structure your site, and build components as you go.
</p>
            
<h3 id="how-can-you-contribute-or-extend-it">How can you contribute or extend it?
</h3>
            
<p>You can start by 
<a href="https://github.com/new?template_name=Blazorade-Scraibe&amp;template_owner=Blazorade">using it in your own projects
</a> and adapting it to your needs.
</p>
            
<p>If you want to contribute back, you can submit pull requests to the 
<a href="https://github.com/Blazorade/Blazorade-Scraibe">Blazorade Scraibe repository
</a> or participate in discussions around new features and improvements.
</p>
            
<h2 id="summary-and-key-takeaways">Summary and Key Takeaways
</h2>
            
<p>Blazor WebAssembly applications, like other SPA frameworks, render content in the browser. That works well for users, but it also means that search engines and AI agents often never see the actual content.
</p>
            
<p>Blazorade Scraibe solves this by generating static content at build time. The content becomes part of the initial response, while the Blazor application still provides interactivity in the browser.
</p>
            
<p>At the same time, it embraces a different way of building sites. With 
<a href="https://github.com/features/copilot">GitHub Copilot
</a>, you are no longer working alone. Scraibe is designed so that Copilot can act as a site builder, helping you create content, structure your site, and build components as you go.
</p>
            
<p>This gives you a practical middle ground:
</p>
            
<ul><li>Content that is readable and discoverable
</li><li>A full Blazor application for interactivity
</li><li>A workflow where Copilot helps you move faster and stay consistent
</li></ul>
            
<p>Blazorade Scraibe works well for content-driven sites such as documentation, blogs, campaign pages, and company websites. It is especially useful when you want to combine discoverable content with the flexibility of building in Blazor.
</p>
            
<p>If you want to get started, you can create a new project by clicking the button below.
</p>
            
<p><a href="https://github.com/new?template_name=Blazorade-Scraibe&amp;template_owner=Blazorade">Create your new repository here
</a></p>
            
<p>Also have a look at my other 
<a href="/category/blazorade/">Blazorade
</a> and 
<a href="/category/blazor/">Blazor
</a> articles to read more.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Protecting APIs with Microsoft Entra ID</title>
      <description>Learn how app roles and scopes let your API make solid authorization decisions. A simple guide to protecting APIs with Entra ID.</description>
      <link>https://stage.mikaberglund.com/protecting-apis-with-microsoft-entra-id</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/protecting-apis-with-microsoft-entra-id</guid>
      <pubDate>Sat, 13 Dec 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="protecting-apis-with-microsoft-entra-id" alt="Protecting APIs with Microsoft Entra ID" />
                
<figcaption>Protecting APIs with Microsoft Entra ID
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/from-words-to-meaning-vector-search-cosmos-db" role="button" aria-label="Previous article: From Words to Meaning: The Rise of Vector Search in Cosmos DB"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/blazorade-scraibe-making-blazor-content-discoverable" role="button" aria-label="Next article: Blazorade Scraibe: Making Blazor Content Discoverable"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="protecting-apis-with-microsoft-entra-id">Protecting APIs with Microsoft Entra ID
</h1>
            
<p class="article-meta">December 13, 2025
</p>

            
<p>In this article I describe a practical approach to protecting APIs with Entra ID. The goal is to configure your app registrations so that your API app gets the information it needs to make solid authorization decisions for each request. Entra ID provides that information through app roles and scopes, and your API app can rely on these when the setup is clean.
</p>
            
<p>I use two simple applications named Web app and API app to keep the examples concrete. This pattern is common and shows how an application can act on behalf of a user. In that scenario the API app must understand what the user can do and what the application can do for the user. Entra ID supports this model out of the box when the app registrations match the requirements.
</p>
            
<p>This article walks through the structure behind protecting APIs with Entra ID and shows how the OAuth authorization flows enable it. The result works the same way whether a Web app, a background service, or an AI agent calls your API.
</p>
            
<p>Remember also to have a look at my 
<a href="/category/microsoft-entra-id/">other Microsoft Entra ID articles
</a>.
</p>
            
<h2 id="overview">Overview
</h2>
            
<p>When you build an API that serves data or functionality to different applications, you want the API app to get the information it needs to make solid authorization decisions for every request. In Microsoft Entra ID, this information comes from app roles and scopes. When structured well, they give the API app a predictable model for deciding whether a request should be allowed or denied.
</p>
            
<p>Here is how the flow works:
</p>
            
<ol><li>The Web app asks Entra ID for an access token with one or more scopes.
</li><li>Entra ID returns a token that contains both the scopes the user granted and the user’s assigned app roles.
</li><li>The Web app calls the API app and includes the access token.
</li><li>The API app verifies that Entra ID issued the token.
</li><li>The API app reads the app roles and scopes from the token and derives what is allowed for this request.
</li><li>The API app performs the action or denies it.
</li></ol>
            
<p>This structure gives the API app a clear and consistent way to enforce authorization using only information from the token. It is also a practical foundation for protecting APIs with Entra ID in real systems.
</p>
            
<h2 id="what-is-the-difference-between-app-roles-and-scopes">What Is the Difference Between App Roles and Scopes?
</h2>
            
<p>App roles and scopes both describe permissions in Entra ID, but they serve different purposes. App roles define what a user or an application can do in the API app. Each role represents one action or a set of actions. When you assign a role to a user, a group, or an application, you set the limits for what that identity can do.
</p>
            
<p>Scopes describe what the Web app may do on the user’s behalf. The Web app requests scopes during the OAuth flow, and the user can grant or deny them. Inside the API app, scopes tell you what permissions the user has delegated to the Web app for this specific call.
</p>
            
<p>The two signals work together. App roles set the hard limits for what the user or application is allowed to do. Scopes narrow down which of those allowed actions the Web app can take for the user. Scopes never expand permissions.
</p>
            
<p>App roles answer: 
<em>What is this user or application allowed to do at all?
</em>Scopes answer: 
<em>What has the user allowed this Web app to do for them in this call?
</em></p>
            
<p>When the request reaches the API app, the user must hold the right role and the caller must present the right scope. This pattern forms the core of protecting APIs with Entra ID.
</p>
            
<h2 id="creating-the-app-registrations">Creating the App Registrations
</h2>
            
<p>Everything starts with the app registrations. The API app defines the permission model with its app roles and scopes. The Web app asks for those permissions when it calls the API. Both registrations must line up so that Entra ID can issue tokens that the API app can trust.
</p>
            
<p>In the next sections we create the two registrations and look at the settings that matter for authorization.
</p>
            
<h3 id="api-app-application-registration">API App Application Registration
</h3>
            
<p>We start by creating the app registration for the API app. Then we add app roles to it. These roles define what the user or the calling application can do. Each role represents an action or a set of actions. You can choose whether a role can be assigned to users and groups, to applications, or to both. Make this choice early, since it shapes how the API app is meant to be used.
</p>
            
<p>Next, expose the API. Set the Application ID URI to identify the API app as a resource. Then define at least one scope. In this example we add the 
<code>user_impersonation
</code> scope. Calling applications request this scope when they want to act on behalf of a user with the permissions that the user already has, i.e. the app roles the user has been assigned to through direct assignment or group membership.
</p>
            
<p>Add any additional scopes the API app needs. Scopes describe what the calling application wants to do for the user. The API app uses them to understand the caller’s intent.
</p>
            
<p>The API app holds the full definition of its permission model. This keeps the structure predictable and sets the baseline for the Web app registration.
</p>
            
<h3 id="web-app-application-registration">Web App Application Registration
</h3>
            
<p>Create a second app registration for the Web app. This registration represents the Web app that calls the API app on behalf of the user. Configure the platform settings so the Web app can sign users in and request tokens from Entra ID.
</p>
            
<p>Next, give the Web app access to the API app. Add the delegated permission that corresponds to the 
<code>user_impersonation
</code> scope you created earlier. The Web app requests this scope during the OAuth flow when it needs to act for the user. The user can grant or deny this permission, depending on how the application is used. You can also add the Web app as a trusted application to the API app. Users are not asked for consent to trusted applications.
</p>
            
<p>The Web app does not define app roles. All roles belong to the API app, because all actions that need protection happen in the API app. The Web app is just the UI to the functionality exposed by the API app. The Web app only asks for the scopes it needs, and it relies on Entra ID to include the user’s app roles in the access token that it receives.
</p>
            
<p>With both app registrations in place, we can move on to assigning app roles to users, groups, and applications.
</p>
            
<h2 id="assigning-app-roles-to-users-groups-and-applications">Assigning App Roles to Users, Groups, and Applications
</h2>
            
<p>Once the API app defines its app roles, you can start assigning them to the identities that need access. You can assign a role directly to a user, to a security group, or to an application. The choice depends on how you want to manage access.
</p>
            
<p>Assigning a role directly to a user gives that user the permissions described by the role. This works for small setups, but it does not scale well. Assigning roles to security groups works better. You place users into groups that represent your access model, and the API app receives the user’s effective roles in the access token.
</p>
            
<p>Some roles are intended only for applications. These roles describe what an application can do when it calls the API as itself. Assign the role directly to the calling application.
</p>
            
<p>When the Web app calls the API on behalf of a user, the access token includes the roles the user holds, either through direct assignments or group memberships. When a background service calls the API as itself, the token includes the roles assigned to that application. In both cases, the API app receives the information it needs to understand what the caller is allowed to do.
</p>
            
<h2 id="authorization-inside-the-api-app">Authorization Inside the API App
</h2>
            
<p>When a request reaches the API app, it carries an access token from Entra ID. That token contains app roles, scopes, and other claims. The API app uses these to decide what the caller is allowed to do and should rely on the token as much as possible. This model is a core part of protecting APIs with Entra ID.
</p>
            
<h3 id="how-to-use-app-roles-for-authorization">How to Use App Roles for Authorization?
</h3>
            
<p>App roles describe what a user or an application can do in the API app. These roles map directly to the functionality the API exposes. When the Web app calls on behalf of a user, the token contains the user’s roles. When a background service calls as itself, the token contains the roles assigned to that application. The API app checks these roles to see if the caller may perform the requested action.
</p>
            
<p>You should design your app roles to match how you talk about access in the system. If the API exposes operations for reading and writing data, then roles should reflect those capabilities. The API app then checks for the required role before it performs an operation.
</p>
            
<h3 id="how-to-use-scopes-for-delegated-permissions">How to Use Scopes for Delegated Permissions?
</h3>
            
<p>Scopes describe what the Web app may do on the user’s behalf. During the OAuth flow, the Web app asks for one or more scopes, and the user can grant or deny them. When the user grants a scope, the user delegates a subset of their permissions to the Web app.
</p>
            
<p>Inside the API app, scopes tell you what the user has allowed the Web app to do in this specific call. Scopes do not change what the 
<strong>*user
</strong>* is allowed to do in general. They only limit what the Web app can do for the user. The API app uses the scopes in the token to understand how far the Web app may act on the user’s behalf.
</p>
            
<h3 id="how-to-derive-allowed-actions-from-app-roles-and-scopes">How to Derive Allowed Actions from App Roles and Scopes?
</h3>
            
<p>The API app derives the allowed actions by applying the scopes to the user’s roles. The user must hold the role for the action. The Web app must hold a scope that lets it act for the user for that same action.
</p>
            
<p>App roles and scopes must have unique names inside the API app’s registration, so you cannot name a scope the same as a role. Because of this, the API app needs a simple way to map scopes to the roles that apply for a request. Entra ID does not provide a built-in mapping mechanism. A static mapping in the API app usually works well. App roles and scopes change rarely, so a static structure does not add much maintenance.
</p>
            
<p>For example, a scope named 
<code>Profile.Manage.My
</code> could map to several app roles, such as 
<code>Account.Read.My
</code>, 
<code>Account.Write.My
</code>, 
<code>ContactInfo.Read.My
</code>, 
<code>ContactInfo.Write.My
</code>, and 
<code>ContactInfo.Delete.My
</code>. When a request arrives, the API app looks at the scopes included in the access token and uses its mapping to determine which app roles apply for this call. It then compares that set with the roles the user actually holds. Any role in the mapping that the user does not have is not included in the effective permissions for this request.
</p>
            
<p>The API app checks both signals for each request and then performs or denies the action.
</p>
            
<h2 id="a-practical-example-a-crm-application-protecting-its-apis-with-entra-id">A Practical Example: A CRM Application Protecting Its APIs with Entra ID
</h2>
            
<p>To illustrate how app roles and scopes work together, let’s look at how this could be implemented in a simple CRM application. The CRM system exposes its functionality through an API app. A Web app provides the user interface and calls the API on behalf of the user.
</p>
            
<p>The API app includes operations for managing accounts, contacts, and proposals. These operations must be protected so that users and applications can access only what they are allowed to.
</p>
            
<h3 id="example-app-roles">Example App Roles
</h3>
            
<p>The API app defines these app roles:
</p>
            
<ul><li><strong>Accounts
</strong>: 
<code>Accounts.Read.Team
</code>, 
<code>Accounts.Read.All
</code>, 
<code>Accounts.Write.Team
</code>, 
<code>Accounts.Write.All
</code></li><li><strong>Contacts
</strong>: 
<code>Contacts.Read.My
</code>, 
<code>Contacts.Read.Team
</code>, 
<code>Contacts.Read.All
</code>, 
<code>Contacts.Write.My
</code>, 
<code>Contacts.Write.Team
</code>, 
<code>Contacts.Write.All
</code></li><li><strong>Proposals
</strong>: 
<code>Proposals.ReadWrite.Team
</code>, 
<code>Proposals.ReadWrite.All
</code></li></ul>
            
<p>These roles reflect the functionality the CRM API exposes and follow a simple constraint model:
</p>
            
<ul><li><strong>My
</strong>: actions on objects owned by the user
</li><li><strong>Team
</strong>: actions within the user’s team or department
</li><li><strong>All
</strong>: actions across the entire tenant
</li></ul>
            
<p>This keeps the permission model predictable while still giving the CRM system enough flexibility for everyday scenarios. Users gain access through direct role assignments or group membership. Background services can also be assigned roles when calling the API as themselves.
</p>
            
<h3 id="example-scopes">Example Scopes
</h3>
            
<p>The API app exposes the following scopes:
</p>
            
<ul><li><strong>CRM
</strong>: 
<code>Crm.Read.My
</code>, 
<code>Crm.Read.Team
</code>, 
<code>Crm.Read.All
</code>, 
<code>Crm.ReadWrite.My
</code>, 
<code>Crm.ReadWrite.Team
</code>, 
<code>Crm.ReadWrite.All
</code></li><li><strong>Sales
</strong>: 
<code>Sales.ReadWrite.Team
</code>, 
<code>Sales.ReadWrite.All
</code></li></ul>
            
<p>The CRM scopes map to the Accounts and Contacts roles:
</p>
            
<ul><li><code>Crm.Read.*
</code> maps to the read roles for Accounts and Contacts with the same suffix.
</li><li><code>Crm.ReadWrite.*
</code> maps to both read and write roles for Accounts and Contacts with the same suffix.
</li></ul>
            
<p>The Sales scopes map to the Proposals roles:
</p>
            
<ul><li><code>Sales.ReadWrite.Team
</code> maps to 
<code>Proposals.ReadWrite.Team
</code>.
</li><li><code>Sales.ReadWrite.All
</code> maps to 
<code>Proposals.ReadWrite.All
</code>.
</li></ul>
            
<p>At runtime, the API app uses this mapping together with the user’s app roles in the access token. The scopes tell the API app what the user has allowed the Web app to do. The app roles tell the API app what the user is actually allowed to do. The intersection of the two becomes the effective permissions for the request.
</p>
            
<h3 id="end-to-end-example">End-to-End Example
</h3>
            
<ol><li>The user signs in to the Web app.
</li><li>The Web app requests an access token for the API app and includes 
<code>Crm.ReadWrite.Team
</code>.
</li><li>Entra ID issues a token containing the scopes the user granted and the app roles assigned to the user.
</li><li>The Web app calls the CRM API and includes the access token.
</li><li>The API app verifies that Entra ID issued the token.
</li><li>The API app looks at the user’s app roles to understand what the user is allowed to do.
</li><li>The API app looks at the scopes in the token to understand what the user has allowed the Web app to do for them.
</li><li>The API app uses its internal mapping to translate 
<code>Crm.ReadWrite.Team
</code> into the corresponding Accounts and Contacts roles:
<code>Accounts.Read.Team
</code>, 
<code>Accounts.Write.Team
</code>, 
<code>Contacts.Read.Team
</code>, 
<code>Contacts.Write.Team
</code>.It then intersects this set with the roles the user actually holds.
</li><li>The API app derives the effective permissions for the request.
</li><li>The API app performs the action or denies it.
</li></ol>
            
<p>This example shows how roles and scopes work together in a real CRM system. The app roles define what the user or an application may do. The scopes define what the user allows the Web app to do on their behalf. The API app derives the effective permissions by applying one to the other and then makes a clear authorization decision.
</p>
            
<h2 id="summary">Summary
</h2>
            
<p>Protecting APIs with Entra ID works best when the Web app and the API app follow a clear structure. The API app should rely on the access token as much as possible. The token carries what the API needs for authorization, including the user’s app roles and the scopes the user has granted to the Web app.
</p>
            
<p>App roles define what the user or an application is allowed to do in the API app. Scopes define what the user allows the Web app to do on their behalf in a specific call. Roles and scopes must have unique names, and the API app must implement the mapping between them. The API app then derives the effective permissions by intersecting the user’s app roles with the roles implied by the granted scopes. This gives the API app predictable and secure authorization decisions without extra custom layers.
</p>
            
<p>A clean set of app registrations, a simple mapping model, and a consistent authorization approach inside the API provide a solution that is easy to understand, easy to maintain, and flexible enough for most real systems. This pattern gives you a solid and repeatable way of protecting APIs with Entra ID in real applications.
</p>
            
<h2 id="references">References
</h2>
            
<ul><li><a href="https://learn.microsoft.com/entra/identity-platform/howto-add-app-roles-in-apps">Add app roles and get them from a token
</a></li><li><a href="https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis">Configure an application to expose a web API
</a></li><li><a href="https://learn.microsoft.com/entra/identity-platform/scenario-protected-web-api-verification-scope-app-roles">Verify scopes and app roles in a protected web API
</a></li><li><a href="https://datatracker.ietf.org/doc/html/rfc6749">OAuth 2.0 Authorization Framework (RFC 6749)
</a></li><li><a href="https://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core Specification
</a></li><li><a href="https://auth0.com/docs/secure/tokens/access-tokens/define-scopes">Understanding OAuth 2.0 Scopes
</a></li></ul>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>From Words to Meaning: The Rise of Vector Search in Cosmos DB</title>
      <description>Learn how Cosmos DB Vector Search moves beyond keywords to understand meaning. Compare traditional search with vector-based similarity for natural language.</description>
      <link>https://stage.mikaberglund.com/from-words-to-meaning-vector-search-cosmos-db</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/from-words-to-meaning-vector-search-cosmos-db</guid>
      <pubDate>Fri, 07 Nov 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="vector-search" alt="Abstract digital landscape showing paths of meaning representing vector search in Cosmos DB." />
                
<figcaption>Abstract digital landscape showing paths of meaning representing vector search in Cosmos DB.
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/keyed-services-not-supported-in-azure-functions" role="button" aria-label="Previous article: Keyed Services Not Supported in Azure Functions"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/protecting-apis-with-microsoft-entra-id" role="button" aria-label="Next article: Protecting APIs with Microsoft Entra ID"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="from-words-to-meaning-the-rise-of-vector-search-in-cosmos-db">From Words to Meaning: The Rise of Vector Search in Cosmos DB
</h1>
            
<p class="article-meta">November 7, 2025
</p>

            
<p>Modern search is shifting from looking at words to understanding meaning. Traditional indexing and search have worked well for decades, but they were designed for documents and keywords, not natural language. When users ask questions, paraphrase content, or mix languages, old search models struggle.
</p>
            
<p>With 
<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search">Cosmos DB Vector Search
</a>, Microsoft brings semantic understanding directly into the database. Instead of matching words, it measures 
<em>similarity of meaning
</em>. This article explains how traditional word-based search works, why it often fails for natural language, and how vector search changes the game.
</p>
            
<h2 id="introduction">Introduction
</h2>
            
<p>Traditional search engines depend on rules. They split text into words, remove noise, and match tokens. It’s efficient, but mechanical.
</p>
            
<p>Cosmos DB’s 
<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search">Vector Search for NoSQL API
</a> introduces a completely different approach. It represents content as vectors, arrays of numbers that capture the semantic context of words, sentences, or entire documents. Similar meanings are stored close to each other in multidimensional space.
</p>
            
<p>This article compares these two approaches and explains why vector-based similarity search is better suited for natural language and AI-powered applications.
</p>
            
<h2 id="how-traditional-search-works">How Traditional Search Works
</h2>
            
<p>Before understanding why vector search matters, it helps to know how traditional search functions.
</p>
            
<p>Most search engines use 
<strong>inverted indexes
</strong>. They break text into tokens, often using 
<em>word breakers
</em> and 
<em>analyzers
</em>. Each unique word points to a list of documents that contain it. To improve accuracy, search systems also use 
<strong>stemmers
</strong> and 
<strong>lemmatizers
</strong> that reduce words to their root forms.
</p>
            
<ul><li>“driving”, “driven”, and “driver” are reduced to “drive”
</li><li>“cats” and “catlike” both relate to “cat”
</li></ul>
            
<p>When you search for 
<em>“renew car insurance”
</em>, the engine looks up “renew”, “car”, and “insurance” in the index. It might find “renew car insurance policy” but miss “automobile coverage renewal”. The reason is simple, the system compares words, not meanings.
</p>
            
<p>Traditional systems work well for structured or predictable text, such as product names, SKUs, or codes. But they fail when wording changes or when people use synonyms.
</p>
            
<h2 id="where-keyword-search-falls-short">Where Keyword Search Falls Short
</h2>
            
<p>Word-based indexing is fragile. It relies on exact matches and does not understand context.
</p>
            
<ul><li><em>“How to reset password”
</em> vs. 
<em>“forgot login credentials”
</em></li><li><em>“car insurance”
</em> vs. 
<em>“automobile coverage”
</em></li><li><em>“apple”
</em> as a fruit vs. 
<em>“Apple”
</em> as a company
</li></ul>
            
<p>A word-based system cannot tell that these phrases are semantically close. Even with advanced analyzers, it still matches on form, not intent.
</p>
            
<p>In multilingual contexts, the problem grows. A Finnish query like 
<em>“vaihda salasana”
</em> would never match an English document titled 
<em>“reset password instructions”
</em> without translation or manual mapping.
</p>
            
<p>This is where similarity search provides a better model.
</p>
            
<h2 id="understanding-vector-search">Understanding Vector Search
</h2>
            
<p>Instead of storing words, vector search stores 
<a href="https://learn.microsoft.com/azure/cosmos-db/gen-ai/vector-embeddings"><strong>vector embeddings
</strong></a>. These are fixed-length numeric arrays that represent the meaning of text.
</p>
            
<p>Each vector is a list of floating-point numbers. The distance between two vectors shows how similar their meanings are. Closer vectors mean more similar content.
</p>
            
<p>You can think of a vector as a path through a landscape of meanings. Each point along the path represents a topic or concept found in the text. If another path runs close to it or even crosses it, then the content that the second path represents is likely similar to the first. This is what similarity search looks for, paths that travel close together in meaning.
</p>
            
<p>For example, vectors generated from 
<em>“how to reset password”
</em> and 
<em>“forgot login credentials”
</em> will be close together in vector space, even though they share no words.
</p>
            
<p>Embeddings are created using 
<a href="https://learn.microsoft.com/azure/ai-studio/what-is-ai-studio"><strong>AI models
</strong></a> such as 
<code>text-embedding-3-small
</code> or 
<code>text-embedding-3-large
</code>. These models turn text into a mathematical representation of its meaning.
</p>
            
<p>Once stored, these embeddings make it possible to run similarity searches that find items with vectors close to a given query vector.
</p>
            
<h2 id="how-cosmos-db-vector-search-works">How Cosmos DB Vector Search Works
</h2>
            
<p>Cosmos DB now supports storing and searching vector embeddings directly inside your NoSQL containers. The feature is available under the 
<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search"><strong>Vector Search for NoSQL API
</strong></a>.
</p>
            
<p>You enable it by defining a 
<strong>vector policy
</strong> when creating a container. The policy specifies:
</p>
            
<ul><li>The JSON path to the embedding array
</li><li>The data type (usually 
<code>float
</code>)
</li><li>The number of dimensions (for example, 1536 for 
<code>text-embedding-3-small
</code>)
</li><li>The distance function to use (typically 
<em>cosine similarity
</em>)
</li></ul>
            
<p>After embeddings are stored, you can use the built-in 
<code>VectorDistance()
</code> function in SQL queries to find semantically similar documents.
</p>
            
<pre class="code-block" data-language="sql"><code>
            
<span class="code-line">SELECT TOP 5 c.id, c.content, VectorDistance(c.embedding.vector, &#64;queryVector) AS similarity
</span>
            
<span class="code-line">FROM c
</span>
            
<span class="code-line">WHERE c.partition = &#39;help-articles&#39;
</span>
            
<span class="code-line">ORDER BY VectorDistance(c.embedding.vector, &#64;queryVector)
</span>
            
</code></pre>
            
<p>This query returns the five most similar documents to the query vector. You can combine similarity search with metadata filters such as customer ID or region, just like any other Cosmos DB query.
</p>
            
<p>You can find a step-by-step walkthrough of how to enable and configure this feature in my earlier article 
<a href="/cosmos-db-vector-search-an-introduction/"><strong>Cosmos DB Vector Search – An Introduction
</strong></a>.
</p>
            
<p>This makes it easy to mix semantic ranking with structured filtering in the same data store.
</p>
            
<h2 id="why-vector-search-fits-natural-language">Why Vector Search Fits Natural Language
</h2>
            
<p>Vector embeddings capture the 
<em>meaning
</em> of language rather than its exact form. This makes them ideal for unstructured, user-generated, or multilingual data.
</p>
            
<ul><li><strong>Semantic understanding:
</strong> Matches content with similar intent, even with no shared words.
</li><li><strong>Synonym and paraphrase tolerance:
</strong> Recognizes “cancel booking” and “terminate reservation” as related.
</li><li><strong>Language agnostic behavior:
</strong> Similarity depends on meaning, not language.
</li><li><strong>Contextual clustering:
</strong> Similar documents naturally group together, simplifying recommendations and content discovery.
</li><li><strong>Support for exploratory search:
</strong> Vector embeddings help you find information even when you are not sure what you are looking for. They surface related concepts and content that might not contain the words you use but still carry the same meaning.
</li></ul>
            
<p>This semantic flexibility makes vector search a perfect fit for AI-driven scenarios such as:
</p>
            
<ul><li><a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation"><strong>RAG (Retrieval-Augmented Generation)
</strong></a> pipelines
</li><li>Chatbots and virtual assistants
</li><li>Knowledge bases and document search
</li><li>Product and content recommendations
</li></ul>
            
<h2 id="when-to-use-keyword-search-and-when-to-use-vector-search">When to Use Keyword Search and When to Use Vector Search
</h2>
            
<p>Keyword and vector search complement each other.
</p>
            
<p>| Use Keyword Search                      | Use Vector Search                             | | --------------------------------------- | --------------------------------------------- | | When you need exact matches or codes    | When queries are phrased naturally            | | When data is structured and predictable | When language is varied or unstructured       | | For short or technical terms            | For synonyms, paraphrases, and long-form text | | For strict filters and sorting          | For finding semantically similar documents    |
</p>
            
<p>You can also combine both approaches. A 
<strong>hybrid search
</strong> first narrows down documents with keywords or metadata, then re-ranks the results using vector similarity. This gives the accuracy of filtering with the intelligence of semantic matching.
</p>
            
<h2 id="building-a-simple-similarity-search-flow">Building a Simple Similarity Search Flow
</h2>
            
<ol><li><strong>Generate embeddings
</strong> for your text using 
<a href="https://learn.microsoft.com/azure/ai-studio/what-is-ai-studio"><strong>Azure AI Foundry
</strong></a>.
</li><li><strong>Store documents and their embeddings
</strong> in a Cosmos DB container with a vector policy.
</li><li>When a user submits a query, 
<strong>generate a query vector
</strong> using the same embedding model.
</li><li>Run a Cosmos DB SQL query with 
<code>VectorDistance()
</code> to find the most similar documents.
</li><li>Optionally combine with structured filters or use the results in a downstream AI model.
</li></ol>
            
<p>This simple flow powers applications that need fast, semantically aware search without moving data to a separate vector database.
</p>
            
<h2 id="common-questions-and-answers">Common Questions and Answers
</h2>
            
<h3 id="why-not-just-use-full-text-search">Why not just use full-text search?
</h3>
            
<p>Full-text search matches exact words or phrases. It is fast and works well for technical or structured text. However, it fails to capture meaning. Vector search complements it by handling natural language and synonyms.
</p>
            
<h3 id="do-i-need-a-separate-ai-service-to-create-vectors">Do I need a separate AI service to create vectors?
</h3>
            
<p>Yes, you need an embedding model such as 
<code>text-embedding-3-large
</code> deployed in 
<a href="https://learn.microsoft.com/azure/ai-studio/what-is-ai-studio"><strong>Azure AI Foundry
</strong></a>. The model transforms text into a numeric vector that you then store in Cosmos DB.
</p>
            
<h3 id="can-vector-search-handle-multiple-languages">Can vector search handle multiple languages?
</h3>
            
<p>Yes. Vector search can be language agnostic because embedding models capture the semantic meaning of text, not the literal words or language used.
</p>
            
<h3 id="should-i-replace-all-keyword-searches-with-vector-search">Should I replace all keyword searches with vector search?
</h3>
            
<p>No. Use keyword search for identifiers or precise filters. Use vector search for meaning-based retrieval or conversational AI. They work best together.
</p>
            
<h2 id="summary">Summary
</h2>
            
<p>Traditional search engines match words. Vector search matches meaning.
</p>
            
<p>By storing and querying embeddings directly in Cosmos DB, developers can build applications that understand language, not just text. The same system can now support both structured and semantic queries, making Cosmos DB a powerful choice for AI-driven solutions.
</p>
            
<p>Cosmos DB Vector Search lets you move from searching by words to searching by intent. It is a small change in how data is stored, but a big step toward systems that understand human language.
</p>
            
<h2 id="references">References
</h2>
            
<ul><li><a href="https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search">Vector Search in Azure Cosmos DB for NoSQL
</a></li><li><a href="https://learn.microsoft.com/azure/cosmos-db/gen-ai/vector-embeddings">Vector Embeddings in Azure Cosmos DB
</a></li><li><a href="https://learn.microsoft.com/azure/ai-studio/">Azure AI Foundry Embedding Models
</a></li><li><a href="/cosmos-db-vector-search-an-introduction/">Cosmos DB Vector Search – An Introduction
</a></li><li><a href="/vector-embedding-model-comparison-with-multilingual-text/">Vector Embedding Model Comparison with Multilingual Text
</a></li></ul>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Keyed Services Not Supported in Azure Functions</title>
      <description>A while ago I was working on an Azure function application, and ran into a very weird problem. Suddenly I started getting a pretty confusing error, that actually pointed me away from the correct solution. The error I got was the following. If you are looking for a solution to</description>
      <link>https://stage.mikaberglund.com/keyed-services-not-supported-in-azure-functions</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/keyed-services-not-supported-in-azure-functions</guid>
      <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="keyed-service-not-supported-in-azure-functions" alt="Keyed Services Not Supported in Azure Functions" />
                
<figcaption>Keyed Services Not Supported in Azure Functions
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/simplify-text-chunking-for-vector-embeddings" role="button" aria-label="Previous article: Simplify Text Chunking for Vector Embeddings"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/from-words-to-meaning-vector-search-cosmos-db" role="button" aria-label="Next article: From Words to Meaning: The Rise of Vector Search in Cosmos DB"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="keyed-services-not-supported-in-azure-functions">Keyed Services Not Supported in Azure Functions
</h1>
            
<p class="article-meta">September 22, 2025
</p>

            
<p>A while ago I was working on an Azure function application, and ran into a very weird problem. Suddenly I started getting a pretty confusing error, that actually pointed me away from the correct solution. The error I got was the following.
</p>
            
<pre class="code-block"><code>
            
<span class="code-line">Expecting the instance to be stored in singleton scope, but unable to find anything here.
</span>
            
<span class="code-line">Likely, you&#39;ve called UseInstance from the scoped container, but resolving from another container or injecting into a singleton.
</span>
            
</code></pre>
            
<p>If you are looking for a solution to a similar error, you’ve come to the right place.
</p>
            
<h2 id="initial-troubleshooting">Initial Troubleshooting
</h2>
            
<p>In my function application, I have quite a few services registered. Many of them depend on other services. So at first I thought that this is an easy case, and I have tried to inject a scoped or transient service into a singleton service. So I started to dig deeper into my dependency chains for my services. But even after digging around for several hours, I could not find anything that would help me solve the problem. Finally I ended up registering all my services as singleton services, just to make sure that I am not injecting a scoped service into a singleton service. Guess what, I still got the same error.
</p>
            
<h2 id="reproducing-the-problem">Reproducing the Problem
</h2>
            
<p>Before I reveal the solution, which you probably already guesses from the title of this article, I’ll talk a bit about how to reproduce the problem. I’ve created a 
<a href="https://github.com/MikaBerglund/KeyedServicesInAzureFunctions">Github repository
</a> that contains code that demonstrates how you can reproduce this problem.
</p>
            
<blockquote>Note! This problem affects only Azure function applications that use the in-process worker model. Isolated worker model is not affected. So, this problem will probably go away during the coming year, since the in-process worker model will run out of support in November 2026.
</blockquote>
            
<p>One of the things that made this so tricky for me to resolve was that fact that I did not need keyed services in my application. I just used another library that we use in many different kinds of applications, which itself registers keyed services. If I would have injected a keyed service into my functions class using the 
<a href="https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute"><code>FromKeyedServicesAttribute
</code></a> attribute, or requested a keyed service using the 
<a href="https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderkeyedserviceextensions.getkeyedservices"><code>IServiceProvider.GetKeyedService
</code></a> method, I would have gotten a much more descriptive error message, such as the one below.
</p>
            
<pre class="code-block"><code>
            
<span class="code-line">Anonymously Hosted DynamicMethods Assembly: This service provider doesn&#39;t support keyed services.
</span>
            
</code></pre>
            
<p>That would have immediately told me where the actual problem is instead of pointing me in the wrong direction trying to find conflicts in my service scopes.
</p>
            
<p>Below is the 
<code>Configure
</code> method from the 
<code>Startup
</code> class in my in-process function app that works.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">public override void Configure(IFunctionsHostBuilder builder)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    builder.Services
</span>
            
<span class="code-line">        .AddSingleton&lt;SharedServices.MiscService&gt;();
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>The code below adds one keyed service of the same type as is already in use by the application as a “normal” registered service. This code will throw the exception described in the introduction of this article. Even if you don’t even use the keyed service.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">public override void Configure(IFunctionsHostBuilder builder)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    builder.Services
</span>
            
<span class="code-line">        .AddSingleton&lt;SharedServices.MiscService&gt;()
</span>
            
<span class="code-line">        .AddKeyedSingleton&lt;SharedServices.MiscService&gt;(&quot;foo&quot;)
</span>
            
<span class="code-line">        ;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<h2 id="the-problem-keyed-services">The Problem – Keyed Services
</h2>
            
<p>As you probably guessed, the cause for the problem was keyed services. The in-process worker model in Azure functions does not support keyed services. I am writing this article in September 2025. Support for the in-process worker model will run out of support in November 2026. So I don’t expect this to be fixed in the in-process worker model. It is not a security related or otherwise a critical problem. As you can see from my 
<a href="https://github.com/MikaBerglund/KeyedServicesInAzureFunctions">code sample
</a>, this is not a problem in isolated worker model. I guess the official story is that if you need keyed services, you need to migrate to isolated worker model.
</p>
            
<h2 id="further-reading">Further Reading
</h2>
            
<p>Over the years, I’ve come across several problems with Azure functions that have been quite tricky to resolve. You can read more about them in my 
<a href="/category/azure-functions/">other Azure functions articles
</a>.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Simplify Text Chunking for Vector Embeddings</title>
      <description>The Denomica.OpenAI.Extensions Nuget package offers a simple and extensible way to chunk long text for vector embeddings in Azure AI Foundry.</description>
      <link>https://stage.mikaberglund.com/simplify-text-chunking-for-vector-embeddings</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/simplify-text-chunking-for-vector-embeddings</guid>
      <pubDate>Sun, 17 Aug 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="simplify-text-chunking-for-vector-embeddings" alt="Simplify Text Chunking for Vector Embeddings" />
                
<figcaption>Simplify Text Chunking for Vector Embeddings
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/vector-embedding-model-comparison-with-multilingual-text" role="button" aria-label="Previous article: Vector Embedding Model Comparison with Multilingual Text"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/keyed-services-not-supported-in-azure-functions" role="button" aria-label="Next article: Keyed Services Not Supported in Azure Functions"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="simplify-text-chunking-for-vector-embeddings">Simplify Text Chunking for Vector Embeddings
</h1>
            
<p class="article-meta">August 17, 2025
</p>

            
<p><strong>Text chunking
</strong> is crucial when working with 
<strong>vector embedding models
</strong>, no matter where you host them. In my previous article, 
<a href="/vector-embeddings-for-long-documents/">Vector Embedding for Long Documents
</a>, I explored how to combine multiple vectors into one. In this post, I’ll share sample code for how to do both chunking long text and combining multiple vectors into one. With the help of 
<a href="https://www.nuget.org/packages/Denomica.OpenAI.Extensions/"><code>Denomica.OpenAI.Extensions
</code></a>, which is published on 
<a href="https://www.nuget.org/packages/Denomica.OpenAI.Extensions/">NuGet
</a>, this task is a breeze. This library is built specifically for use with 
<a href="https://azure.microsoft.com/products/ai-foundry">Azure AI Foundry
</a> and designed to be both simple and extensible. The source code for this library is also available on 
<a href="https://github.com/Denomica/Denomica.OpenAI.Extensions">Github
</a>. If you like this library and find it useful, I would appreciate if you would give 
<a href="https://github.com/Denomica/Denomica.OpenAI.Extensions">the repository
</a> a star.
</p>
            
<h2 id="text-chunking-and-vector-embedding">Text Chunking and Vector Embedding
</h2>
            
<p>Before we dive into code, let’s have a few words about why text chunking is so important when dealing with vector embeddings. All embedding models, or at least the ones I know of, have limits on how much text they can process. So, if your text exceeds this limit, you have to chunk up your text into smaller parts.
</p>
            
<p>There are a few problems that the 
<code>Denomica.OpenAI.Extensions
</code> library tries to address, in current or future versions.
</p>
            
<ul><li>Embedding model limits are measured in tokens – not characters, words, or sentences. While tokens can be tricky to estimate, in normal English text a token typically averages around four characters.
</li><li>Because of these limits, you have to chunk up your text into smaller pieces so that you don’t exceed the limits.
</li><li>After chunking up text and generating an embedding for each vector, you typically want to aggregate them into one embedding representing your original text.
</li></ul>
            
<p>The chapters below describe in more detail how 
<code>Denomica.OpenAI.Extensions
</code> addresses these problems.
</p>
            
<h2 id="text-chunking-in-denomicaopenaiextensions">Text Chunking in Denomica.OpenAI.Extensions
</h2>
            
<p>The 
<code>Denomica.OpenAI.Extensions
</code> library provides a mechanism that allows you to register a chunking service that will take care of the chunking. When you use the 
<code>EmbeddingProvider
</code> class to generate embeddings, the registered chunking service is automatically used to chunk up your text. There is one chunking service provided by the library, the 
<code>LineChunkingService
</code>. This service chunks up the text line by line. One chunk can contain several lines of text as long as it does not exceed the maximum configured size of a chunk.
</p>
            
<p>The code below shows how you can register a chunking service and how to use the 
<code>EmbeddingProvider
</code> to generate embeddings.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">using Denomica.OpenAI.Extensions.Embeddings;
</span>
            
<span class="code-line">using Denomica.OpenAI.Extensions.Text;
</span>
            
<span class="code-line">using Microsoft.Extensions.DependencyInjection;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">var provider = new ServiceCollection()
</span>
            
<span class="code-line">    .AddOpenAIExtensions()
</span>
            
<span class="code-line">    .WithEmbeddingModel((opt, sp) =&gt;
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        opt.Endpoint = $&quot;https://[Your Azure AI Foundry hub name].openai.azure.com&quot;;
</span>
            
<span class="code-line">        opt.ApiKey = &quot;[Your API key goes here]&quot;;
</span>
            
<span class="code-line">        opt.Name = &quot;[The name of the embedding model deployment]&quot;;
</span>
            
<span class="code-line">    })
</span>
            
<span class="code-line">    .WithChunkingService&lt;LineChunkingService&gt;()
</span>
            
<span class="code-line">    .Services.BuildServiceProvider();
</span>
            
<span class="code-line"></span>
            
<span class="code-line">var embeddingProvider = provider.GetRequiredService&lt;EmbeddingProvider&gt;();
</span>
            
<span class="code-line">var embedding = await embeddingProvider.GenerateEmbeddingAsync(&quot;Hello World!&quot;);
</span>
            
</code></pre>
            
<h3 id="configuring-the-linechunkingservice">Configuring the LineChunkingService
</h3>
            
<p>The LineChunkingService chunks up text line by line into chunks that do not exceed the configured length in characters. Most of the embedding models in Azure AI Foundry have a token limit around 8000 tokens. A rough estimate is that one token evaluates to about 4 characters. That would mean that the maximum length of text that can be used for embedding is around 32 000 characters.
</p>
            
<p>The 
<code>LineChunkingService
</code> uses a default limit of 25 000 characters in order to be safely under the 8000 token limit. Of course, this is not the best solution in every case. You can configure this limit to a value that better suits your needs. The code below shows you how to do this.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">var provider = new ServiceCollection()
</span>
            
<span class="code-line">    .AddOpenAIExtensions()
</span>
            
<span class="code-line">    .WithEmbeddingModel((opt, sp) =&gt;
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        // Configure your embedding model here.
</span>
            
<span class="code-line">    })
</span>
            
<span class="code-line">    .WithChunkingService(sp =&gt;
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        return new LineChunkingService { MaxChunkSize = 10000 };
</span>
            
<span class="code-line">    })
</span>
            
<span class="code-line">    .Services
</span>
            
<span class="code-line">    .BuildServiceProvider();
</span>
            
</code></pre>
            
<h3 id="create-your-own-chunking-service">Create Your Own Chunking Service
</h3>
            
<p>The 
<code>Denomica.OpenAI.Extensions
</code> library also allows you to write your own chunking service, and register that to be used by EmbeddingProvider. Below is a simple version of a custom chunking service that you can then develop into your own text chunking service.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">using Denomica.OpenAI.Extensions.Text;
</span>
            
<span class="code-line">using System.IO;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">public class MyCustomChunkingService : IChunkingService
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    public int MaxChunkSize { get; set; } = 8000;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    public virtual async IAsyncEnumerable&lt;string&gt; GetChunksAsync(Stream input)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        string inputString;
</span>
            
<span class="code-line">        using(var reader = new StreamReader(input))
</span>
            
<span class="code-line">        {
</span>
            
<span class="code-line">            inputString = await reader.ReadToEndAsync();
</span>
            
<span class="code-line">        }
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    public async IAsyncEnumerable&lt;string&gt; GetChunksAsync(string input)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        // Do your chunking logic here.
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>When you have implemented your chunking service, you register it like shown below.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">var provider = new ServiceCollection()
</span>
            
<span class="code-line">    .AddOpenAIExtensions()
</span>
            
<span class="code-line">    .WithChunkingService&lt;MyCustomChunkingService&gt;()
</span>
            
<span class="code-line">    .Services
</span>
            
<span class="code-line">    .BuildServiceProvider();
</span>
            
</code></pre>
            
<h2 id="aggregating-multiple-vector-embeddings">Aggregating Multiple Vector Embeddings
</h2>
            
<p>The 
<code>EmbeddingProvider
</code> automatically uses the configured chunking service to chunk up text. If the chunking service produces more than one chunk, there will also be more than one embeddings generated. It will also automatically use the registered embedding aggregation service to aggregate multiple embeddings into one before returning. If no aggregation service is registered, the default 
<code>WeightedAverageAggregationService
</code> is used.
</p>
            
<p>The 
<code>WeightedAverageAggregationService
</code> uses a weighted average approach to aggregate multiple embeddings into one. This aggregation service uses the total amount of tokens used to produce the embedding as weight. The more tokens consumed, the longer the text typically is, or is otherwise more significant in relation to other embeddings.
</p>
            
<h3 id="create-your-own-aggregation-service">Create Your Own Aggregation Service
</h3>
            
<p>You can also create your own custom aggregation service and register that to be used instead of the default. The code below shows how to implement your custom aggregation service.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">using System.Collections.Generic;
</span>
            
<span class="code-line">using System.Threading.Tasks;
</span>
            
<span class="code-line">using Denomica.OpenAI.Extensions.Embeddings;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">public class MyCustomAggregationService : IEmbeddingAggregationService
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    public Task&lt;EmbeddingResponse?&gt; AggregateAsync(IEnumerable&lt;EmbeddingResponse&gt;? embeddings)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        // Implement your aggregation logic here.
</span>
            
<span class="code-line">        return null;
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>When you have implemented your custom aggregation service, you can register it like shown below.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">var provider = new ServiceCollection()
</span>
            
<span class="code-line">    .AddOpenAIExtensions()
</span>
            
<span class="code-line">    .WithEmbeddingModel((opt, sp) =&gt;
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">    })
</span>
            
<span class="code-line">    .WithEmbeddingAggregationService&lt;MyCustomAggregationService&gt;()
</span>
            
<span class="code-line">    .Services
</span>
            
<span class="code-line">    .BuildServiceProvider();
</span>
            
</code></pre>
            
<h2 id="wrapping-up-smarter-embeddings-with-less-effort">Wrapping Up: Smarter Embeddings with Less Effort
</h2>
            
<p>Working with long input text and vector embedding models doesn’t have to be complicated. 
<code>Denomica.OpenAI.Extensions
</code> provides a clean and extensible way to handle both text chunking and embedding aggregation – without writing boilerplate code or reinventing the wheel. Whether you’re using the default weighted average logic or plugging in your own services, the library is designed to fit naturally into your AI-driven .NET applications. Give it a try via 
<a href="https://www.nuget.org/packages/Denomica.OpenAI.Extensions/">NuGet
</a>, and feel free to explore, extend, or 
<a href="https://github.com/Denomica/Denomica.OpenAI.Extensions">contribute
</a>.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Vector Embedding Model Comparison with Multilingual Text</title>
      <description>A vector embedding model comparison focusing on multilingual text using embedding models in Azure AI Foundry.</description>
      <link>https://stage.mikaberglund.com/vector-embedding-model-comparison-with-multilingual-text</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/vector-embedding-model-comparison-with-multilingual-text</guid>
      <pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="2025-06-12-145505-dall-e-3" alt="Vector Embedding Model Comparison with Multilingual Text" />
                
<figcaption>Vector Embedding Model Comparison with Multilingual Text
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/vector-embeddings-for-long-documents" role="button" aria-label="Previous article: Vector Embeddings For Long Documents"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/simplify-text-chunking-for-vector-embeddings" role="button" aria-label="Next article: Simplify Text Chunking for Vector Embeddings"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="vector-embedding-model-comparison-with-multilingual-text">Vector Embedding Model Comparison with Multilingual Text
</h1>
            
<p class="article-meta">July 8, 2025
</p>

            
<p>Lately, I’ve been spending quite a lot of time working with vector embeddings and 
<a href="https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search">Cosmos DB Vector Search
</a>. Since I live in Finland, I work a lot with information in another language than English. That’s why I set out to do this vector embedding model comparison using a handful of the vector embedding models available in Azure AI Foundry.
</p>
            
<h2 id="comparison-focus">Comparison Focus
</h2>
            
<p>As I wrote in a previous article, 
<a href="/cosmos-db-vector-search-an-introduction/">embedding models can be language agnostic
</a>. In this comparison, I also wanted to get some kind of understanding of how language agnostic they can. I also wanted to see whether there is any difference between various embedding models.
</p>
            
<p>Another area I wanted to focus on was to see how good different embedding models are to capture the semantic context of an input instead of literally capturing the meaning. What I mean by this is that I wanted to check how similar input is when it’s written differently, but essentially means the same thing. So for instance “the weather is nice” should be quite similar to “enjoyable weather” or “the sun is shining from a clear blue sky”.
</p>
            
<h3 id="vector-embedding-model-list">Vector Embedding Model List
</h3>
            
<p>In this comparison, I used the following embedding models available in 
<a href="https://azure.microsoft.com/products/ai-foundry">Azure AI Foundry
</a>.
</p>
            
<ul><li><a href="https://ai.azure.com/explore/models/embed-v-4-0/version/4/registry/azureml-cohere">embed-v-4-0
</a></li><li><a href="https://ai.azure.com/explore/models/Cohere-embed-v3-english/version/1/registry/azureml-cohere">Cohere-embed-v3-english
</a></li><li><a href="https://ai.azure.com/explore/models/Cohere-embed-v3-multilingual/version/1/registry/azureml-cohere">Cohere-embed-v3-multilingual
</a></li><li><a href="https://ai.azure.com/explore/models/text-embedding-3-large/version/1/registry/azure-openai">text-embedding-3-large
</a></li><li><a href="https://ai.azure.com/explore/models/text-embedding-3-small/version/1/registry/azure-openai">text-embedding-3-small
</a></li><li><a href="https://ai.azure.com/explore/models/text-embedding-ada-002/version/2/registry/azure-openai">text-embedding-ada-002
</a></li></ul>
            
<h2 id="test-rig-for-vector-embedding-model-comparison">Test Rig for Vector Embedding Model Comparison
</h2>
            
<p>I created a simple console application for running my vector embedding model comparison. It takes two strings, creates a vector for both strings using the embedding models I included in the comparison, and then calculates a similarity score for the produced vectors. The similarity score is calculated using the following function that calculates the similarity using cosine similarity.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">static double CosineSimilarity(float[] v1, float[] v2)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    if (v1.Length != v2.Length)
</span>
            
<span class="code-line">        throw new ArgumentException(&quot;Vectors must be of same length.&quot;);
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    double dot = 0.0, mag1 = 0.0, mag2 = 0.0;
</span>
            
<span class="code-line">    for (int i = 0; i &lt; v1.Length; i++)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        dot += v1[i] * v2[i];
</span>
            
<span class="code-line">        mag1 += v1[i] * v1[i];
</span>
            
<span class="code-line">        mag2 += v2[i] * v2[i];
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">    if (mag1 == 0 || mag2 == 0)
</span>
            
<span class="code-line">        return 0; // Avoid division by zero
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    return dot / (Math.Sqrt(mag1) * Math.Sqrt(mag2));
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<h2 id="vector-embedding-model-comparison">Vector Embedding Model Comparison
</h2>
            
<p>So let’s start testing. I’ve added each test case as a sub chapter. Each test case defines the two texts used, and a table representing the similarity produced by the embedding models included in this comparison.
</p>
            
<p>The similarity score using cosine similarity metrics can vary between -1 (least similar) to +1 (most similar). In the tables, similarity score is rounded to 2 decimals.
</p>
            
<h3 id="test-1">Test #1
</h3>
            
<p>Let’s start with a very simple test with two short, very similar texts.
</p>
            
<ol><li>Apples are red. Bananas are yellow.
</li><li>Apples have a red color. Bananas are colored yellow.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.86             | | Cohere-embed-v3-english      | 0.97             | | Cohere-embed-v3-multilingual | 0.97             | | text-embedding-3-large       | 0.87             | | text-embedding-3-small       | 0.91             | | text-embedding-ada-002       | 0.97             |
</p>
            
<h3 id="test-2">Test #2
</h3>
            
<p>Now let’s use the same text from the previous test, but translate the other text into Finnish, as precisely as possible.
</p>
            
<ol><li>Apples are red. Bananas are yellow.
</li><li>Omenat ovat punaisia. Banaanit ovat keltaisia.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.53             | | Cohere-embed-v3-english      | 0.37             | | Cohere-embed-v3-multilingual | 0.66             | | text-embedding-3-large       | 0.57             | | text-embedding-3-small       | 0.49             | | text-embedding-ada-002       | 0.84             |
</p>
            
<h3 id="test-3">Test #3
</h3>
            
<p>Let’s move on to describing the sun setting.
</p>
            
<ol><li>The sun sets beautifully over the calm ocean.
</li><li>The sun descends gracefully above the tranquil sea.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.80             | | Cohere-embed-v3-english      | 0.82             | | Cohere-embed-v3-multilingual | 0.92             | | text-embedding-3-large       | 0.76             | | text-embedding-3-small       | 0.78             | | text-embedding-ada-002       | 0.96             |
</p>
            
<h3 id="test-4">Test #4
</h3>
            
<p>Now let’s do the same as in the previous test, but translate the first language into Swedish.
</p>
            
<ol><li>The sun sets beautifully over the calm ocean.
</li><li>Solen g&#229;r vackert ner &#246;ver det lugna havet.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.68             | | Cohere-embed-v3-english      | 0.26             | | Cohere-embed-v3-multilingual | 0.81             | | text-embedding-3-large       | 0.63             | | text-embedding-3-small       | 0.61             | | text-embedding-ada-002       | 0.87             |
</p>
            
<h3 id="test-5">Test #5
</h3>
            
<p>Now we move on to a bit longer texts. First, two texts in English written in different ways, still keeping the semantic meaning intact.
</p>
            
<ol><li>In the south-western Finnish archipelago, a summer sunset paints the sky in hues of gold, pink, and deep orange, reflecting over the calm sea. The warm evening air carries the scent of saltwater and blooming wildflowers, while gentle waves lap against the rocky shoreline. As the sun dips below the horizon, the archipelago’s countless small islands cast long, soft shadows over the tranquil waters, creating a breathtakingly serene scene.
</li><li>In the south-western archipelago of Finland, a summer evening sky is illuminated with shades of gold, pink, and rich orange, mirrored on the still surface of the sea. The mild night air is filled with the aroma of sea spray and blossoming wildflowers, as soft waves brush against the rocky coast. When the sun sinks beneath the horizon, the many tiny islands of the archipelago stretch their gentle shadows across the peaceful waters, forming an extraordinarily calm and beautiful landscape.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.86             | | Cohere-embed-v3-english      | 0.94             | | Cohere-embed-v3-multilingual | 0.94             | | text-embedding-3-large       | 0.95             | | text-embedding-3-small       | 0.92             | | text-embedding-ada-002       | 0.98             |
</p>
            
<h3 id="test-6">Test #6
</h3>
            
<p>Now we take the text from the previous test and translate it to Finnish.
</p>
            
<ol><li>In the south-western Finnish archipelago, a summer sunset paints the sky in hues of gold, pink, and deep orange, reflecting over the calm sea. The warm evening air carries the scent of saltwater and blooming wildflowers, while gentle waves lap against the rocky shoreline. As the sun dips below the horizon, the archipelago’s countless small islands cast long, soft shadows over the tranquil waters, creating a breathtakingly serene scene.
</li><li>Kes&#228;inen auringonlasku Lounais-Suomen saaristossa maalaa taivaan kultaisiin, pinkkeihin ja syv&#228;noransseihin s&#228;vyihin, jotka heijastuvat tyyneen mereen. L&#228;mmin iltailma kantaa mukanaan suolaveden ja kukkivien luonnonkukkien tuoksua, kun lempe&#228;t aallot huuhtovat kivikkoista rantaa. Auringon vajotessa horisontin taakse lukuisat pikkusaaret heitt&#228;v&#228;t pitki&#228;, pehmeit&#228; varjoja rauhallisen veden ylle, luoden henke&#228;salpaavan seesteisen maiseman.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.34             | | Cohere-embed-v3-english      | 0.41             | | Cohere-embed-v3-multilingual | 0.74             | | text-embedding-3-large       | 0.74             | | text-embedding-3-small       | 0.66             | | text-embedding-ada-002       | 0.91             |
</p>
            
<h3 id="test-7">Test #7
</h3>
            
<p>Let’s twist up things a little more and take the Finnish text from the previous test and translate that into Swedish.
</p>
            
<ol><li>Kes&#228;inen auringonlasku Lounais-Suomen saaristossa maalaa taivaan kultaisiin, pinkkeihin ja syv&#228;noransseihin s&#228;vyihin, jotka heijastuvat tyyneen mereen. L&#228;mmin iltailma kantaa mukanaan suolaveden ja kukkivien luonnonkukkien tuoksua, kun lempe&#228;t aallot huuhtovat kivikkoista rantaa. Auringon vajotessa horisontin taakse lukuisat pikkusaaret heitt&#228;v&#228;t pitki&#228;, pehmeit&#228; varjoja rauhallisen veden ylle, luoden henke&#228;salpaavan seesteisen maiseman.
</li><li>En somrig solnedg&#229;ng i sydv&#228;stra Finlands sk&#228;rg&#229;rd m&#229;lar himlen i gyllene, rosa och djupt orange nyanser som speglas i det stilla havet. Den varma kv&#228;llsluften b&#228;r med sig doften av saltvatten och blommande vilda blommor, medan mjuka v&#229;gor sk&#246;ljer mot den steniga stranden. N&#228;r solen sjunker bakom horisonten kastar de otaliga sm&#229; &#246;arna l&#229;nga, mjuka skuggor &#246;ver det lugna vattnet och skapar ett andl&#246;st fridfullt landskap.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.41             | | Cohere-embed-v3-english      | 0.49             | | Cohere-embed-v3-multilingual | 0.75             | | text-embedding-3-large       | 0.83             | | text-embedding-3-small       | 0.66             | | text-embedding-ada-002       | 0.90             |
</p>
            
<h3 id="test-8">Test #8
</h3>
            
<p>Now, let’s twist this up even further. We create two texts in different language, and try to make them as different as possible. The first text is the same text we’ve used in the previous tests. The second text is a product description for the 
<a href="https://www.kalevala.fi/en/products/kosmos-pendant-bronze-turquoise">Kalevala Kosmos Pedant in bronze
</a> in Finnish.
</p>
            
<ol><li>In the south-western Finnish archipelago, a summer sunset paints the sky in hues of gold, pink, and deep orange, reflecting over the calm sea. The warm evening air carries the scent of saltwater and blooming wildflowers, while gentle waves lap against the rocky shoreline. As the sun dips below the horizon, the archipelago’s countless small islands cast long, soft shadows over the tranquil waters, creating a breathtakingly serene scene.
</li><li>Kosmoksen kirkas pronssi hehkuu l&#228;mpimiss&#228; s&#228;vyiss&#228;. Kullanhohtoisen pronssin ja mustan tai turkoosin korukiven yhdistelm&#228; luo n&#228;ytt&#228;viin Kosmos-koruihin j&#228;nnitett&#228; ja dramatiikkaa. Papuketjussa s&#228;&#228;dett&#228;v&#228; pituus 45/42 cm. Papukaijalukko. Jos ketju on sinulle liian lyhyt, saat siihen k&#228;tev&#228;sti lis&#228;&#228; mittaa erikseen myyt&#228;v&#228;ll&#228; jatkopalalla. Jatkopala kiinnitet&#228;&#228;n siin&#228; olevalla papukaijalukolla korun ketjuun jatkeeksi. Jatkopalan pituus 5 cm.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.21             | | Cohere-embed-v3-english      | 0.35             | | Cohere-embed-v3-multilingual | 0.33             | | text-embedding-3-large       | 0.17             | | text-embedding-3-small       | 0.19             | | text-embedding-ada-002       | 0.79             |
</p>
            
<h3 id="test-9">Test #9
</h3>
            
<p>Let’s do one final test. Use two texts in English and try to make them as different as possible.
</p>
            
<ol><li>The impressive Kosmos pendant is embellished with a gleaming black onyx or a tranquil turquoise. Made of bright bronze, the pendant glows like gold and is a true eye-catcher. Adjustable length 42/45 cm. Lobster clasp. If the chain is too short for you, you can easily add length to it with a 5 cm extension piece sold separately.
</li><li>To be, or not to be, that is the question: Whether ’tis nobler in the mind to suffer the slings and arrows of outrageous fortune, or to take arms against a sea of troubles and by opposing end them.
</li></ol>
            
<p>| Embedding Model              | Similarity Score | | ---------------------------- | ---------------- | | embed-v-4-0                  | 0.19             | | Cohere-embed-v3-english      | 0.10             | | Cohere-embed-v3-multilingual | 0.27             | | text-embedding-3-large       | 0.07             | | text-embedding-3-small       | 0.07             | | text-embedding-ada-002       | 0.71             |
</p>
            
<h2 id="summary">Summary
</h2>
            
<p>So what did I get out of this vector embedding model comparison? At first I was surprised how good the 
<code>text-embedding-ada-002
</code> was performing to find similarity between different texts. However, in the last two tests, where the two texts are very different, that model still thought the texts were quite similar. Because of this, I would not use the 
<code>text-embedding-ada-002
</code> model for anything.
</p>
            
<p>The second observation is that the 
<code>text-embedding-3-large
</code> model performs as well, or even better than the 
<code>Cohere-embed-v3-multilingual
</code> model, except for with very short input. However, when comparing vectors for short text in different languages, the 
<code>Cohere-embed-v3-multilingual
</code> model is not performing significantly better than for instance 
<code>text-embedding-3-large
</code>, or even the 
<code>text-embedding-3-small
</code> model.
</p>
            
<p>So which one should you pick for your purpose? As with many architectural decisions – It depends, and I would say it depends on whether your inputs (the texts that you vectorize) are all in the same language, or if you need to be able to find similarities (or differences) across different languages.
</p>
            
<p>If your inputs are all in the same language, the 
<code>text-embedding-3-large
</code> seems to perform quite well. The 
<code>text-embedding-3-small
</code> works also well, and is a 
<a href="/cosmos-db-vector-search-an-introduction/">much cheaper option
</a>.
</p>
            
<p>But, if you are processing inputs in different languages, and processing texts that are anything than short sentences, I would go with either the 
<code>text-embedding-3-large
</code> or 
<code>Cohere-embed-v3-multilingual
</code> model. Since the cost for the 
<code>Cohere-embed-v3-multilingual
</code> is $0.0001 / 1000 tokens or roughly 0.000093 EUR / 1000 tokens, it is about the same as 
<code>text-embedding-3-large
</code>, which is around 0.000112 EUR / 1000 tokens.
</p>
            
<p>To make those figures a bit more tangible, the cost for 10 000 000 tokens for each of these two models is:
</p>
            
<ul><li><code>text-embedding-3-large
</code>: 1.12 EUR / 10 000 000 tokens
</li><li><code>Cohere-embed-v3-multilingual
</code>: 0.93 EUR / 10 000 000 tokens
</li></ul>
            
<p>Because the 
<code>text-embedding-3-large
</code> embedding model is better at recognizing differences and on par with the 
<code>Cohere-embed-v3-multilingual
</code> model, I would go with the 
<code>text-embedding-3-large model
</code> in multi-lingual scenarios, even if that model is just a tad more expensive. The difference is still quite negligible.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Vector Embeddings For Long Documents</title>
      <description>This blog article discusses a method for creating vector embeddings for long documents exceeding the input limit of an embedding model.</description>
      <link>https://stage.mikaberglund.com/vector-embeddings-for-long-documents</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/vector-embeddings-for-long-documents</guid>
      <pubDate>Mon, 31 Mar 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="vector-embeddings-for-long-text" alt="Vector embeddings for long text." />
                
<figcaption>Vector embeddings for long text.
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/cosmos-db-vector-search-an-introduction" role="button" aria-label="Previous article: Cosmos DB Vector Search \\u2013 An Introduction"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/vector-embedding-model-comparison-with-multilingual-text" role="button" aria-label="Next article: Vector Embedding Model Comparison with Multilingual Text"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="vector-embeddings-for-long-documents">Vector Embeddings For Long Documents
</h1>
            
<p class="article-meta">March 31, 2025
</p>

            
<p>How do you create vector embeddings for long documents or long texts? This might be a question you find asking yourself when working with vector embeddings. This article is a follow-up to my 
<a href="/cosmos-db-vector-search-an-introduction/">previous article
</a> where I showed you how you can create vector embeddings with Azure AI Foundry and store them in Cosmos DB.
</p>
            
<h2 id="problem-description">Problem Description
</h2>
            
<p>Currently, all embedding models in Azure AI Foundry have a limit on how long the input text can be. Limits may vary from model to model, but there is a limit on each embedding model. You can read more about the details in my 
<a href="/cosmos-db-vector-search-an-introduction/">previous article
</a>. The main point is – If your input text is longer than the limit, you need to chunk up your input text, and generate the vectors for each chunk separately, as 
<a href="https://learn.microsoft.com/en-us/azure/ai-foundry/model-inference/how-to/use-embeddings?pivots=programming-language-rest#create-embeddings">suggested on Microsoft Learn
</a>.
</p>
            
<p>This will result in your input text being represented by more than one vector embedding. This might be an acceptable solution in some cases. However, I think that in most cases you would also want to have a vector embedding that represents the entire input text, regardless of how long the input text is.
</p>
            
<h2 id="combining-multiple-vectors-into-one">Combining Multiple Vectors Into One
</h2>
            
<p>One good solution is to take each chunk of input text and create a vector embedding out of the text. Then you would combine each chunk vector into a single vector that represents the entire input text. Now how would you do that then?
</p>
            
<p>Remember that the length of a vector generated by an embedding model is constant. For instance, the 
<code>text-embedding-3-small
</code> embedding model generates a vector with 1536 dimensions, by default. You can then just take the average of each dimension in each of your chunk vector. This would create quite a good result.
</p>
            
<p>However, to make this result even better, you could create the resulting vector as a 
<strong>weighted
</strong> average. When you use embedding models in Azure AI Foundry, in addition to the embedding vector, you also get a score that indicates how many tokens were consumed to produce that vector embedding. This consumption score is a good option to be used as the weight. The more tokens that you consume to generate an embedding vector, the longer and more complex the input text is. And because of that, it should also have a bigger influence on the resulting average. Let me explain using an example.
</p>
            
<p>Let’s take the following two short and simple vector embedding responses.
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">// Vector Embedding #1
</span>
            
<span class="code-line"></span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    &quot;data&quot;: [
</span>
            
<span class="code-line">        &quot;embedding&quot;: [1,2,1]
</span>
            
<span class="code-line">    ],
</span>
            
<span class="code-line">    &quot;usage&quot;: {
</span>
            
<span class="code-line">        &quot;total_tokens&quot;: 10
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">// Vector Embedding #2
</span>
            
<span class="code-line"></span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    &quot;data&quot;: [
</span>
            
<span class="code-line">        &quot;embedding&quot;: [1,2,5]
</span>
            
<span class="code-line">    ],
</span>
            
<span class="code-line">    &quot;usage&quot;: {
</span>
            
<span class="code-line">        &quot;total_tokens&quot;: 2
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>These two vector embedding results are overly simplified for a better overview. Each vector has 3 dimensions. The resulting vector would then also have 3 dimensions.
</p>
            
<p>If the dimension value in the first vector is 
<code>D1
</code>, the dimension value in the second vector is 
<code>D1
</code>, the number of consumed tokens for the first vector is 
<code>T1
</code> and the consumed tokens for the second vector is 
<code>T2
</code>, then each dimension value for the resulting vector embedding (
<code>Tr
</code>) would be calculated as below.
</p>
            
<pre class="code-block"><code>
            
<span class="code-line">Tr = ((D1 * T1) + (D2 * T2)) / (T1 + T2)
</span>
            
</code></pre>
            
<p>This would result in the following dimension values using the sample vectors from above.
</p>
            
<ol><li>((1 
<em> 10) + (1 
</em> 2)) / (10 + 2) = 
<strong>1
</strong></li><li>((2 
<em> 10) + (2 
</em> 2)) / (10 + 2) = 
<strong>2
</strong></li><li>((1 
<em> 10) + (5 
</em> 2)) / (10 + 2) = 
<strong>1.66
</strong></li></ol>
            
<p>So the resulting vector using weighted average is 
<code>[1,2,1.66]
</code>. A pure average operation without weights would have produced a vector 
<code>[1,2,3]
</code>.
</p>
            
<h2 id="conclusion">Conclusion
</h2>
            
<p>Using a weighted average is better that a pure average, since your chunks can vary is both size and complexity. However, this solves only half of the problem. You still have to figure out 
<strong>*how
</strong>* to produce your chunks from your long input text.
</p>
            
<p>I am currently working on a library that will help you with both chunking and calculating vector embeddings from chunked text. I will publish a link to that library when it’s ready for publishing, so stay tuned.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Cosmos DB Vector Search \u2013 An Introduction</title>
      <description>Explore Azure Cosmos DB as a vector database and generate embeddings with Azure AI Foundry\u0027s LLMs for powerful text search and analysis.</description>
      <link>https://stage.mikaberglund.com/cosmos-db-vector-search-an-introduction</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/cosmos-db-vector-search-an-introduction</guid>
      <pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="cosmos-db-vector-search" alt="Cosmos DB Vector Search \u2013 An Introduction" />
                
<figcaption>Cosmos DB Vector Search \u2013 An Introduction
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid" role="button" aria-label="Previous article: Blazor Apps With Mermaid Diagrams Using Blazorade Mermaid"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/vector-embeddings-for-long-documents" role="button" aria-label="Next article: Vector Embeddings For Long Documents"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="cosmos-db-vector-search-u2013-an-introduction">Cosmos DB Vector Search \u2013 An Introduction
</h1>
            
<p class="article-meta">February 22, 2025
</p>

            
<p>Applications powered by 
<a href="https://en.wikipedia.org/wiki/Generative_artificial_intelligence">Generative AI
</a> are becoming more and more popular. Often these applications employ some kind of 
<a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG
</a> (Retrieval-Augmented Generation) solution patterns. For RAG powered applications, the 
<strong><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/vector-search">Cosmos DB Vector Search
</a></strong> feature can be very useful. In this article I’ll describe this feature in more detail. This will give you better information to base your decisions on when deciding whether you could leverage this in your applications.
</p>
            
<p>You might also want to have a look at another article where I compare 
<a href="/from-words-to-meaning-vector-search-cosmos-db/">Cosmos DB vector search with traditional indexing
</a>.
</p>
            
<h2 id="understanding-vector-embeddings">Understanding Vector Embeddings
</h2>
            
<p>A vector embedding is basically an array of floating-point numbers. What is also characteristic for embeddings is that they are fixed-sized. It does not matter how long or complex the input is. The vector embedding generated by one model always has the same number of dimensions. However, some models allow you to specify the number of dimensions you want in the vector embedding.
</p>
            
<p>But what makes vector embeddings so useful is that they encapsulate the semantic context of the input. The closer a vector is to another vector, the more similar the inputs represented by the vectors are. You can think of a vector as a path. If your input talks about bananas, the resulting vector or “path” passes close to the “area” where fruits are discussed. If your input talks about cars, then the path goes by the “area” where vehicles are discussed. This analogy helped me to get my mind around the concept of vector embeddings. This document from Microsoft Learn about 
<a href="https://learn.microsoft.com/azure/cosmos-db/gen-ai/vector-embeddings">Vector Embeddings in Cosmos DB
</a> can also give you a better idea of how vector embeddings are designed to work.
</p>
            
<p>You use Cosmos DB Vector Search to find content similar to something else. To find similar content you first 
<a href="/cosmos-db-vector-search-an-introduction/">generate a vector
</a> (“query vector”) for the data you want to find similar content to. Then you search for content with vectors that are closest to your query vector. You must generate the query vector in the same way as you generated the vectors for your content. This includes also using the same embedding model. If you decide to switch model, you need to regenerate the vectors that you have stored.
</p>
            
<h3 id="vector-embeddings-can-be-language-agnostic">Vector Embeddings Can Be Language Agnostic
</h3>
            
<p>Since embedding models capture the semantic context of the input rather than the actual content, they can also be language agnostic. However, this is not always the case. Some models might be better on capturing semantics regardless of the language than others. If you have an embedding model that is truly language agnostic, the vectors they produce for input with the same meaning but in different languages, should be very similar. This can be helpful if your content is in another language that you use when creating your query vectors. If this is an important requirement for you, you need to do your due diligence and make sure that you pick an embedding model that can properly handle different languages.
</p>
            
<h2 id="setting-up-cosmos-db-vector-search">Setting up Cosmos DB Vector Search
</h2>
            
<p>The first thing you have to do to turn on Cosmos DB Vector Search is to enable that feature on your Cosmos DB account. There are two steps to enable Cosmos DB Vector Search:
</p>
            
<ol><li>Turn on the feature for your Cosmos DB Account
</li><li>Create a container with a vector policy
</li></ol>
            
<blockquote><strong>Note!
</strong> Once you have turned that feature on, you cannot turn it off again. It is a one-way street, so remember this before you go on turning on this feature on all your Cosmos DB accounts.
</blockquote>
            
<p>You can turn this feature on in the Azure portal. Navigate to your Cosmos DB account and select 
<strong>Settings/Features
</strong> from the menu. Then turn on the 
<strong>Vector Search for NoSQL API
</strong> feature.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#cosmos-db-vector-search-an-introduction-image-1" data-bs-toggle="modal" data-bs-target="#cosmos-db-vector-search-an-introduction-image-1" aria-label="Open image"><img class="article-content-image" src="/img/posts/cosmos-db-vector-search-an-introduction/image.png" alt="image" /></a></figure><div class="modal fade" id="cosmos-db-vector-search-an-introduction-image-1" tabindex="-1" aria-labelledby="cosmos-db-vector-search-an-introduction-image-1-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="cosmos-db-vector-search-an-introduction-image-1-label">image
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/cosmos-db-vector-search-an-introduction/image.png" alt="image" /></div></div></div></div>
            
<p>Next you need to create a container with a vector policy.
</p>
            
<blockquote>Note! Currently, vector policies in Cosmos DB are immutable, meaning that you can’t change the policy after the container has been created. If you need to change it, you must create a new container with a new vector policy, and migrate your data to the new container.
</blockquote>
            
<p>You define the vector policy on the same dialog where you create your container.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#cosmos-db-vector-search-an-introduction-image-2" data-bs-toggle="modal" data-bs-target="#cosmos-db-vector-search-an-introduction-image-2" aria-label="Open image-1"><img class="article-content-image" src="/img/posts/cosmos-db-vector-search-an-introduction/image-1.png" alt="image-1" /></a></figure><div class="modal fade" id="cosmos-db-vector-search-an-introduction-image-2" tabindex="-1" aria-labelledby="cosmos-db-vector-search-an-introduction-image-2-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="cosmos-db-vector-search-an-introduction-image-2-label">image-1
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/cosmos-db-vector-search-an-introduction/image-1.png" alt="image-1" /></div></div></div></div>
            
<ul><li><strong>Path
</strong>: This is the path to the attribute on your JSON documents that contains the vector.
</li><li><strong>Data type
</strong>: The data type for the values in your vector. This value needs to match the data type your embedding model supports. Typically, embedding models create vectors containing floating-point numbers.
</li><li><strong>Distance function
</strong>: The function to use for calculating vector distance. A cosine function returns a value between -1 and +1 representing the similarity. A score of -1 is least similar and +1 is most similar.
</li><li><strong>Dimensions
</strong>: The number of dimensions of your vectors, i.e. items in the floating-point number array. You need to check the number of dimensions supported by the embedding model you plan to use.
</li><li><strong>Index type
</strong>: The index type to use. Read more about 
<a href="https://learn.microsoft.com/en-us/azure/cosmos-db/index-policy#vector-indexes">index types for Cosmos DB
</a>. With the index type, you can potentially decrease the the amount of Request Units (RUs) your vector queries consume, so be sure to check this out.
</li></ul>
            
<h2 id="generating-vectors-with-embedding-models-in-azure-ai-foundry">Generating Vectors with Embedding Models in Azure AI Foundry
</h2>
            
<p>There are many ways to generate vector embeddings. However, in this article I focus on generating them with the help of models deployed in 
<a href="https://learn.microsoft.com/en-us/azure/ai-studio/what-is-ai-studio">Azure AI Foundry
</a>. You can use different kinds of SDKs to communicate with model deployments in Azure AI Foundry. I will not use any of those, but use simple REST calls to demonstrate how to communicate. I believe that this will give you the best understanding of how things actually work. It will then make it much easier for you to use an SDK or write your own, when you know how stuff works under the hood.
</p>
            
<h3 id="provisioning-the-required-azure-resources">Provisioning the Required Azure Resources
</h3>
            
<p>To generate a vector, you need to provision an Azure OpenAI resource.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#cosmos-db-vector-search-an-introduction-image-3" data-bs-toggle="modal" data-bs-target="#cosmos-db-vector-search-an-introduction-image-3" aria-label="Open image-2"><img class="article-content-image" src="/img/posts/cosmos-db-vector-search-an-introduction/image-2.png" alt="image-2" /></a></figure><div class="modal fade" id="cosmos-db-vector-search-an-introduction-image-3" tabindex="-1" aria-labelledby="cosmos-db-vector-search-an-introduction-image-3-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="cosmos-db-vector-search-an-introduction-image-3-label">image-2
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/cosmos-db-vector-search-an-introduction/image-2.png" alt="image-2" /></div></div></div></div>
            
<p>Then you need to deploy a model that is designed to be used for embeddings. There are several embedding models available. The 
<strong>text-embedding-3-small
</strong> and 
<strong>text-embedding-3-large
</strong> are probably the most recent models currently, at the time of writing (late Feb 2025).
</p>
            
<p>I will use the 
<strong>text-embedding-3-small
</strong> model in this example. It is much cheaper than its larger counterpart. However, you can pick any model that is capable of creating vector embeddings from text. Note that there are also embedding models that can create vector embeddings from images. Those models are out of the scope of this article. I’ll have to come back to those in a later article.
</p>
            
<h3 id="creating-the-http-requests">Creating the HTTP Requests
</h3>
            
<p>After you have deployed your embedding model, for instance the 
<strong>text-embedding-3-small
</strong>, select the deployed model to open the 
<strong>Details
</strong> page for the deployment.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#cosmos-db-vector-search-an-introduction-image-4" data-bs-toggle="modal" data-bs-target="#cosmos-db-vector-search-an-introduction-image-4" aria-label="Open image-4"><img class="article-content-image" src="/img/posts/cosmos-db-vector-search-an-introduction/image-4.png" alt="image-4" /></a></figure><div class="modal fade" id="cosmos-db-vector-search-an-introduction-image-4" tabindex="-1" aria-labelledby="cosmos-db-vector-search-an-introduction-image-4-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="cosmos-db-vector-search-an-introduction-image-4-label">image-4
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/cosmos-db-vector-search-an-introduction/image-4.png" alt="image-4" /></div></div></div></div>
            
<p>On the Details page, copy the 
<strong>Target URI
</strong> and 
<strong>Key
</strong> to use when sending requests. All requests to this endpoint are 
<code>POST
</code> requests with the key added to the 
<code>api-key
</code> request header. In 
<strong><a href="https://www.postman.com/">Postman
</a></strong>, you can create one collection and define the authorization settings on the collection level. This way, all requests in that collection will inherit the same settings. The Postman settings are below.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#cosmos-db-vector-search-an-introduction-image-5" data-bs-toggle="modal" data-bs-target="#cosmos-db-vector-search-an-introduction-image-5" aria-label="Open image-5"><img class="article-content-image" src="/img/posts/cosmos-db-vector-search-an-introduction/image-5.png" alt="image-5" /></a></figure><div class="modal fade" id="cosmos-db-vector-search-an-introduction-image-5" tabindex="-1" aria-labelledby="cosmos-db-vector-search-an-introduction-image-5-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="cosmos-db-vector-search-an-introduction-image-5-label">image-5
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/cosmos-db-vector-search-an-introduction/image-5.png" alt="image-5" /></div></div></div></div>
            
<p>The request body looks like this.
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">{
</span>
            
<span class="code-line">    &quot;input&quot;: &quot;Your text to generate the vector embedding for.&quot;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>You can also specify specify multiple input strings by setting the 
<code>input
</code> attribute to an array of strings, like this:
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">{
</span>
            
<span class="code-line">    &quot;input&quot;: [
</span>
            
<span class="code-line">        &quot;This is your first string.&quot;,
</span>
            
<span class="code-line">        &quot;The second string to generate vector embeddings for.&quot;
</span>
            
<span class="code-line">    ]
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>When you specify multiple input strings, you will also get the same amount of vector embeddings in the response. With embedding models like 
<strong>text-embedding-3-small
</strong> and 
<strong>text-embedding-3-large
</strong>, you can also specify the number of dimensions you want in the resulting vector embedding. The number of dimensions are specified in the request body as shown below.
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">{
</span>
            
<span class="code-line">    &quot;input&quot;: &quot;My name is Bond. James Bond.&quot;,
</span>
            
<span class="code-line">    &quot;dimensions&quot;: 1024
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>For the 
<strong>text-embedding-3-small
</strong> model, the minimum number of dimensions is 
<strong>1
</strong>, and the maximum number is 
<strong>1536
</strong>. For 
<strong>text-embedding-3-large
</strong>, the minimum number of dimensions is also 
<strong>1
</strong>, but the maximum number is 
<strong>3072
</strong>. The more dimensions you have, the better you can capture different nuances in the input text. On the other hand, larger vectors take up more storage space in Cosmos DB and consume more Request Units when you query for data.
</p>
            
<blockquote>Note! Currently, there is a limitation for how long the input can be. Unfortunately there is no absolute limit that you could easily check before you send the request. Instead, the limit is expressed in 
<strong>tokens
</strong>. The maximum input tokens for 
<strong>text-embedding-3-small
</strong> and 
<strong>text-embedding-3-large
</strong> are 8191 tokens. How many tokens that are consumed by a given text depends on many factors, such as language, text complexity etc. In general, 
<strong>a fair estimate for standard English text is that around 4 characters consume 1 token
</strong>. This means that in theory, you should be able to generate vector embeddings as described above for a text containing 32 764 characters. Some text can be longer and some text can be shorter.
</blockquote>
            
<p>There are various patterns you can employ to generate vectors for larger text. I’ve written about the weighted average pattern in a previous article 
<a href="/vector-embeddings-for-long-documents/">Vector Embeddings for Long Documents
</a>.
</p>
            
<h2 id="storing-the-generated-vector-for-cosmos-db-vector-search">Storing the Generated Vector for Cosmos DB Vector Search
</h2>
            
<p>When sending the request as described above, you will get a response that looks like this.
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line"> {
</span>
            
<span class="code-line">    &quot;object&quot;: &quot;list&quot;,
</span>
            
<span class="code-line">    &quot;data&quot;: [
</span>
            
<span class="code-line">        {
</span>
            
<span class="code-line">            &quot;object&quot;: &quot;embedding&quot;,
</span>
            
<span class="code-line">            &quot;index&quot;: 0,
</span>
            
<span class="code-line">            &quot;embedding&quot;: [
</span>
            
<span class="code-line">                0.006165823,
</span>
            
<span class="code-line">                0.013940725,
</span>
            
<span class="code-line">                -0.066804506,
</span>
            
<span class="code-line">                0.015688516,
</span>
            
<span class="code-line">                // ...
</span>
            
<span class="code-line">                0.056068067,
</span>
            
<span class="code-line">                0.036370724,
</span>
            
<span class="code-line">                -0.0026667705,
</span>
            
<span class="code-line">                -0.008225721
</span>
            
<span class="code-line">            ]
</span>
            
<span class="code-line">        }
</span>
            
<span class="code-line">    ],
</span>
            
<span class="code-line">    &quot;model&quot;: &quot;text-embedding-3-small&quot;,
</span>
            
<span class="code-line">    &quot;usage&quot;: {
</span>
            
<span class="code-line">        &quot;prompt_tokens&quot;: 8,
</span>
            
<span class="code-line">        &quot;total_tokens&quot;: 8
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>This response message is pretty self-explanatory. If you created your Cosmos DB container and vector policy as above, then you would probably store this vector in your container using a JSON document similar to the following.
</p>
            
<pre class="code-block" data-language="json"><code>
            
<span class="code-line">{
</span>
            
<span class="code-line">    &quot;id&quot;: &quot;5b39d118-65b6-46f4-a402-95c54b9e2648&quot;,
</span>
            
<span class="code-line">    &quot;partition&quot;: &quot;partition-1&quot;,
</span>
            
<span class="code-line">    &quot;content&quot;: {
</span>
            
<span class="code-line">        &quot;text&quot;: &quot;My name is Bond. James Bond.&quot;
</span>
            
<span class="code-line">    },
</span>
            
<span class="code-line">    &quot;embedding&quot;: {
</span>
            
<span class="code-line">        &quot;vector&quot;: [
</span>
            
<span class="code-line">            0.006165823,
</span>
            
<span class="code-line">            0.013940725,
</span>
            
<span class="code-line">            -0.066804506,
</span>
            
<span class="code-line">            0.015688516,
</span>
            
<span class="code-line">            // ...
</span>
            
<span class="code-line">            0.056068067,
</span>
            
<span class="code-line">            0.036370724,
</span>
            
<span class="code-line">            -0.0026667705,
</span>
            
<span class="code-line">            -0.008225721
</span>
            
<span class="code-line">        ],
</span>
            
<span class="code-line">        &quot;totalTokens&quot;: 8
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>Since this is a completely normal JSON document stored in a completely normal Cosmos DB container, you can of course add whatever data you think you need to store in this document. This can be anything you need to be able to effectively query the data later on.
</p>
            
<h2 id="querying-data-in-cosmos-db-vector-search">Querying Data in Cosmos DB Vector Search
</h2>
            
<p>When querying your data using Cosmos DB Vector Search, you pretty much build your SQL query just as you normally would, with a few distinct differences.
</p>
            
<ol><li>You use the built-in 
<code>VectorDistance
</code> function to order your results
</li><li>You must always specify 
<code>SELECT TOP n
</code> to limit the number of items returned in the result
</li></ol>
            
<p>The 
<code>VectorDistance
</code> function compares a given vector to the vector of each document matching the filters of the query. The result is then ordered so that the documents with the vectors closest to the vector specified in the query are listed first. The sample SQL query below uses the 
<code>&#64;queryVector
</code> parameter as placeholder for the vector.
</p>
            
<pre class="code-block" data-language="sql"><code>
            
<span class="code-line">SELECT TOP 5 c.id,c.partition,c.content, VectorDistance(c.embedding.vector, &#64;queryVector) AS distance
</span>
            
<span class="code-line">FROM c
</span>
            
<span class="code-line">WHERE c.distance &gt;= 0.8
</span>
            
<span class="code-line">order by VectorDistance(c.embedding.vector, &#64;queryVector)
</span>
            
</code></pre>
            
<p>This query would return 5 JSON documents where the embedding vector is closest to your query vector. This means that the content in the resulting JSON documents is semantically most similar to the text that you used to generate the query vector.
</p>
            
<p>The 
<code>WHERE
</code> clause will further filter out the result to include only those documents that are actually similar to the query vector. This is to make sure that the result actually contains content similar to the query and not just something that is most similar even though not even close to what you are looking for.
</p>
            
<p>Imagine that you have a container only containing documents describing various car models. Now, if you would query the container for recipes on deserts with bananas, you would get 5 documents describing car models. They are not similar at all to what you were looking for, but are the closest ones when there are no desert recipes in your container.
</p>
            
<h3 id="filtering-on-similarity">Filtering on Similarity
</h3>
            
<p>There is no exact description for what the values returned by the 
<code>VectorDistance
</code> function actually mean, but you can use the following for some kind of basis for your decisions. If you are using the 
<code>Cosine
</code> function as your distance function, the 
<code>VectorDistance
</code> function returns a floating-point number between -1 and +1. -1 means no similarity at all, and +1 means very similar, if not even identical.
</p>
            
<ul><li><strong>+0.80 to +1
</strong>: Very similar. These results are highly relevant and closely match your query.
</li><li><strong>+0.50 to +0.79
</strong>: Moderately similar. These results are somewhat relevant but may not be exact matches.
</li><li><strong>0 to +0.49
</strong>: Low similarity. These results are not very relevant to your query.
</li><li><strong>Below 0
</strong>: Not similar. These results are quite dissimilar to your query.
</li></ul>
            
<p>Remember that what is considered “similar enough” can vary depending on your specific use case and context. Setting the similarity filter to +0.80 or above ensures that the result contains only highly relevant documents. I would suggest that you would run some tests with your content vectors and match them to expected query vectors to get the similarity filter to best suit your needs.
</p>
            
<h2 id="practical-examples-of-using-cosmos-db-vector-search">Practical Examples of Using Cosmos DB Vector Search
</h2>
            
<p>So what would you use the Cosmos DB Vector Search for? Why bother? Why not just use the vector indexes provided in Azure AI Search or Azure AI Foundry? Well, one thing for sure is that you have more control over what content is included in your query results. You also have control over how you generate vector embeddings for your content. If you have long documents, you could also split the documents into smaller parts like paragraphs or chapters, and generate vector embeddings for each part separately.
</p>
            
<p>And you don’t have to settle for just searching for content similar to your query vector. You can add whatever filters you need to find the right content. Remember, that you are executing normal Cosmos DB SQL queries. In the chapters below I outline a few practical examples where you can leverage Cosmos DB Vector Search, some of which I have been working on, or will be working on myself.
</p>
            
<h3 id="customer-knowledge-base">Customer Knowledge Base
</h3>
            
<p>Let’s say you have a customer extranet that your customers log in to. On this extranet you have a lot of guidance and instructions related to the services that you offer your customers. But not all of your customers subscribe to the same services. Some of the instructions you publish are irrelevant to many of your customers. You might even have customer specific instructions that are relevant only to one customer and its representants.
</p>
            
<p>With Cosmos DB Vector Search you could easily create vector embeddings for your instructions as shown in this article. You would then store these vectors in your Cosmos DB container along with information about what or which services each document is related to, or it it is related to just one particular customer. Then when your customers want to find something, you create a query vector from the customer’s search prompt and use that to find the most relevant instructions. You would also add more filters to include only instructions that are relevant to the customer and the services it subscribes to.
</p>
            
<h3 id="suggest-similar-products-or-content">Suggest Similar Products or Content
</h3>
            
<p>Imagine you have an online store with a bunch of products. Your product information along with the product description is stored in a Cosmos DB container. You would also store the vector embedding for the product description with the rest of the product information.
</p>
            
<p>Now when a visitor views a product, you could take the vector embedding for that product and find other products in your Cosmos DB container that are similar to the viewed product. The product being viewed would of course be the most similar, so you would need to filter out that particular product from the query results. Since you are running a standard Cosmos DB SQL query, that would be quite simple.
</p>
            
<p>This same approach can be useful also for suggesting any type of content that is similar to for instance what is currently being viewed in an application. Take the previous example of a Customer Knowledge Base. When you display one instruction document, you could also offer other similar instructions, again filtering on the services the logged in customer is subscribed to.
</p>
            
<h3 id="process-survey-responses">Process Survey Responses
</h3>
            
<p>If you are in the business of doing surveys, you might find yourself in a situation where you are overwhelmed with the amount of responses. You might struggle with properly processing each response separately.
</p>
            
<p>In some cases, you might be able to process responses in groups. To find these groups you might want to try to find similar responses, and then process the most similar responses in batches.
</p>
            
<h3 id="document-classification">Document Classification
</h3>
            
<p>Some times you might have systems that store a lot of documents, and you need to categorize and classify these. Then you could pick one uncategorized document and categorize it manually. With the help of Cosmos DB Vector Search you find similar documents with the document that you categorized manually, and then categorize the most similar documents the same way you categorized the first document manually.
</p>
            
<h3 id="multi-lingual-services">Multi-lingual Services
</h3>
            
<p>Since vector embeddings can be 
<a href="/cosmos-db-vector-search-an-introduction/">language agnostic
</a>, you could offer multi-language 
<a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">RAG
</a> solutions, without having to publish content in multiple languages. You could allow your users to give their prompts in whatever language they want, and create a vector embedding (query vector) from that. Then query the most similar documents using the query vector, and use the resulting documents as input to a chat completion model that would use the given documents to provide a response to the original prompt, and in the same language as that prompt. Of course, you would have to make sure that the embedding model you choose properly handles and supports multiple languages.
</p>
            
<p>If you would like to display the documents returned by the similarity query, you could use a translation service to translate the original document into the language that the original user prompt was written in. You could also try to use a chat completion model to do the translation. Optionally, you could store that translation for future use so that you would not have to use a translation service every time.
</p>
            
<h2 id="how-much-does-it-cost">How Much Does It Cost
</h2>
            
<p>So what does it cost to generate vector embeddings? That depends on the embedding model you use. I will use the 
<strong>text-embedding-3-small
</strong> and 
<strong>text-embedding-3-large
</strong> embedding models as example.
</p>
            
<p>Remember that I talked about tokens above. The pricing for generating vector embeddings is also based on how many tokens you consume. The prices below (at the time of writing on late Feb 2025) are for 1000 tokens.
</p>
            
<ul><li><strong>text-embedding-3-small
</strong>: 0.000020 EUR/1000 tokens
</li><li><strong>text-embedding-3-large
</strong>: 0.000125 EUR/1000 tokens
</li></ul>
            
<p>If we take the maximum number of tokens both of these embedding models support, 8191 tokens, and estimate that to a text of the length of 32 764 characters, it would incur the following costs with both of these models.
</p>
            
<ul><li><strong>text-embedding-3-small
</strong>: 0.000164 EUR -&gt; 0.0164 cents
</li><li><strong>text-embedding-3-large
</strong>: 0.00102 EUR -&gt; 0.102 cents
</li></ul>
            
<p>Both of these costs are clearly peanuts, so we need to add more volume to the sample. Let’s assume that you have 10 000 documents that are all the maximum size supported by the embedding models, the cost for generating vector embeddings for all of those documents with each of the models would then be.
</p>
            
<ul><li><strong>text-embedding-3-small
</strong>: 1,64 EUR
</li><li><strong>text-embedding-3-large
</strong>: 10,24 EUR
</li></ul>
            
<p>Still a pretty manageable cost, even for personal use I would say.
</p>
            
<h2 id="conclusion-and-further-reading">Conclusion and Further Reading
</h2>
            
<p>I hope that this article has given you at least some new information about what Cosmos DB Vector Search is, and what you might use it for. I believe that it is good to have options to choose from when designing solutions to tackle various problems. Of course, the more options you have, the more likely you are also to pick an option that is perhaps not the optimal for a particular case. The more you know and understand about the options you have, the better decisions you can make.
</p>
            
<p>The following links provide you with further reading to get more information for even better decisions.
</p>
            
<ul><li><a href="/category/cosmos-db/">My other Cosmos DB articles
</a></li><li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/vector-database">What is a Vector Database
</a></li><li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/vector-search">Vector Search in Azure Cosmos DB for NoSQL
</a></li><li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/index-policy#vector-indexes">Vector indexes in Cosmos DB
</a></li><li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/query/vectordistance">VectorDistance function
</a></li><li><a href="https://learn.microsoft.com/en-us/azure/cosmos-db/gen-ai/vector-embeddings">Vector Embeddings in Azure Cosmos DB
</a></li><li><a href="https://openai.com/index/introducing-text-and-code-embeddings/">Introducing Text and Code Embeddings
</a></li></ul>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Blazor Apps With Mermaid Diagrams Using Blazorade Mermaid</title>
      <description>Demonstrates how you can easily render Mermaid diagrams in your Blazor application without having to worry about JavaScript interop.</description>
      <link>https://stage.mikaberglund.com/blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid</guid>
      <pubDate>Wed, 28 Feb 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="Blazorade-Mermaid" alt="Blazor Apps With Mermaid Diagrams Using Blazorade Mermaid" />
                
<figcaption>Blazor Apps With Mermaid Diagrams Using Blazorade Mermaid
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts" role="button" aria-label="Previous article: Understanding TOTP Codes: A Short Guide to Securing Your Accounts"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/cosmos-db-vector-search-an-introduction" role="button" aria-label="Next article: Cosmos DB Vector Search \\u2013 An Introduction"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid">Blazor Apps With Mermaid Diagrams Using Blazorade Mermaid
</h1>
            
<p class="article-meta">February 28, 2024
</p>

            
<p><a href="https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor">Blazor
</a> and 
<a href="https://mermaid.js.org/">Mermaid
</a> are two technologies that merge into one with the help of 
<a href="https://github.com/Blazorade/Blazorade-Mermaid/wiki"><strong>Blazorade Mermaid
</strong></a>. It is the latest member to the 
<a href="/category/blazorade/">Blazorade
</a> family. This article shows you how easy it is to add Mermaid diagrams to your Blazor applications by using Blazorade Mermaid.
</p>
            
<h2 id="mermaid-in-short">Mermaid in Short
</h2>
            
<p><a href="https://mermaid.js.org/">Mermaid
</a> is a JavaScript based diagramming and charting tool that renders diagrams in any web application. It takes simple formatted text inspired by 
<a href="https://en.wikipedia.org/wiki/Markdown">Markdown
</a>, and turns that into visual elements.
</p>
            
<p>Since Mermaid is essentially JavaScript, it might be a bit tricky to utilize it in your Blazor applications. Even if JavaScript interop is pretty easy to manage in Blazor, you still have to know how JavaScript works to do it successfully.
</p>
            
<h2 id="blazor-mermaid-blazorade-mermaid">Blazor + Mermaid = Blazorade Mermaid
</h2>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid-image-1" data-bs-toggle="modal" data-bs-target="#blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid-image-1" aria-label="Open image-1-300x204"><img class="article-content-image" src="/img/posts/blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid/image-1-300x204.png" alt="image-1-300x204" /></a></figure><div class="modal fade" id="blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid-image-1" tabindex="-1" aria-labelledby="blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid-image-1-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid-image-1-label">image-1-300x204
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid/image-1-300x204.png" alt="image-1-300x204" /></div></div></div></div>
            
<p>One of the key principles in Blazorade is to make it as easy as possible to create Blazor applications and use popular libraries like Mermaid. With libraries or frameworks that expose functionality through JavaScript this means that Blazorade will take care of that communication. Your application does not need to figure out how to do that.
</p>
            
<p>Again, I’m not saying that calling into JavaScript from a Blazor app is hard. On the contrary! Blazor makes it relatively easy. But still, it is way easier to call into and interact with a component that looks and works just like any other Blazor component.
</p>
            
<p>This is what Blazorade Mermaid does for your application. You just add the 
<a href="https://www.nuget.org/packages/Blazorade.Mermaid/">Blazorade Mermaid Nuget package
</a> to your application and add the 
<code>&lt;MermaidDiagram /&gt;
</code> component wherever you want to have a diagram. You don’t even have to worry about how and where to reference the Mermaid JavaScript library. That’s all taken care of by Blazorade Mermaid.
</p>
            
<p>And if you want to change or update the diagram, just change the 
<code>MermaidDiagram.Definition
</code> parameter, and it will automatically update for you. Can’t be much easier than that, can it?
</p>
            
<h2 id="getting-started-with-blazorade-mermaid">Getting Started With Blazorade Mermaid
</h2>
            
<p>There are three steps to start using Blazorade Mermaid in your Blazor application.
</p>
            
<ul><li>Add a reference to the 
<a href="https://www.nuget.org/packages/Blazorade.Mermaid/">Blazorade Mermaid
</a> Nuget package.
</li></ul>
            
<ul><li>Add 
<code>&#64;using Blazorade.Mermaid.Components
</code> to your 
<code>_Imports.razor
</code> file.
</li></ul>
            
<ul><li>Add the 
<code>&lt;MermaidDiagram /&gt;
</code> component to your page or component.
</li></ul>
            
<p>Again, you don’t even have to add any JavaScript references to your page layout. That’s all taken care of by Blazorade Mermaid.
</p>
            
<h2 id="sample-blazor-with-mermaid-diagrams">Sample Blazor With Mermaid Diagrams
</h2>
            
<p>So it’s time to have a look at an example that demonstrates how to Mermaid diagrams to your Blazor application.
</p>
            
<pre class="code-block" data-language="html"><code>
            
<span class="code-line">&lt;div&gt;
</span>
            
<span class="code-line">    &lt;MermaidDiagram Definition=&quot;&#64;this.diagramDef&quot; /&gt;
</span>
            
<span class="code-line">&lt;/div&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&#64;code {
</span>
            
<span class="code-line">    string diagramDef = string.Empty;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>Whenever you update the 
<code>diagramDef
</code> variable in your code, that will immediately update the rendered diagram too.
</p>
            
<p>Now let’s take that a bit further and switch between two different diagrams using two buttons.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">&lt;button &#64;onclick=&quot;() =&gt; this.diagramDef = def1&quot;&gt;Flowchart&lt;/button&gt;
</span>
            
<span class="code-line">| &lt;button &#64;onclick=&quot;() =&gt; this.diagramDef = def2&quot;&gt;Mindmap&lt;/button&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&lt;MermaidDiagram Definition=&quot;&#64;this.diagramDef&quot; /&gt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">&#64;code {
</span>
            
<span class="code-line">    string diagramDef = def1;
</span>
            
<span class="code-line">    
</span>
            
<span class="code-line">    const string def1 = &#64;&quot;
</span>
            
<span class="code-line">flowchart LR
</span>
            
<span class="code-line">A --&gt; B
</span>
            
<span class="code-line">B --&gt; C
</span>
            
<span class="code-line">C --&gt; A
</span>
            
<span class="code-line">&quot;;
</span>
            
<span class="code-line">    
</span>
            
<span class="code-line">    const string def2 = &#64;&quot;
</span>
            
<span class="code-line">mindmap
</span>
            
<span class="code-line">Responsibilities
</span>
            
<span class="code-line">  HR
</span>
            
<span class="code-line">    Salaries
</span>
            
<span class="code-line">    Healthcare
</span>
            
<span class="code-line">  IT
</span>
            
<span class="code-line">    Workstations
</span>
            
<span class="code-line">    Phones
</span>
            
<span class="code-line">  Sales
</span>
            
<span class="code-line">    Sell, sell, sell!
</span>
            
<span class="code-line">  Operations
</span>
            
<span class="code-line">    Operate
</span>
            
<span class="code-line">  Marketing
</span>
            
<span class="code-line">    Branding
</span>
            
<span class="code-line">    Website
</span>
            
<span class="code-line">&quot;;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>In this sample, whenever you click either the 
<strong>*Flowchart
</strong><em> or 
</em><strong>Mindmap
</strong>* buttons, the diagram will change accordingly.
</p>
            
<p>You can find more samples on the 
<a href="https://github.com/Blazorade/Blazorade-Mermaid/wiki">Blazorade Mermaid
</a> wiki.
</p>
            
<h2 id="future-of-blazorade-mermaid">Future of Blazorade Mermaid
</h2>
            
<p>At the time of writing this article (Feb 2024), the first stable version of Blazorade Mermaid has just been released. The first versions of Blazorade Mermaid focus on viewing diagrams and making that as easy as possible. However, there are a few other areas that Blazorade Mermaid will focus on in coming versions.
</p>
            
<h3 id="support-for-themes">Support for Themes
</h3>
            
<p>It is completely possible to define themes and customize them in the diagram definition that is rendered by Mermaid. But sometimes you might want to separate that from the diagram definition. You might even want to prevent themes and customization completely in the diagram definitions, and override that with your own.
</p>
            
<p>You can track this feature 
<a href="https://github.com/Blazorade/Blazorade-Mermaid/issues/1">on the backlog
</a>.
</p>
            
<h3 id="diagram-interaction">Diagram Interaction
</h3>
            
<p>Certain types of diagrams have built-in support for some level of interaction. For instance a flowchart allows you to define nodes as links. You can also define JavaScript callbacks directly in the diagram definition.
</p>
            
<p>However, this is not the optimal way of handling things. You are effectively blending content with application logic. A better option is to allow the application to handle clicks, and then make decisions based on the node that was clicked.
</p>
            
<p>You can track this feature 
<a href="https://github.com/Blazorade/Blazorade-Mermaid/issues/2">on the backlog
</a>.
</p>
            
<h3 id="support-for-net-maui">Support for .NET MAUI
</h3>
            
<p>Originally, Blazorade started out as a browser-only kind of thing. That was before .NET MAUI and its support for using Blazor components through
<a href="https://learn.microsoft.com/aspnet/core/blazor/hybrid/tutorials/maui">.NET MAUI Blazor Hybrid applications
</a>.
</p>
            
<p>Having MAUI support in Blazorade libraries would allow Blazorade libraries to be used in a wider range of applications. I think it would be cool to use Blazorade libraries for instance in Android TV applications.
</p>
            
<p>So, I decided that Blazorade libraries should, as far as possible, support any 
<a href="https://learn.microsoft.com/dotnet/maui/supported-platforms">platform that supports Blazor
</a>, including desktop and mobile platforms.
</p>
            
<p>You can track this feature 
<a href="https://github.com/Blazorade/Blazorade-Mermaid/issues/3">on the backlog
</a>.
</p>
            
<h2 id="contributing-to-blazorade-mermaid">Contributing to Blazorade Mermaid
</h2>
            
<p>So as you can see, there is still a bit left to do before Blazorade Mermaid is “perfect” ;). You are welcome to join that work. You can let me know by adding a comment to this article, or by starting a discussion on the 
<a href="https://github.com/Blazorade/Blazorade-Mermaid/discussions">discussion board
</a> for Blazorade Mermaid.
</p>
            
<p>And if you feel like just using Blazorade Mermaid in your application, that is of course completely fine too. I hope that Blazorade Mermaid will make your application development a bit easier.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Understanding TOTP Codes: A Short Guide to Securing Your Accounts</title>
      <description>In the world of online security, TOTP codes add an extra layer of protection to make sure only you get into your accounts.</description>
      <link>https://stage.mikaberglund.com/understanding-totp-codes-a-short-guide-to-securing-your-accounts</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/understanding-totp-codes-a-short-guide-to-securing-your-accounts</guid>
      <pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="totp" alt="TOTP - Time-based One-Time Passwords." />
                
<figcaption>TOTP - Time-based One-Time Passwords.
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/how-to-do-mfa-login-with-playwright" role="button" aria-label="Previous article: How to Do MFA Login With Playwright"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/blazor-apps-with-mermaid-diagrams-using-blazorade-mermaid" role="button" aria-label="Next article: Blazor Apps With Mermaid Diagrams Using Blazorade Mermaid"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="understanding-totp-codes-a-short-guide-to-securing-your-accounts">Understanding TOTP Codes: A Short Guide to Securing Your Accounts
</h1>
            
<p class="article-meta">January 22, 2024
</p>

            
<p>In the world of online security, 
<strong>*TOTP
</strong><em> codes (
</em><em>T
</em><em>ime-based 
</em><em>O
</em><em>ne-
</em><em>T
</em><em>ime 
</em><em>P
</em>*assword) are like your account’s secret superhero. They add an extra layer of protection to make sure only you get into your accounts. Let’s break down what TOTP codes are, what they’re used for, and how they’re cooked up without diving into fancy tech talk.
</p>
            
<h2 id="what-are-totp-codes">What Are TOTP Codes?
</h2>
            
<p>Imagine TOTP codes as digital bodyguards for your online accounts. They create a special, ever-changing password that acts like a secret handshake to keep the bad guys out.
</p>
            
<h2 id="what-are-they-used-for">What Are They Used For?
</h2>
            
<p>Okay, so you’ve probably seen that extra step when logging into your email or social media – the one where it asks for a code from your authenticator app. That’s where TOTP codes step in. They’re like the bouncer at the club, making sure only the right person (you) gets in.
</p>
            
<h2 id="how-are-they-cooked-up">How Are They Cooked Up?
</h2>
            
<p>Now, let’s talk about how these TOTP codes are whipped up in the kitchen of online security:
</p>
            
<ol><li><strong>Setting Up
</strong>: When you turn on two-factor (sometimes referred to as 
<em>multi-factor
</em>) authentication (2FA/MFA) for your account, you usually connect it to an app on your phone. This app becomes your sidekick in the fight against unauthorized access.
</li><li><strong>Secret Key
</strong>: Your account and the app share a secret key. It’s like having a special ingredient that only you and your app know about. This key kicks off the process of making your unique TOTP codes.
</li><li><strong>Time Magic
</strong>: There’s a secret sauce called an algorithm. It’s like a recipe that involves the current time and your secret key. Your app and the online service both use this magic to cook up the TOTP code.
</li><li><strong>Changing Every 30 – 60 Seconds
</strong>: TOTP codes don’t stick around for long – they change every 30 – 60 seconds. This is like changing the locks on your digital door regularly. It keeps things extra secure.
</li><li><strong>Time Sync
</strong>: For this to work smoothly, your phone and the online service need to agree on the time. But don’t worry, this is usually done automatically – no need to set your digital clocks.
</li></ol>
            
<h3 id="conclusion">Conclusion
</h3>
            
<p>TOTP codes might sound like tech wizardry, but they’re really just your online account’s way of putting on an extra lock. They make it way harder for someone to sneak in, even if they somehow get your password. So, the next time you see that prompt for a code from your app, think of it as your account doing a secret handshake to keep your information safe. Stay safe out there!
</p>
            
<h2 id="further-reading">Further Reading
</h2>
            
<p>If you want to read more about TOTP codes and the algorithms behind them, have a look at 
<a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">Time-based one-time passwords
</a> on Wikipedia.
</p>
            
<p>If you want to create applications that produce TOTPs programmatically, like 
<a href="/how-to-do-mfa-login-with-playwright/">UI tests and browser automation systems with Playwright
</a>, add a reference to the 
<a href="https://www.nuget.org/packages/Otp.NET/">Otp.NET
</a> assembly in your code.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>How to Do MFA Login With Playwright</title>
      <description>UI test automation can be tricky if you need to do do multi-factor login. This article describes how to use Playwright for MFA login.</description>
      <link>https://stage.mikaberglund.com/how-to-do-mfa-login-with-playwright</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/how-to-do-mfa-login-with-playwright</guid>
      <pubDate>Sun, 21 Jan 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="robot-and-laptop" alt="playwright mfa login" />
                
<figcaption>playwright mfa login
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/create-a-shared-account-with-mfa-in-microsoft-entra-id" role="button" aria-label="Previous article: Create a Shared Account With MFA in Microsoft Entra ID"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts" role="button" aria-label="Next article: Understanding TOTP Codes: A Short Guide to Securing Your Accounts"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="how-to-do-mfa-login-with-playwright">How to Do MFA Login With Playwright
</h1>
            
<p class="article-meta">January 21, 2024
</p>

            
<p>Last year, in 2023, I started looking more into 
<a href="https://playwright.dev/">Playwright
</a> for both testing and browser automation. I also ended up in a situation where I needed to do login to 
<a href="https://www.microsoft.com/security/business/identity-access/microsoft-entra-id">Microsoft Entra ID
</a> with an account with MFA enabled. This article describes how to write your Playwright code to successfully perform 
<a href="https://learn.microsoft.com/entra/identity/authentication/concept-mfa-howitworks">MFA login to Microsoft Entra ID
</a>.
</p>
            
<h2 id="what-is-playwright">What Is Playwright?
</h2>
            
<p>Before we dive in, a few words about Playwright if you are not that familiar with it yet. 
<a href="https://playwright.dev/">Playwright
</a> is a cross-platform library for automating web browsers. It allows you to write end-to-end tests, capture screenshots, generate PDFs, and perform web scraping using a consistent and user-friendly API. Playwright supports Chromium, Firefox, and WebKit browsers, and can run on Windows, Linux, and macOS.
</p>
            
<p>You might have heard of 
<a href="https://www.selenium.dev/">Selenium
</a>. Playwright is kind of the new kid on the block in browser automation. I used to do a lot of testing and browser automation with Selenium back in the day. But Playwright has really become my favourite browser automation library. I feel the code with Playwright is much simpler and lighter than with Selenium. For instance, I don’t have to add random delays or other checks to make sure that an element is visible or clickable before I start using it in my code. Playwright does all of that for me. With Selenium, this is not the case. Your Selenium tests may still fail randomly because your timing is not right. Or then you just add unnecessary long delays or something like that. That’s all history with Playwright.
</p>
            
<blockquote>Disclaimer! It’s been several years since I’ve last worked with Selenium, so the issues I described above may be all history.
</blockquote>
            
<h2 id="whats-the-problem-with-playwright-and-mfa-login">What’s the Problem With Playwright and MFA Login?
</h2>
            
<p>If you would have just a username and password, it would be quite simple to create automation with Playwright that would take care of login. But with MFA, login is a bit trickier. How do you get a 
<a href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts/">TOTP
</a> (
<strong>T
</strong>ime-based 
<strong>O
</strong>ne-
<strong>T
</strong>ime 
<strong>P
</strong>assword) from your authenticator app to your Playwright automations?
</p>
            
<p>Fortunately, it is quite simple at the end of the day. The main points of the solution are:
</p>
            
<ul><li>Configure MFA for the account so that it can be leveraged in code
</li><li>Add code to your automations that use the configuration to produce 
<a href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts/">TOTPs
</a> as needed
</li></ul>
            
<p>This article walks you through all the steps you need to master MFA login in your Playwright automations.
</p>
            
<h2 id="configure-user-account">Configure User Account
</h2>
            
<p>To create and configure a user account that has MFA enabled and can be used with Playwright automations, follow my 
<a href="/create-a-shared-account-with-mfa-in-microsoft-entra-id/">Create a Shared Account With MFA in Microsoft Entra ID
</a> article that I wrote previously.
</p>
            
<h2 id="code-for-playwright-supporting-mfa-login">Code for Playwright Supporting MFA Login
</h2>
            
<p>In this chapter I’ll go through each step that is required to get your MFA login working in Playwright. You find the code examples in the chapters below in a 
<a href="https://github.com/MikaBerglund/mfa-login-with-playwright">Github repository
</a> I set up. This repository contains a fully working example of how to log in to the Microsoft 365 portal with a user account that has MFA enabled. The code checks whether MFA is required during login, and adapts accordingly. The sub chapters are associated with named blocks in the sample code.
</p>
            
<h3 id="referenced-libraries">Referenced Libraries
</h3>
            
<p>The application references the following Nuget packages.
</p>
            
<ul><li><a href="https://www.nuget.org/packages/Microsoft.Playwright/">Microsoft.Playwright
</a> – The library that contains everything you need to run browser automations with Playwright.
</li><li><a href="https://www.nuget.org/packages/Otp.NET/">Otp.NET
</a> – A library that provides 
<a href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts/">TOTP
</a> computing capabilities.
</li></ul>
            
<h3 id="initialize-objects">Initialize Objects
</h3>
            
<p>All Playwright automations start by creating a Playwright object using 
<code>Playwright.CreateAsync()
</code>. You will also need a 
<em>browser type
</em> and 
<em>browser
</em> object. In the sample code I create the browser object directly by using the 
<code>pw.Chromium.LaunchAsync()
</code> method. However, if you would like to run your automations on different browsers, you would create a separate browser type object, and then send that away to the implementation that does the actual automation. Playwright supports 3 browser types – 
<strong>*Chromium
</strong><em>, 
</em><strong>Firefox
</strong><em> and 
</em><strong>Webkit
</strong>*.
</p>
            
<p>With the browser object, you create a 
<em>browser context
</em> that represents the session you are running. Using the browser context, you create a new page object that you then use in your automations.
</p>
            
<h3 id="navigate-to-application">Navigate to Application
</h3>
            
<p>In order to have a web application to interact with, you first navigate to the page. In the sample application we use the URL 
<a href="https://www.microsoft365.com/login">https://www.microsoft365.com/login
</a> which would redirect directly to the login page. Another option would be to navigate to 
<a href="https://www.microsoft365.com/">https://www.microsoft365.com/
</a> and click the Sign in button.
</p>
            
<h3 id="define-common-selectors">Define Common Selectors
</h3>
            
<p>There are several CSS selectors that we use across the application. These are defined at the start of the application, so that we can reuse them across the rest of the code.
</p>
            
<h3 id="provide-username-and-password">Provide Username and Password
</h3>
            
<p>Now this is where the magic starts to happen. The first thing we do is to fill in the username in the username text box. This is done with the 
<code>FillAsync()
</code> method of the 
<em>page
</em> object. Then we click the 
<strong>*Next
</strong>* button by using the 
<code>ClickAsync()
</code> method.
</p>
            
<blockquote>Note! Playwright takes care of waiting for an element to be available for interaction. This means that Playwright will not perform the interaction before the element is visible, clickable, or otherwise available for interaction. This makes your code so much clearer, when you don’t have to add random delays between every line of code, just to be on the safe side. The default timeout of 30 seconds is more than enough. If an element takes longer than the assigned timeout to become available, Playwright will throw an exception.
</blockquote>
            
<p>After clicking the 
<strong>*Next
</strong><em> button, the code waits for the username text box to be detached from the DOM. This needs perhaps a bit of explanation. As you know, the login to Microsoft cloud services such as Microsoft 365 is built up of multiple views. The transitions between these views use a lot of animation. In addition, the different views reuse the same CSS selectors on elements that have different meanings on different views. For instance, the 
</em><strong>Next
</strong><em> button on the username view matches the same CSS selector as the 
</em><strong>Yes
</strong>* button on the Keep me signed in view.
</p>
            
<p>So, we wait for the username text box to disappear before we start processing the password.
</p>
            
<blockquote>Note that this is not the same as waiting for a fixed amount of seconds. This is waiting until something has occurred, which is completely fine.
</blockquote>
            
<p>Next step is to handle the password. This follows the same logic as handling the username.
</p>
            
<ul><li>Fill in the password
</li><li>Click on the Next button
</li><li>Wait for the password text box to disappear from the DOM
</li></ul>
            
<h3 id="handle-mfa-login">Handle MFA Login
</h3>
            
<p>Since MFA is not required for every login, we need to determine whether MFA is required for the current login that we are processing. We do this by waiting for one of two elements.
</p>
            
<ol><li>TOTP text box
</li><li>Keep me signed in (KMSI) checkbox
</li></ol>
            
<p>Whichever element is found first determines how we proceed in the code. If the KMSI checkbox was found first, then we know that MFA was not required and we complete the login process by clicking the 
<strong>*Yes
</strong>* button one more time.
</p>
            
<p>On the other hand, if the TOTP text box was found, then we know that we need to handle MFA login. This is where we use the MFA secret key that you preferably have stored in your password manager. Use the 
<a href="https://www.nuget.org/packages/Otp.NET/">Otp.NET
</a> library produce a TOTP. Then, fill the TOTP text box and click the 
<strong>*Next
</strong>* button.
</p>
            
<p>Again, after clicking the 
<strong>*Next
</strong>* button, we wait for the TOTP text box to disappear from the DOM before continuing to the next step.
</p>
            
<h3 id="keep-me-signed-in">Keep Me Signed In
</h3>
            
<p>This is the last step in the login process. Here, it actually does not make any sense to check the keep me signed in checkbox, because each browser context you start in Playwright is a clean session. It’s just like you would start your browser in the In Private/Incognito mode every time you fire up a browser with Playwright.
</p>
            
<blockquote>You can populate a browser context with storage state from a previous session including login information, but I’m skipping that here for simplicity.
</blockquote>
            
<p>So, we just click on the Yes button to complete the login.
</p>
            
<h2 id="conclusion">Conclusion
</h2>
            
<p>So, with these steps it should be possible for you to test any application that you log in to using a Microsoft Entra ID account that has MFA enabled. The same principles apply also to accounts in Microsoft Entra External ID or Azure AD B2C. I would assume that the same works with any identity provider that supports TOTP.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Create a Shared Account With MFA in Microsoft Entra ID</title>
      <description>How to share user accounts that require MFA with your team? Read this article to find out how to do that with Microsoft Entra ID.</description>
      <link>https://stage.mikaberglund.com/create-a-shared-account-with-mfa-in-microsoft-entra-id</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/create-a-shared-account-with-mfa-in-microsoft-entra-id</guid>
      <pubDate>Sat, 20 Jan 2024 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="login-banner" alt="Share accounts and secrets across teams." />
                
<figcaption>Share accounts and secrets across teams.
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/from-azure-ad-b2c-to-microsoft-entra-external-id" role="button" aria-label="Previous article: From Azure AD B2C to Microsoft Entra External ID"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/how-to-do-mfa-login-with-playwright" role="button" aria-label="Next article: How to Do MFA Login With Playwright"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="create-a-shared-account-with-mfa-in-microsoft-entra-id">Create a Shared Account With MFA in Microsoft Entra ID
</h1>
            
<p class="article-meta">January 20, 2024
</p>

            
<p>There are many cases where you need to create a user account in 
<a href="https://www.microsoft.com/security/business/identity-access/microsoft-entra-id">Microsoft Entra ID
</a> (“ME-ID”) and share it with a bunch of people. Accounts created for testing purposes is a very common use for shared accounts. Usually you would just create an account with a username and password. Then you store that in a password management application and share it with your peers. My favourite password manager is definitely 
<a href="https://bitwarden.com/">Bitwarden
</a>.
</p>
            
<blockquote>Note! In Microsoft 365 there are typically restrictions that apply to how shared user accounts may be used. As a rule of thumb, always create personal accounts for all your team members for normal operations. Shared accounts should only be used for development and testing purposes.
</blockquote>
            
<p>But nowadays, ME-ID has 
<a href="https://learn.microsoft.com/entra/fundamentals/security-defaults">security defaults
</a> enabled by default, or has MFA enabled in other ways. That makes things more complicated. How do you share the ability to perform MFA with your team? What if you need to do MFA login in your test automation?
</p>
            
<p>Luckily there is an easy way. I’ll go through the necessary steps in this article.
</p>
            
<h2 id="create-a-normal-account">Create a Normal Account
</h2>
            
<p>You start with creating a user account like you normally would. Head over to 
<a href="https://entra.microsoft.com/">Microsoft Entra admin center
</a>, and log in with a user account that is allowed to create user accounts. Then from the menu, select 
<strong>*Users / All users
</strong><em>. Above your users list, click the 
</em><strong>New user
</strong><em> menu item, and select 
</em><strong>Create new user
</strong>*.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-1" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-1" aria-label="Open image"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image.png" alt="image" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-1" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-1-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-1-label">image
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image.png" alt="image" /></div></div></div></div>
            
<p>In the 
<em>Create new user
</em> wizard, type in the information about the user you need.
</p>
            
<blockquote>Note! If you select the 
<strong>Auto-generate password
</strong> option, you need to copy the password using the 
<strong>Copy to clipboard
</strong> button next to the password field.  
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-2" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-2" aria-label="Open image-1"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-1.png" alt="image-1" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-2" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-2-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-2-label">image-1
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-1.png" alt="image-1" /></div></div></div></div></blockquote>
            
<p>When you’re done, head over to the 
<strong>*Review + create
</strong><em> tab. If you did not yet copy the username, you can do it here by clicking the 
</em>Copy to clipboard* button.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-3" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-3" aria-label="Open image-2"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-2.png" alt="image-2" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-3" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-3-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-3-label">image-2
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-2.png" alt="image-2" /></div></div></div></div>
            
<p>When you have copied both the password and the username, and stored it in your password manager, for instance 
<a href="https://bitwarden.com/">Bitwarden
</a>, then click the 
<strong>*Create
</strong>* button to create the user.
</p>
            
<h2 id="first-time-login">First Time Login
</h2>
            
<p>Before you share the account with your peers, you must complete the account by logging in the first time. During the first login, you take care of the following things.
</p>
            
<ul><li>Change the temporary password
</li><li>Configure MFA
</li><li>Optional other authentication methods
</li></ul>
            
<p>I usually log in to 
<a href="https://myaccount.microsoft.com/">myaccount.microsoft.com
</a> in these kinds of situations. Remember to start your browser in the In Private mode (Edge) or Incognito (Chrome) so that you don’t mess up your regular browser profile that you are using.
</p>
            
<h3 id="change-password">Change Password
</h3>
            
<p>The first thing you need to do after logging is to change the temporary password.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-4" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-4" aria-label="Open image-3"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-3.png" alt="image-3" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-4" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-4-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-4-label">image-3
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-3.png" alt="image-3" /></div></div></div></div>
            
<p>You can use your password manager, for instance 
<a href="https://bitwarden.com/">Bitwarden
</a>, to generate a unique and strong password. Remember to save your new password in your password manager. Then click 
<strong>*Sign in
</strong>* to continue.
</p>
            
<h3 id="configure-mfa">Configure MFA
</h3>
            
<p>When you completed changing the password, you need to configure multi-factor authentication settings for the account.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-5" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-5" aria-label="Open image-4"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-4.png" alt="image-4" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-5" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-5-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-5-label">image-4
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-4.png" alt="image-4" /></div></div></div></div>
            
<p>Even though you have 14 days to complete this step, it is a good idea to do it right away. Especially if you plan on using the account for longer than 2 weeks. You’ll have to do it anyway.
</p>
            
<p>To start the MFA configuration, click on the 
<strong>*Next
</strong>* button.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-6" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-6" aria-label="Open image-5"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-5.png" alt="image-5" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-6" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-6-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-6-label">image-5
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-5.png" alt="image-5" /></div></div></div></div>
            
<p>The default authenticator application is obviously Microsoft Authenticator. But, since you are planning on sharing the account with others, it is 
<strong>*very important
</strong><em> that you click the 
</em><strong>I want to use a different authenticator app
</strong>* link. Clicking that link will take you to the generic authenticator application configuration, as shown below.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-7" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-7" aria-label="Open image-6"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-6.png" alt="image-6" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-7" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-7-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-7-label">image-6
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-6.png" alt="image-6" /></div></div></div></div>
            
<blockquote>Note! The reason why you must not select the Microsoft Authenticator option is because that will configure the MFA for the account to send challenges back to the authenticator app for you to approve. That becomes a bit tricky if you want to share the account with others. You can still use the Microsoft Authenticator application, or any other authenticator application. You just have to take the generic configuration route.
</blockquote>
            
<p>Click the 
<strong>*Next
</strong>* button to continue with the configuration.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-8" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-8" aria-label="Open image-7"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-7.png" alt="image-7" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-8" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-8-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-8-label">image-7
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-7.png" alt="image-7" /></div></div></div></div>
            
<p>Click on the 
<strong>*Can’t scan image?
</strong>* button to show the secret key that you need to save.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-9" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-9" aria-label="Open image-8"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-8.png" alt="image-8" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-9" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-9-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-9-label">image-8
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-8.png" alt="image-8" /></div></div></div></div>
            
<p>Copy the secret key to your clipboard, and click the 
<strong>*Next
</strong>* button to continue. Remember to save the secret key. You will never see it again after you leave this screen.
</p>
            
<blockquote>Note! At this point, if you would like to add the account to your Microsoft Authenticator, scroll down to 
<a href="/create-a-shared-account-with-mfa-in-microsoft-entra-id/">Add the Account to Microsoft Authenticator
</a>, and follow the steps there. Instead of entering the code manually, you can actually scan the QR code above.
</blockquote>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-10" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-10" aria-label="Open image-11"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-11.png" alt="image-11" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-10" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-10-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-10-label">image-11
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-11.png" alt="image-11" /></div></div></div></div>
            
<p>In 
<a href="https://bitwarden.com/">Bitwarden
</a>, you store this secret key in the 
<strong>*Authenticator key (TOTP)
</strong>* field.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-11" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-11" aria-label="Open image-18"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-18.png" alt="image-18" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-11" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-11-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-11-label">image-18
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-18.png" alt="image-18" /></div></div></div></div>
            
<p>One nice thing about 
<a href="https://bitwarden.com/">Bitwarden
</a> is that it can work as an authenticator app for you. When you save the account with the secret key stored in the 
<em>Authenticator key
</em> field, Bitwarden will show you the 
<a href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts/">TOTP
</a> (
<strong>T
</strong>ime-based 
<strong>O
</strong>ne-
<strong>T
</strong>ime 
<strong>P
</strong>assword) that will work as the MFA challenge. To add the account to Microsoft Authenticator, have a look at 
<a href="/create-a-shared-account-with-mfa-in-microsoft-entra-id/">Adding the Account to Microsoft Authenticator
</a> below.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-12" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-12" aria-label="Open image-9"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-9.png" alt="image-9" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-12" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-12-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-12-label">image-9
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-9.png" alt="image-9" /></div></div></div></div>
            
<p>Getting back to the MFA wizard. Here you can now try the 
<a href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts/">TOTP
</a> (time-based one-time password) generated by Bitwarden. Click Next to verify that the code you entered checked out.
</p>
            
<h3 id="optional-authentication-methods">Optional Authentication Methods
</h3>
            
<p>Depending on the configuration in the ME-ID tenant you are setting up the account in, you may need to configure more authentication options. These can be for instance one-time passwords sent by text message to your mobile phone or by e-mail.
</p>
            
<p>These are not required for normal use, since the username, password, and 
<a href="/understanding-totp-codes-a-short-guide-to-securing-your-accounts/">TOTP
</a> are enough for several persons to log in with the same account. Still, if they are required, you need to complete the steps in order to complete the first login for the account.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-13" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-13" aria-label="Open image-13"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-13.png" alt="image-13" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-13" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-13-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-13-label">image-13
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-13.png" alt="image-13" /></div></div></div></div>
            
<p>I chose the email option above. If you want to configure this step using text messages, click on the 
<strong>*I want to set up a different method
</strong>* link. Select from the available options.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-14" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-14" aria-label="Open image-14"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-14.png" alt="image-14" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-14" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-14-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-14-label">image-14
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-14.png" alt="image-14" /></div></div></div></div>
            
<p>In my case, there are only two options.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-15" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-15" aria-label="Open image-16"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-16.png" alt="image-16" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-15" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-15-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-15-label">image-16
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-16.png" alt="image-16" /></div></div></div></div>
            
<p>When you come this far, tap your self on the shoulder. You’ve completed the initial setup for your account. When you click the Done button, you will be taken to the My Account portal.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-16" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-16" aria-label="Open image-17-1024x507"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-17-1024x507.png" alt="image-17-1024x507" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-16" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-16-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-16-label">image-17-1024x507
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-17-1024x507.png" alt="image-17-1024x507" /></div></div></div></div>
            
<p>Remember to share the username, password and Authenticator key with your team, or whoever you were planning on sharing the account with.
</p>
            
<h2 id="enforcing-mfa-login">Enforcing MFA Login
</h2>
            
<p>Even with security defaults enabled in your tenant, you may still not be required to complete an MFA challenge every time you log in. That decision is made by the security mechanisms enabled by security defaults. If you want to make sure that you are required to provide MFA verification each time you log in, you can do so by logging in to the 
<a href="https://account.activedirectory.windowsazure.com/UserManagement/MultifactorVerification.aspx">Multifactor Verification
</a> page.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-17" data-bs-toggle="modal" data-bs-target="#create-a-shared-account-with-mfa-in-microsoft-entra-id-image-17" aria-label="Open image-19"><img class="article-content-image" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-19.png" alt="image-19" /></a></figure><div class="modal fade" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-17" tabindex="-1" aria-labelledby="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-17-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="create-a-shared-account-with-mfa-in-microsoft-entra-id-image-17-label">image-19
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/create-a-shared-account-with-mfa-in-microsoft-entra-id/image-19.png" alt="image-19" /></div></div></div></div>
            
<p>Select the user account you want to configure, and select 
<strong>*Enable
</strong><em> / 
</em><strong>Disable
</strong><em> / 
</em><strong>Enforce
</strong>*.
</p>
            
<h2 id="adding-the-account-to-microsoft-authenticator">Adding the Account to Microsoft Authenticator
</h2>
            
<p>To add the account to your Microsoft Authenticator app, follow the steps below. This is what your team mates or other peers would typically do when they start to use shared account. These instructions come without screenshots, because at least on my phone, Microsoft Authenticator does not allow to take screen shots.
</p>
            
<ol><li>Start Microsoft Authenticator, and click the 
<strong>*+
</strong>* sign above the list of accounts you have configured
</li><li>Select 
<strong>*Other account (Google, Facebook, etc.)
</strong>*
</li></ol>
            
<ul><li>Do not select any of the Microsoft options
</li></ul>
            
<ol><li>On the Scan 
<em>QR Code screen
</em>, click 
<strong>*Or enter code manually
</strong>* at the bottom of the screen
</li><li>Enter a name for the account and the authentication key, and click 
<strong>*Finish
</strong>*.
</li><li>Open the account, and verify that the changing one-time password matches the one that you have in 
<a href="https://bitwarden.com/">Bitwarden
</a>.
</li></ol>
            
<h2 id="conclusion">Conclusion
</h2>
            
<p>At the end of the day, this is pretty straight forward. Personally, I am doing this whenever I create an account in 
<a href="/from-azure-ad-b2c-to-microsoft-entra-external-id/">Microsoft Entra
</a><a href="/category/microsoft-entra-id/">ID
</a>, 
<a href="/category/azure-ad-b2c/">Azure AD B2C
</a> or 
<a href="/category/microsoft-entra-external-id/">Microsoft Entra External ID
</a>, that I know that I need to be able to access, even if I don’t have my phone available. Also, if I know that I am creating an account that I want to share with my team, I follow the same process.
</p>
            
<p>Hope you found this article useful.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>From Azure AD B2C to Microsoft Entra External ID</title>
      <description>Are working with Azure AD B2C? Then you need to learn about Microsoft Entra External ID, the next generation of CIAM solutions from Microsoft.</description>
      <link>https://stage.mikaberglund.com/from-azure-ad-b2c-to-microsoft-entra-external-id</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/from-azure-ad-b2c-to-microsoft-entra-external-id</guid>
      <pubDate>Wed, 08 Nov 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="Computer-in-chains" alt="From Azure AD B2C to Microsoft Entra External ID" />
                
<figcaption>From Azure AD B2C to Microsoft Entra External ID
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/durable-functions-pitfalls-in-azure-functions" role="button" aria-label="Previous article: Durable Functions Pitfalls in Azure Functions"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/create-a-shared-account-with-mfa-in-microsoft-entra-id" role="button" aria-label="Next article: Create a Shared Account With MFA in Microsoft Entra ID"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="from-azure-ad-b2c-to-microsoft-entra-external-id">From Azure AD B2C to Microsoft Entra External ID
</h1>
            
<p class="article-meta">November 8, 2023
</p>

            
<p>You may have heard that 
<a href="https://learn.microsoft.com/azure/active-directory/fundamentals/new-name">Azure AD will get a new name
</a> – Microsoft Entra ID. Even though this does not affect Azure AD B2C directly, it still might be of interest to you. Especially if you are building solutions on Azure AD B2C. You see, many of the most popular features from Azure AD B2C are already built into Microsoft Entra External ID. In this article I’ll go through the most useful features in Microsoft Entra External ID that you could use to build your solutions on top of that instead of Azure AD B2C.
</p>
            
<h2 id="abbreviations-and-acronyms">Abbreviations and Acronyms
</h2>
            
<p>Before I dive into the article, I guess it’s a good idea to go over some abbreviations and acronyms. Azure AD, Azure AD B2C, Microsoft Entra ID and Microsoft Entra External ID are all very long names. Since I’ll be using those names quite a lot, so I better try shortening them up.
</p>
            
<ul><li><strong>AAD
</strong> – Azure AD
</li><li><strong>AADB2C
</strong> or just 
<strong>B2C
</strong> – Azure AD B2C
</li><li><strong>ME-ID
</strong> – Microsoft Entra ID. This is in fact the official acronym for Microsoft Entra ID, the new name of Azure AD.
</li><li><strong>ME-EID
</strong> – Although not the official acronym for Microsoft Entra External ID, I have to come up with my own, because I’ll be talking about Microsoft Entra External ID quite a lot in this article. And if I would spell out the name every time, it would be a lot of repetition.
</li></ul>
            
<h2 id="the-future-of-azure-ad-b2c">The Future of Azure AD B2C
</h2>
            
<p>AADB2C is still fully supported. I have not come across any kind of end-of-life announcement for B2C either (early November 2023). So there is absolutely no need for panic, if you have built your solutions around AADB2C.
</p>
            
<p>On the other hand, why would Microsoft have two cloud products with very similar features in it? Two cloud products that solve more or less the same problems? You might remember Azure Access Control Service (ACS). It used to have somewhat similar capabilities as Azure AD B2C. However, 
<a href="https://azure.microsoft.com/blog/acs-access-control-service-namespace-creation-restriction/">back in 2017 it was more or less discontinued
</a>, in favour of Azure AD B2C.
</p>
            
<p>What really says it all for me is the following note from 
<a href="https://learn.microsoft.com/azure/active-directory/external-identities/customers/overview-customers-ciam#about-azure-ad-b2c">this overview page about Microsoft Entra External ID
</a>.
</p>
            
<blockquote>Keep in mind that the next generation Microsoft Entra External ID platform represents the future of CIAM for Microsoft, and rapid innovation, new features and capabilities will be focused on this platform. By choosing the next generation platform from the start, you will receive the benefits of rapid innovation and a future-proof architecture.
</blockquote>
            
<p>You can’t say it any clearer that that without actually saying it – Microsoft Entra External ID will eventually replace Azure AD B2C. I don’t know when Microsoft will announce that AAD B2C will be discontinued. I am not a Microsoft employee nor am I an MVP (yet). But I am willing to bet that the day will come sooner or later. Azure AD B2C will probably still be around for many years though.
</p>
            
<h3 id="should-you-switch-to-microsoft-entra-external-id">Should You Switch to Microsoft Entra External ID?
</h3>
            
<p>If you have solutions running on AADB2C now in production, I would say that you are in no hurry at all to migrate your users and applications to Microsoft Entra External ID. I guess that all existing namespaces for ACS still work today even though you haven’t been able to create new namespaces since 2017. Similarly, all existing Azure AD B2C tenants will still work as they used to years after the last Azure AD B2C tenant was created.
</p>
            
<p>But if you are planning on building a solution for managing the identities of your customers or other external users, then you should get and stay informed about Microsoft Entra External ID. At Integrata, the company I currently work for, we are building solutions where we have decided to use Azure AD B2C for managing external user identities. But now after the announcement of Microsoft Entra External ID, we have decided to switch to Microsoft Entra External ID. We have the luxury of being able to wait until Microsoft Entra External ID is ready for production use.
</p>
            
<h2 id="what-is-microsoft-entra-external-id">What is Microsoft Entra External ID
</h2>
            
<p>To put it short, Microsoft Entra External ID is a feature in Microsoft Entra ID (formerly known as Azure AD) to manage identities for users that are external to your own tenant, such as your customers. Currently (early November 2023), Microsoft Entra External ID is in public preview.
</p>
            
<p>Currently, it looks like you can’t take an existing ME-ID tenant, and turn on the External ID features in it. However, if you log in to 
<a href="https://entra.microsoft.com/">entra.microsoft.com
</a> in your existing ME-ID tenant, you can 
<a href="https://entra.microsoft.com/#view/Microsoft_AAD_IAM/UserCreateBlade/initialMode~/2/b2cEnabled~/true/ciamEnabled~/true">open the view
</a> that allows you to create a customer user, i.e. an external user. But the user creation fails when when you try to create the user. The creation fails even if you enable external user collaboration in that tenant. Remember, Microsoft Entra External ID is still in public preview, so this might be something that will change in the future.
</p>
            
<p>Even if this will change in the future, I would say that it is a good thing to keep your external users in a separate tenant. It is a good thing to have a Microsoft Entra ID tenant as a security boundary between your employees and external users.
</p>
            
<h2 id="features-supporting-identity-management-for-external-users">Features Supporting Identity Management for External Users
</h2>
            
<p>So what features are there in Microsoft Entra External ID for external users that do not exist in Microsoft Entra ID (formerly known as Azure AD)? While the list of features described below is not a complete list, at least I have found them useful in previous assignments when creating identity solutions for users from outside one single organisation.
</p>
            
<h3 id="flexible-usernames">Flexible Usernames
</h3>
            
<p>One of the key features you absolutely need to have is more flexible username options than you have in ME-ID. In ME-ID you can only have user accounts that use one of the domains registered with the tenant. Obviously that won’t cut it when creating user accounts for your customers or other external users. In Microsoft Entra External ID you can create user accounts using any e-mail address. You can also use more traditional usernames that don’t have to be e-mail addresses, and follow any naming convention you like. One very popular convention back in the on-prem days was the 5 + 3 convention – Take 5 first chars of your last name and append the 3 first chars of your first name. For me, that would be BerglMik.
</p>
            
<h3 id="token-augmentation">Token Augmentation
</h3>
            
<p>Another very useful feature I have found myself using in most of the external user identity assignments I’ve worked with is the ability to augment the identity token with custom claims. In Azure AD B2C, this required you to write custom policies. And to create custom policies, you need to write a lot of XML. And I mean 
<strong>a lot
</strong>. In Microsoft Entra External ID you don’t have to do that anymore. You just configure the URL of your HTTP endpoint that you want to return your custom claims, and you’re done! That’s a huge improvement!
</p>
            
<h3 id="custom-user-flows">Custom User Flows
</h3>
            
<p>Then there’s one more thing that I find pretty useful in cases that involve identities for external users. That is the possibility to create custom user flows for things like signing up, signing in and self-service password changes. In ME-ID you have the flows you have, and they provide very limited amount of flexibility. In Microsoft Entra External ID user flows you can customize for instance the following.
</p>
            
<ul><li>Select how users sign in
</li><li>Add custom attributes
</li><li>Specify what attributes to collect during sign-up
</li><li>Add custom authentication extensions
</li><li>Customize translations
</li></ul>
            
<p>You also have some limited ways to modify the fields for collecting user attributes, and specifying whether a field is required or not.
</p>
            
<h2 id="comparing-microsoft-entra-external-id-to-azure-ad-b2c">Comparing Microsoft Entra External ID to Azure AD B2C
</h2>
            
<p>All in all, I think Microsoft Entra External ID is moving in the right direction compared to Azure AD B2C. There are two main reasons why I think so.
</p>
            
<h3 id="no-more-xml">No More XML
</h3>
            
<p>First of all, you don’t have to write a huge amount of XML to resolve common uses cases. And I hope thinks stay like that too! At least for me, the feature set in Microsoft Entra External ID is enough, already in preview stage. I can’t think of a single project with external identities that I have worked on that we could not have implemented with Microsoft Entra External ID. Sure there are things that would have been nice to have. But none of them would have been a deal-breaker.
</p>
            
<h3 id="an-actual-microsoft-entra-id-tenant">An Actual Microsoft Entra ID Tenant
</h3>
            
<p>The second thing that makes Microsoft Entra External ID so interesting is that it is an actual Microsoft Entra ID tenant. This was not the case with Azure AD and Azure AD B2C. This means that you have exactly the same features available for external users as you have for your employees. This brings up a few interesting use cases. Let’s say you are starting up a company, and create a tenant to host your employees’ user accounts. What if you create this tenant as a Microsoft Entra External ID tenant? If you then provision Microsoft 365 services to this tenant like Outlook, SharePoint and Teams, can you then have your customer users use this features just like your employees?
</p>
            
<p>I guess that should be possible at least on an academic level, since the authority is exactly the same tenant. Needless to say that this will most certainly create some very weird scenarios. I still think that it is best to keep your external users in a separate tenant, but this is an interesting idea not to say the least. I will have to look into this a bit deeper in a future article.
</p>
            
<h3 id="ui-customization-still-missing">UI Customization Still Missing
</h3>
            
<p>However, there is one feature currently missing in Microsoft Entra External ID that I really liked in Azure AD B2C. That is customizing the UI. In Azure AD B2C you can completely define your own HTML to use in your user flows. The only requirement for that HTML is that it is publicly available on the Internet, and that it contains a 
<code>&lt;div id=&quot;api&quot; /&gt;
</code> element. That element is a placeholder for Azure AD B2C to inject its own markup. This feature does not exist in Microsoft Entra External ID. The only way to customize your UI is to use similar branding features that you have in ME-ID. I hope that the custom UI feature comes to Microsoft Entra External ID some day. But, I can live without it too.
</p>
            
<h2 id="further-reading">Further Reading
</h2>
            
<p>Below are some links that you might find useful when researching more about Microsoft Entra External ID.
</p>
            
<ul><li><a href="https://techcommunity.microsoft.com/t5/microsoft-entra-azure-ad-blog/microsoft-entra-external-id-public-preview-developer-centric/ba-p/3823766">Microsoft Entra External ID​ public preview: Developer-centric platform
</a></li><li><a href="https://www.microsoft.com/security/business/identity-access/microsoft-entra-external-id">Microsoft Entra External ID
</a></li></ul>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Durable Functions Pitfalls in Azure Functions</title>
      <description>Avoid these Durable Functions pitfalls to effectively leverage the Durable Functions extension to Azure Functions.</description>
      <link>https://stage.mikaberglund.com/durable-functions-pitfalls-in-azure-functions</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/durable-functions-pitfalls-in-azure-functions</guid>
      <pubDate>Tue, 29 Aug 2023 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="Durable-Functions" alt="Durable Functions" />
                
<figcaption>Durable Functions
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/resolving-the-could-not-load-file-or-assembly-system-identitymodel-tokens-jwt-error" role="button" aria-label="Previous article: Resolving the \\u201CCould Not Load File or Assembly System.IdentityModel.Tokens.Jwt\\u201D Error"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/from-azure-ad-b2c-to-microsoft-entra-external-id" role="button" aria-label="Next article: From Azure AD B2C to Microsoft Entra External ID"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="durable-functions-pitfalls-in-azure-functions">Durable Functions Pitfalls in Azure Functions
</h1>
            
<p class="article-meta">August 29, 2023
</p>

            
<p>Durable Functions is an extension to Azure Functions that allows you to write long-running workflows in code. I will not go in much detail on Durable Functions. Instead I will simply point you to 
<a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview">this article
</a> that gives you a detailed description. In this article, I will talk about the pitfalls that I’ve fallen into several times when working with Durable Functions. Naturally, you can also consider it as best practices for Durable Functions to avoid these pitfalls. I hope that with this article I can help you along the way to perfecting your Durable Functions applications.
</p>
            
<h2 id="brief-description-of-durable-functions">Brief Description of Durable Functions
</h2>
            
<p>I will start with a little bit of explanation about what Durable Functions is. Just to set the context of this article. But it’ll be brief. There are three different types of functions, with different features, responsibilities and constraints.
</p>
            
<ol><li><strong><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-types-features-overview#orchestrator-functions">Orchestration Functions
</a></strong> – The backbone of your code based workflows where you define the logic
</li><li><strong><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-types-features-overview#activity-functions">Activity Functions
</a></strong> – The workhorse of your workflows where you do all of your “heavy lifting”
</li><li><strong><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-types-features-overview#entity-functions">Entity Functions
</a></strong> – Allows you to store objects with data and are accessible to your orchestration functions
</li></ol>
            
<p>In this article I’ll focus on orchestration functions, since that is where the most pitfalls are. Orchestration functions are also the type of Durable Functions with the most 
<a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-code-constraints">constraints
</a> and potential pitfalls. I will cover entity functions in a future article, or update this one.
</p>
            
<h2 id="orchestration-functions-must-be-deterministic">Orchestration Functions Must Be Deterministic
</h2>
            
<p>Orchestration functions 
<strong>*MUST
</strong><em> be deterministic! This is probably among the first pitfalls you will fall into when working with Durable Functions. Especially if you don’t understand this properly. This means that when an orchestration function executes with the same input, it must always produce the exact same result. 
</em>I can’t stress the importance of this enough!* For me, this is by far the biggest reason for running into problems with durable functions. If you are lucky, you get a runtime exception if your function violates this constraint. However, in my experience, your function just behaves in a very weird way and you spend a lot of time trying to figure out what the problem is.
</p>
            
<p>OK, so what does this mean in practice? It means that each and every line of code in your orchestration must produce exactly the same result with the same input. Even if the line of code runs multiple times. And that is exactly what happens in orchestration functions. The Durable Functions runtime potentially replays an orchestration function several times. That is the price we have to pay for durability.
</p>
            
<h3 id="lets-look-at-some-code">Let’s Look at Some Code
</h3>
            
<p>Let’s take a closer look at how this happens in practice. Take the very simple sample code below. It is just some meaningless code to describe the problem that you may run into.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof(MyOrchestration))]
</span>
            
<span class="code-line">public async Task MyOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    Guid id = Guid.NewGuid();
</span>
            
<span class="code-line">    string id1 = context.CallActivityAsync&lt;string&gt;(nameof(MyActivity1), id);
</span>
            
<span class="code-line">    string id2 = context.CallActivityAsync&lt;string&gt;(nameof(MyActivity2), id);
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    bool result = id1 == id2;
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(MyActivity1))]
</span>
            
<span class="code-line">public async Task&lt;string&gt; MyActivity1([ActivityTrigger] Guid input)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    return input.ToString();
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(MyActivity2))]
</span>
            
<span class="code-line">public async Task&lt;string&gt; MyActivity2([ActivityTrigger] Guid input)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    return input.ToString();
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>Now when the 
<code>MyOrchestration
</code> function runs, it executes each line of code, until the first activity function (or entity function) call on the orchestration context is encountered. At that time, the durable functions runtime pauses the orchestration, and schedules the activity function with its input for execution. The runtime then executes these scheduled function in a first-come-first-served manner. So it may take quite a while before your activity function executes. There might be other orchestration instances running that have scheduled activity functions for execution. That all depends on how much load your application is experiencing. But eventually it will execute. That’s the promise of Durable Functions.
</p>
            
<p>When the activity function eventually executes and returns, the runtime will replay the orchestration function, and execute each line of code again. From the beginning! The orchestration function then executes until it encounters the next activity function, and does the same thing. This is called 
<em>replaying
</em>.
</p>
            
<h3 id="what-happens-during-replaying">What Happens During Replaying?
</h3>
            
<p>To give you a concrete understanding of what replaying means, we can have a look at the sample code above. I’ll list the lines in the order of execution in the list below (skipping the function signatures and other irrelevant lines).
</p>
            
<ol><li><strong>Line #4
</strong>: The variable 
<code>id
</code> is assigned a value.
</li><li><strong>Line #5
</strong>: The orchestration encounters an uncalled activity function and schedules it for execution.
</li><li><strong>Line #14
</strong>: The activity function executes and returns a value (the value assigned to the 
<code>id
</code> variable in step #1).
</li><li><strong>Line #4
</strong> (2nd run): The orchestration replays for the first time and assigns a value to the 
<code>id
</code> variable. Note! This value is different from the value in step #1.
</li><li><strong>Line #5
</strong> (2nd run): The orchestration encounters an activity function that it called previously. The runtime returns the value from the execution history of the orchestration function without calling the activity function.
</li><li><strong>Line #6
</strong>: The orchestration encounters another uncalled activity function, and schedules it for execution.
</li><li><strong>Line #20
</strong>: The second activity function executes and returns a value (the value assigned to the id variable in step #4.
</li><li><strong>Line #4
</strong> (3rd run): Again, the orchestration function starts from the beginning, and assigns yet another value to the 
<code>id
</code> variable.
</li><li><strong>Line #5
</strong> (3rd run): The orchestration encounters an activity function that it previously executed, and the orchestration context returns the result of that function call from the execution history.
</li><li><strong>Line #6
</strong> (2nd run): Another line of code that the runtime replays and returns the value from the execution history.
</li><li><strong>Line #8
</strong>: The orchestration compares the results of both activity functions.
</li></ol>
            
<p>In short, the executed code lines are (replayed lines are bolded): 4, 5, 14, 
<strong>4
</strong>, 
<strong>5
</strong>, 6, 20, 
<strong>4
</strong>, 
<strong>5
</strong>, 
<strong>6
</strong>, 8.
</p>
            
<p>As you probably have guessed, the 
<code>result
</code> variable is 
<code>false
</code>, and not 
<code>true
</code>, as you normally would expect. This is the result of replaying. It will come back and bite you, if you don’t keep this in mind when working with Durable Functions.
</p>
            
<h3 id="how-to-make-your-orchestration-functions-deterministic">How to Make Your Orchestration Functions Deterministic
</h3>
            
<p>In my code example above I used a 
<code>Guid
</code> to demonstrate how your function can go wrong without any runtime exceptions. The same goes for timestamps and random numbers too, just to mention a few. For instance, 
<code>DateTime.UtcNow
</code> returns a different value each time you call it. The 
<code>Random.Next
</code> method also returns a different value each time. It wouldn’t be random otherwise, now would it?
</p>
            
<p>Luckily you can fix some of these quite easily with the help of the orchestration context. The 
<code>IDurableOrchestrationContext.CurrentUtcDateTime
</code> property returns a value that is guaranteed to be deterministic. The same goes for the 
<code>IDurableOrchestrationContext.NewGuid
</code> method.
</p>
            
<p>However, the orchestration context does not support generating random numbers. That’s no problem though. You just need to wrap your random number generation in an activity function, and you are good to go.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof(MyOrchestration))]
</span>
            
<span class="code-line">public async Task MyOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var rndInt = await context.CallActivityAsync&lt;int&gt;(nameof(), new Tuple&lt;int, int&gt;(1, 10));
</span>
            
<span class="code-line">    // rndInt is the same regardless of how many times the orchestration is replayed.
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(GenerateRandomIntActivity))]
</span>
            
<span class="code-line">public async Task&lt;int&gt; GenerateRandomIntActivity([ActivityTrigger] Tuple&lt;int, int&gt; input)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var rnd = new Random();
</span>
            
<span class="code-line">    return rnd.Next(input.Item1, input.Item2);
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>I hope that I have managed to emphasize the importance well enough with these examples. As I wrote above, I’ve spent countless hours on banging my head against anything hard around my desk trying to figure out why my application is not working as I planned. All too many times I found out that the root cause to my problems was that one of my orchestration functions I wrote was not behaving in a deterministic fashion.
</p>
            
<h2 id="async-function-calls-in-orchestration-functions">Async Function Calls in Orchestration Functions
</h2>
            
<p>Another one of the Durable Functions pitfalls that have cause me headaches all too many times is that you 
<strong>*MUST NOT
</strong>* call async methods in orchestration functions. The only async methods that you are allowed to call are the methods that the 
<a href="https://learn.microsoft.com/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.idurableorchestrationcontext#methods">orchestration context
</a> defines. These functions mainly allow you to work with orchestrations, sub-orchestrations, activities and entity functions.
</p>
            
<p>If you need to call any other async methods like accessing a database, you need to wrap that logic into an activity function. You cannot call the async method directly from your orchestration function.
</p>
            
<p>In earlier version of Durable Functions, this used to be the case also for HTTP requests. Luckily there is now an async method on the orchestration context that helps with HTTP requests. The 
<code>CallHttpAsync
</code> method allows you to do HTTP requests directly in your orchestration functions.
</p>
            
<p>So, whenever you call an awaitable method in one of your orchestration functions, make sure that those calls are all going through the orchestration context.
</p>
            
<h2 id="looping-in-orchestration-functions">Looping in Orchestration Functions
</h2>
            
<p>Depending on how you structure your code, iterating over a collection of data in an orchestration function can either follow the 
<a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview#chaining">function chaining pattern
</a>, or the 
<a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview#fan-in-out">fan-out / fan-in
</a> pattern. Especially if you call activity functions in each iteration one by one, you may run into serious performance issues. This is particularly true when your activity function returns a large set of data. This is also one of the Durable Functions pitfalls that may require you to restructure your code pretty much to avoid.
</p>
            
<p>Let’s start with an example. Suppose you are writing an application that processes a collection of cities and stores the hourly weather forecasts in each city for the next day in your database. You use your own database to store the cities and an external weather service to get the forecasts from. The simplified sample code below shows the setup.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof(ProcessCityForecastsOrchestration))]
</span>
            
<span class="code-line">public async Task ProcessCityForecastsOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var cities = await context.CallActivityAsync&lt;IEnumerable&lt;City&gt;&gt;(nameof(GetCitiesActivity));
</span>
            
<span class="code-line">    foreach(var city in cities)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        var forecasts = await context.CallActivityAsync&lt;IEnumerable&lt;Forecast&gt;&gt;(nameof(GetCityForecastsForTomorrowActivity), city);
</span>
            
<span class="code-line">        // Here we would then store the forecasts, but we&#39;ll skip that for simplicity.
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(GetCitiesActivity))]
</span>
            
<span class="code-line">public async Task&lt;IEnumerable&lt;City&gt;&gt; GetCitiesActivity([ActivityTrigger] IDurableActivityContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    IEnumerable&lt;City&gt; cities;
</span>
            
<span class="code-line">    // Read the cities from your database.
</span>
            
<span class="code-line">    return cities;
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(GetCityForecastsForTomorrowActivity))]
</span>
            
<span class="code-line">public async Task&lt;IEnumerable&lt;Forecast&gt;&gt; GetCityForecastsForTomorrowActivity([ActivityTrigger] City city)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    IEnumerable&lt;Forecast&gt; forecasts;
</span>
            
<span class="code-line">    // Use an external weather service to get the weather forecast for the given city.
</span>
            
<span class="code-line">    return forecasts;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>This code works, but is far from optimal. As the number of cities get bigger, there is more and more replaying taking place. Remember that for every activity that the orchestration encounters, the orchestration schedules the activity for execution. And when that activity completes, the runtime replays the orchestration from the start.
</p>
            
<p>Replaying also involves deserializing data returned by previously executed activity functions. So, for every city in the iteration in the sample code above, there is one more activity function that already ran and returned weather forecasts. And each time, the runtime will deserialize the forecasts for the orchestration function to use.
</p>
            
<p>Deserializing is generally pretty quick, but if the amount of data that is deserialized increases each time you go over an iteration, and do many iterations, it starts to slow your application down more and more. And the decrease can be very dramatic!
</p>
            
<h3 id="looking-at-the-numbers">Looking at the Numbers
</h3>
            
<p>Imagine that you have 1000 cities in your database. For every city, you get 24 hourly forecasts for the next day. That is 24 000 weather forecasts each day. For the sake of simplicity, let’s also say that it takes 10 milliseconds to deserialize one hourly weather forecast.
</p>
            
<blockquote>Note! The call to the 
<code>GetCitiesActivity
</code> activity function will also cause the list of cities to be deserialized for every replay, but that does not get slower for every iteration. The time is constant for every replay, which is why I excluded that from this numbers game to keep this sample as simple as possible.
</blockquote>
            
<p>So, for the forecasts returned for the first city, the deserialization will take 24 * 10 ms = 240 ms. That’s not bad. For the second city, the orchestration is replayed, and the 
<code>foreach
</code> loop is played from the beginning. So to get the forecasts for the second city, we also have to deserialize the forecasts for the first city. Even though we already saved them during the first iteration. In other words, that deserialization is completely unnecessary.
</p>
            
<p>Deriving from this, the time it takes to deserialize the forecasts for the second city is 24 
<em> 10 ms + 24 
</em> 10 ms = 480 ms. Still not that bad, but you start to get the picture, right?
</p>
            
<p>Now fast forwarding to the 100th city. Before we can even get to call for the forecasts to the 100th city, we would have to deserialize the forecasts to 99 cities. With our sample values that would sum up as 99 
<em> 24 
</em> 10 ms = 23 760 ms. That is over 23 seconds of deserializing, all of which is unnecessary.
</p>
            
<h4 id="getting-really-ugly">Getting Really Ugly
</h4>
            
<p>OK, so what’s the situation at the 500th city. That would mean that you’d have to deserialize the forecasts of 499 cities, all in vain. And that would cost you 119 760 ms, which is almost 2 minutes.
</p>
            
<p>Remember that for every iteration except for the first one, you are doing increasingly more and more unnecessary deserialization.
</p>
            
<p>The last iteration for the 1000th city, the iteration would take 4 minutes! And that’s just the last iteration!
</p>
            
<h3 id="improving-performance">Improving Performance
</h3>
            
<p>Fortunately there is quite a lot you can do to improve on the performance. The key is to avoid repeating unnecessary deserialization. Instead of having one main orchestration that would first get the forecasts with one activity function and then store them with another, you would make use of sub orchestrations and the 
<a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview#fan-in-out">fan-out/fan-in pattern
</a>. The sub orchestration would be responsible for getting and storing the forecasts for one single city. The code below shows how you would structure this.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof(ProcessCityForecastsOrchestration))]
</span>
            
<span class="code-line">public async Task ProcessCityForecastsOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var cities = await context.CallActivityAsync&lt;IEnumerable&lt;City&gt;&gt;(nameof(GetCitiesActivity));
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    var taskList = new List&lt;Task&gt;();
</span>
            
<span class="code-line">    foreach(var city in cities)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        taskList.Add(context.CallSubOrchestratorAsync(nameof(StoreSingleCityForecastsOrchestration), city));
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">    await Task.WhenAll(taskList);
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(StoreSingleCityForecastsOrchestration))]
</span>
            
<span class="code-line">public async Task StoreSingleCityForecastsOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var city = context.GetInput&lt;City&gt;();
</span>
            
<span class="code-line">    var forecasts = await context.CallActivityAsync&lt;IEnumerable&lt;Forecast&gt;&gt;(nameof(GetCityForecastsForTomorrowActivity), city);
</span>
            
<span class="code-line">    // Here we would then store the forecasts for the given city.
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(GetCitiesActivity))]
</span>
            
<span class="code-line">public async Task&lt;IEnumerable&lt;City&gt;&gt; GetCitiesActivity([ActivityTrigger] IDurableActivityContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    IEnumerable&lt;City&gt; cities;
</span>
            
<span class="code-line">    // Read the cities from your database.
</span>
            
<span class="code-line">    return cities;
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(GetCityForecastsForTomorrowActivity))]
</span>
            
<span class="code-line">public async Task&lt;IEnumerable&lt;Forecast&gt;&gt; GetCityForecastsForTomorrowActivity([ActivityTrigger] City city)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    IEnumerable&lt;Forecast&gt; forecasts;
</span>
            
<span class="code-line">    // Use an external weather service to get the weather forecast for the given city.
</span>
            
<span class="code-line">    return forecasts;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>The performance improvements in this example come from two separate areas. First of all, the 
<code>cities
</code> collection on line #4 is deserialized only two times, and not once for every city. The first time when the 
<code>GetCitiesActivity
</code> function is called for the first time, and the second time when the orchestration is replayed after returning back from line #11. So 2 times instead of 1000 times.
</p>
            
<p>The second performance improvement, the most significant, comes from the fact that each weather forecast is deserialized only one time. Fanning out like this dramatically decreases the number of replays, thus cutting down the time spent on doing meaningless deserialization. What this means in practice is that the iteration on lines 7-10 runs in one go, since we don’t call any sub orchestration function with the 
<code>await
</code> keyword. Instead, we create a collection of tasks representing the orchestration function calls, and then await on them all together on line #11. At that point, the main orchestration would pause and wait for all of the sub orchestration functions to run and return before continuing.
</p>
            
<p>You don’t have to use sub orchestrations to fan out. You can do that with activity functions as well. In this example I was just using sub orchestrations, because the sub orchestration then was calling two separate activity functions.
</p>
            
<h2 id="cleaning-up-function-history">Cleaning up Function History
</h2>
            
<p>Durable Functions provides a high level of resilience towards unexpected incidents. These can be network problems, application crashes or something similar. This is achieved using persistent storage to store all orchestration, activity and entity function calls before actually performing the actions. This allows Durable Functions to recover from virtually any incident, except for deleting the storage. Even if you delete the application and recreate it and connect it to the same storage, your functions will continue running.
</p>
            
<p>Over time, this storage fills up, because Durable Functions does not clean it up. This really hit me this summer when we started experiencing a lot of errors in a somewhat busy Durable Functions application that we are building. These errors were then multiplied because we used a retry policy on most of our activity calls that retried the call several times. So the work items in the history table really started piling up. At some point the storage account was the resource that incurred most of the costs in the resource group hosting our application. This was really a new situation for me, since typically storage accounts in Azure cost peanuts. At some point I think our history table was using over 100 GB!
</p>
            
<h3 id="add-your-own-cleaning-logic">Add Your Own Cleaning Logic
</h3>
            
<p>Fortunately there is quite an easy way to take care of this. The 
<a href="https://learn.microsoft.com/dotnet/api/microsoft.azure.webjobs.extensions.durabletask.idurableorchestrationclient">Durable Functions client
</a> defines a method, 
<code>PurgeInstanceHistoryAsync
</code>, that you can call regularly to clean up the history table. The simplest way to call this is to use a timer trigger, as shown in the code below.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof(CleanOrchestrationHistoryTimer))]
</span>
            
<span class="code-line">public async Task CleanOrchestrationHistoryTimer([TimerTrigger(&quot;0 0 20 * * *&quot;)] TimerInfo timer, [DurableClient] IDurableOrchestrationClient client)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var statusList = new List&lt;OrchestrationStatus&gt; { OrchestrationStatus.Terminated, OrchestrationStatus.Completed, OrchestrationStatus.Canceled };
</span>
            
<span class="code-line">    await client.PurgeInstanceHistoryAsync(DateTime.UtcNow.AddYears(-2), DateTime.UtcNow.AddDays(-14), statusList);
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>This would run your cleanup routine every day at 20:00 UTC. It would clean up all of your history that is newer than 2 years, but older than 14 days. This would remove all instances that are marked as completed, terminated or cancelled. If you would like to clean out other statuses as well, then you can just add those statuses to the 
<code>statusList
</code> collection.
</p>
            
<p>To make this more robust, you could or course wrap your cleaning in an activity function that you call from an orchestration function. Then use the timer trigger to start a new orchestration instance. The code below shows you how to do that.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof(CleanOrchestrationHistoryTimer))]
</span>
            
<span class="code-line">public async Task CleanOrchestrationHistoryTimer([TimerTrigger(&quot;0 0 20 * * *&quot;)] TimerInfo timer, [DurableClient] IDurableOrchestrationClient client)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    await client.StartNewAsync(nameof(CleanOrchestrationHistoryOrchestration));
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(CleanOrchestrationHistoryOrchestration))]
</span>
            
<span class="code-line">public async Task CleanOrchestrationHistoryOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    await context.CallActivityAsync(nameof(CleanOrchestrationHistoryActivity), null);
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof(CleanOrchestrationHistoryActivity))]
</span>
            
<span class="code-line">public async Task CleanOrchestrationHistoryActivity([ActivityTrigger] IDurableActivityContext context, [DurableClient] IDurableOrchestrationClient client)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var statusList = new List&lt;OrchestrationStatus&gt; { OrchestrationStatus.Terminated, OrchestrationStatus.Completed, OrchestrationStatus.Canceled };
</span>
            
<span class="code-line">    await client.PurgeInstanceHistoryAsync(DateTime.UtcNow.AddYears(-2), DateTime.UtcNow.AddDays(-14), statusList);
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<h2 id="renaming-functions">Renaming Functions
</h2>
            
<p>Sometimes you need to refactor your code, and restructure it. This can sometimes also include renaming methods. Under normal conditions, renaming a method is not that big of a deal. But with Durable Functions renaming functions may cause you trouble. Perhaps not one of the deepest Durable Functions pitfalls, but at least it is something you need to keep in mind.
</p>
            
<p>As implied earlier, all function calls in Durable Functions are logged and stored in a history table. Each row in this table contains the name of the function, its input and output, and some other things.
</p>
            
<p>When your orchestration function runs, it schedules all sub orchestrations and activities to be run. If you stop your application before they had the chance to run, then they will run after you restart your application.
</p>
            
<p>Now if you stopped your application in order to update it with a refactored version where you have changed the name of a function that was scheduled for execution, there is no way that the Durable Functions runtime would know what your function is called in the updated version. This would lead into a runtime exception, and the function that the previous version had will never run.
</p>
            
<h3 id="considerations-for-renaming-functions">Considerations for Renaming Functions
</h3>
            
<p>There is not very much you can do about this other than not to rename your functions. That’s not a good solution though. I have not had that much of a problem with this, but what I typically do is that I leave the old function as is, and write a new one with the improved functionality, and give it a new name. I typically also mark the old function as obsolete just so that I have the help of the compiler to find that function at a later time. Then update the application and let all instances pointing to the old function run out. You can then delete the old function in a future update after you’ve made sure you don’t have any instances of the old function running.
</p>
            
<p>Be aware though, that orchestrations can last for a very long time. As I’ve understood it, there is not limit for how long an orchestration can last. For instance, if you wait for an external event in your orchestration, there is no upper time limit for how long the orchestration can wait for the event. You can also use durable timers to create a delay in your orchestration. Durable functions implemented in .NET support arbitrary long timers.
</p>
            
<h2 id="monitoring-durable-functions">Monitoring Durable Functions
</h2>
            
<p>So how would you know what functions are running? Luckily there is an extension to Visual Studio Code called 
<a href="https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor">Durable Functions Monitor
</a>. With this extension you can easily connect to the task hub of your Durable Functions application and find the orchestrations you want by filtering and sorting the list of orchestrations.
</p>
            
<p>With the extension you can then work with each orchestration instance and get a better view of what it has done and what is still is going to do. You can also perform actions on the orchestration with this tool such as suspend and resume, terminate and purge, just to mention a few. There’s a lot more you can do with this extension, so be sure to install it. Personally, I wouldn’t do any Durable Functions development without this tool anymore.
</p>
            
<h2 id="examples-from-my-personal-experience">Examples From My Personal Experience
</h2>
            
<p>Now that you’ve come this far with this article, I think you deserve to get a reward for that. This time, the reward is in the form me describing perhaps my most embarrassing examples of falling into the pitfalls that I’ve described in this article. Have fun reading &#128521;
</p>
            
<h3 id="creating-new-user-accounts">Creating New User Accounts
</h3>
            
<p>This is perhaps one of the problems that I spent the most time on trying to figure out why the heck my app was not working. It was many years ago when I was working on an Extranet solution for residents. We had a feature where residents could sign up using their Finnish bank IDs. In order not to have residents use bank IDs every time they logged in, we created a user account with their e-mail and a generated password, that we then sent to the given e-mail. The residents then used their e-mail and the generated password to log in the following times. Of course they had to change the password on first login, of course.
</p>
            
<p>Already back then I was a fan of Durable Functions. And I still am. So I decided to implement the user account creation feature using Durable Functions. I created an orchestration function that called one activity function to create the user account with a random password. Then call another activity to send that e-mail address and password in an e-mail to the resident. The code below is a much simplified version of that logic.
</p>
            
<h4 id="sample-code">Sample Code
</h4>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">[FunctionName(nameof())]
</span>
            
<span class="code-line">public async Task CreateAndSendUserAccountOrchestration([OrchestrationTrigger] IDurableOrchestrationContext context)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    var email = context.GetInput&lt;string&gt;(); // Assume the e-mail address is sent as input to the orchestration.
</span>
            
<span class="code-line">    var pwd = this.GenerateRandomPassword();
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    var accountInfo = new Tuple&lt;string, string&gt;(email, pwd);
</span>
            
<span class="code-line">    await context.CallActivityAsync(nameof(CreateUserAccountActivity), accountInfo);
</span>
            
<span class="code-line">    await context.CallActivityAsync(nameof(SendUserAccountActivity), accountInfo);
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof())]
</span>
            
<span class="code-line">public async Task CreateUserAccountActivity([ActivityTrigger] Tuple&lt;string, string&gt; input)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    // Here we would use the username and password sent in the input to connect to our user registry
</span>
            
<span class="code-line">    // and create a new user account.
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">[FunctionName(nameof())]
</span>
            
<span class="code-line">public async Task SendUserAccountActivity([ActivityTrigger] Tuple&lt;string, string&gt; input)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    // Here we would take the username and password from the input, add that information to an e-mail template
</span>
            
<span class="code-line">    // and send it to the given e-mail address.
</span>
            
<span class="code-line">}
</span>
            
<span class="code-line"></span>
            
<span class="code-line">private string GenerateRandomPassword()
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    string pwd;
</span>
            
<span class="code-line">    // Use System.Random or some other mechanism to generate a random password and return it.
</span>
            
<span class="code-line"></span>
            
<span class="code-line">    return pwd;
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>Very innocent looking code, don’t you think? But since you’ve now read this article, you probably immediately spot the problem. We’ll I did not! I got the user account created, and the e-mail was sent properly. But when I tried to log in, I got a login failed error. Every time! Incorrect username and/or password!
</p>
            
<h4 id="a-very-easy-solution">A Very Easy Solution
</h4>
            
<p>I don’t remember how many times I deleted the generated user account and retried the whole thing. The same result every time. I could not understand what was going wrong. It was really a mystery. I even used break points in the activity functions to step through them. Yes, my code was in fact creating a user account, and yes, my code actually sent the e-mail. There was only one orchestration instance running at a time. I made sure of that to ensure that there was no mixing of data between orchestration instances.
</p>
            
<p>It wasn’t until I started looking closer at the inputs to the activity functions that I pretty quickly realized what the problem was. My code generated the password twice! One password that I used to create the user account with. And another password that I sent out in the e-mail. Afterwards, I’ve been trying to figure out how on earth did I not notice that earlier. Can’t say for sure, but the only thing I can think of is that the call to the 
<code>GenerateRandomPassword()
</code> method looks so innocent, since it is not an async function. Well, we all know better now, don’t we?
</p>
            
<p>So, I simply wrapped the password generation logic in an activity function. The problem was solved with just a few additional lines of code. I am still beating myself over this, and how could I have been so blind. Enough time has now elapsed, and at least I can now share it as a funny lesson.
</p>
            
<h2 id="further-reading">Further Reading
</h2>
            
<p>Here is a list of links that I think provide you with additional information when working with Durable Functions. Hopefully you have found this article useful.
</p>
            
<ul><li><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview">What are Durable Functions?
</a></li><li><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-types-features-overview">Durable Functions types and features
</a></li><li><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-code-constraints">Orchestrator function code constraints
</a></li><li><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-orchestrations">Durable orchestrations
</a></li><li><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-sub-orchestrations">Sub-orchestrations in Durable Functions
</a></li><li><a href="https://marketplace.visualstudio.com/items?itemName=DurableFunctionsMonitor.durablefunctionsmonitor">Durable Functions Monitor
</a></li><li><a href="https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-billing">Durable Functions billing
</a></li><li><a href="/category/durable-functions/">My other Durable Functions articles
</a></li></ul>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Resolving the \u201CCould Not Load File or Assembly System.IdentityModel.Tokens.Jwt\u201D Error</title>
      <description>A description on how I resolved the \u0022Could not load file or assembly System.IdentityModel.Tokens.Jwt\u0022 in an Azure Functions application.</description>
      <link>https://stage.mikaberglund.com/resolving-the-could-not-load-file-or-assembly-system-identitymodel-tokens-jwt-error</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/resolving-the-could-not-load-file-or-assembly-system-identitymodel-tokens-jwt-error</guid>
      <pubDate>Fri, 09 Dec 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="swagger-ui" alt="Swagger UI" />
                
<figcaption>Swagger UI
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/blazor-authentication-with-jwt-token-using-blazorade-msal" role="button" aria-label="Previous article: Blazor Authentication with JWT Token Using Blazorade MSAL"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/durable-functions-pitfalls-in-azure-functions" role="button" aria-label="Next article: Durable Functions Pitfalls in Azure Functions"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="resolving-the-u201ccould-not-load-file-or-assembly-systemidentitymodeltokensjwtu201d-error">Resolving the \u201CCould Not Load File or Assembly System.IdentityModel.Tokens.Jwt\u201D Error
</h1>
            
<p class="article-meta">December 9, 2022
</p>

            
<p>I was working on an Azure Functions application to create a REST API that accepts bearer tokens. When I fired up the app in Visual Studio, I was greeted with the following error message.
</p>
            
<p><code>Could not load file or assembly &#39;System.IdentityModel.Tokens.Jwt, Version=6.23.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35&#39;. The system cannot find the file specified.
</code></p>
            
<p>This article describes the steps I took to resolve the issue for my Azure Functions application.
</p>
            
<h2 id="could-not-load-file-or-assembly-fix-1">Could Not Load File or Assembly – Fix #1
</h2>
            
<p>The first fix was quite easy to 
<a href="https://www.google.com/search?q=Could+Not+Load+File+or+Assembly+System.IdentityModel.Tokens.Jwt">find on Google
</a>. There are many posts that talk about a problem in the Azure Functions SDK. It seems that the SDK is too eager to clean out referenced assemblies that it thinks are not used. So, you need to instruct the Functions SDK to skip cleaning out by adding the following property group to the Azure Functions app project file.
</p>
            
<pre class="code-block" data-language="xml"><code>
            
<span class="code-line">&lt;PropertyGroup&gt;
</span>
            
<span class="code-line">    &lt;_FunctionsSkipCleanOutput&gt;true&lt;/_FunctionsSkipCleanOutput&gt;
</span>
            
<span class="code-line">&lt;/PropertyGroup&gt;
</span>
            
</code></pre>
            
<p>That fixed the issue for me when running locally in debug mode in Visual Studio.
</p>
            
<h2 id="could-not-load-file-or-assembly-fix-2">Could Not Load File or Assembly – Fix #2
</h2>
            
<p>That was an easy fix, I thought, so I deployed the application to my Azure App Service and fired off a request to one of the endpoints. However, I got the same error message as I did earlier.
</p>
            
<p><code>Could not load file or assembly &#39;System.IdentityModel.Tokens.Jwt, Version=6.23.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35&#39;. The system cannot find the file specified.
</code></p>
            
<p>Oh snap! Using the App Service Editor, I verified that the 
<code>System.IdentityModel.Tokens.Jwt.dll
</code> assembly really was not available in the 
<code>bin
</code> folder. A lot of files, but unfortunately not the required. So apparently the 
<code>_FunctionsSkipCleanOutput
</code> property did not fix everything.
</p>
            
<p>I noticed that my Functions app did not contain a direct reference to 
<code>System.IdentityModel.Tokens.Jwt
</code> but instead, my Functions app references another library, that had a reference to the Tokens assembly. So, I thought that I could easily fix the error by adding a direct reference to the missing package to my Functions app too.
</p>
            
<p>But no. I was wrong! Still, the same error. I realized that if the Functions SDK incorrectly cleans out unused assemblies, it might not be enough just to add a reference to it. I would need to use it too. So I added the following code to the constructor of one of the function classes I had in my project.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">using Microsoft.Extensions.Logging;
</span>
            
<span class="code-line">using System.IdentityModel.Tokens.Jwt;
</span>
            
<span class="code-line"></span>
            
<span class="code-line">public class ReportsFunctions
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    public ReportFunctions(ILogger&lt;ReportsFunctions&gt; logger)
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        var token = new JwtSecurityToken();
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<p>Now, when I deployed this project to Azure, it started working. I also verified that the 
<code>System.IdentityModel.Tokens.Jwt.dll
</code> now was present in the bin folder.
</p>
            
<h2 id="conclusion">Conclusion
</h2>
            
<p>After the second fix I thought that maybe I don’t need the first fix. The second fix would fix all my problems. That is however not true. Seems that you still need Fix #1, even after applying Fix #2. At least when running the app locally. And Fix #2 is not something that I found by Googling, so I hope this tip will come in handy, should you end up on this page one day.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Blazor Authentication with JWT Token Using Blazorade MSAL</title>
      <description>Describes how Blazorade MSAL help you implement Blazor authentication in applications that access APIs protected with JWT access tokens.</description>
      <link>https://stage.mikaberglund.com/blazor-authentication-with-jwt-token-using-blazorade-msal</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/blazor-authentication-with-jwt-token-using-blazorade-msal</guid>
      <pubDate>Tue, 01 Mar 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="IMG_20220301_163009" alt="blazor authentication jwt" />
                
<figcaption>blazor authentication jwt
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/azure-devops-project-lifecycle-from-sales-to-production" role="button" aria-label="Previous article: Azure DevOps Project Lifecycle – From Sales to Production"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/resolving-the-could-not-load-file-or-assembly-system-identitymodel-tokens-jwt-error" role="button" aria-label="Next article: Resolving the \\u201CCould Not Load File or Assembly System.IdentityModel.Tokens.Jwt\\u201D Error"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="blazor-authentication-with-jwt-token-using-blazorade-msal">Blazor Authentication with JWT Token Using Blazorade MSAL
</h1>
            
<p class="article-meta">March 1, 2022
</p>

            
<p>Lately I’ve been working a bit on 
<a href="https://www.nuget.org/packages/Blazorade.Msal/">Blazorade MSAL
</a> and updating it to work with .NET6. At the same time, I thought I’d write a sample application that demonstrates how to do Blazor authentication with JWT tokens when connecting to Microsoft Graph. When working on that sample application, I came up with a few improvements to Blazorade MSAL. These improvements make it super easy to implement Blazor authentication with JWT tokens to connect to any REST API. This article describes the most important parts of this sample application. You find the 
<a href="https://github.com/MikaBerglund/access-tokens-with-blazorade-msal">sample application on Github
</a>.
</p>
            
<h2 id="overview">Overview
</h2>
            
<p>When talking about Blazor authentication with 
<a href="https://jwt.io/introduction">JWT tokens
</a>, I’m referring primarily to access tokens. Your Blazor application uses these tokens to authenticate to a REST API on behalf of the user. This is also called 
<em>delegated permissions
</em> – A user delegates permissions to an application. In the sample application described in this article, I’ll cover connecting to 
<a href="https://docs.microsoft.com/graph/api/overview">Microsoft Graph
</a>. But the same principles applies to any Blazor application that implements authentication with JWT access tokens. You can even create your own REST APIs with for instance 
<a href="/category/azure-functions/">Azure Functions
</a> and use JWT tokens from your Blazor application.
</p>
            
<h2 id="set-up-for-first-use">Set Up for First Use
</h2>
            
<p>This sample application requires a settings file that contains information about your application. I’ve intentionally left this information out of the repository. Application settings typically contain sensitive information.
</p>
            
<p>Detailed instructions for setting up the sample application on your local computer is available in the 
<a href="https://github.com/MikaBerglund/access-tokens-with-blazorade-msal/tree/main/GraphClientSample">source code repository
</a>.
</p>
            
<p>If you haven’t used Blazorade MSAL before, be sure to check out this 
<a href="https://github.com/Blazorade/Blazorade-MSAL/wiki/Getting-Started">introduction section
</a>.
</p>
            
<h2 id="implementing-blazor-authentication-with-jwt-tokens">Implementing Blazor Authentication With JWT Tokens
</h2>
            
<p>Now that you have the sample application configured to run on your local computer, it’s time to have a closer look at it. I’m not going to go through all of the application, but only highlight the main points.
</p>
            
<p>The sample application sends HTTP requests to 
<a href="https://docs.microsoft.com/graph/api/overview">Microsoft Graph
</a>. This application does that by using 
<code>HttpRequestMessage
</code> objects. These object are “pre-authenticated” by the 
<code>BlazoradeRequestFactory
</code> service class implementation. The service class is injected on the 
<a href="https://github.com/MikaBerglund/access-tokens-with-blazorade-msal/blob/main/GraphClientSample/Pages/Index.razor">Index page
</a>, as shown below. I’ve also injected an 
<code>HttpClient
</code> service, because I’ll use that later on when sending the requests.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">&#64;page &quot;/&quot;
</span>
            
<span class="code-line">&#64;inject BlazoradeRequestFactory RequestFactory
</span>
            
<span class="code-line">&#64;inject HttpClient HttpClient
</span>
            
</code></pre>
            
<p>With this, all you have to do now to get a request object is to call the 
<code>RequestFactory
</code> as shown below.
</p>
            
<pre class="code-block" data-language="csharp"><code>
            
<span class="code-line">using(var request = this.RequestFactory.CreateGetRequestAsync(&quot;https://graph.microsoft.com/v1.0/me&quot;, &quot;User.Read&quot;)
</span>
            
<span class="code-line">{
</span>
            
<span class="code-line">    using(var response = await this.HttpClient.SendAsync(request))
</span>
            
<span class="code-line">    {
</span>
            
<span class="code-line">        // Process the response here...
</span>
            
<span class="code-line">    }
</span>
            
<span class="code-line">}
</span>
            
</code></pre>
            
<h3 id="what-happens-behind-the-scenes">What Happens Behind the Scenes?
</h3>
            
<p>This looks pretty simple, right? That’s the added value of Blazorade MSAL! However, there’s a lot going on behind the scenes though. First of all, the 
<code>BlazoradeRequestFactory
</code> uses the 
<code>BlazoradeMsalService
</code> service to acquire an access token for the current user that grants the 
<code>User.Read
</code> permission. This permission is required when calling the 
<code>/me
</code> endpoint in 
<a href="https://docs.microsoft.com/graph/api/overview">Microsoft Graph
</a>.
</p>
            
<p>The BlazoradeMsalService uses the 
<a href="https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser">JavaScript version of MSAL
</a> to acquire an access token. That JavaScript library caches the tokens for you. If a valid token does not exist in the cache, MSAL will take the user to the authentication process. In case the user has not consented to the permissions requested by your application, the login process will show the consent dialog too. If the authentication passes, and the user consents to the requested permissions, MSAL will return the access token to the 
<code>BlazoradeMsalService
</code>.
</p>
            
<p>Now the 
<code>BlazoradeMsalService
</code> hands over the access token to the 
<code>BlazoradeRequestFactory
</code> service. This service then creates an 
<code>HttpRequestMessage
</code> object instance pointing to the URL specified by your application, and adds the access token as bearer token in the authorization header. The request will then be returned to your application. All you now have to do in your application is to use the injected 
<code>HttpClient
</code> service to send away the request and handle the response.
</p>
            
<h2 id="further-reading">Further Reading
</h2>
            
<p>To learn more about Blazorade MSAL, check out my other 
<a href="/tag/blazorade-msal/">Blazorade MSAL articles
</a>.
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
    <item>
      <title>Azure DevOps Project Lifecycle – From Sales to Production</title>
      <description>Did you know that with Azure DevOps, you can start your project already during the sales phase with the help of Boards and Excel integration.</description>
      <link>https://stage.mikaberglund.com/azure-devops-project-lifecycle-from-sales-to-production</link>
      <guid isPermaLink="true">https://stage.mikaberglund.com/azure-devops-project-lifecycle-from-sales-to-production</guid>
      <pubDate>Thu, 10 Feb 2022 00:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
    
<div class="content-page">
        
<article class="content-article">
            
<figure class="article-featured-figure">
                
<img class="article-featured-image" src="@FeaturedImage" title="Azure-DevOps-Project-Lifecycle" alt="Azure DevOps Project Lifecycle" />
                
<figcaption>Azure DevOps Project Lifecycle
</figcaption>

            
<nav class="article-top-navigation" aria-label="Article navigation">
                
<a class="carousel-control-prev article-top-navigation-link" href="/pulumi-tutorial-how-to-store-your-state-in-azure" role="button" aria-label="Previous article: Pulumi Tutorial: How to Store Your State in Azure"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous
</span></a>
                
<a class="carousel-control-next article-top-navigation-link" href="/blazor-authentication-with-jwt-token-using-blazorade-msal" role="button" aria-label="Next article: Blazor Authentication with JWT Token Using Blazorade MSAL"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next
</span></a>
            
</nav>

</figure>
            

            
<h1 id="azure-devops-project-lifecycle-from-sales-to-production">Azure DevOps Project Lifecycle – From Sales to Production
</h1>
            
<p class="article-meta">February 10, 2022
</p>

            
<p>In this article I thought I’d write about something not so technical – Azure DevOps Project Lifecycle. Did you know that 
<a href="https://azure.microsoft.com/services/devops/">Azure DevOps
</a> is not just for developers and architects? You can get a lot of benefit from Azure DevOps already during the sales phase of a new project.
</p>
            
<p>Very often when talking about project lifecycle, not just in Azure DevOps, we tend to focus only on 
<em>Design
</em> – 
<em>Build
</em> – 
<em>Test
</em> – 
<em>Deploy
</em>.
</p>
            
<p>[](https://mikaberglund.com/wp-admin/edit.php?post_type=post)
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#azure-devops-project-lifecycle-from-sales-to-production-image-1" data-bs-toggle="modal" data-bs-target="#azure-devops-project-lifecycle-from-sales-to-production-image-1" aria-label="Open Project-Lifecycle-1024x425"><img class="article-content-image" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/Project-Lifecycle-1024x425.png" alt="Project-Lifecycle-1024x425" /></a></figure><div class="modal fade" id="azure-devops-project-lifecycle-from-sales-to-production-image-1" tabindex="-1" aria-labelledby="azure-devops-project-lifecycle-from-sales-to-production-image-1-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="azure-devops-project-lifecycle-from-sales-to-production-image-1-label">Project-Lifecycle-1024x425
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/Project-Lifecycle-1024x425.png" alt="Project-Lifecycle-1024x425" /></div></div></div></div>
            
<p>But, there’s a lot that happens before you even get to the Design phase. And that is the sales phase. In my experience, companies very often tend to handle the sales phase very much isolated from the actual implementation phases.
</p>
            
<p>But it shouldn’t be that way. And with Azure DevOps it does’t have to be that way either.
</p>
            
<h2 id="introduction">Introduction
</h2>
            
<p>So what’s the problem I’m trying to solve? To put it short, it’s the complete disconnection between work estimates that you give during the sales phase and the features and functionality that you design and build during a project. I’ve seen this so many times in different companies.
</p>
            
<p>Here is a scenario that I believe is quite common. You can probably recognize at least some of these phases.
</p>
            
<ul><li>Request for Proposal
</li><li>A potential customer sends you an RFP for something they want you to build
</li><li>The RFP contains a detailed, prioritized and categorized list of requirements
</li><li>The requirements list is very often an Excel workbook
</li><li>The customer wants you to add your work estimates to the requirements list
</li></ul>
            
<ul><li>Proposal
</li><li>Your sales team produces a proposal
</li><li>The sales team asks a potential implementation team to participate in the work estimation
</li></ul>
            
<ul><li>Adjustments
</li><li>After the initial proposal, you typically iterate over the requirements with the potential customer
</li><li>Often some features are moved to later phases
</li><li>Requirements are reprioritized and recategorized
</li><li>Work estimates can change based on better insights into customer requirements
</li></ul>
            
<ul><li>Implementation
</li><li>In an ideal world the customer would accept your proposal
</li><li>You start the implementation, sometimes with a different team than the one responsible for work estimation
</li><li>During requirements workshops, you add Feature and User Story work items to your backlog
</li><li>The implementation team processes the backlog and adds their work estimates to the work items
</li></ul>
            
<p>At this point, the original requirements list is already quite disconnected from the work that you have planned to start working on. Sometimes the original work estimates that your sales team sold are completely different from what you have planned.
</p>
            
<h2 id="solving-the-problem-with-azure-devops">Solving the Problem With Azure DevOps
</h2>
            
<p>Did you recognize the situation I described above? Do you think that it may be a problem in your organization too? If so, then I suggest you continue reading.
</p>
            
<p>The solution is actually pretty simple. You just create your Azure DevOps project already before starting to work on your proposal. Then you add all the features and requirements you got from the customer as 
<em>Feature
</em> work items, and add your work estimations using the 
<em>Effort
</em> field. If you want, you can also prioritize the features using the 
<em>Business Value
</em> field.
</p>
            
<p>Then use the Sprints in Azure DevOps to associate the features with different phases. You might also want to map your features to 
<em>Epic
</em> work items. This is especially useful if your customer has categorized the features in the RFP with different business initiatives.
</p>
            
<h3 id="connecting-to-azure-devops-from-excel">Connecting to Azure DevOps From Excel
</h3>
            
<p>To open your Features Work Items in Excel, you need to install the 
<a href="https://visualstudio.microsoft.com/downloads/#other-family">Azure DevOps Office Integration
</a> add-on from Microsoft. After that, you need to build a query that returns the Feature work items you want to include in your proposal. The Office Integration add-on adds a 
<strong>*Team
</strong>* tab to the Office ribbon in Excel.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#azure-devops-project-lifecycle-from-sales-to-production-image-2" data-bs-toggle="modal" data-bs-target="#azure-devops-project-lifecycle-from-sales-to-production-image-2" aria-label="Open image"><img class="article-content-image" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/image.png" alt="image" /></a></figure><div class="modal fade" id="azure-devops-project-lifecycle-from-sales-to-production-image-2" tabindex="-1" aria-labelledby="azure-devops-project-lifecycle-from-sales-to-production-image-2-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="azure-devops-project-lifecycle-from-sales-to-production-image-2-label">image
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/image.png" alt="image" /></div></div></div></div>
            
<p>All you have to do now is to open the query you created by using the 
<em>New List
</em> button. By default, the Work Items returned by the query will show up like this.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#azure-devops-project-lifecycle-from-sales-to-production-image-3" data-bs-toggle="modal" data-bs-target="#azure-devops-project-lifecycle-from-sales-to-production-image-3" aria-label="Open image-1-1024x150"><img class="article-content-image" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/image-1-1024x150.png" alt="image-1-1024x150" /></a></figure><div class="modal fade" id="azure-devops-project-lifecycle-from-sales-to-production-image-3" tabindex="-1" aria-labelledby="azure-devops-project-lifecycle-from-sales-to-production-image-3-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="azure-devops-project-lifecycle-from-sales-to-production-image-3-label">image-1-1024x150
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/image-1-1024x150.png" alt="image-1-1024x150" /></div></div></div></div>
            
<p>From here on you just need to apply some Excel magic to make your features and work estimates look something like in the pictures below.
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#azure-devops-project-lifecycle-from-sales-to-production-image-4" data-bs-toggle="modal" data-bs-target="#azure-devops-project-lifecycle-from-sales-to-production-image-4" aria-label="Open Feature-table-2-1024x395"><img class="article-content-image" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/Feature-table-2-1024x395.jpg" alt="Feature-table-2-1024x395" /></a></figure><div class="modal fade" id="azure-devops-project-lifecycle-from-sales-to-production-image-4" tabindex="-1" aria-labelledby="azure-devops-project-lifecycle-from-sales-to-production-image-4-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="azure-devops-project-lifecycle-from-sales-to-production-image-4-label">Feature-table-2-1024x395
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/Feature-table-2-1024x395.jpg" alt="Feature-table-2-1024x395" /></div></div></div></div>
            
<p>Feature table with descriptions
</p>
            
<figure class="article-image-figure"><a class="image-modal-trigger" href="#azure-devops-project-lifecycle-from-sales-to-production-image-5" data-bs-toggle="modal" data-bs-target="#azure-devops-project-lifecycle-from-sales-to-production-image-5" aria-label="Open image-2"><img class="article-content-image" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/image-2.png" alt="image-2" /></a></figure><div class="modal fade" id="azure-devops-project-lifecycle-from-sales-to-production-image-5" tabindex="-1" aria-labelledby="azure-devops-project-lifecycle-from-sales-to-production-image-5-label" aria-hidden="true"><div class="modal-dialog modal-dialog-centered modal-xl"><div class="modal-content"><div class="modal-header"><h2 class="modal-title" id="azure-devops-project-lifecycle-from-sales-to-production-image-5-label">image-2
</h2><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body"><img class="img-fluid" src="/img/posts/azure-devops-project-lifecycle-from-sales-to-production/image-2.png" alt="image-2" /></div></div></div></div>
            
<p>The Feature table converted into a Pivot table
</p>
            
<p>These tables and pivot tables are very similar to what you typically have in proposals. In case you need to iterate over the phases, prioritization and even your work estimates, you do that work in Azure DevOps. When you are done, you just hit the Refresh button in Excel, and you have an updated version for your customer right away.
</p>
            
<p>Compared to disconnected tables in a Word document, these present a huge advantage for you. They contain actual Azure DevOps Work Items that your development team will start working one once your proposal is approved. And that means that you have a better chance of delivering your project within the given work estimates. And that will result in happier customers and better profit. Wouldn’t you agree?
</p>
        
</article>
    
</div>

]]></content:encoded>
    </item>
  </channel>
</rss>