<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jsakamoto</title>
    <description>The latest articles on DEV Community by jsakamoto (@j_sakamoto).</description>
    <link>https://dev.to/j_sakamoto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50463%2F3cd9e955-f2da-40c3-848e-1e6adc988084.png</url>
      <title>DEV Community: jsakamoto</title>
      <link>https://dev.to/j_sakamoto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/j_sakamoto"/>
    <language>en</language>
    <item>
      <title>The Missing Third Config Layer: Adding User Secrets to Blazor WebAssembly</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Wed, 04 Mar 2026 13:04:20 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/the-missing-third-config-layer-adding-user-secrets-to-blazor-webassembly-2a5a</link>
      <guid>https://dev.to/j_sakamoto/the-missing-third-config-layer-adding-user-secrets-to-blazor-webassembly-2a5a</guid>
      <description>&lt;h2&gt;
  
  
  🤔 The Problem with Blazor WebAssembly
&lt;/h2&gt;

&lt;p&gt;A while back, I wrote a Japanese article about how to manage API keys for error monitoring services like Raygun and Sentry in a Blazor WebAssembly app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://qiita.com/jsakamoto/items/e72bbf0681994307d07c" rel="noopener noreferrer"&gt;https://qiita.com/jsakamoto/items/e72bbf0681994307d07c&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In that article, I touched on how to handle configuration values during development. Here is one passage I wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I think it would be a good idea to exclude &lt;code&gt;appsettings.Development.json&lt;/code&gt; from version control using a &lt;code&gt;.gitignore&lt;/code&gt; file, but I will skip the details in this article. If you were using a project other than Blazor WebAssembly, User Secrets would be a convenient and reasonable option. Unfortunately, Blazor WebAssembly does not support User Secrets, so using &lt;code&gt;appsettings.Development.json&lt;/code&gt; seemed like the safest approach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, &lt;strong&gt;Blazor WebAssembly does not support User Secrets&lt;/strong&gt;. And this turns out to be more of a problem than it first appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 The Three-Layer Configuration Problem
&lt;/h3&gt;

&lt;p&gt;In .NET apps, I think most developers want to manage configuration values in three layers, where each layer overrides the one before it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Release configuration&lt;/strong&gt;: values for the production environment. Committed to source control and shared with everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared development configuration&lt;/strong&gt;: values for the development environment, shared with all team members. Also committed to source control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal developer configuration&lt;/strong&gt;: values specific to each developer's local environment. Not committed to source control.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In Blazor WebAssembly, &lt;code&gt;appsettings.json&lt;/code&gt; (the first layer) and &lt;code&gt;appsettings.Development.json&lt;/code&gt; (the second layer) cover the first two cases nicely.&lt;/p&gt;

&lt;p&gt;The problem is the &lt;strong&gt;third layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In that earlier article, I mentioned excluding &lt;code&gt;appsettings.Development.json&lt;/code&gt; from source control using &lt;code&gt;.gitignore&lt;/code&gt;. But if you think about it, doing that removes the place where you would normally store shared development configuration (the second layer). Trying to keep both "shared team settings" and "personal settings" in a single file, and then excluding that file from source control, is a dead end.&lt;/p&gt;

&lt;p&gt;In a regular ASP.NET Core project, User Secrets (via the &lt;code&gt;dotnet user-secrets set&lt;/code&gt; command) can serve as this third layer. It lets each developer save personal settings locally without committing them to the repository. But since Blazor WebAssembly does not run on the server side, this mechanism simply was not available.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 I Thought, "Why Not Build It Myself?"
&lt;/h2&gt;

&lt;p&gt;While writing that article, I had a sudden idea.&lt;/p&gt;

&lt;p&gt;"What if I intercepted requests to the dev server and merged the User Secrets values into the response for &lt;code&gt;appsettings.*.json&lt;/code&gt; files?"&lt;/p&gt;

&lt;p&gt;But when I thought about how to implement that, it felt a bit wasteful to build just a dedicated tool for merging User Secrets. If I could build a general-purpose mechanism for extending the dev server, it could be useful for all kinds of purposes in the future too.&lt;/p&gt;

&lt;p&gt;So I decided to take a two-part approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extensible Dev Server&lt;/strong&gt;: a Blazor WebAssembly dev server that can be extended with custom middleware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Secrets extension&lt;/strong&gt;: an extension that runs on the Extensible Dev Server and merges User Secrets values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;Extensible Dev Server&lt;/strong&gt; is designed as a drop-in replacement for the standard Blazor WebAssembly dev server NuGet package (&lt;code&gt;Microsoft.AspNetCore.Components.WebAssembly.DevServer&lt;/code&gt;). The mechanism for plugging in custom middleware is built on top of the ASP.NET Core &lt;strong&gt;Hosting Startup&lt;/strong&gt; feature. Hosting Startup lets you inject middleware from a separately built assembly into an existing ASP.NET Core app, without touching the app itself. I wrote a separate article on dev.to explaining this feature in more detail:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/j_sakamoto/extending-an-aspnet-core-app-with-external-middleware-using-hosting-startup-345b" class="crayons-story__hidden-navigation-link"&gt;Extending an ASP.NET Core App with External Middleware Using "Hosting Startup"&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/j_sakamoto" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50463%2F3cd9e955-f2da-40c3-848e-1e6adc988084.png" alt="j_sakamoto profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/j_sakamoto" class="crayons-story__secondary fw-medium m:hidden"&gt;
              jsakamoto
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                jsakamoto
                
              
              &lt;div id="story-author-preview-content-3255756" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/j_sakamoto" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50463%2F3cd9e955-f2da-40c3-848e-1e6adc988084.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;jsakamoto&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/j_sakamoto/extending-an-aspnet-core-app-with-external-middleware-using-hosting-startup-345b" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/j_sakamoto/extending-an-aspnet-core-app-with-external-middleware-using-hosting-startup-345b" id="article-link-3255756"&gt;
          Extending an ASP.NET Core App with External Middleware Using "Hosting Startup"
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aspnetcore"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aspnetcore&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/j_sakamoto/extending-an-aspnet-core-app-with-external-middleware-using-hosting-startup-345b#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




&lt;p&gt;I then implemented User Secrets merging as the first extension for the Extensible Dev Server and released both as NuGet packages.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Blazor WebAssembly Extensible Dev Server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jsakamoto/Toolbelt.Blazor.WebAssembly.ExtensibleDevServer" rel="noopener noreferrer"&gt;https://github.com/jsakamoto/Toolbelt.Blazor.WebAssembly.ExtensibleDevServer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Secrets Extension for Blazor WebAssembly Extensible Dev Server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jsakamoto/Toolbelt.Blazor.WebAssembly.ExtensibleDevServer.UserSecretsExtension" rel="noopener noreferrer"&gt;https://github.com/jsakamoto/Toolbelt.Blazor.WebAssembly.ExtensibleDevServer.UserSecretsExtension&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ How to Use It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🛠️ 1. Add the Packages
&lt;/h3&gt;

&lt;p&gt;Add the following two packages to your Blazor WebAssembly project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Toolbelt.Blazor.WebAssembly.ExtensibleDevServer
dotnet add package Toolbelt.Blazor.WebAssembly.ExtensibleDevServer.UserSecretsExtension
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You no longer need the existing &lt;code&gt;Microsoft.AspNetCore.Components.WebAssembly.DevServer&lt;/code&gt; package, so remove it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet remove package Microsoft.AspNetCore.Components.WebAssembly.DevServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🛠️ 2. Set Up User Secrets
&lt;/h3&gt;

&lt;p&gt;After that, set up User Secrets the same way you would in any other .NET project.&lt;/p&gt;

&lt;p&gt;In Visual Studio, right-click the project and select "Manage User Secrets".&lt;/p&gt;

&lt;p&gt;From the CLI, run these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet user-secrets init
dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"SomeSection:SomeKey"&lt;/span&gt; &lt;span class="s2"&gt;"my-custom-value"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🚀 3. Run the App
&lt;/h3&gt;

&lt;p&gt;Then just run the app as usual with &lt;code&gt;dotnet run&lt;/code&gt; or through Visual Studio. No extra code is needed, and there is nothing else to configure.&lt;/p&gt;

&lt;p&gt;Your app's configuration will automatically include the values from User Secrets, merged in. It almost feels too easy 😅&lt;/p&gt;




&lt;h2&gt;
  
  
  🔰 How It Works
&lt;/h2&gt;

&lt;p&gt;The Extensible Dev Server intercepts HTTP GET requests for &lt;code&gt;appsettings.*.json&lt;/code&gt; files. The User Secrets extension handles those requests and does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the original &lt;code&gt;appsettings.*.json&lt;/code&gt; from the &lt;code&gt;wwwroot&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Read the User Secrets configured for the project&lt;/li&gt;
&lt;li&gt;Merge them together and return the result as the HTTP response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The files on disk are never modified. Only the HTTP response content is changed.&lt;/p&gt;

&lt;p&gt;The configuration priority order looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;appsettings.json (lowest priority)
  overridden by
appsettings.Development.json
  overridden by
User Secrets (highest priority)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ User Secrets Values Are Not Actually Secret
&lt;/h2&gt;

&lt;p&gt;Before using this package, please understand one important point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When developing a Blazor WebAssembly app with this package applied, values stored in User Secrets are returned as plain text in the HTTP response to the browser. In other words, they are not actually secret.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is probably why Microsoft has not added official support for User Secrets in Blazor WebAssembly. In a regular server-side .NET app, User Secrets stores values in a safe location separate from the compiled code. Applying that same idea to Blazor WebAssembly would cause confusion: the values would be fully visible in the browser despite the word "Secrets" in the name. I imagine Microsoft decided that supporting such misleading behavior was not the right call.&lt;/p&gt;

&lt;p&gt;This package intentionally works around that limitation. It does not treat User Secrets as a vault for sensitive information. Instead, it &lt;strong&gt;borrows the User Secrets storage mechanism purely as a third configuration layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So please do not store passwords, authentication tokens, or any other information that must not be exposed. This package is intended for configuration values that are safe to be public, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local dev server URLs&lt;/li&gt;
&lt;li&gt;Flags to enable personal or debug features&lt;/li&gt;
&lt;li&gt;API keys for error monitoring services like Raygun and Sentry (as discussed in the original article)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎉 Summary
&lt;/h2&gt;

&lt;p&gt;With this package, you can now manage personal developer configuration in Blazor WebAssembly using &lt;code&gt;dotnet user-secrets&lt;/code&gt;, just like in any other .NET project.&lt;/p&gt;

&lt;p&gt;For example, API keys for error monitoring services like Raygun and Sentry (the very problem from the original article) can now be stored in each developer's own User Secrets. You can keep shared team settings in &lt;code&gt;appsettings.Development.json&lt;/code&gt; and separate each person's personal settings into User Secrets. This cleanly solves the problem of having "settings you want to share" and "settings you do not want to commit" living in the same file.&lt;/p&gt;

&lt;p&gt;Just add the packages and you are ready to go. No extra code needed.&lt;/p&gt;

&lt;p&gt;If you try it out, or if you have any questions or feedback, feel free to share them in the comments! 👇&lt;/p&gt;

&lt;p&gt;❤️ Happy coding!&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>webdev</category>
      <category>aspnet</category>
    </item>
    <item>
      <title>Your Component Catalog Already Knows Everything. So Why Not Make It an MCP Server?</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Wed, 25 Feb 2026 12:03:26 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/your-component-catalog-already-knows-everything-so-why-not-make-it-an-mcp-server-486f</link>
      <guid>https://dev.to/j_sakamoto/your-component-catalog-already-knows-everything-so-why-not-make-it-an-mcp-server-486f</guid>
      <description>&lt;h2&gt;
  
  
  🔰 Introduction
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A quick note before we dive in: the implementation described in this article was carried out in the spring of 2025, nearly a year before I sat down to write about it. The AI ecosystem moves at a breathtaking pace, so some specifics may feel a bit dated by now. That said, I believe the core ideas and approach still hold value, and I hope this article proves useful to at least some of you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I recently published the following article, which introduces a project I've been working on since March 2023 called &lt;strong&gt;"Blazing Story"&lt;/strong&gt;, a Storybook clone fully reimplemented in Blazor (licensed under Mozilla Public License 2.0).&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/j_sakamoto/storybook-doesnt-support-blazor-so-i-reimplemented-it-in-c-411p" class="crayons-story__hidden-navigation-link"&gt;Storybook Doesn't Support Blazor, So I Reimplemented It in C#&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/j_sakamoto" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50463%2F3cd9e955-f2da-40c3-848e-1e6adc988084.png" alt="j_sakamoto profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/j_sakamoto" class="crayons-story__secondary fw-medium m:hidden"&gt;
              jsakamoto
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                jsakamoto
                
              
              &lt;div id="story-author-preview-content-3278105" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/j_sakamoto" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F50463%2F3cd9e955-f2da-40c3-848e-1e6adc988084.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;jsakamoto&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/j_sakamoto/storybook-doesnt-support-blazor-so-i-reimplemented-it-in-c-411p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 23&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/j_sakamoto/storybook-doesnt-support-blazor-so-i-reimplemented-it-in-c-411p" id="article-link-3278105"&gt;
          Storybook Doesn't Support Blazor, So I Reimplemented It in C#
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/storybook"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;storybook&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/blazor"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;blazor&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/j_sakamoto/storybook-doesnt-support-blazor-so-i-reimplemented-it-in-c-411p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




&lt;p&gt;This article is a follow-up to that one. Here, I'll walk you through how I added an &lt;strong&gt;MCP server&lt;/strong&gt; feature to Blazing Story.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For an introduction to Storybook, Blazor, and Blazing Story itself, please refer to the article linked above.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🤔 What is MCP?
&lt;/h2&gt;

&lt;p&gt;If you're reading this, you're probably already familiar with MCP. But just in case, here's a quick overview.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP&lt;/strong&gt; stands for &lt;strong&gt;Model Context Protocol&lt;/strong&gt;. It is a standard interface that allows generative AI to interact with the outside world. In a typical setup, the generative AI acts as the MCP "client," while the systems that provide information to the AI (or receive instructions from it) are called MCP "servers." When both sides follow the MCP specification, they can communicate with each other seamlessly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83pgrw4pob6x0x0wfnff.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83pgrw4pob6x0x0wfnff.PNG" alt="MCP architecture overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before MCP came along, connecting generative AI to external systems was still possible, but every AI platform had its own proprietary way of doing it. When MCP emerged as a shared standard, a flood of MCP servers were released in a short time, and AI platforms quickly adopted support for it. Working with generative AI in real-world applications became dramatically easier as a result.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 The Idea: Exposing an In-House Design System via MCP
&lt;/h2&gt;

&lt;p&gt;In the spring of 2025, the following article made quite a splash in the Japanese developer community:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://zenn.dev/ubie_dev/articles/f927aaff02d618" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fzenn%2Fimage%2Fupload%2Fs--_1Klui_T--%2Fc_fit%252Cg_north_west%252Cl_text%3Anotosansjp-medium.otf_55%3A%2525E7%2525A4%2525BE%2525E5%252586%252585%2525E3%252583%252587%2525E3%252582%2525B6%2525E3%252582%2525A4%2525E3%252583%2525B3%2525E3%252582%2525B7%2525E3%252582%2525B9%2525E3%252583%252586%2525E3%252583%2525A0%2525E3%252582%252592MCP%2525E3%252582%2525B5%2525E3%252583%2525BC%2525E3%252583%252590%2525E3%252583%2525BC%2525E5%25258C%252596%2525E3%252581%252597%2525E3%252581%25259F%2525E3%252582%252589UI%2525E5%2525AE%25259F%2525E8%2525A3%252585%2525E3%252581%25258C%2525E7%252588%252586%2525E9%252580%25259F%2525E3%252581%2525AB%2525E3%252581%2525AA%2525E3%252581%2525A3%2525E3%252581%25259F%252Cw_1010%252Cx_90%252Cy_100%2Fg_south_west%252Cl_text%3Anotosansjp-medium.otf_34%3A%2525E3%252581%252590%2525E3%252582%25258A%2525E3%252581%252593%252Cx_220%252Cy_108%2Fbo_3px_solid_rgb%3Ad6e3ed%252Cg_south_west%252Ch_90%252Cl_fetch%3AaHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL3plbm4tdXNlci11cGxvYWQvYXZhdGFyL2VlOWMzMWRhODMuanBlZw%3D%3D%252Cr_20%252Cw_90%252Cx_92%252Cy_102%2Fco_rgb%3A6e7b85%252Cg_south_west%252Cl_text%3Anotosansjp-medium.otf_30%3AUbie%252520%2525E3%252583%252586%2525E3%252583%252583%2525E3%252582%2525AF%2525E3%252583%252596%2525E3%252583%2525AD%2525E3%252582%2525B0%252Cx_220%252Cy_160%2Fbo_4px_solid_white%252Cg_south_west%252Ch_50%252Cl_fetch%3AaHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL3plbm4tdXNlci11cGxvYWQvYXZhdGFyLzMzYWVmZGQyNDIuanBlZw%3D%3D%252Cr_max%252Cw_50%252Cx_139%252Cy_84%2Fv1627283836%2Fdefault%2Fog-base-w1200-v2.png%3F_a%3DBACAGSGT" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://zenn.dev/ubie_dev/articles/f927aaff02d618" rel="noopener noreferrer" class="c-link"&gt;
            社内デザインシステムをMCPサーバー化したらUI実装が爆速になった
          &lt;/a&gt;
        &lt;/h2&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstatic.zenn.studio%2Fimages%2Flogo-transparent.png"&gt;
          zenn.dev
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;&lt;em&gt;(This article is written in Japanese.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It describes how a team built an MCP server that scans the source code of their in-house UI components (written in JavaScript/TypeScript) and returns information about them. By connecting this MCP server to a generative AI, when they asked the AI to generate UI through a text prompt, the AI correctly used those in-house components instead of making things up.&lt;/p&gt;

&lt;p&gt;This addresses a real pain point. Without such a setup, when you ask generative AI to write UI code, it tends to build everything from scratch using something like Tailwind CSS directly, creating its own ad-hoc components. Having an MCP server that informs the AI about your existing UI component library is a genuine game-changer for AI-assisted UI development.&lt;/p&gt;

&lt;p&gt;But do you always have to build this kind of MCP server from scratch? Is there a way to reuse something you already have?&lt;/p&gt;




&lt;h2&gt;
  
  
  🤩 Wait, Doesn't Storybook Already Know Everything?
&lt;/h2&gt;

&lt;p&gt;That's when an idea hit me.&lt;/p&gt;

&lt;p&gt;In many frontend development teams, &lt;strong&gt;Storybook is already in use&lt;/strong&gt;, and Storybook actually &lt;strong&gt;"knows a lot"&lt;/strong&gt; about the components in a project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What components exist, with usage examples (from story definitions)&lt;/li&gt;
&lt;li&gt;The purpose and overview of each component (from JSDoc)&lt;/li&gt;
&lt;li&gt;Real markup examples (from story definitions)&lt;/li&gt;
&lt;li&gt;What parameters a component accepts, including their types and descriptions (from type definitions and JSDoc)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho0m2ti2whkbx2v74x5p.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho0m2ti2whkbx2v74x5p.PNG" alt="What Storybook knows about components"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the idea was simple: &lt;strong&gt;What if Storybook could talk to the AI through MCP?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Adding an MCP Server Feature to Blazing Story
&lt;/h2&gt;

&lt;p&gt;Personally, I work more with Blazor (C#/.NET) than with JavaScript/TypeScript-based frontend frameworks. Blazing Story is a Blazor-based alternative to Storybook, and I develop and maintain it myself.&lt;/p&gt;

&lt;p&gt;Since I know the internal structure of Blazing Story well (and can actually make changes to it), I decided to implement this idea for real.&lt;/p&gt;

&lt;p&gt;Fortunately, a C#/.NET SDK for building MCP servers was already available. Using this SDK, adding MCP server functionality to Blazing Story turned out to be surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F246amqhe7jy1kq039f1h.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F246amqhe7jy1kq039f1h.PNG" alt="Blazing Story with MCP server"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Trying It Out
&lt;/h2&gt;

&lt;p&gt;With the MCP server feature in place, I tried asking a generative AI to generate UI code, just to see what it would come up with.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛠️ Setting Up the Environment
&lt;/h3&gt;

&lt;p&gt;This experiment was done around July 2025, so the tooling is a bit dated, but here's what I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Studio Code&lt;/strong&gt; with &lt;strong&gt;GitHub Copilot&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Sonnet 4&lt;/strong&gt; as the AI model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET SDK&lt;/strong&gt; for the Blazor project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ubjctov81pajc873p0m.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ubjctov81pajc873p0m.PNG" alt="Development environment setup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the MCP integration, I configured two things in advance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;code&gt;.vscode/mcp.json&lt;/code&gt;, I specified the HTTP Streaming API endpoint of Blazing Story's MCP server&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;, I added a custom instruction: &lt;em&gt;"When generating UI code, query the Blazing Story MCP server and prioritize using the available components"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn2jt596yagg5zpo3und.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn2jt596yagg5zpo3und.PNG" alt="MCP configuration in VSCode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ✍️ First Try: Generating UI from a Text Prompt
&lt;/h3&gt;

&lt;p&gt;As a first step, I typed the following prompt into the GitHub Copilot chat in VSCode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please implement a checkout form for an e-commerce site inside the currently open Checkout.razor file.

- Add a "Checkout" title at the very top of the page.
- Below that, place a stepper component showing the following 3 steps.
  - Information
  - Confirmation
  - Complete
- For now, focus on building the "Information" form.
- The form should include the following sections and fields.
  - Shipping Information
  - Full Name
  - Postal Code
  - Address
  - Phone Number
  - Email Address
  - Shipping Options
  - Preferred Delivery Date
  - Preferred Delivery Time (choose from Morning, Afternoon, or Evening)
  - Payment Method (choose from Cash on Delivery, Credit Card, or PayPay)
  - A "Next" button
- Use a select component for the Preferred Delivery Time field.
- Use a vertically stacked radio group component for the Payment Method field.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0pj5z2eni5qd5tg3a2m.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0pj5z2eni5qd5tg3a2m.PNG" alt="Copilot chat prompt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result was honestly surprising. The AI generated exactly the Razor component UI code I was looking for. &lt;strong&gt;It used the components already registered in Blazing Story&lt;/strong&gt; rather than making up its own. And because it understood what parameters those components accept, it produced &lt;strong&gt;perfectly working code with no build errors on the very first prompt&lt;/strong&gt;. 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytcj7gllutqg0xyubn3y.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytcj7gllutqg0xyubn3y.PNG" alt="AI-generated UI result"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  🖼️ Second Try: Generating UI from a Sketch Image
&lt;/h3&gt;

&lt;p&gt;Next, I tried generating UI from an image. Instead of using Figma, I drew a rough "hand-drawn style" sketch of the desired layout in PowerPoint, exported it as an image, and submitted it to the same Copilot chat setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7e4duyg5n9p30u8d4f5w.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7e4duyg5n9p30u8d4f5w.PNG" alt="Hand-drawn sketch of the layout"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This also worked without any trouble. Just like with the text prompt, the AI generated Razor component source code based on the layout shown in the sketch, using the component info provided by Blazing Story's MCP server. &lt;strong&gt;It worked so well that it felt almost too easy.&lt;/strong&gt; 😅&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivy61c869gzbtmh5qpmb.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivy61c869gzbtmh5qpmb.PNG" alt="UI generated from the sketch"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also watch a video of this experiment here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blazingstory.github.io/docs/mcp-server-feature?_highlight=mcp#summary" rel="noopener noreferrer"&gt;https://blazingstory.github.io/docs/mcp-server-feature?_highlight=mcp#summary&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  📝 What We Learned
&lt;/h2&gt;

&lt;p&gt;Those results revealed something important.&lt;/p&gt;

&lt;p&gt;By feeding UI component information to the AI through an MCP server, the AI generated highly accurate UI source code from a single prompt. It used the existing components rather than inventing its own. And it avoided the hallucinations that generative AI is often prone to, like confidently setting parameters that sound plausible but simply don't exist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18ashd9nql9oopyo5dfq.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F18ashd9nql9oopyo5dfq.PNG" alt="MCP server value overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also noticed something interesting on the software engineering side. &lt;strong&gt;The principles that have always mattered in human team development still matter just as much when working with generative AI:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write documentation carefully (JSDoc or XML doc comments in C#/Blazor), because the AI reads them too&lt;/li&gt;
&lt;li&gt;Use clear, meaningful names for your components and parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If these are neglected, the AI produces incorrect or confusing code for the exact same reasons human developers would struggle. Good practices remain good practices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8xt808dfy3td027a3g3.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8xt808dfy3td027a3g3.PNG" alt="Code quality and AI output"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🤷 But Isn't Writing Stories a Lot of Work?
&lt;/h2&gt;

&lt;p&gt;Fair question. Building the MCP server described above still requires humans to write stories one by one. If your only goal is to expose UI component info to the AI, it might actually be more efficient to implement a lightweight MCP server that scans component source code directly.&lt;/p&gt;

&lt;p&gt;But here's the thing: in practice, frontend teams often need Blazing Story (or Storybook) &lt;strong&gt;anyway&lt;/strong&gt;, for component-level E2E testing, visual regression testing, and as a reference resource for developers learning the codebase. Since you're already building stories for those reasons, it makes total sense to let that existing setup serve as the MCP server too. You get the best of both worlds without extra overhead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsl1xmxu1k9hfbz0wrryx.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsl1xmxu1k9hfbz0wrryx.PNG" alt="Stories as a dual-purpose investment"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🔍 What About the Official Storybook?
&lt;/h2&gt;

&lt;p&gt;When I first looked into this in July 2025, the official Storybook did not yet have this kind of MCP integration. Some community-made add-ons existed, but they were focused on helping AI &lt;em&gt;write stories&lt;/em&gt;, not on feeding component information to AI for UI generation. The Storybook GitHub repo had active discussions about the need for something like this, but nothing official had landed yet.&lt;/p&gt;

&lt;p&gt;Since then, the Storybook team has moved in exactly this direction. Starting in late 2025, they released official MCP server packages (a standalone library and a Storybook addon) that expose component metadata to AI agents so they can generate UI code using the right components and props. By late February 2026, the project had seen 40+ releases and continues to be actively developed.&lt;/p&gt;

&lt;p&gt;It's encouraging to see the same core idea validated by the broader ecosystem. The Blazor/.NET world that Blazing Story serves is a different territory from the JavaScript-centric official Storybook, but the underlying principle of giving AI structured knowledge about your components is clearly the right direction regardless of the tech stack.&lt;/p&gt;


&lt;h2&gt;
  
  
  🎉 Conclusion: It Works at a Practical Level
&lt;/h2&gt;

&lt;p&gt;To sum up: I added an MCP server feature to Blazing Story, used it to let generative AI generate Blazor UI code, and the results were genuinely practical and useful.&lt;/p&gt;

&lt;p&gt;Even in the age of AI, UI component catalogs like Blazing Story continue to play an important role in development workflows, and now they can directly power your AI-assisted development as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ume4rpkm67qqsoaxro9.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ume4rpkm67qqsoaxro9.PNG" alt="Blazing Story with MCP in action"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're developing Blazor applications, I strongly encourage you to consider adopting Blazing Story along with its MCP server feature. It can make a real difference in your day-to-day workflow.&lt;/p&gt;

&lt;p&gt;For step-by-step instructions on setting up the MCP server feature in your own Blazing Story project, check out the official documentation:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://blazingstory.github.io/docs/mcp-server-feature/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblazingstory.github.io%2Fdocs%2Fimg%2Fsocial-preview.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://blazingstory.github.io/docs/mcp-server-feature/" rel="noopener noreferrer" class="c-link"&gt;
            MCP Server Feature | Blazing Story
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Summary
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblazingstory.github.io%2Fdocs%2Fimg%2Ffavicon.ico"&gt;
          blazingstory.github.io
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;I hope this article was helpful, and I hope you enjoy building Blazor apps even more. ❤️ Happy coding!&lt;/p&gt;

</description>
      <category>storybook</category>
      <category>blazor</category>
      <category>webdev</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Storybook Doesn't Support Blazor, So I Reimplemented It in C#</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Mon, 23 Feb 2026 13:48:47 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/storybook-doesnt-support-blazor-so-i-reimplemented-it-in-c-411p</link>
      <guid>https://dev.to/j_sakamoto/storybook-doesnt-support-blazor-so-i-reimplemented-it-in-c-411p</guid>
      <description>&lt;p&gt;If you're building web apps with Blazor, you've probably wished for something like Storybook, a UI component catalog for browsing and developing components in isolation. The problem is, Storybook doesn't support Blazor. So I built one from scratch.&lt;/p&gt;

&lt;p&gt;I'm the developer of &lt;strong&gt;"Blazing Story"&lt;/strong&gt; (Mozilla Public License 2.0), a Storybook clone reimplemented entirely in Blazor. I first released it in March 2023, presented it at local conference events, but never wrote about it as a technical article until now. This is the story of how and why I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔍 What Is Storybook?
&lt;/h2&gt;

&lt;p&gt;For component-based front-end frameworks like Angular, React, Vue, and Svelte, there's an open source library called &lt;strong&gt;"Storybook"&lt;/strong&gt; (MIT License).&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://storybook.js.org/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorybook.js.org%2Fopengraph-image.jpg%3F841db310ba5a3a1e" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://storybook.js.org/" rel="noopener noreferrer" class="c-link"&gt;
            Storybook: Frontend workshop for UI development
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Storybook is a frontend workshop for building UI components and pages in isolation. Thousands of teams use it for UI development, testing, and documentation. It's open source and free.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fstorybook.js.org%2Ficon.svg"&gt;
          storybook.js.org
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;Storybook provides what's commonly called a "UI component catalog," which is a web UI that works alongside your existing front-end project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxg64aki2nav0coo6e53.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flxg64aki2nav0coo6e53.PNG" alt="Screenshot of the Storybook website showing the UI component catalog web interface"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each component, you write a small JavaScript (TypeScript) file called a "story" that describes how the component should be rendered. Storybook then displays all those stories together in its web UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8daueuhtpvegpsp2xfe.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8daueuhtpvegpsp2xfe.PNG" alt="Screenshot of Storybook UI displaying multiple component stories listed in the sidebar"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Storybook UI, you can see how to use each component, inspect its markup, and explore what parameters it accepts, all without touching the main application. You can change parameters on the fly and immediately see how the component looks. This makes it easy for everyone on the team to share knowledge about which components are available and how to use them, helping the whole team move faster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbjqc72qwhuteer7k14w.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbjqc72qwhuteer7k14w.PNG" alt="Screenshot of Storybook's controls panel allowing interactive editing of component parameters in real time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Storybook also works as a development workbench for individual components. Instead of developing inside a full application, you can focus on a single component in isolation. This makes it an excellent platform for end-to-end testing per component, and a natural target for screenshot-based visual regression testing (VRT). In fact, Chromatic (the company behind Storybook) offers a SaaS product that lets you deploy your Storybook project for team collaboration and automated visual regression tests on every deployment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86jlpxo5gmrsqo7givwz.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86jlpxo5gmrsqo7givwz.PNG" alt="Screenshot of Chromatic's SaaS dashboard for automated visual regression testing integrated with Storybook"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, Storybook has become an essential part of modern component-based front-end development.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ What Is Blazor?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Blazor"&lt;/strong&gt; is an open source framework by Microsoft for building various web applications, including SPAs, using &lt;strong&gt;C#&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdotnet.microsoft.com%2Fblob-assets%2Fimages%2Fdotnet-icons%2Fsquare.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor" rel="noopener noreferrer" class="c-link"&gt;
            Blazor | Build client web apps with C# | .NET
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Blazor is a feature of ASP.NET for building interactive web UIs using C# instead of JavaScript. It's real .NET running in the browser on WebAssembly.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
          dotnet.microsoft.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;Blazor supports several hosting models. Blazor WebAssembly, for example, compiles your C# code to MSIL binary and runs it directly in the browser using a CLR reimplemented on top of WebAssembly. In other words, &lt;strong&gt;.NET assemblies (.dll files) run right inside your browser.&lt;/strong&gt; That's how Blazor WebAssembly delivers single-page applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4kka889kilum1vidn22.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4kka889kilum1vidn22.PNG" alt="Diagram showing Blazor WebAssembly architecture where .NET assemblies run inside the browser via a CLR built on WebAssembly"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Like other modern frameworks, Blazor is component-based. You write components in &lt;code&gt;.razor&lt;/code&gt; files, mixing HTML with C# code. Think of it as the C# equivalent of JSX (or TSX).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw2zv58njzy8vmc3r0bij.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw2zv58njzy8vmc3r0bij.PNG" alt="Code snippet of a Blazor .razor file mixing HTML markup with C# code, similar to JSX syntax"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a real-world Blazor application, you may occasionally need to call JavaScript code, but for the most part everything is implemented in C#. And since Blazor doesn't transpile C# to JavaScript but instead uses .NET assemblies at the binary level, the development ecosystem is exactly the same as any other .NET project. You don't need to know anything about JavaScript bundlers. Blazor lets you build rich, interactive web applications entirely in C#.&lt;/p&gt;

&lt;h2&gt;
  
  
  😔 The Problem: No Storybook for Blazor
&lt;/h2&gt;

&lt;p&gt;Since Blazor is a component-based framework, it's natural to want the same kind of UI component catalog that Storybook provides.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storybook Doesn't Support Blazor
&lt;/h3&gt;

&lt;p&gt;Unfortunately, &lt;strong&gt;Storybook doesn't support Blazor&lt;/strong&gt;, and it's easy to understand why.&lt;/p&gt;

&lt;p&gt;Blazor brings a full .NET runtime into the browser, which is a completely different world from what Storybook expects. A Blazor build outputs .NET assembly files containing MSIL binary code. Storybook, on the other hand, expects importable JavaScript modules. The two simply can't work together.&lt;/p&gt;

&lt;h3&gt;
  
  
  There's a Workaround, But...
&lt;/h3&gt;

&lt;p&gt;Blazor does allow you to instantiate a component from the JavaScript side and mount it to a DOM element. You can even wrap it as a Web Components custom element for use in other JS frameworks. So technically, it might be possible to use Blazor components inside Storybook.&lt;/p&gt;

&lt;p&gt;But would Blazor developers actually want that?&lt;/p&gt;

&lt;p&gt;You chose Blazor specifically to build web apps in C#. Having to learn the JavaScript ecosystem just to use Storybook, and worse, having to write stories in JavaScript or TypeScript, defeats a big part of the purpose. It's a significant burden.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6wj5wstznrafvgzs3nis.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6wj5wstznrafvgzs3nis.PNG" alt="Diagram illustrating the friction of forcing Blazor (C#) developers to write Storybook stories in JavaScript or TypeScript"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what else can we do?&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 What If We Reimplemented Storybook in Blazor?
&lt;/h2&gt;

&lt;p&gt;That's the conclusion I eventually reached.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing Blazing Story
&lt;/h3&gt;

&lt;p&gt;So I went ahead and did it. I started in December 2022, and about three months later, in March 2023, I released the first version: &lt;strong&gt;"Blazing Story"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ibg2o0fnkks3vv4v6im.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ibg2o0fnkks3vv4v6im.PNG" alt="Screenshot of Blazing Story's main UI, a Storybook-equivalent UI component catalog implemented entirely in Blazor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://blazingstory.github.io/docs/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblazingstory.github.io%2Fdocs%2Fimg%2Fsocial-preview.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://blazingstory.github.io/docs/" rel="noopener noreferrer" class="c-link"&gt;
            Overview | Blazing Story
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            The clone of "Storybook" for Blazor, a frontend workshop for building UI components and pages in isolation.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblazingstory.github.io%2Fdocs%2Fimg%2Ffavicon.ico"&gt;
          blazingstory.github.io
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;With Blazing Story, you get a full UI component catalog experience for Blazor development, just like Storybook.&lt;/p&gt;

&lt;p&gt;A live demo is available on GitHub Pages. Feel free to open it and see it in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jsakamoto.github.io/BlazingStory/?path=/custom/configure-your-project" rel="noopener noreferrer"&gt;https://jsakamoto.github.io/BlazingStory/?path=/custom/configure-your-project&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Goal: Match Storybook Pixel by Pixel
&lt;/h3&gt;

&lt;p&gt;Blazing Story is designed to look exactly like Storybook visually. The key thing is: &lt;strong&gt;I basically didn't look at Storybook's source code.&lt;/strong&gt; I did reuse Storybook's icon images (SVG), which are available under the MIT License, but everything else I built by comparing pixels in the browser by eye, writing my own DOM structure and CSS to match.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fti0tlxb1m13xzho7x3kb.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fti0tlxb1m13xzho7x3kb.PNG" alt="Side-by-side pixel comparison of Blazing Story and the original Storybook, showing near-identical visual appearance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a result, Blazing Story's screen looks nearly identical to Storybook's at the pixel level, but the underlying DOM structure is completely different. I used color picker tools and browser dev tools to match colors and fonts, and URL routing follows Storybook's specification.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesrgfv9i834n0cnypaft.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fesrgfv9i834n0cnypaft.PNG" alt="Screenshot of Blazing Story showing a story written in a .razor file, demonstrating the Blazor-native story format"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The way you write stories is inspired by Storybook, but it's its own format, with stories written in &lt;code&gt;.razor&lt;/code&gt; files.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ Limitations of a Personal Project
&lt;/h3&gt;

&lt;p&gt;Blazing Story is a personal project, so it naturally has far more limitations than the original Storybook.&lt;/p&gt;

&lt;p&gt;Development capacity is the biggest difference. I develop it alone, in the gaps between work and daily life. Even three years after the initial release, many features remain unimplemented, and there are likely bugs that haven't been reported yet. There's also the natural question of what happens if I lose motivation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj4uyvn0i11t3dyhopbpc.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj4uyvn0i11t3dyhopbpc.PNG" alt="Illustration representing the challenge of maintaining a large open source project as a solo developer alongside daily work"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blazing Story also can't use Storybook's plugin ecosystem, which makes sense since it runs on the .NET runtime rather than JavaScript. And the SaaS services Chromatic offers for Storybook are not available either.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmizitq9tq5cukqb993y1.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmizitq9tq5cukqb993y1.PNG" alt="Diagram listing Blazing Story's current limitations: no Storybook plugin ecosystem support and no Chromatic SaaS integration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Blazing Story Still Have a Reason to Exist?
&lt;/h3&gt;

&lt;p&gt;Given all those limitations, you might wonder whether Blazing Story is worth using.&lt;/p&gt;

&lt;p&gt;I believe it is.&lt;/p&gt;

&lt;p&gt;For one thing, it's a UI component catalog you can use &lt;strong&gt;right now&lt;/strong&gt; in your Blazor development, without waiting for Storybook to add Blazor support (which, realistically, may never happen).&lt;/p&gt;

&lt;p&gt;I also think Blazing Story is a compelling showcase for what Blazor can do. It proves that you can build a Storybook equivalent, a sophisticated developer tooling application, entirely in Blazor. On performance: yes, Blazor WebAssembly has the well-known drawback of a slow initial load. But as you can see in the live demo, it's not slow enough to hurt practical usability. And the fact that a single developer was able to reach a first release of something like Storybook in just three months speaks to Blazor's development productivity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnokzph5vs2uwgrigh19j.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnokzph5vs2uwgrigh19j.PNG" alt="Screenshot of the Blazing Story live demo on GitHub Pages, showcasing its practical usability and Blazor WebAssembly performance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📝 Summary
&lt;/h2&gt;

&lt;p&gt;In modern front-end development, Storybook has become a go-to tool for component-driven development, knowledge sharing, and testing. But Blazor, the framework that lets you build web apps in C#, can't use it.&lt;/p&gt;

&lt;p&gt;So I developed and released &lt;strong&gt;"Blazing Story"&lt;/strong&gt; as an open source project: a UI component catalog for Blazor that reimplements Storybook 100% in Blazor. You can start using it in your Blazor project today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9jjoot3k4lw7b4brxln.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9jjoot3k4lw7b4brxln.PNG" alt="Screenshot of Blazing Story, an open source UI component catalog for Blazor that reimplements Storybook entirely in C#"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Being a personal project, there are real concerns about missing features and long-term sustainability. Even so, I believe it has a clear reason to exist, both as a practical solution available today and as proof that Blazor is capable of building applications at this level.&lt;/p&gt;

&lt;p&gt;If Blazing Story can be even a little helpful in your Blazor development, nothing would make me happier as its author. ❤️&lt;/p&gt;

</description>
      <category>storybook</category>
      <category>blazor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Extending an ASP.NET Core App with External Middleware Using "Hosting Startup"</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Sat, 14 Feb 2026 07:37:03 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/extending-an-aspnet-core-app-with-external-middleware-using-hosting-startup-345b</link>
      <guid>https://dev.to/j_sakamoto/extending-an-aspnet-core-app-with-external-middleware-using-hosting-startup-345b</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;ASP.NET Core has a feature called "Hosting Startup". It allows us to run additional code when an ASP.NET Core Web application starts up. We can also add middleware through this feature.&lt;/p&gt;

&lt;p&gt;Using Hosting Startup, we can add middleware to an already-built ASP.NET Core Web application without rebuilding it. The middleware is implemented in a separate assembly that is built independently.&lt;/p&gt;

&lt;p&gt;The official documentation about Hosting Startup is available here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/platform-specific-configuration" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/platform-specific-configuration&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Hosting Startup Works
&lt;/h2&gt;

&lt;p&gt;Let me explain how Hosting Startup works in more detail.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare a pre-built assembly (a Hosting Startup assembly) that implements the &lt;code&gt;IHostingStartup&lt;/code&gt; interface and is marked with the &lt;code&gt;HostingStartup&lt;/code&gt; assembly attribute.&lt;/li&gt;
&lt;li&gt;Set the environment variable &lt;code&gt;ASPNETCORE_HOSTINGSTARTUPASSEMBLIES&lt;/code&gt; to the name of the Hosting Startup assembly created in step 1.&lt;/li&gt;
&lt;li&gt;Run the separately-built ASP.NET Core application.&lt;/li&gt;
&lt;li&gt;When the ASP.NET Core application starts up, the &lt;code&gt;IHostingStartup.Configure(IWebHostBuilder builder)&lt;/code&gt; method in the Hosting Startup assembly (specified by the environment variable) is called. Inside this method, we can use the &lt;code&gt;IWebHostBuilder&lt;/code&gt; object passed as an argument to register additional services to the DI container and perform other initialization tasks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To add middleware to the ASP.NET Core HTTP request pipeline, we need a few more steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inside the &lt;code&gt;IHostingStartup.Configure(IWebHostBuilder)&lt;/code&gt; method of the Hosting Startup assembly, register a class that implements &lt;code&gt;IStartupFilter&lt;/code&gt; with the DI container.&lt;/li&gt;
&lt;li&gt;After the host is built in the main ASP.NET Core application, when the HTTP request pipeline is being constructed, the &lt;code&gt;IStartupFilter&lt;/code&gt; class registered above is instantiated and its &lt;code&gt;IStartupFilter.Configure(Action&amp;lt;IApplicationBuilder&amp;gt; next)&lt;/code&gt; method is called. This method returns a function that registers middleware.&lt;/li&gt;
&lt;li&gt;The "function that registers middleware" returned from the above method is then executed. This is how the middleware specified in the Hosting Startup assembly gets added to the target ASP.NET Core application.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementing a Hosting Startup Assembly and Seeing It in Action
&lt;/h2&gt;

&lt;p&gt;Let's actually implement a Hosting Startup assembly that adds middleware and see it load when an ASP.NET Core Web application starts up.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prepare a Simple ASP.NET Core Web Application
&lt;/h3&gt;

&lt;p&gt;First, let's prepare a simple ASP.NET Core Web application. Open a terminal and run the following dotnet commands to create a new ASP.NET Core Web application and run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet new web &lt;span class="nt"&gt;-n&lt;/span&gt; MyAspNetCoreApp &lt;span class="nt"&gt;-f&lt;/span&gt; net10.0
&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet build ./MyAspNetCoreApp
&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet ./MyAspNetCoreApp/bin/Debug/net10.0/MyAspNetCoreApp.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open another terminal and check if it works using the curl command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:5000/
HTTP/1.1 200 OK
Content-Type: text/plain&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
Date: Sat, 14 Feb 2026 02:17:10 GMT
Server: Kestrel
Transfer-Encoding: chunked

Hello World!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks good! Now, press Ctrl + C to stop the running ASP.NET Core Web application for now.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Implement the Hosting Startup Assembly
&lt;/h3&gt;

&lt;p&gt;Next, let's implement the Hosting Startup assembly. A Hosting Startup assembly is just a regular class library. So run the following dotnet commands to create a new class library project. Additionally, add the Microsoft.AspNetCore.Hosting package to the project, since we need the types for implementing a Hosting Startup assembly. Then open it in VSCode and start implementing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet new classlib &lt;span class="nt"&gt;-n&lt;/span&gt; MyHostingStartup &lt;span class="nt"&gt;-f&lt;/span&gt; net10.0
&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet add package Microsoft.AspNetCore.Hosting &lt;span class="nt"&gt;--project&lt;/span&gt; ./MyHostingStartup
&lt;span class="nv"&gt;$ &lt;/span&gt;code ./MyHostingStartup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will work in reverse order from how things are called at runtime. Let's first implement the &lt;code&gt;IStartupFilter&lt;/code&gt; class, which "returns a function that registers middleware".&lt;br&gt;
Add a file named "MyStartupFilter.cs" to the Hosting Startup project and write the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MyHostingStartup/MyStartupFilter.cs&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStartupFilter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IStartupFilter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Return a function that adds middleware&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// In this sample, add a custom header to the HTTP response&lt;/span&gt;
                &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Custom-Header"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyCustomValue"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// Call the next middleware in the HTTP request pipeline&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nextMiddleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="c1"&gt;// Call the next middleware registration process&lt;/span&gt;
            &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this implementation, as an example, the middleware adds a response header called "X-Custom-Header" to every HTTP response.&lt;/p&gt;

&lt;p&gt;Next, add a class that implements the &lt;code&gt;IHostingStartup&lt;/code&gt; interface. This class is called when the ASP.NET Core Web application starts up, and it registers the &lt;code&gt;IStartupFilter&lt;/code&gt; class above with the DI container. Add a file named "MyStartup.cs" to the project and write the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MyHostingStartup/MyStartup.cs&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.AspNetCore.Hosting&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;HostingStartup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyStartup&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStartup&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHostingStartup&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IWebHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Register IStartupFilter to the service collection&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IStartupFilter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyStartupFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is all the source code we need. Let's build it with the dotnet command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet build ./MyHostingStartup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Run the ASP.NET Core Web Application with the Hosting Startup Assembly
&lt;/h3&gt;

&lt;p&gt;Now, let's set the environment variable &lt;code&gt;ASPNETCORE_HOSTINGSTARTUPASSEMBLIES&lt;/code&gt; to the assembly name "MyHostingStartup" that we just built.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ASPNETCORE_HOSTINGSTARTUPASSEMBLIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MyHostingStartup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using PowerShell, run the following instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; $env:ASPNETCORE_HOSTINGSTARTUPASSEMBLIES="MyHostingStartup"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setting in place, let's run the ASP.NET Core Web application that we created earlier again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet ./MyAspNetCoreApp/bin/Debug/net10.0/MyAspNetCoreApp.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But unfortunately, it does not start successfully. We get an error saying "the assembly 'MyHostingStartup' was not found", as shown below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
crit: Microsoft.AspNetCore.Hosting.Diagnostics[11]
      Hosting startup assembly exception
      System.InvalidOperationException: Startup assembly MyHostingStartup failed to execute. See the inner exception for more details.
       ---&amp;gt; System.IO.FileNotFoundException: Could not load file or assembly 'MyHostingStartup, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
      File name: 'MyHostingStartup, Culture=neutral, PublicKeyToken=null'
         at System.Reflection.RuntimeAssembly.InternalLoad(AssemblyName assemblyName, StackCrawlMark&amp;amp; stackMark, AssemblyLoadContext assemblyLoadContext, RuntimeAssembly requestingAssembly, Boolean throwOnFileNotFound)
         at System.Reflection.Assembly.Load(AssemblyName assemblyRef)
         at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.ExecuteHostingStartups()
         --- End of inner exception stack trace ---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actually, this is expected. The .NET runtime does not have enough information to determine where to load the assembly MyHostingStartup.dll, so it fails with a "file not found" error.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make the Hosting Startup Assembly Loadable by Specifying the Dependency
&lt;/h3&gt;

&lt;p&gt;So, we need to add MyHostingStartup.dll as a dependency of the ASP.NET Core Web application and tell the runtime where to find it (so it can be resolved to an absolute path). To do this, we need to take care of a couple of things.&lt;/p&gt;

&lt;p&gt;First, to make things simple, let's place the Hosting Startup assembly in the same folder as the ASP.NET Core Web application assembly. We can either copy the built Hosting Startup assembly file manually or rebuild the Hosting Startup project with the output folder set to the same folder as the ASP.NET Core Web application output, like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet build ./MyHostingStartup &lt;span class="nt"&gt;-o&lt;/span&gt; ./MyAspNetCoreApp/bin/Debug/net10.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a JSON file that describes the .NET dependency. The file name can be anything, but here we will call it "additional.deps.json". Let's create the file in the current folder and open it in an editor like VSCode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; additional.deps.json
&lt;span class="nv"&gt;$ &lt;/span&gt;code ./additional.deps.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this JSON file, write the following to declare that MyHostingStartup.dll is a dependency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtimeTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".NETCoreApp,Version=v10.0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;".NETCoreApp,Version=v10.0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"MyHostingStartup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"MyHostingStartup.dll"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"libraries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MyHostingStartup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"serviceable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"sha512"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving this JSON file and closing the editor, we set the environment variable &lt;code&gt;DOTNET_ADDITIONAL_DEPS&lt;/code&gt; to the path of this dependency JSON file so that it is loaded at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DOTNET_ADDITIONAL_DEPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./additional.deps.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you are using PowerShell,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; $env:DOTNET_ADDITIONAL_DEPS = './additional.deps.json'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After doing this, let's run the ASP.NET Core Web application project again. This time, it should start without any errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dotnet ./MyAspNetCoreApp/bin/Debug/net10.0/MyAspNetCoreApp.dll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's check with the curl command to see if the Hosting Startup assembly was really loaded and the middleware was added.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:5000/
HTTP/1.1 200 OK
Content-Type: text/plain&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;utf-8
Date: Sat, 14 Feb 2026 04:10:47 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Custom-Header: MyCustomValue

Hello World!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at the curl output above, we can see that the "X-Custom-Header" response header has been added. This confirms that the Hosting Startup assembly MyHostingStartup.dll was loaded and its &lt;code&gt;IHostingStartup.Configure(IWebHostBuilder builder)&lt;/code&gt; method was called.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using the Hosting Startup feature, we can add middleware to an existing ASP.NET Core Web application without rebuilding it. The middleware is implemented in a separately-built assembly and added at runtime.&lt;/p&gt;

&lt;p&gt;The basic steps to implement a Hosting Startup assembly are to build a class library that contains a class implementing the required interfaces and the assembly attribute following the conventions, and then start the ASP.NET Core Web application with the assembly name specified in the &lt;code&gt;ASPNETCORE_HOSTINGSTARTUPASSEMBLIES&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;However, because of how .NET application execution works, we need to explicitly specify assembly dependencies along with the location of the assembly files in a JSON file. The Hosting Startup assembly is no exception. We need to declare the dependency in a JSON file from the perspective of the target ASP.NET Core Web application and specify that JSON file in the &lt;code&gt;DOTNET_ADDITIONAL_DEPS&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;So in short, the Hosting Startup feature lets us add middleware to an existing ASP.NET Core Web application to extend its functionality. I think this is a feature that opens up many possibilities depending on your ideas. Please give it a try!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>Tired of Accidentally Zipping Build Artifacts? Try "dnx zipsrc"!</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Fri, 09 Jan 2026 12:45:56 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/tired-of-accidentally-zipping-build-artifacts-try-dnx-zipsrc-3304</link>
      <guid>https://dev.to/j_sakamoto/tired-of-accidentally-zipping-build-artifacts-try-dnx-zipsrc-3304</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Every Developer Knows
&lt;/h2&gt;

&lt;p&gt;We've all been there. You need to quickly share your project with a colleague, attach it to a support ticket, or upload it somewhere. You right-click, select "Compress to ZIP," and then... wait. Why is it taking so long? Why is the file 500MB?&lt;/p&gt;

&lt;p&gt;Oh right. You just zipped &lt;code&gt;node_modules&lt;/code&gt;. Or &lt;code&gt;bin/obj&lt;/code&gt;. Or that massive &lt;code&gt;.nuget&lt;/code&gt; cache.&lt;/p&gt;

&lt;p&gt;Now you have to manually exclude folders, or worse, copy everything to a new location first. What should take 5 seconds becomes a 5-minute ordeal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing &lt;code&gt;dnx zipsrc&lt;/code&gt; 🎉
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;dnx zipsrc&lt;/strong&gt; is a smart CLI tool that creates zip archives containing &lt;em&gt;only&lt;/em&gt; your source code. It automatically excludes build outputs, dependencies, and other artifacts you don't want to share.&lt;/p&gt;

&lt;p&gt;The best part? With .NET 10's new &lt;code&gt;dnx&lt;/code&gt; command, you don't even need to install it first!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnx zipsrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One command. Your source code is now neatly packaged in a zip file, ready to share.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Smart Exclusion with .gitignore
&lt;/h3&gt;

&lt;p&gt;Instead of reinventing the wheel, &lt;code&gt;dnx zipsrc&lt;/code&gt; leverages your existing &lt;code&gt;.gitignore&lt;/code&gt; patterns. If your project has a &lt;code&gt;.gitignore&lt;/code&gt; file, those rules are automatically honored.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;.gitignore&lt;/code&gt;? No problem! The tool falls back to sensible defaults equivalent to &lt;code&gt;dotnet new gitignore&lt;/code&gt;, covering common patterns for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C#/.NET&lt;/strong&gt;: &lt;code&gt;bin/&lt;/code&gt;, &lt;code&gt;obj/&lt;/code&gt;, &lt;code&gt;*.user&lt;/code&gt;, &lt;code&gt;*.suo&lt;/code&gt;, &lt;code&gt;.vs/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: &lt;code&gt;node_modules/&lt;/code&gt;, &lt;code&gt;dist/&lt;/code&gt;, &lt;code&gt;.cache/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And many more&lt;/strong&gt;: Build outputs, IDE files, logs, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It even respects &lt;code&gt;.gitignore&lt;/code&gt; files in subdirectories and correctly handles negation patterns (&lt;code&gt;!important-file.txt&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Under the hood, this is powered by &lt;a href="https://github.com/Guiorgy/GitignoreParserNet" rel="noopener noreferrer"&gt;GitignoreParserNet&lt;/a&gt;, a robust .gitignore parser library for .NET. It handles all the edge cases so I didn't have to reinvent that wheel!&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent Naming
&lt;/h3&gt;

&lt;p&gt;Don't want to think about file names? Neither do I.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;dnx zipsrc&lt;/code&gt; without specifying an output name:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If your folder contains a &lt;code&gt;.sln&lt;/code&gt; or &lt;code&gt;.slnx&lt;/code&gt; file, the zip is named after that solution&lt;/li&gt;
&lt;li&gt;Otherwise, it uses the folder name&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Already have a &lt;code&gt;MyProject.zip&lt;/code&gt;? The tool automatically creates &lt;code&gt;MyProject (2).zip&lt;/code&gt; instead of overwriting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Zip the Current Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnx zipsrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Zip a Specific Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnx zipsrc &lt;span class="nt"&gt;-d&lt;/span&gt; ./src/MyAwesomeApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Specify a Custom Output Name
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnx zipsrc &lt;span class="nt"&gt;-d&lt;/span&gt; ./src/MyApp &lt;span class="nt"&gt;-n&lt;/span&gt; ./archives/MyAppSource
&lt;span class="c"&gt;# Creates: ./archives/MyAppSource.zip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.zip&lt;/code&gt; extension is automatically added if you forget it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;dnx&lt;/code&gt; Instead of &lt;code&gt;dotnet tool install&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;.NET 10 introduced the &lt;code&gt;dnx&lt;/code&gt; command, which lets you run .NET global tools &lt;em&gt;without installing them first&lt;/em&gt;. It downloads, caches, and runs the tool in one step.&lt;/p&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ No cluttering your global tools list&lt;/li&gt;
&lt;li&gt;✅ Always runs the latest version&lt;/li&gt;
&lt;li&gt;✅ Works immediately on any machine with .NET 10+&lt;/li&gt;
&lt;li&gt;✅ Perfect for one-off tasks like zipping source code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, if you use it frequently, you can still install it the traditional way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; zipsrc
zipsrc &lt;span class="nt"&gt;-d&lt;/span&gt; ./MyProject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📧 &lt;strong&gt;Email attachments&lt;/strong&gt;: Share clean source code without the bloat&lt;/li&gt;
&lt;li&gt;🎫 &lt;strong&gt;Support tickets&lt;/strong&gt;: Attach minimal reproductions to bug reports&lt;/li&gt;
&lt;li&gt;📚 &lt;strong&gt;Teaching&lt;/strong&gt;: Distribute starter projects to students&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Quick backups&lt;/strong&gt;: Archive your work before major refactoring&lt;/li&gt;
&lt;li&gt;🤝 &lt;strong&gt;Code reviews&lt;/strong&gt;: Share snapshots with teammates who aren't on your repo&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  "But Wait, Doesn't This Already Exist?"
&lt;/h2&gt;

&lt;p&gt;You're right! Tools for zipping source code aren't new. There are already some options out there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=SigDev.Zipper" rel="noopener noreferrer"&gt;Zipper Extension&lt;/a&gt;&lt;/strong&gt; - A Visual Studio add-in that can zip your solution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nuget.org/packages/dotnet-zip-source" rel="noopener noreferrer"&gt;dotnet-zip-source&lt;/a&gt;&lt;/strong&gt; - A .NET global tool with similar goals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So why create yet another tool? Here's what motivated me:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Naming Conventions and Output Location
&lt;/h3&gt;

&lt;p&gt;Existing tools often have opinionated defaults for where the zip file goes and what it's named. I wanted control over the naming logic—specifically, automatically using the solution file name when available, and placing the output exactly where I expect it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. No Auto-Incrementing for Repeated Runs
&lt;/h3&gt;

&lt;p&gt;When I zip a project multiple times (maybe after making changes), I don't want to manually rename files or have the previous archive overwritten. &lt;code&gt;dnx zipsrc&lt;/code&gt; automatically appends &lt;code&gt;(2)&lt;/code&gt;, &lt;code&gt;(3)&lt;/code&gt;, etc., so you never lose a previous archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Better Default Ignore Rules
&lt;/h3&gt;

&lt;p&gt;When there's no &lt;code&gt;.gitignore&lt;/code&gt; file, some tools have minimal or no exclusion rules. &lt;code&gt;dnx zipsrc&lt;/code&gt; falls back to comprehensive defaults equivalent to &lt;code&gt;dotnet new gitignore&lt;/code&gt;, which covers a much wider range of common artifacts out of the box.&lt;/p&gt;

&lt;p&gt;These might seem like small things, but when you're zipping projects multiple times a day, these little conveniences add up!&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;.NET SDK 10.0 or later&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started Now!
&lt;/h2&gt;

&lt;p&gt;Ready to stop accidentally zipping &lt;code&gt;node_modules&lt;/code&gt;? Just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dnx zipsrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the project on GitHub: &lt;a href="https://github.com/jsakamoto/dnx-zipsrc" rel="noopener noreferrer"&gt;jsakamoto/dnx-zipsrc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install from NuGet: &lt;a href="https://www.nuget.org/packages/zipsrc/" rel="noopener noreferrer"&gt;zipsrc on nuget.org&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Have questions or feedback? Drop a comment below or open an issue on GitHub. Happy zipping! 🗜️&lt;/p&gt;




&lt;h2&gt;
  
  
  Acknowledgments
&lt;/h2&gt;

&lt;p&gt;Special thanks to &lt;a href="https://github.com/Guiorgy" rel="noopener noreferrer"&gt;Guiorgy&lt;/a&gt; for creating &lt;a href="https://github.com/Guiorgy/GitignoreParserNet" rel="noopener noreferrer"&gt;GitignoreParserNet&lt;/a&gt;. This library handles all the complex &lt;code&gt;.gitignore&lt;/code&gt; parsing logic, making it easy to build tools that respect ignore patterns correctly. Open source makes building tools like this so much easier! 🙏&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was written with the help of VS Code and GitHub Copilot (Claude Opus 4.5 model). As a Japanese developer who is not a native English speaker, I appreciate your understanding if there are any awkward expressions!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>productivity</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Quickly Test Your Blazor App on a Real Smartphone</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Mon, 10 Nov 2025 08:50:23 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/how-to-quickly-test-your-blazor-app-on-a-real-smartphone-24h1</link>
      <guid>https://dev.to/j_sakamoto/how-to-quickly-test-your-blazor-app-on-a-real-smartphone-24h1</guid>
      <description>&lt;h2&gt;
  
  
  Using Dev Tunnels to Access Your Development App from Your Phone via the Internet
&lt;/h2&gt;

&lt;p&gt;Sometimes I want to test my Blazor application on my phone while developing it. Of course, if I just need to check the layout, I can use the device emulation feature in the developer tools in desktop browsers. However, there are times when testing on an actual device is faster. Additionally, I sometimes need to use browser APIs that are difficult to test without a physical device.&lt;/p&gt;

&lt;p&gt;In such cases, if you are developing with Visual Studio on Windows, you can use a feature called "Dev Tunnels." With Dev Tunnels, you can temporarily relay and publish HTTP traffic from your Blazor application, running on localhost, to the Internet. Let me explain this more. When you run your app with Dev Tunnels through Visual Studio, a unique public URL is created on the Internet. When you open this URL in a browser on your smartphone (which has Internet access), HTTP requests to that Internet URL are relayed to your Blazor application running on localhost. It's like creating a tunnel from the Internet to localhost.&lt;/p&gt;

&lt;p&gt;You'll need a GitHub or Microsoft account to use Dev Tunnels.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use Dev Tunnels
&lt;/h2&gt;

&lt;p&gt;Here are the steps to use Dev Tunnels.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating and Enabling a Dev Tunnel
&lt;/h3&gt;

&lt;p&gt;First, start Visual Studio and open your Blazor application project.&lt;/p&gt;

&lt;p&gt;Next, open the dropdown list next to the Debug Execution Tool button and select the [Dev Tunnels] item. If you have never used the Dev Tunnels feature before, you will see a screen like the one below, so please select &lt;code&gt;[Create a Tunnel...]&lt;/code&gt; shown in the red circle below. If you have already created a Dev Tunnel before, the name of the existing Dev Tunnel should be listed here. In that case, you can click and select that existing Dev Tunnel name instead of creating a new one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0eoaui1gbcc7fg1h97x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0eoaui1gbcc7fg1h97x.png" alt="Selecting " width="740" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click &lt;code&gt;[Create a Tunnel...]&lt;/code&gt;, a dialog box will open to create a new tunnel. Here, you need to specify the following settings.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select the GitHub account or Microsoft account to use for creating the tunnel&lt;/li&gt;
&lt;li&gt;Specify the tunnel name so you can distinguish each tunnel in your Visual Studio work (In the example below, I specified "default" as the tunnel name)&lt;/li&gt;
&lt;li&gt;Select the expiration period of the tunnel URL (In the example below, I selected "Temporary", which creates a new URL each time you restart Visual Studio)&lt;/li&gt;
&lt;li&gt;Select whether to require authentication when accessing the published tunnel URL (In the example below, I selected "Private", which denies access without authentication)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg4bln4xhfzr7nw8g6da8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg4bln4xhfzr7nw8g6da8.png" alt="A creating new dev tunnel dialog" width="750" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After specifying all of these, click [OK].&lt;/p&gt;

&lt;p&gt;Then, a message will appear showing that a new "Dev Tunnel" has been created and that this Dev Tunnel is now set to be used when running this Blazor application project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0dcduktule2y3v51iwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0dcduktule2y3v51iwj.png" alt="A message box that shows a new dev tunnel has been created" width="433" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking Dev Tunnel Active Status and Running the App
&lt;/h3&gt;

&lt;p&gt;When you reopen the dropdown list next to the Debug Execution tool button in Visual Studio again, you can see that the Dev Tunnel "default" you just created is selected and active, as shown below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqkofiim7jz7qaa3svyt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqkofiim7jz7qaa3svyt.png" alt="The dev tunnel " width="718" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this state, press &lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;F5&lt;/code&gt; or click the "Run without debugging" tool button to run this Blazor application. (Of course, you can also run it with debugging.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzba6xvhauxavy8hgvm6n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzba6xvhauxavy8hgvm6n.png" alt="The " width="500" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After waiting a moment, the browser will launch and open the URL published to the Dev Tunnel. If you selected &lt;code&gt;[Private]&lt;/code&gt; for &lt;code&gt;[Access]&lt;/code&gt; when creating the Dev Tunnel, you will be prompted to authenticate with your GitHub account or Microsoft account, then complete the authentication as needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnwgycodlw0lmq23buw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnwgycodlw0lmq23buw6.png" alt="An authentication view" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When accessing for the first time, a confirmation message will appear &lt;br&gt;
asking whether you want to proceed. Of course, in this case, you know that this is access to the Dev Tunnel you created yourself. So click &lt;code&gt;[Continue]&lt;/code&gt; to proceed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s3cqlswrnn6glsxhbxj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1s3cqlswrnn6glsxhbxj.png" alt="A confirmation to access" width="694" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, your Blazor application, which is under development and should be running on localhost, becomes accessible via a public URL on the Internet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqswl423qyi1hkmjyjt6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqswl423qyi1hkmjyjt6g.png" alt="A Blazor app though the dev tunnel" width="800" height="566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessing from Your Smartphone via the Internet
&lt;/h3&gt;

&lt;p&gt;Now, all you need to do is open this Dev Tunnel public URL in your smartphone browser. That's it. Modern web browsers have a feature that allows you to share the URL of the page you are viewing as a QR code. You can use this feature to open it on your smartphone. (The screenshot below shows how to generate a QR code in the Microsoft Edge browser by right-clicking on the page and selecting it from the menu.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekzy7kvvutsfe8bxszm7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekzy7kvvutsfe8bxszm7.png" alt="A right click menu" width="793" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6iglrjbhzpr1ezehde6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv6iglrjbhzpr1ezehde6.png" alt="A QR code that represents the current page URL" width="792" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deactivating the Dev Tunnel
&lt;/h3&gt;

&lt;p&gt;To stop running via the Dev Tunnel, reopen the dropdown list next to the Debug Execution tool button again and click &lt;code&gt;[None]&lt;/code&gt; from the &lt;code&gt;[Dev Tunnels]&lt;/code&gt; item.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9x95q69y7frkib3fm75.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9x95q69y7frkib3fm75.png" alt="The " width="709" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Review and Precautions
&lt;/h3&gt;

&lt;p&gt;As explained above, if you are using Visual Studio on Windows, you can make your Blazor application under development accessible via a URL on the Internet by using the Dev Tunnels feature. Since you can also access this Dev Tunnel URL from your smartphone, you can easily test your app. However, please be careful from a security perspective. This means you are allowing access and intrusion from the Internet to the program running in your local development environment. If you set the access to private when setting up the Dev Tunnel, you can protect it so that it can only be accessed with your GitHub account or Microsoft account. This provides a certain level of safety.&lt;/p&gt;

&lt;h3&gt;
  
  
  If You Are Not Using Visual Studio or Not on Windows
&lt;/h3&gt;

&lt;p&gt;If you are not using Visual Studio, Visual Studio Code also has the same feature. It's called "Port forwarding." You can use that. Additionally, the Dev Tunnels feature itself is actually a service of Microsoft Azure, Microsoft's public cloud service. A CLI tool called "devtunnels" is provided for creating, activating, and managing Dev Tunnels. With the "devtunnels" CLI, you can use Dev Tunnels in any environment. Of course, it seems to be available not only on Windows but also on macOS and various Linux distributions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compared to Other Methods
&lt;/h3&gt;

&lt;p&gt;Besides using Dev Tunnels or similar methods (like ngrok), you can also test your Blazor application on your smartphone by connecting your smartphone to the same LAN as your development PC, configuring the Blazor application to listen on a hostname or external IP address instead of localhost, adjusting the firewall settings of your development PC's OS, and so on. However, the steps I just wrote here are quite complicated, right?&lt;/p&gt;

&lt;p&gt;In that regard, although you need to be careful when using it, if you know about the Dev Tunnels feature, testing your Blazor application on an actual device becomes much easier and more convenient. Let's use it well.&lt;/p&gt;

</description>
      <category>blazor</category>
    </item>
    <item>
      <title>Creating chip-tuned square &amp; triangle wave sounds on web browsers with Blazor WebAssembly</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Sat, 31 Dec 2022 12:37:51 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/creating-chip-tuned-square-triangle-wave-sounds-on-web-browsers-with-blazor-webassembly-32ji</link>
      <guid>https://dev.to/j_sakamoto/creating-chip-tuned-square-triangle-wave-sounds-on-web-browsers-with-blazor-webassembly-32ji</guid>
      <description>&lt;h2&gt;
  
  
  "SoundMaker" Library
&lt;/h2&gt;

&lt;p&gt;I found an interesting post in my Twitter timeline last month.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1588806133275594752-736" src="https://platform.twitter.com/embed/Tweet.html?id=1588806133275594752"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1588806133275594752-736');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1588806133275594752&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;The tweet introduced a library that allows us to create chip-tuned square &amp;amp; triangle wave sounds and save them to a .wav file by &lt;strong&gt;only C#&lt;/strong&gt;, "SoundMaker". (The GitHub repository URL is below.)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AutumnSky1010" rel="noopener noreferrer"&gt;
        AutumnSky1010
      &lt;/a&gt; / &lt;a href="https://github.com/AutumnSky1010/SoundMaker" rel="noopener noreferrer"&gt;
        SoundMaker
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      You can do The following content with this library.   1. make the sound of chiptune(old game sound)  2. export sound to a file of wave format.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://user-images.githubusercontent.com/66455966/206901705-974f5a63-46db-435c-bdb1-1717e8bb7883.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fuser-images.githubusercontent.com%2F66455966%2F206901705-974f5a63-46db-435c-bdb1-1717e8bb7883.png" alt="SoundMakerCover"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/AutumnSky1010/SoundMaker/actions/workflows/build.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/AutumnSky1010/SoundMaker/actions/workflows/build.yml/badge.svg" alt="Pipeline"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🗺️言語(Language)&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/AutumnSky1010/SoundMaker#%E6%A6%82%E8%A6%81" rel="noopener noreferrer"&gt;日本語&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AutumnSky1010/SoundMaker#overview" rel="noopener noreferrer"&gt;English&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎵概要&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;本ライブラリを用いると、以下の事が可能です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;チップチューンサウンド？を作成する&lt;/li&gt;
&lt;li&gt;waveファイルにサウンドを書き込む&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📑ドキュメント&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/AutumnSky1010/SoundMaker/wiki" rel="noopener noreferrer"&gt;Wiki&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;⛰️要件&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;.NET 8 以降&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;⏬インストール方法&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;NuGet&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a href="https://www.nuget.org/packages/SoundMaker/" rel="nofollow noopener noreferrer"&gt;SoundMaker&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🎶簡単な使い方&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;using SoundMaker
using SoundMaker.Sounds;
using SoundMaker.Sounds.Score;
using SoundMaker.Sounds.SoundChannels;
using SoundMaker.WaveFile;

namespace YourNamespace;
public static class YourClass
{
    private static void Main()
    {
        // サウンドの形式を作成する。
        var builder = FormatBuilder.Create()
            .WithFrequency(48000)
            .WithBitDepth(16)
            .WithChannelCount(2);

        var soundFormat = builder.ToSoundFormat();
        StereoWave wave = MakeStereoWave(soundFormat);

        // ファイルに書き込む。
        var sound = new SoundWaveChunk(wave.GetBytes(soundFormat.BitRate));
        var waveFileFormat = builder.ToFormatChunk();
        var writer = new WaveWriter(waveFileFormat, sound);
        string filePath = "sample.wav";
        writer.Write(filePath);
    }

    private static StereoWave MakeStereoWave(SoundFormat format)
    {
        // 一分間の四分音符の個数
        int tempo = 100;
        // まず、音のチャンネルを作成する必要がある。
        // 現段階では矩形波、三角波、疑似三角波、ロービットノイズに対応している。
        var rightChannel = new SquareSoundChannel(tempo, format, SquareWaveRatio.Point25, PanType.Right)
        {
            // ISoundComponentを実装したクラスのオブジェクトをチャンネルに追加していく。
            // 現段階では普通の音符、休符、タイ、連符を使うことができる。
            new Note(Scale.C, 5, LengthType.Eighth, isDotted: true),
            new Tie(new Note(Scale.D, 5, LengthType.Eighth), LengthType.Eighth),
            new Tuplet(GetComponents(), LengthType.Quarter)
        };
        var rightChannel2 = new SquareSoundChannel(tempo, format, SquareWaveRatio.Point125, PanType.Right)
        {
            new Note(Scale.C, 4, LengthType.Eighth, isDotted: true),
            new Note(Scale.D, 4, LengthType.Quarter),
            new Rest(LengthType.Quarter)
        };
        var leftChannel = new&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AutumnSky1010/SoundMaker" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The "SoundMaker" is distributed under the MIT license.&lt;/p&gt;

&lt;p&gt;When I saw that tweet, one idea came to my mind. He said the "SoundMaker" is built on 100% pure C#. If it is true, &lt;strong&gt;we should also be able to run the "SoundMaker" on web browsers powered by Blazor WebAssembly.&lt;/strong&gt; So I tried it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement it
&lt;/h2&gt;

&lt;p&gt;There is &lt;a href="https://github.com/AutumnSky1010/SoundMaker#usage" rel="noopener noreferrer"&gt;a simple example code&lt;/a&gt; on how to use the "SoundMaker" to create and save square &amp;amp; triangle wave sounds on the README document in the GitHub repository of the "SoundMaker". I created a new Blazor WebAssembly project and copied that example code, almost as is, into a &lt;code&gt;@code { ... }&lt;/code&gt; code block of a Razor component file (.razor) in that Blazor WebAssembly project.&lt;/p&gt;

&lt;p&gt;The outline of the code is like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;@code&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GenerateWavFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Specify sound format, and generate sound waves.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;soundFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SoundFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SoundMaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SamplingFrequencyType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FourtyEightKHz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SoundMaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BitRateType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SixteenBit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SoundMaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stereo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wave&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;MakeStereoWave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soundFormat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Save it into a file.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sound&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SoundWaveChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wave&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;soundFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BitRate&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;waveFileFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FormatChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SoundMaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaveFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SamplingFrequencyType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FourtyEightKHz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SoundMaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaveFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BitRateType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SixteenBit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SoundMaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaveFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChannelType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stereo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WaveWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;waveFileFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sound&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sample.wav"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;StereoWave&lt;/span&gt; &lt;span class="nf"&gt;MakeStereoWave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SoundFormat&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Number of quarter notes per minute.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tempo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;// can mix with StereoMixer class. &lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StereoMixer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Mix&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But, wait, wait.&lt;/p&gt;

&lt;p&gt;If you look closely at the above code, you will find that &lt;strong&gt;the program is doing a write to a file.&lt;/strong&gt; Here is an excerpt.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GenerateWavFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Save it into a file.&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sample.wav"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This program is a Blazor WebAssembly program that is loaded into a web browser and executed in its sandbox. So what happens when you do this &lt;strong&gt;write to a local file&lt;/strong&gt;? Of course, it would not be written to the local storage of the device on which the web browser is actually running. So, would this process fail with an exception? That said, the &lt;code&gt;WaveWriter&lt;/code&gt; class in the SoundMaker library apparently does not have the ability to write to an arbitrary &lt;code&gt;Stream&lt;/code&gt;, and &lt;strong&gt;there is no way to write a .wav file other than specifying the file path&lt;/strong&gt;. Is this that the "SoundMaker" library is not available on Blazor WebAssembly, unfortunately?&lt;/p&gt;
&lt;h2&gt;
  
  
  Writing to local files in Blazor WebAssembly apps will be a success
&lt;/h2&gt;

&lt;p&gt;In fact, &lt;strong&gt;writing to local files in Blazor WebAssembly apps will be a success!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Blazor WebAssembly uses Emscripten as its foundation to generate WebAssembly code generation, and because of this, the &lt;strong&gt;on-memory filesystem simulation&lt;/strong&gt; (virtual file system) provided by Emscripten is also available in Blazor WebAssembly! (For more information, please refer to the following links.)&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://emscripten.org/docs/porting/files/file_systems_overview.html" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;emscripten.org&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Therefore, writing a .wav file into the local file system by the &lt;code&gt;WaveWriter&lt;/code&gt; class in the "SoundMaker" library will be successful. The written file exists on the virtual file system in the web browser's WebAssembly process memory space. And, we can read the .wav file written to the virtual file system into an array of &lt;code&gt;byte&lt;/code&gt;, and can do anything, such as playing back, downloading, etc., from the web browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real example
&lt;/h2&gt;

&lt;p&gt;I've publicly pushed the Blazor WebAssembly project source code I explained above on the following GitHub repository.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sample-by-jsakamoto" rel="noopener noreferrer"&gt;
        sample-by-jsakamoto
      &lt;/a&gt; / &lt;a href="https://github.com/sample-by-jsakamoto/Blazor-SoundMaker-on-BlazorWasm" rel="noopener noreferrer"&gt;
        Blazor-SoundMaker-on-BlazorWasm
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Demonstration of the "SoundMaker" on Blazor WebAssembly
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;And, this example has been deployed on GitHub Pages by a GitHub Actions script. You can try it immediately by following the next link.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://sample-by-jsakamoto.github.io/Blazor-SoundMaker-on-BlazorWasm/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;sample-by-jsakamoto.github.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The screenshot below shows how the above link looks when opened.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ss2kz1yrgtuvsknkg9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ss2kz1yrgtuvsknkg9m.png" alt="image.png" width="800" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click the "Generate Sound and Play" button, the sound generated on the fly by the C# program I explained above will be played back. When you click the "Generate Sound and Download" button, the .wav file will be downloaded instead of playing back it. Please remember the generating sound process runs on your web browser! I confirmed that the example site can also be run on Apple mobile devices. &lt;strong&gt;I could generate the chip-tuned sound with that example site and play it on my iPhone SE2.&lt;/strong&gt; That's amazing!&lt;/p&gt;

&lt;p&gt;Moreover, as I already mentioned above, due to the source code of the example site being hosted on the GitHub repository, when you create a new "GitHub Codespace" for this repository, like the following screenshot,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdu2u560cpitomrwidsda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdu2u560cpitomrwidsda.png" alt="image.png" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As shown in the screenshot below, &lt;strong&gt;you can edit and run the code ("dotnet run" command execution) of this example site in a web browser immediately, without the need to set up a local development environment on your PC!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jlum2wnl3t4dxeos3bw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2jlum2wnl3t4dxeos3bw.png" alt="image.png" width="800" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now you can easily enjoy chip-tuned sound generation programming with "SoundMaker" anytime, anywhere, as long as you are connected to the Internet and use any device that supports GitHub Codespaces! That's really amazing!  &lt;/p&gt;

&lt;p&gt;Thanks to the author of the "SoundMaker", &lt;a href="https://github.com/AutumnSky1010" rel="noopener noreferrer"&gt;AutumnSky1010&lt;/a&gt;, for publishing such a great library as an Open Source!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy Coding :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How to install a ".xpi" browser extension through remote session of Selenium WebDriver C# binding for testing it.</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Mon, 31 Oct 2022 12:57:20 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/how-to-install-a-xpi-browser-extension-through-remote-session-of-selenium-webdriver-c-binding-for-testing-it-4b7b</link>
      <guid>https://dev.to/j_sakamoto/how-to-install-a-xpi-browser-extension-through-remote-session-of-selenium-webdriver-c-binding-for-testing-it-4b7b</guid>
      <description>&lt;h2&gt;
  
  
  Prologue
&lt;/h2&gt;

&lt;p&gt;The "GeckoDriver" (a.k.a "FirefoxDriver"), one of the WebDriver to remote-operate Web browsers, provides us with the ability to install a ".xpi" Mozilla browser extension file in its remote-operation session.&lt;/p&gt;

&lt;p&gt;This feature is convenient for conducting End-to-End tests for a ".xpi" Mozilla browser extension.&lt;/p&gt;

&lt;p&gt;Unfortunately, I ran into some pitfalls when I tried to do that. So I'll explain the best way to install a ".xpi" file via Selenium GeckoDriver in this article.&lt;/p&gt;

&lt;p&gt;By the way, I'm a .NET guy, so I'll explain based on the C# binding of Selenium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't use the "FirefoxProfile.AddExtension"
&lt;/h2&gt;

&lt;p&gt;First, you might notice that there is the "AddExtension()" method in the "FirefoxProfile" class. The name of that method seems good to use the installing an extension.&lt;/p&gt;

&lt;p&gt;However, unexpectedly, the "FirefoxProfile.AddExtension()" method didn't work correctly. I don't know why this method doesn't work, but anyway, I've never succeeded in installing an extension with that method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ Don't do this.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FirefoxProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathToXpiFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use the "FirefoxDriver.InstallAddOnFromFile"
&lt;/h2&gt;

&lt;p&gt;Instead, please use the "InstallAddOnFromFile()" method of the "FirefoxDriver". The argument of this method is a path to a ".xpi" file. The "FirefoxDriver.InstallAddOnFromFile" method will work fine as we expected, and there is nothing too difficult.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👍 Do this.&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FirefoxDriver&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InstallAddOnFromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathToXpiFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "FirefoxDriver" class also exposes the "InstallAddOn()" method, and this method accepts the binary contents of the browser extension as a base64 encoded string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't use the "FirefoxProfile.SetPreference"
&lt;/h2&gt;

&lt;p&gt;Usually, during the End-to-End testing of a browser extension, the ".xpi" browser extension file is not signed because it is in the development phase before submitting it for store review. But web browsers usually reject installing an unsigned browser extension file. So we have to disable this restriction of browsers during the testing.&lt;/p&gt;

&lt;p&gt;To do that, you may find a way to set the preference "xpinstall.signatures.required" to false from the Internet search result.&lt;/p&gt;

&lt;p&gt;However, the code below that uses the "SetPreference" method of the "FirefoxProfile" class will never work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ Don't do this.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FirefoxProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pathToXpiFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do that, you will run into an exception like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"System.ArgumentException: Preference xpinstall.signatures.required may not be overridden: frozen value=False, requested value=False"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use the "FirefoxOptions.SetPreference"
&lt;/h2&gt;

&lt;p&gt;Instead, please use the "SetPreference" method of the "FirefoxOptions" class, not of the "FirefoxProfile" class. This way will work fine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👍 Do this.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FirefoxOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPreference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"xpinstall.signatures.required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FirefoxDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Don't use the normal edition of Firefox
&lt;/h2&gt;

&lt;p&gt;After doing the above step, you will still see an unexpected error at runtime like the bellow, even though you set the "xpinstall.signatures.required" preference to false:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Could not install add-on: ....xpi: ERROR_SIGNEDSTATE_REQUIRED: The addon must be signed and isn't."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Firefox Browser of the normal edition is designed as it never allows us to install an unsigned ".xpi" browser extension file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the Developer or Enterprise edition of Firefox
&lt;/h2&gt;

&lt;p&gt;Instead, you must install the Developer Edition or Enterprise Edition of Firefox web browser on your PC for this scenario.Please follow the link below for more details about the Firefox Developer Edition.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mozilla.org/firefox/developer/" rel="noopener noreferrer"&gt;https://www.mozilla.org/firefox/developer/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I'm not sure why there are some pitfalls that are hard to notice to install a ".xpi" browser extension file via Selenium WebDriver. So I hope this article will save the time of other developers.&lt;/p&gt;

&lt;p&gt;You can see the entire sample code of this article in the following GitHub repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sample-by-jsakamoto/InstallAddOnTestOnSeleniumAndFirefox" rel="noopener noreferrer"&gt;https://github.com/sample-by-jsakamoto/InstallAddOnTestOnSeleniumAndFirefox&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have a nice coding :)&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>selenium</category>
    </item>
    <item>
      <title>Why can't your Blazor WebAssembly PWA with offline support update even though you reload it?</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Wed, 07 Sep 2022 06:44:51 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/why-cant-your-blazor-webassembly-pwa-with-offline-support-update-even-though-you-reload-it-31ho</link>
      <guid>https://dev.to/j_sakamoto/why-cant-your-blazor-webassembly-pwa-with-offline-support-update-even-though-you-reload-it-31ho</guid>
      <description>&lt;h2&gt;
  
  
  😥 Introduction
&lt;/h2&gt;

&lt;p&gt;Please imagine the case that you have a Blazor WebAssembly PWA site that supports offline mode, and you've been opening that PWA by a web browser.&lt;br&gt;
If you published a new version of that PWA, you might want to verify it, I guess. In this case, you will reload the page on that web browser.&lt;/p&gt;

&lt;p&gt;However, you might &lt;strong&gt;never see the contents of the new version&lt;/strong&gt; unexpectedly even though you hard-reload it. Disabling cache via the browser's developer tools window or clicking the "Empty cache and hard refresh" menu item from right-clicking the browser's reload icon button &lt;strong&gt;doesn't have any effect at all&lt;/strong&gt;.😱&lt;/p&gt;

&lt;p&gt;To enable the new version of that PWA, you must &lt;strong&gt;close the browser's "all" tabs&lt;/strong&gt; that is opening that PWA site once. After closing them once and reopening that PWA site, you will see the contents of the new version of that PWA. &lt;/p&gt;
&lt;h2&gt;
  
  
  🤔 This behavior is "by design"
&lt;/h2&gt;

&lt;p&gt;This is by design come from web browser's specifications and from the default implementation of the service worker JavaScript that is generated by the project template of Visual Studio or .NET SDK.&lt;/p&gt;

&lt;p&gt;First of I should explain is a service worker will be updated to a new one "atomically". When a web browser detects a new version of service worker JavaScript code, a web browser will load it but makes it remain in the "waiting to activate" status. This is a standard web platform behavior. A service worker is a process shared by all browser tabs and control caching mechanism offline support. Therefore if its update mechanism is not "atomically", it will cause critical confusion, such as different tabs of the same PWA different behave or the page contents of the current version being partially contaminated by the new versions' contents.&lt;/p&gt;
&lt;h2&gt;
  
  
  ⚙️ Update to the next version without closing all tab
&lt;/h2&gt;

&lt;p&gt;However, in some cases, you might need to make the PWA site able to update to the next version immediately without closing all tabs.&lt;/p&gt;

&lt;p&gt;In this case, the better practice is notifying the next version is ready to users via its user interface. And if users respond to that UI to update to the next version, your service worker can be implemented to do that via &lt;a href="https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting" rel="noopener noreferrer"&gt;the &lt;code&gt;skipWaiting()&lt;/code&gt; method&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, the implementation will be complicated a little.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;To be able to invoke the &lt;code&gt;skipWaiting()&lt;/code&gt; method in the service worker process from outside of the service worker process, such as user interaction code, implement service worker JavaScript to handle the specific "message" and call the &lt;code&gt;skipWaiting()&lt;/code&gt; method in that message handler.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Implement the user interface that will show a message such as "The next version is ready" and a button such as "Update now". And make that UI send the specific message to the service worker process to invoke the &lt;code&gt;skipWaiting()&lt;/code&gt; method of the service worker process when users click the button.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handle the promise callback of the &lt;code&gt;navigator.serviceWorker.register&lt;/code&gt; method.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;if there is a "waiting" service worker process (that is the new version), then notify the next version is ready to users via UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;handle the &lt;code&gt;updatefound&lt;/code&gt; event of the service worker registration object. If the &lt;code&gt;updatefound&lt;/code&gt; event is fired, handle the &lt;code&gt;statechange&lt;/code&gt; event of the service worker process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the &lt;code&gt;statechange&lt;/code&gt; event of the service worker process is fired, then check the current status of that service worker process. If the status is "installed", then notify the next version is ready to users via UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the status is "activated", that means users respond to the UI, and the &lt;code&gt;skipWaiting()&lt;/code&gt; method in the service worker process is invoked. Therefore reload the current page by calling the &lt;code&gt;window.location.reload()&lt;/code&gt; method.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;...Please don't misunderstand me, but are you keeping up with this explanation? 😅&lt;/p&gt;
&lt;h2&gt;
  
  
  🎉 The "Blazor PWA Updater" NuGet package and component
&lt;/h2&gt;

&lt;p&gt;Fortunately, you don't need to implement all of the code above by yourself. Instead, you can use the &lt;strong&gt;"Blazor PWA Updater"&lt;/strong&gt; NuGet package and its component.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install the &lt;code&gt;Toolbelt.Blazor.PWA.Updater&lt;/code&gt; NuGet package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Register a "PWA updater" service to a service provider at startup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Place a &lt;code&gt;&amp;lt;PWAUpdater&amp;gt;&lt;/code&gt; component somewhere in your Blazor PWA&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the &lt;code&gt;"SKIP_WAITING"&lt;/code&gt; message handler to the &lt;code&gt;service-worker.published.js&lt;/code&gt; file and invoke the &lt;code&gt;skipWaiting()&lt;/code&gt; in that message handler.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replace the script element &lt;code&gt;&amp;lt;script&amp;gt;navigator.serviceWorker.register('service-worker.js');&amp;lt;/script&amp;gt;&lt;/code&gt; inside of the &lt;code&gt;index.html&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;&amp;lt;script src="_content/Toolbelt.Blazor.PWA.Updater.Service/script.min.js"&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's all. After the implementation above, you will get the ability on your Blazor PWA to update to the new version immediately via user interaction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8nhf0yf55xcq0z2z5cc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8nhf0yf55xcq0z2z5cc.gif" width="700" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also customize the appearance of the &lt;code&gt;&amp;lt;PWAUpdater&amp;gt;&lt;/code&gt;, such as notification text, button caption, text color, background color, sizes, and position.&lt;/p&gt;

&lt;p&gt;If you don't prefer the appearance and behavior of the &lt;code&gt;&amp;lt;PWAUpdater&amp;gt;&lt;/code&gt; at all, you can also implement the user interface from scratch with minimal effort.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IPWAUpdaterService&lt;/code&gt; service lived in the &lt;code&gt;Toolbelt.Blazor.PWA.Updater.Service&lt;/code&gt; NuGet package is exposing &lt;code&gt;NextVersionIsWaiting&lt;/code&gt; event  and &lt;code&gt;SkipWaitingAsync()&lt;/code&gt; method. So you can build your user interface with this service by yourself.&lt;/p&gt;

&lt;p&gt;For more details, please visit the GitHub repository of the "Blazor PWA Updater" and read the README of it.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/jsakamoto" rel="noopener noreferrer"&gt;
        jsakamoto
      &lt;/a&gt; / &lt;a href="https://github.com/jsakamoto/Toolbelt.Blazor.PWA.Updater" rel="noopener noreferrer"&gt;
        Toolbelt.Blazor.PWA.Updater
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Provide "Update Now" UI and feature to your Blazor PWA that appears when the next version of one is available.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Blazor PWA Updater&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Toolbelt.Blazor.PWA.Updater/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f416afa402a75be46b5ae4f896567e2b26701f688683c4a4eafb430aafd9b43c/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f762f546f6f6c62656c742e426c617a6f722e5057412e557064617465722e737667" alt="NuGet Package"&gt;&lt;/a&gt; &lt;a href="https://github.com/jsakamoto/Toolbelt.Blazor.PWA.Updater/actions/workflows/unit-tests.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/jsakamoto/Toolbelt.Blazor.PWA.Updater/actions/workflows/unit-tests.yml/badge.svg" alt="unit tests"&gt;&lt;/a&gt; &lt;a href="https://discord.com/channels/798312431893348414/1202165955900473375" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0c6e80c0dbf21e446bc108b06267d5cc5b82a9d49385582fb6ff468b3878381d/68747470733a2f2f696d672e736869656c64732e696f2f646973636f72642f3739383331323433313839333334383431343f7374796c653d666c6174266c6f676f3d646973636f7264266c6f676f436f6c6f723d7768697465266c6162656c3d426c617a6f72253230436f6d6d756e697479266c6162656c436f6c6f723d35383635663226636f6c6f723d67726179" alt="Discord"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📝 Summary&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Provide "Update Now" UI and feature to your Blazor PWA that appears when the next version of one is available.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/jsakamoto/Toolbelt.Blazor.PWA.Updater/main/.assets/fig.001.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fjsakamoto%2FToolbelt.Blazor.PWA.Updater%2Fmain%2F.assets%2Ffig.001.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Supported platforms&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;.NET 8, 9, or later. Both Blazor Server and Blazor Assembly are supported.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🤔 Backgrounds&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Typically, a service worker of PWA is never updated even when updated contents have been deployed to a server, even if you reload the page of that PWA. After the user has navigated away from the PWA in all tabs, updates will complete. This is not specific to Blazor, but rather is a standard web platform behavior.&lt;/p&gt;
&lt;p&gt;For more detail, please see also the following link on the Microsoft Docs site.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/aspnet/core/blazor/progressive-web-app?view=aspnetcore-6.0&amp;amp;tabs=visual-studio#update-completion-after-user-navigation-away-from-app" rel="nofollow noopener noreferrer"&gt;&lt;em&gt;"ASP.NET Core Blazor Progressive Web App (PWA)"&lt;/em&gt; | Miceooft Docs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;However, sometimes, a site owner or a developer may want updates completed as soon as possible. In that case, all we can do is notify the user…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/jsakamoto/Toolbelt.Blazor.PWA.Updater" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  ⚠️ Notice
&lt;/h2&gt;

&lt;p&gt;Please keep in your mind that &lt;strong&gt;kind of these updating notifications sometimes makes users feel annoyed&lt;/strong&gt;, especially if it happens so often.&lt;/p&gt;

&lt;p&gt;If you don't have enough reason to implement such a notification user interface to update the new version immediately, it will be better to keep it by the default implementation.&lt;/p&gt;

&lt;p&gt;❤️&lt;em&gt;Happy coding :)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>pwa</category>
    </item>
    <item>
      <title>3 reasons why I've created yet another library for the downloading on Blazor apps</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Wed, 29 Jun 2022 13:42:52 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/3-reasons-why-ive-created-yet-another-library-for-the-downloading-on-blazor-apps-2cpk</link>
      <guid>https://dev.to/j_sakamoto/3-reasons-why-ive-created-yet-another-library-for-the-downloading-on-blazor-apps-2cpk</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A few days ago, I created a library that provides the ability to implement a download feature on Blazor apps and published it on nuget.org.&lt;/p&gt;

&lt;p&gt;However, maybe you already know we can easily find other libraries on nuget.org that provide the same feature, and those libraries were already published a few years ago.&lt;/p&gt;

&lt;p&gt;Then, why did I create yet another new library even though there are already the same kinds of stuff?&lt;br&gt;&lt;br&gt;
There are three reasons why I did it.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. They are using "eval()"
&lt;/h2&gt;

&lt;p&gt;One of the reasons is that those other libraries are using the &lt;code&gt;eval()&lt;/code&gt; JavaScript function to load a helper JavaScript code into a web browser.&lt;br&gt;&lt;br&gt;
However, the strict level of JavaScript code has recently risen, so I often see the websites that prohibit using the &lt;code&gt;eval()&lt;/code&gt; JavaScript function by the Content-Security-Policy (CSP) HTTP header.&lt;/p&gt;

&lt;p&gt;So I'd felt I wanted to get the library that doesn't use the &lt;code&gt;eval()&lt;/code&gt; JavaScript function.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. They are as a service
&lt;/h2&gt;

&lt;p&gt;Another reason is that those other libraries provide its feature as a "service". In this context, the "service" is the object that will be created from a DI container. So those library users have to register the type of service in that library to a DI container at the startup code inside their "Program.cs" manually.&lt;/p&gt;

&lt;p&gt;But I felt it is boring to add those startup ceremony codes.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. They are optimized performce
&lt;/h2&gt;

&lt;p&gt;The last reason is that those other libraries include tons of codes to optimize the performance of the downloading feature to the limit using the undocumented API. Those great work and code must be deserved to prise. &lt;/p&gt;

&lt;p&gt;But it has increased the size of the library. And I feel ordinary users and use cases don't pursue the speed of the downloading operation. And I think the default implementation of transferring a byte array for the downloading operation on .NET 6 or later is fast enough.&lt;/p&gt;

&lt;p&gt;So I'd felt I wanted to get a simple but small package size library.&lt;/p&gt;
&lt;h2&gt;
  
  
  So I created a new one by myself
&lt;/h2&gt;

&lt;p&gt;So I created a new one by myself.&lt;/p&gt;

&lt;p&gt;"Toolbelt.Blazor.InvokeDownloadAsync"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.nuget.org/packages/Toolbelt.Blazor.InvokeDownloadAsync" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/Toolbelt.Blazor.InvokeDownloadAsync&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  1. 100% &lt;code&gt;eval()&lt;/code&gt; free
&lt;/h3&gt;

&lt;p&gt;It uses the standard &lt;code&gt;InvokeAsync("import",...)&lt;/code&gt; method calling to import helper JavaScript code that is written as an ES module. 100% &lt;code&gt;eval()&lt;/code&gt; free. (Instead, it can not use for apps on .NET Core 3.x.)&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Minimal setup
&lt;/h3&gt;

&lt;p&gt;It is an extension method for the &lt;code&gt;IJSRuntime&lt;/code&gt; interface. You can use the downloading feature immediately if there is a JavaScript runtime object. You don't need to write any code in your startup.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Small package size
&lt;/h3&gt;

&lt;p&gt;It doesn't have massive code optimized for performance. It is simple and small content size.&lt;/p&gt;
&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;Once you added the package to your Blazor app project,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Toolbelt.Blazor.InvokeDownloadAsync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can use the &lt;code&gt;InvokeDownloadAsync()&lt;/code&gt; extension method for &lt;code&gt;IJSRuntime&lt;/code&gt; interface to start to download the contents bytes, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@using&lt;/span&gt; &lt;span class="n"&gt;Toolbelt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blazor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Extensions&lt;/span&gt;
&lt;span class="n"&gt;@inject&lt;/span&gt; &lt;span class="n"&gt;IJSRuntime&lt;/span&gt; &lt;span class="n"&gt;JSRuntime&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;@code&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;BeginDownloadAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="c1"&gt;// 👇"InvokeDownloadAsync()" ex method&lt;/span&gt;
    &lt;span class="c1"&gt;//   starts to download the contents bytes.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeDownloadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s"&gt;"Foo.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;pictureBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all. 👍&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These are the reasons why I have created a new library, even though we are halfway through 2022.&lt;/p&gt;

&lt;p&gt;Happy coding! :)&lt;/p&gt;

</description>
      <category>blazor</category>
    </item>
    <item>
      <title>Run JavaScript unit tests for a Razor Class Library on Visual Studio 2022 Test Explorer</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Fri, 06 May 2022 12:42:47 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/run-javascript-unit-tests-for-a-razor-class-library-on-visual-studio-2022-test-explorer-2n2k</link>
      <guid>https://dev.to/j_sakamoto/run-javascript-unit-tests-for-a-razor-class-library-on-visual-studio-2022-test-explorer-2n2k</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Since Visual Studio 2022, JavaScript unit tests have been able to be discovered and run on Test Explorer of Visual Studio easily. That is much more convenient for developing Razor Class Library for Blazor. When I was developing a Razor Class Library that contains JavaScript helper code for use from C# code, I often wanted to execute unit tests for both C# code and JavaScript code. Visual Studio 2022 has made me able to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to do it?
&lt;/h2&gt;

&lt;p&gt;Suppose you already have a unit test project for a Razor Class Library, particularly for the C# code. In that case, you can make that unit test project do tests for JavaScript code additionally just by adding a few steps like below.&lt;/p&gt;

&lt;p&gt;1) Add the &lt;code&gt;&amp;lt;JavaScriptTestFramework&amp;gt;&lt;/code&gt; property in your unit test project file (.csproj) to specify which JavaScript unit test framework you use. And also, add the &lt;code&gt;&amp;lt;JavaScriptTestRoot&amp;gt;&lt;/code&gt; property to specify the relative path where unit test codes are placed.&lt;/p&gt;

&lt;p&gt;The following example shows using "mocha", one of the well-known JavaScript test frameworks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- A .csproj file of your unit test project --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;JavaScriptTestFramework&amp;gt;&lt;/span&gt;Mocha&lt;span class="nt"&gt;&amp;lt;/JavaScriptTestFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;JavaScriptTestRoot&amp;gt;&lt;/span&gt;tests\&lt;span class="nt"&gt;&amp;lt;/JavaScriptTestRoot&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Configure your JavaScript unit test environment. For example, to use "mocha", execute the following commands in a terminal console on the test project directory.&lt;/p&gt;

&lt;p&gt;(As a premise, Node.js must be installed in your environment.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; mocha
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And please remember to exclude the "node_modules" folder from the unit test project like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- A .csproj file of your unit test project --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
  &lt;span class="c"&gt;&amp;lt;!-- 👇 exclude the "node_modules" folder for any purpose. --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Compile&lt;/span&gt; &lt;span class="na"&gt;Remove=&lt;/span&gt;&lt;span class="s"&gt;"node_modules\**"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;EmbeddedResource&lt;/span&gt; &lt;span class="na"&gt;Remove=&lt;/span&gt;&lt;span class="s"&gt;"node_modules\**"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;None&lt;/span&gt; &lt;span class="na"&gt;Remove=&lt;/span&gt;&lt;span class="s"&gt;"node_modules\**"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Write your unit test code according to the JavaScript unit test framework you use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./tests/fooTest.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fooTest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo is bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;this test will alwasy pass.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After doing that, you will see those JavaScript unit tests on the Test Explorer, and you can run those unit tests in Visual Studio's usual way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4og2952wz1ukhdx6ozlg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4og2952wz1ukhdx6ozlg.png" alt="Running JavaScript unit tests on Visual Studio Test Explorer" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more information, please see also the following link to official document site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.microsoft.com/visualstudio/javascript/unit-testing-javascript-with-visual-studio?view=vs-2022&amp;amp;tabs=mocha" rel="noopener noreferrer"&gt;https://docs.microsoft.com/visualstudio/javascript/unit-testing-javascript-with-visual-studio?view=vs-2022&amp;amp;tabs=mocha&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Write test codes by TypeScript
&lt;/h2&gt;

&lt;p&gt;Of course, we can write those JavaScript unit test codes by TypeScript. For example, to do that, execute the following steps.&lt;/p&gt;

&lt;p&gt;1) Install the "Microsoft.TypeScript.MSBuild" NuGet package into the unit test project. One of the ways to do that is by executing the &lt;code&gt;dotnet add package&lt;/code&gt; command on the unit test project directory like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; dotnet add package Microsoft.TypeScript.MSBuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Add type definition files (.d.ts) for TypeScript code that you need, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @types/mocha @types/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Add the "tsconfig.json" file with the following contents into the unit test project directory to configure the behavior of the TypeScript compiler. (Please tweak the "target" configuration according to your needs.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;./tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2017"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CommonJS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compileOnSave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After doing that, you can write unit test codes by TypeScript like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./tests/fooTest.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mocha&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fooTest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo is bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;this test will always pass.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem - incompatible module system
&lt;/h2&gt;

&lt;p&gt;By the way, I also write a helper JavaScript code inside a Razor Class Library ― that will be a target of unit tests in this scenario ― by TypeScript.&lt;/p&gt;

&lt;p&gt;And the type of module system of them &lt;strong&gt;must be "ES Module"&lt;/strong&gt;, not "CommonJS", because those JavaScript codes will be invoked from Blazor applications by using &lt;code&gt;IJSRuntime.InvokeAsync&amp;lt;IJSObjectReferense&amp;gt;("import", &amp;lt;the path of .js file&amp;gt;)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;tsconfig.json&lt;/code&gt; in a Razor Class Library project will be like the following configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tsconfig.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Razor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Library&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;project&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2015"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;👈&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CommonJS.&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the compiled JavaScript code, for example, the "CatClass", will be the following code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// catClass.js in the Razor Class Library project&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CatClass&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;meows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Meow!!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But unfortunately, &lt;strong&gt;"mocha" doesn't work with ES Module mode&lt;/strong&gt; on Visual Studio expectedly. &lt;br&gt;
I will omit the details, but anyway, you must configure unit test codes for "mocha" that are available for Visual Studio Test Explorer to CommonJS mode.&lt;/p&gt;

&lt;p&gt;For example, if you have the test code like below that imports the "catClass.js" as a test target,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./tests/catClassTest.ts in the unit test project&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CatClass&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../../RazorClassLibrary1/catClass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CatClass&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meows&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CatClass&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Meow!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;meows&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you will run into the error &lt;strong&gt;"SyntaxError: Unexpected token 'export'"&lt;/strong&gt; when  Visual Studio tries to detect unit tests like the above.&lt;br&gt;
(In fact, "mocha" itself can work with ES Module mode since ver.9. But there is no way to enable ES Module mode of "mocha" when Visual Studio detects or runs tests so that it will fail.)&lt;/p&gt;

&lt;p&gt;What should I do?&lt;/p&gt;
&lt;h2&gt;
  
  
  My solution - it's complicated but works
&lt;/h2&gt;

&lt;p&gt;Fortunately, those my JavaScript codes are all written by TypeScript source codes.&lt;br&gt;
So I decided to resolve that problem by compiling all TypeScript files by the "CommonJS" module mode into under the unit test project folder, not only files for the unit test but also for files for the test target.&lt;/p&gt;

&lt;p&gt;Let's take a look at what I did.&lt;/p&gt;

&lt;p&gt;First of all, I placed the unit test TypeScript files in the root folder of the unit test project.&lt;br&gt;
This is important for compiling at once TypeScript files, including both unit test code and library code (test target).&lt;br&gt;
Of course, I also tweaked the path of importing the test target module (TypeScript file) in the unit test TypeScript files, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// move the ./tests/catClassTest.ts to ./catClassTest.ts&lt;/span&gt;
&lt;span class="c1"&gt;//   in the unit test project&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;// 👇 Fix the path of importing the test target module.&lt;/span&gt;

&lt;span class="c1"&gt;//import { CatClass } from "../../RazorClassLibrary1/catClass";&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CatClass&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../RazorClassLibrary1/catClass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I added two options to the configuration of the TypeScript compiler "tsconfig.json" in the root folder of the unit test project.&lt;br&gt;
One of them is adding the folder of the Razor Class Library (it is the test target) to the compile target folder.&lt;br&gt;
And another one is the configuration of the output folder explicitly to follow the &lt;code&gt;&amp;lt;JavaScriptTestRoot&amp;gt;&lt;/code&gt; property value in the unit test project file (.csproj).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;./tsconfig.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;project&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;👇&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;these&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;two&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;options.&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDirs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"../RazorClassLibrary1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After I made the above changes and rebuilt the entire solution, I got the result like the following figure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqq5zj5ulg5v9u31q0rh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqq5zj5ulg5v9u31q0rh.png" alt="Explain the result of TypeScript compiling" width="599" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I've been able to run the JavaScript unit tests properly on Visual Studio's Test Explorer! 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsgnw928wkzy68h5hu0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcsgnw928wkzy68h5hu0a.png" alt="Running JavaScript unit tests on Visual Studio Test Explorer" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The ability to run unit tests both for C# and for JavaScript at once on Visual Studio Test Explorer UI is very convenient for me when developing a Razor Class Library for Blazor.&lt;/p&gt;

&lt;p&gt;But the JavaScript test framework "mocha" can work only in CommonJS module mode on Visual Studio Test Explorer, even though the module type of the test target JavaScript code is ES Module.&lt;/p&gt;

&lt;p&gt;Fortunately, I always write JavaScript code by TypeScript, so I resolved that problem by compiling test target TypeScript files to JavaScript files twice with different module modes ("ES Module" and "CommonJS").&lt;/p&gt;

&lt;p&gt;The sample code has been published on the GitHub repository at the following link:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sample-by-jsakamoto/Runs-JavaScript-tests-on-Visual-Studio-2022-Test-Explorer" rel="noopener noreferrer"&gt;https://github.com/sample-by-jsakamoto/Runs-JavaScript-tests-on-Visual-Studio-2022-Test-Explorer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, actually, I'm not sure whether my solution is the best.&lt;br&gt;
Really Is there no way to run "mocha" in ES Module mode on Visual Studio Test Explorer?&lt;br&gt;
Or else another JavaScript framework, such as Jest, Jasmine, etc., might allow me to run it in ES Module mode?&lt;/p&gt;

&lt;p&gt;If you have a better solution or idea, I appreciate it if you share that.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Learn, Practice, Share.&lt;/em&gt; :)&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>visualstudio</category>
    </item>
    <item>
      <title>Use Selenium to automate IE mode in Microsoft Edge on Windows 11</title>
      <dc:creator>jsakamoto</dc:creator>
      <pubDate>Mon, 21 Mar 2022 12:55:22 +0000</pubDate>
      <link>https://dev.to/j_sakamoto/use-selenium-to-automate-ie-mode-in-microsoft-edge-on-windows-11-2nad</link>
      <guid>https://dev.to/j_sakamoto/use-selenium-to-automate-ie-mode-in-microsoft-edge-on-windows-11-2nad</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Today, the latest stable version of Windows OS (It is Windows 11 as of the writing of this article) no longer supports Internet Explorer as a standalone application.&lt;/p&gt;

&lt;p&gt;However, Internet Explorer hasn't died yet.&lt;/p&gt;

&lt;p&gt;Internet Explorer is still alive inside Microsoft Edge browser as "IE mode".&lt;/p&gt;

&lt;p&gt;So there is still worth automating IE mode in Microsoft Edge for E2E testing by "Selenium" with Internet Explorer Driver.&lt;/p&gt;

&lt;p&gt;There is even official Microsoft documentation for automating IE mode in Microsoft Edge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/ie-mode" rel="noopener noreferrer"&gt;https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/ie-mode&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In some cases, the official document is not enough...
&lt;/h2&gt;

&lt;p&gt;However, in some cases, you may run into runtime errors at automating IE mode in Microsoft Edge browser by Selenium even you certainly follow the steps in the official document.&lt;/p&gt;

&lt;p&gt;In my case, I was using Selenium .NET binding with C# on Windows 11, I had to do additional two steps as the following list to make it works fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set the &lt;code&gt;IgnoreZoomLevel&lt;/code&gt; property of the &lt;code&gt;InternetExplorerOptions&lt;/code&gt; object to &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set the &lt;code&gt;Turn on Protected Mode&lt;/code&gt; local group policy to &lt;code&gt;Enable&lt;/code&gt; for all zones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will explain more details of the above in the following sections of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignore the zoom level
&lt;/h2&gt;

&lt;p&gt;At first, the C# code I wrote was like below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InternetExplorerDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InternetExplorerOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AttachToEdgeChrome&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;EdgeExecutablePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The C# code above is equivalent to the official document's code.&lt;/p&gt;

&lt;p&gt;But unfortunately, I ran into an error.&lt;/p&gt;

&lt;p&gt;As I mentioned above, I was using Selenium .NET binding, so I saw the unhandled exception message on my application's console as below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Started InternetExplorerDriver server (32-bit)
4.0.0.0
Listening on port 54479
Only local connections are allowed

Unhandled exception. 
System.InvalidOperationException: 
Unexpected error launching Internet Explorer. 
Browser zoom level was set to 175%. It should be set to 100% (SessionNotCreated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exception message said, "the zoom level of the browser must be 100%, but it was not".&lt;/p&gt;

&lt;p&gt;However, I've never changed the zoom level, and the Edge browser zoom level indicated 100% in its menu at that time.&lt;/p&gt;

&lt;p&gt;Therefore, finally, I decided to make Selenium ignore the zoom level forcibly.&lt;/p&gt;

&lt;p&gt;We can do that by the &lt;code&gt;IgnoreZoomLevel&lt;/code&gt; property of the &lt;code&gt;InternetExplorerOptions&lt;/code&gt; object to &lt;code&gt;true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InternetExplorerDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InternetExplorerOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AttachToEdgeChrome&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;EdgeExecutablePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;IgnoreZoomLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// 👈 Added this line.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Turn on "Protected Mode" of IE... on Windows 11!
&lt;/h2&gt;

&lt;p&gt;Unfortunately, even I avoided 1st error above, and I ran into 2nd error.&lt;/p&gt;

&lt;p&gt;The console displayed the error message like below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Started InternetExplorerDriver server (32-bit)
4.0.0.0
Listening on port 56568
Only local connections are allowed

Unhandled exception. 
OpenQA.Selenium.WebDriverException: 
The HTTP request to the remote WebDriver server for URL http://localhost:56568/session/c81cd23a-2f60-45ae-9636-ebd27fea9c22/url timed out after 60 seconds.
 ---&amp;gt; System.Threading.Tasks.TaskCanceledException: 
 The request was canceled due to the configured HttpClient.Timeout of 60 seconds elapsing.
 ---&amp;gt; System.TimeoutException: The operation was canceled.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message said the operation of the browser was timed out.&lt;br&gt;
And when I saw that error message, I was immediately reminded that the error might be related to IE's "Protected Mode" configuration.&lt;/p&gt;

&lt;p&gt;That means we have to set IE's "Protected Mode" configuration to "Enable" for all zones before automating IE.&lt;/p&gt;

&lt;p&gt;That requirement is also mentioned in the official documents of Selenium.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.selenium.dev/documentation/ie_driver_server/#required-configuration" rel="noopener noreferrer"&gt;https://www.selenium.dev/documentation/ie_driver_server/#required-configuration&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;you must set the Protected Mode settings for each zone to be the same value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Therefore I opened the "Internet Properties" dialog from the Control Panel in a hurry for enabling IE's "Protected Mode".&lt;br&gt;
But the "Enable Protected Mode" check box did not exist in the dialog... because &lt;strong&gt;it is Windows 11!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nhxc5o7yf8sen1072uw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nhxc5o7yf8sen1072uw.png" alt="the " width="677" height="891"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I stood there in confusion for a while, but I pulled myself together and started searching on the Internet to resolve this problem.&lt;/p&gt;

&lt;p&gt;Before long, fortunately, I found the answer in the article written in 2016 below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.urtech.ca/2016/01/solved-how-to-disable-protected-mode-in-internet-explorer-using-gpo/" rel="noopener noreferrer"&gt;https://www.urtech.ca/2016/01/solved-how-to-disable-protected-mode-in-internet-explorer-using-gpo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Actually, we could control the IE's "Protected Mode" by Local Group Policy.&lt;/p&gt;

&lt;p&gt;So I opened Group Policy Editor (&lt;code&gt;gpedit.msc&lt;/code&gt;), set the "Protected Mode" policy of all zones to "Enable", and applied it immediately by performing the &lt;code&gt;gpupdate /force&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z4g86b87f5vc41a5abx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0z4g86b87f5vc41a5abx.png" alt="Group Policy Editor" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After I did all of them, my C# code that is automating IE mode in Microsoft Edge had been started to work fine. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope this article will save time for developers who have to automate IE mode in Microsoft Edge by Selenium and IE Driver.&lt;/p&gt;

&lt;p&gt;Learn, Practice, Share. :)&lt;/p&gt;

</description>
      <category>selenium</category>
      <category>iedrive</category>
    </item>
  </channel>
</rss>
