<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss version="2.0">
  <channel>
    <title>Rick Strahl's Web Log</title>
    <link>https://weblog.west-wind.com/</link>
    <image>
      <url>ImageUrl</url>
      <title>Rick Strahl's Weblog</title>
      <link>https://weblog.west-wind.com/</link>
    </image>
    <description>Wind, waves, code and everything in between</description>
    <copyright>(c) West Wind Technologies 2006-2026</copyright>
    <pubDate>2026-06-14T01:23:50.3036054Z</pubDate>
    <lastBuildDate>2026-06-13T16:57:03.640241Z</lastBuildDate>
    <generator>Rick Strahl's West Wind Weblog</generator>
    <xhtml:meta xmlns:xhtml="http://www.w3.org/1999/xhtml" content="noindex" name="robots"/><item>
      <title>Creating a Packaged Single File Web Site Viewer Executable</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/Banner.jpg" alt="Banner"></p>
<p>This post falls squarely into the stupid pet tricks category, but I'm going to post about it anyway, because it was an interesting experiment in dynamically creating a single file, self-contained Windows executable that contains the executable, data and a native dependency.</p>
<p>This is about as edge case as it gets, but let me give you my scenario and why I decided to tackle this, foolishly thinking that this would be a quick afternoon project (punchline: it wasn't).</p>
<p>I have several Web based documentation solutions that I both publish publicly and also use for specific customer projects. Essentially these are Web site generator tools with a specific front end for creating the documentation that makes up the final documentation Web output. Because I deal quite a bit with legacy customers one issue that frequently comes up is the ability to take documentation offline while traveling or otherwise being disconnected from the Internet.</p>
<p>My documentation tools (and many others) generate base Html output for documentation in the form of a static Web site, and the online version of that is typically adequate for the majority of users. But a frequent option is for some form of offline output in some portable format like a PDF file or a self-contained Html document, that can be easily shared and used offline. While PDF or Html documents work and let you access the content in document form, it's more like a book - sequential and not very interactive. Web sites tend to offer a more interactive experience and in most cases a much more attractive reading experience due to richer layout and styling that isn't possible in PDF.</p>
<p>That got me to thinking: It would be nice to create a single file EXE package offline Web viewer, that can run the Web site locally instead. So kind of like a CHM file of old, but it's running the full documentation (or the way it's built - any dynamic Web site).</p>
<p><a href="https://markdownmonster.west-wind.com?ut=weblog"  target="_blank"
	  title="Markdown Monster - Easy to use, yet powerfully productive Markdown Editing for Windows">
<img src="https://weblog.west-wind.com/images/sponsors/banner-example.png?v=1.2" class="da-content-image" />
</a></p>
<h2 id="webpackageviewer">WebPackageViewer</h2>
<p>What I came up with is WebPackageViewer, which you can find here:</p>
<ul>
<li><a href="https://github.com/RickStrahl/WebPackageViewer">WebPackageViewer on GitHub</a></li>
</ul>
<p>WebPackageViewer is essentially a dual purpose application:</p>
<ul>
<li>An Exe Packer that can pack and unpack a Web Site as Zip file into an Exe and then run into a built-in viewer application.</li>
<li>That same application can also run with content from disk directly as a Web Site Viewer without the packaging</li>
</ul>
<p>In practice I use this tool as part of my <a href="https://documentationmonster.com">Documentation Monster</a> documentation solution as one of the output options which packages the generated Documentation Web Site into an self-contained Exe package:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/PublishingasSelfContainedExe.png" alt="Publishing as Self Contained Exe">
<small><strong>Figure 1</strong> - Generating an Exe as part of  publishing options for a documentation solution.</small></p>
<p>This generates a self-contained Windows Exe that essentially lets you run the Web site in an application locally. Here's what the Viewer looks like running a packaged Web site:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/DocumentationMonsterWebViewer.png" alt="Documentation Monster Web Viewer">
<small><strong>Figure 2</strong> - The Documentation Monster Web Viewer packages the generated Html Web site into a single Exe that can be run offline.</small></p>
<p>You can see what that looks like <a href="https://markdownmonster.west-wind.com/docs/MarkdownMonster-Documentation-Viewer.exe">here</a>.<br>
<small><em>(you might get a SmartScreen warning - more on that in a minute)</em></small></p>
<h2 id="how-does-the-web-packaging-work">How does the Web Packaging work?</h2>
<p>What you're looking at in <strong>Figure 2</strong> is a Web site that's launched off a single file Exe shell: The large single file Exe bundles both the executable and the entire zipped up Web site. The packaged Exe then serves both as an unpackager and viewer, running the unpackager first and then launching the viewer using the same executable.</p>
<p>When it runs, the Exe unpackages the embedded Zip file data into a temporary folder, copies the Exe (<em>ie. itself!</em>) into that folder and then relaunches itself in that folder to run the Web Site from that folder. When the app shuts down the temp folder is deleted.</p>
<p>The Exe itself is implemented using a Windows WPF application with a full screen WebView control and uses <strong>virtual folder hosting</strong> rather than an actual Http server to 'run' the Web site in the internal WebView browser. Virtual folder hosting eliminates the potential security issues around local port assignment or exposing an Http endpoint locally.</p>
<p>The Viewer application behaves just as it would inside of a browser, but there's no address bar, menus and widgets and so, it's only the raw Web browser.</p>
<p>Unlike a PDF or Html export, you get a fully interactive site that allows for search and navigation the same way as the online Web site but it works completely locally and offline if needed.</p>
<p>Cool! All of this works smoothly... but, and there's always a butt... 😂</p>
<h2 id="the-fly-in-the-oinment-windows-smartscreen">The Fly in the Oinment: Windows SmartScreen</h2>
<p>This is where the stupid pet tricks part kicks in: When you download an arbitrary Exe from the Internet, Windows security gets real dodgy and wants to protect you:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/WindowsSmartScreenWarning.jpg" alt="Windows SmartScreen pops up on Install from Download">
<small><strong>Figure 3</strong> - Downloading the Documentation Exe comes with warnings!</small></p>
<p>This is not unexpected as <strong>Windows Defender Smartscreen</strong> essentially flags any unsigned, downloaded binary or a binary embedded in a downloaded archive as possibly suspect code. It is possible to bypass SmartScreen by clicking <strong>More info</strong> and then <strong>Run anyway</strong> but it's not exactly a smooth user experience to scare users with a blue screen of doom.</p>
<blockquote>
<p>You can help your chances of not getting a SmartScreen dialog by signing the final executable generated, but unless you're a software developer that already is publishing and signing binary distributions, it's unfortunately not a common thing to have available, nor is the process of setting up a certificate for signing especially simple. But, if you have a certificate and process to sign - you should definitely use it on any generated Exe.</p>
</blockquote>
<p>The security problems are two-fold:</p>
<ul>
<li><p><strong>The downloaded Exe is not signed</strong><br>
Unsigned downloaded or Zip file downloaded Exe's are <strong>always</strong> flagged by Windows SmartScreen. Because the file is generated generically signing is not an option, and...</p>
</li>
<li><p><strong>Reputation is going to be low</strong><br>
SmartScreen works of install reputation, and a custom generated file is unlikely to gain real download and install traction to improve SmartScreen reputation. So even a signed executable - although better - is likely to still show the SmartScreen dialog.</p>
</li>
</ul>
<p>Unfortunately there are no real solutions around this that I could find other than to warn users of the issue and prepare them for bypassing the SmartScreen dialog.</p>
<h3 id="local-installs-are-fine">Local Installs are Fine</h3>
<p>Note that SmartScreen mostly affects <strong>downloaded files</strong> and does not kick in if you install an executable as part of an application install. SmartScreen triggers mostly of MOTW (Mark of the Web), so if you install the Exe via a local installer then there's no MOTW on the Exe and SmartScreen most likely won't be a problem (it can still be based on policy or many failures but that's more rare).</p>
<p>Bottom line: If you plan on downloading these generated packaged Web Viewers you'll likely have to deal with SmartScreen.</p>
<h2 id="exe-packaging-in-net">Exe Packaging in .NET</h2>
<p>FWIW, I was aware of the SmartScreen issue before I started building the packager - mostly because I thought it would be easy to build the packager and for the few people that always pester about offline documentation they would probably be willing to deal with the SmartScreen issue.</p>
<p>What I ended up building is <code>WebPackageViewer.exe</code> which is a generic tool to package WebPackageViewer and a data package into a single Exe. The viewer includes a Web UI application that handles unpacking the data content and then running the Web site inside of the WebView.</p>
<p>There are two parts to this application:</p>
<ul>
<li>The Packager/Unpackager CLI Tool</li>
<li>The WebViewer UI application</li>
</ul>
<h3 id="cli-mode">CLI Mode</h3>
<p>In CLI mode you can create a package by running <code>WebPackageViewer.exe</code> like this:</p>
<pre><code class="language-ps">.\webPackageViewer.exe package  `
		--output .\MarkdownMonster-Docs-Viewer.exe `
		--zipfolder &quot;c:\Web Sites\markdownmonster.west-wind.com&quot; 
</code></pre>
<p>Here's what that looks like:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/CliRunningThePackager.png" alt="Cli Running The Packager"><br>
<small><strong>Figure 4</strong> - Using the Packager to create a self-contained Exe</small></p>
<p>This generates a single file Exe:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/PackagedFileAndPackagerOnDisk.png" alt="Packaged File And Packager On Disk"><br>
<small><strong>Figu
<img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/WindowsSmartScreenWarning.jpg" alt="Windows Smart Screen Warning">
re 5</strong> - The generated output file can be fairly large as it contains all the Zipped up Web site Html and support assets. The packager is relatively small.</small></p>
<p>The base executable <code>WebViewPackager.exe</code> can also be used as a standalone static Web site viewer by running the EXE out of a  folder with Html files. By default it looks for <code>index.html</code> and if it exists displays that in the internal browser.</p>
<p>There are lots of command line options that allow you to customize</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/CommandLineOptions.png" alt="Command Line Options"><br>
<small><strong>Figure 6</strong> - Command line options for the packager, unpackager, and viewer</small></p>
<p><a href="https://documentationmonster.com"  target="_blank"
		  title="Documentation Monster - creating documentation one Markdown at a time">
<img src="https://weblog.west-wind.com/images/sponsors/DocumentationMonster-Display.jpg" class="da-content-image" alt="Documentation Monster - creating documentation one Markdown at a time" />
</a></p>
<h2 id="implementation-in-net">Implementation in .NET</h2>
<p>At the outset I mentioned that I thought this would be a quickie project and the the first run of it ended up being pretty simple. But the devil is in the details and there are lot of little gotchas to watch out for that I didn't run into until I started publishing a few documentation sites.</p>
<h3 id="how-it-works">How it works</h3>
<p>The main premise of this tool is the packaging of the Exe with both the main executable as well as the Web site data which lives in a zip file.</p>
<h4 id="net-framework-executable">.NET Framework Executable</h4>
<p>The first thing about this project is that it's a .NET Framework project rather than a .NET Core project. The reason for this is to create a tiny self-contained Executable for the application that has no framework dependence and - using Windows WPF in this case. .NET Core Single File AOT compilation produces large files when frameworks get involved and wouldn't work with WPF anyway - so in this case I specifically chose .NET framework since it's available on all semi-modern Windows versions in use.</p>
<h4 id="exe-file-packaging">Exe File Packaging</h4>
<p>A Windows Exe file contains a PE header that describes the EXE and its length. When the Exe loads it loads the expected file size into memory. There's a lot more to this, but I'm simplifying to what's relevant here. You can however append more data to the end of the Exe file by simply writing more data after the Exe's data stream using standard file writing APIs. After you do this the Exe still works, but the Exe file now contains additional data that you can access.</p>
<blockquote>
<p>Note if an Exe is Authenticode signed, and you append data to it you'll break the validity of the signature. For this reason the original <code>WebPackagerViewer.exe</code> is provided in both signed and unsigned versions. <code>-unsigned.exe</code> for packaging, and the signed version for running as a drop in Web Site viewer.</p>
</blockquote>
<p>To make this work, the trick is to write a marker into the binary data that separates the original binary Exe file data and the added content which in this case will be a zip file of the Web site.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/WindowsExeWithAdditionalPayload.jpg" alt="Windows Exe With Additional Payload"><br>
<small><strong>Figure 7</strong> - Packaging an Exe with an additional payload of a Zip file</small></p>
<h4 id="build-first-using-package-command">Build First using <code>package</code> Command</h4>
<p>To build the package you use the CLI tooling I showed earlier using the <code>package</code> command.</p>
<pre><code class="language-ps">.\webPackageViewer.exe package  `
		--output .\MarkdownMonster-Docs-Viewer.exe `
		--zipfolder &quot;c:\Web Sites\markdownmonster.west-wind.com&quot; 
</code></pre>
<p>This creates a packaged Exe that contains the zip file data.</p>
<p>Alternately you can also explicitly create a Zip file using the <code>--zipfile</code> parameter and then attach it to the exe instead. The latter is what Documentation Monster does: It creates a custom zip file that includes a few additional files that don't exist in the original Web site folder that is packaged up. It then uses that zip file as input.</p>
<h4 id="launching-the-packaged-exe-triggers-unpacking">Launching the Packaged Exe triggers unpacking</h4>
<p>Once the packaged Exe has been created, launching it now unpacks the attached Zip file, and copies the Exe to a temporary folder. The original exe then launches the copied Exe in the now unpacked Web Folder and shuts itself down.</p>
<h4 id="the-exe-is-re-launched-in-the-web-folder">The Exe is Re-Launched in the Web Folder</h4>
<p>The Exe in the unpacked folder knows that it's running in an unpacked directory, and now fires up the UI interface to display the Webview control.</p>
<h4 id="webview-uses-manual-handling-for-https-urls">WebView uses Manual Handling for Https Urls</h4>
<p>The WebView control then uses local Web resource request and navigation events to capture content load operations and serves images from the local folder using a form of virtual folder mapping.</p>
<p>The WebView doesn't use a Web Server, but rather uses a form of virtual file mapping to execute requests locally.</p>
<p>The WebView supports a built-in virtual folder mapping, but unfortunately I couldn't use that due to the requirement that previewer has to support virtual subfolders (ie. running out <code>/docs/</code> instead of root <code>/</code>). The WebView virtual hosting feature has no support for auto-routing to a sub-folder Url and so the file translation in a virtual folder doesn't work right.</p>
<p>I ended up having to manually intercept the Navigation events to re-route virtual folder access, and then handle the resource loading manually. Sounds complicated but it turned out to be a surprisingly easy implementation.</p>
<p>You can take a look at how this works on in the source <a href="https://github.com/RickStrahl/WebPackageViewer/blob/50cde3b2705c46a945566bde0e27030cd7f2b7da/WebPackageViewer/MainWindow.xaml.cs#L57">code on GitHub</a>.</p>
<h4 id="file-packaging-and-unpackaging">File Packaging and Unpackaging</h4>
<p>The code to package a file and unpackage it is also relatively simple.</p>
<p>Assuming the Zip file has been created - either externally or letting the packager do it - to write out the packaged Exe, here's the logic that writes out the combined exe and data package executable:</p>
<pre><code class="language-csharp">if (first == &quot;package&quot;)
{
    ConsoleHelper.WriteWrappedHeader(&quot;West Wind Web Package Viewer&quot;);
    Console.WriteLine(&quot;📦 Packaging zip file...&quot;);

    string zipFile = ZipFilename;

    var pack = new FilePackager();
    if (!string.IsNullOrEmpty(ZipFolder))
    {
        zipFile = pack.ZipFolder(ZipFolder);
        if (zipFile == null)
        {
            Console.WriteLine(&quot;❌ Error creating Zip file for package: &quot; + pack.ErrorMessage);
            return;
        }
    }

    if (!pack.PackageFile(Path.GetFullPath(OutputPath),
        Path.GetFullPath(ExeFile),
        Path.GetFullPath(zipFile)))
    {
        Console.WriteLine(&quot;❌ Error creating package: &quot; + pack.ErrorMessage);
        return;
    }
    Console.WriteLine(&quot;✅ Package has been created:&quot;);
    ColorConsole.WriteLine(OutputPath, ConsoleColor.DarkYellow);
    return;
}
</code></pre>
<p>If you use the command like with the <code>package</code> command and the <code>--zipfolder</code> parameter, the given folder is directly written into a Zip file using the <code>ZipFile</code> class. Remember this is a .NET Framework executable so we have to stick to the older more limited implementation that doesn't have a lot of options, so I grab all files first, then add some additional files.</p>
<p>The actual packaging is also straight forward:</p>
<pre><code class="language-csharp">public bool PackageFile(string packageFilename, string exeFilename, string dataFilename)
{  
	...
	
    if (File.Exists(packageFilename))           
        File.Delete(packageFilename);

    using (var outFs = new FileStream(packageFilename, FileMode.Create, FileAccess.Write))
    {
        using (var fs = new FileStream(exeFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            fs.CopyTo(outFs);
            outFs.Flush();
            outFs.Write(SeparatorBytes, 0, SeparatorBytes.Length);
        };
        using (var fs = new FileStream(dataFilename, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            fs.CopyTo(outFs);
        }
    }
 
    // optionally sign the new exe
    if (!string.IsNullOrEmpty(App.CommandLine.SignCommand))
    {
        var cmd = App.CommandLine.SignCommand.Replace(&quot;%1&quot;, &quot;\&quot;&quot; + packageFilename + &quot;\&quot;&quot;);
        try
        {
            ExecuteCommandLine(cmd, Path.GetDirectoryName(packageFilename), useShellExecute: false);
        }
        catch {}
    }
    return true;
}
</code></pre>
<p>The code creates a new binary file stream and then simple writes first the original Exe, then the marker string as bytes, and finally the zip file.</p>
<p>If you have a certificate the <code>--signcommand</code> allows invoking of a .cmd script (I use a .cmd script that calls a Powershell script because invoking <code>pwsh</code> via string parameter arg values is insane!). Signing will help at least to some extent with SmartScreen.</p>
<p>The packager code is contained in a <a href="https://github.com/RickStrahl/WebPackageViewer/blob/50cde3b2705c46a945566bde0e27030cd7f2b7da/WebPackageViewer/FilePackager.cs#L17">self-contained FilePackager class</a> that is easily re-usable in your own applications for similar tasking.</p>
<h4 id="creating-a-single-exe-and-packaging-a-native-dependency">Creating a Single Exe and 'Packaging' a Native Dependency</h4>
<p>The <code>WebPackageViewer.exe</code> is compiled down into a single file exe. But the actual application does have a few dependencies, so the actual build output looks like this:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/BuildOutputForWebViewPackager.png" alt="Build Output For Web View Packager"><br>
<small><strong>Figure 8</strong> - Build Output from WebView Packager - more than a single file.</small></p>
<p>In order to package up the file into a single EXE a few things need to happen.</p>
<ul>
<li>Run an IL Merge tool to combined the external assemblies</li>
<li>Package up CoreWebView2.dll as a Resource to manually extract at runtime</li>
</ul>
<p>.NET has support for creating a single executable from many package using a tool called ILMerge. Unfortunately that particular tool doesn't work with WPF applications due to the way it packages resources. However, there's a replacement for ILMerge called <a href="https://github.com/gluck/il-repack">ILRepack</a> that is updated to support a number of situations like WPF that ILMerge can't handle.</p>
<p>Using ILRepack, I take the built assembly output and package it down to a single Exe with this Powershell script:</p>
<pre><code class="language-powershell"># uses IlRepack (dotnet tool)
Set-Location &quot;$PSScriptRoot&quot; 
$release = &quot;$PSScriptRoot\..\WebPackageViewer\bin\Release\net472\win-x64&quot;
Write-Host $release
remove-item $release\*.pdb

$windir = $env:windir
$platform = &quot;v4,$windir\Microsoft.NET\Framework64\v4.0.30319&quot;

$version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo(&quot;$release\WebPackageViewer.exe&quot;).FileVersion
$version = $version.Trim()
$originalVersion = $version
&quot;Initial Version: &quot; + $version

# Remove last two .0 version tuples if it's 0
if($version.EndsWith(&quot;.0.0&quot;)) {
    $version = $version.SubString(0,$version.Length - 4);
}
else {
    if($version.EndsWith(&quot;.0&quot;)) {    
        $version = $version.SubString(0,$version.Length - 2);
    }
}
&quot;Truncated Version: &quot; + $version

# dotnet tool install --global dotnet-ilrepack
# Merge Dlls into single EXE - missing WebView2Loader.dll - has to be manually copied
$ilRepackArgs = @(
    '/target:winexe'
    &quot;/targetplatform:$platform&quot;
    &quot;/ver:$originalVersion&quot;
    '/lib:.'
    '/lib:C:\Windows\Microsoft.NET\Framework64\v4.0.30319'
    '/lib:C:\Windows\Microsoft.NET\Framework64\v4.0.30319\WPF'
    '/out:..\WebPackageViewer.exe'
    &quot;$release\WebPackageViewer.exe&quot;
    &quot;$release\Microsoft.Web.WebView2.Core.dll&quot;
    &quot;$release\Microsoft.Web.WebView2.Wpf.dll&quot;
)

Write-Host &quot;------------- IL Repack Arguments -------------&quot;
Write-Host ($ilRepackArgs -join ' ')
Write-Host &quot;-----------------------------------------------&quot;

&amp; ilrepack @ilRepackArgs

remove-item ../WebPackageViewer.exe.config

# copy unsigned copy
copy ../WebPackageViewer.exe ../WebPackageViewer-Unsigned.exe
copy ../WebPackageViewer.exe &quot;\projects\DocumentationMonster\DocumentationMonster\BinSupport\WebPackageViewer.exe&quot;

# sign with my signing script
&amp; &quot;.\signfile.ps1&quot; -file &quot;..\WebPackageViewer.exe&quot;

exit 0
</code></pre>
<p>This creates both a signed and unsigned version of the packager. The signed version can be used to run packager as Web Site viewer by dropping it into a Web site folder, or running with the folder as a parameter.</p>
<pre><code class="language-ps"># If in the Web site directory (or from Explorer in dir)
.\WebPackageViewer 

# with folder
.\WebPackageViewer &quot;c:\Web Sites\markdownmonster.west-wind.com\docs&quot;
</code></pre>
<p>ILRepack solves the issue of packaging the .NET assemblies. If you look back at <strong>Figure 8</strong> you'll see the <code>runtimes</code> folder which contains native dependencies for <code>CoreWebView2Loader.dll</code>.</p>
<p>ILRepack <strong>cannot package</strong> any native dependencies so it's not included in the list of dependencies on the ILRepack arguments list.</p>
<p>Instead, the WebSitePackager project, embeds <code>CoreWebView2Loader.dll</code> as a project resource:</p>
<pre><code class="language-xml">&lt;ItemGroup&gt;
	&lt;Resource Include=&quot;WebPackageViewer.ico&quot; /&gt;
	&lt;Resource Include=&quot;webview2loader.dll&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>which - in a WPF project - gets embedded into a special resource:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/WpfCompiledResources.png" alt="Wpf Compiled Resources"><br>
<small><strong>Figure 9</strong> - The native <code>CoreWebView2Loader.dll</code> is embedded as a .NET resource into the built exe so we can extract it at runtime.</small></p>
<p>WPF resources are not quite like normal .NET resources and you can't simply retrieve resources by accessing the ResourceStream and querying for a value by name. Instead WPF stores all resources in a single resource dictionary that you have to iterate over.</p>
<p>Here's what that looks like:</p>
<pre><code class="language-csharp">public static class ResourceHelper
{
    /// &lt;summary&gt;
    /// Retrieve WebView2Loader.dll which we can't embed into the exe
    /// directly with ILMerge.
    /// &lt;/summary&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    /// &lt;exception cref=&quot;FileNotFoundException&quot;&gt;&lt;/exception&gt;
    public static byte[] LoadWebView2LoaderBytes()
    {
        var asm = typeof(ResourceHelper).Assembly;

        using var resStream =
            asm.GetManifestResourceStream(&quot;WebPackageViewer.g.resources&quot;);

        if (resStream == null)
            throw new FileNotFoundException(&quot;WebPackageViewer.g.resources not found.&quot;);

        using var reader = new ResourceReader(resStream);

        foreach (DictionaryEntry entry in reader)
        {
            if ((string)entry.Key == &quot;webview2loader.dll&quot;)
            {
                using var stream = (Stream)entry.Value;
                using var ms = new MemoryStream();
                stream.CopyTo(ms);
                return ms.ToArray();
            }
        }

        throw new FileNotFoundException(&quot;WebPackageViewer.g.resources not found.&quot;);
    }
}
</code></pre>
<p>So then when the app is running in Viewer mode, we extract the file and dump it out to disk in the same folder as the Exe.</p>
<pre><code class="language-csharp">if (!File.Exists(&quot;WebView2Loader.dll&quot;))
{
    // If the loader is not present, we may be running from a single file bundle and need to unpack first
    try
    {
        var loaderBytes = ResourceHelper.LoadWebView2LoaderBytes();
        File.WriteAllBytes(&quot;WebView2Loader.dll&quot;, loaderBytes);
    }
    catch
    {
        MessageBox.Show(
            &quot;&quot;&quot;
            An error occurred unpacking the WebView2Loader.dll resource.

            Make sure the application is not running from a read-only location and that you have permissions to write to the current directory.

            Alternately manually copy `WebView2Loader.dll` from the same folder as the WebPackageViewer.exe to the current directory and restart the application.`
            &quot;&quot;&quot;,
            &quot;Web Viewer Error&quot;, MessageBoxButton.OK, MessageBoxImage.Exclamation);
        Environment.Exit(1);
    }
}

MainWindow mainWindow = new MainWindow(config);
mainWindow.Show();
</code></pre>
<blockquote>
<h5 id="--runtime-corewebview2loaderdll-copy"><i class="fas fa-info-circle" style="font-size: 1.1em"></i>  Runtime CoreWebView2Loader.dll Copy</h5>
<p>Doing this type of load and copy operation at runtime <strong>requires file write permissions</strong> in the same folder you're running <code>WebPackageViewer.exe</code> or the generated Packaged Exe file out of. If you don't have permissions the app will fail.</p>
<p>Copying a DLL to disk at runtime is also something that Anti-Virus tends to frown upon, but because <code>CoreWebView2Loader.dll</code> is a well-known Dll I have not actually seen this causing problems. Hyper-sensitive 3rd party AV may think differently though than my base Defender setup.
Worst case scenario you can do this once running as Administrator - once the DLL exists it's not copied again.</p>
</blockquote>
<h3 id="user-interface-creation">User Interface Creation</h3>
<p>The user interface of the WebPackageViewer is very simple - it's a form with a WebView on it basically. Initially the form was a basic WPF form with literally nothing else on it.</p>
<p>But if you look closely at the image in <strong>Figure 2</strong> again you'll notice that the window is not actually a standard WPF window, but rather a dark most Fluent type of window.</p>
<p>I was unaware that WPF actually has some built in support for creating self-drawn windows using <code>System.Windows.Shell</code> integration.</p>
<pre><code class="language-xml">&lt;shell:WindowChrome.WindowChrome&gt;
    &lt;shell:WindowChrome
        UseAeroCaptionButtons=&quot;False&quot;
        CaptionHeight=&quot;32&quot;
        ResizeBorderThickness=&quot;4&quot;
        GlassFrameThickness=&quot;0&quot;
        CornerRadius=&quot;12&quot;/&gt;
&lt;/shell:WindowChrome.WindowChrome&gt;
</code></pre>
<p>With the help of CoPilot I managed to make the window theme aware supporting both light and dark mode frames. However, getting that all to work - even with CoPilot on some of the higher end models - took a lot of back and forth. Once you take over the Window rendering yourself a lot of functionality has to be reimplemented. You can see the end result in <a href="https://github.com/RickStrahl/WebPackageViewer/blob/master/WebPackageViewer/MainWindow.xaml">MainWindow.xaml on GitHub</a>. Normally I would use a framework library like <a href="https://mahapps.com/">MahApps</a> or <a href="https://github.com/ControlzEx/ControlzEx">ControlzEx</a> but that would have added yet another set of dependencies resulting in a larger base Exe yet, so this tedious Window implementation was worth the extra effort.</p>
<p><a href="https://goldenbeartshirts.com"  target="_blank"
	  title="Bears and Beer">
<img src="https://weblog.west-wind.com/images/Sponsors/BearsBeer-Display.jpg" class="da-content-image" />
</a></p>
<h4 id="woof-simple-but-not-simple">Woof: Simple but not Simple</h4>
<p>At the end of the day I got what I was looking for packaging Web Content into a single executable as well as - as a side effect - getting a simple small executable that lets me also run static sites locally simply by dropping an exe into the folder.</p>
<p>While there are other more sophisticated tools to run Web sites locally - like my own <a href="https://github.com/RickStrahl/LiveReloadServer">LiveReloadServer</a> - having a small and truly offline viewer is still something that quite a few people are asking for in documentation solutions. I guess some people are still dreaming of the old days of Help and CHM files. 😄</p>
<p>Interestingly enough I started the original planning for this with CoPilot and the original Ask and Plan Mode interactions actually convinced me to even bother with this edge case tool, because it seemed pretty straight forward. But, as is usually the case, the initial AI implementation underwent nearly a complete re-write to work properly to fix the remain 20% that were missing or simply didn't work quite right.</p>
<p>This seems to echo my experience with AI - I have never had LLMs deliver something that is ready to put into production. It's awesome at building a skeleton and base functionality, but I always end up spending much more time cleaning up and fixing the edge cases that aren't obvious. This isn't a criticism BTW - it feels more like working with somebody interactively through the problem rather than letting them solve the problem entirely which is not a bad thing at all. This was certainly not one-shotting, and not even a half and half collaboration, but more like a hit or miss consultation 😄</p>
<p>Creation of this tooling isn't rocket science obviously, but the road to creating something like this is paved with lots of paper cuts resulting in eating up a lot more time than planned!</p>
<p>As I mentioned - this tool serves an edge case that is highly useful to me for several use cases, but I think there are lots of parts to this implementation that can be applied to other scenarios. EXE packaging, ILMerging, Resource hosting of executable code offer some interesting possibilities for shipping self-contained code. Some of you may find value in this.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/RickStrahl/WebPackageViewer">WebPackageViewer on GitHub</a></li>
<li><a href="https://markdownmonster.west-wind.com/blog/posts/2026/Jun/01/Windows-Protected-your-PC-Dealing-with-Windows-SmartScreen-on-Installation">SmartScreen: Windows Protected your PC: Dealing with Windows SmartScreen on Installation</a></li>
</ul>
<div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;">
    <img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;">
    this post was created and published with the 
    <a href="https://markdownmonster.west-wind.com" target="top">Markdown Monster Editor</a> 
</div>
]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Jun/13/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable</link>
      <guid isPermaLink="false">v7x3mwmnhs55</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Jun/13/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Jun/13/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable</guid>
      <pubDate>Sat, 13 Jun 2026 06:57:03 GMT</pubDate>
      <abstract><![CDATA[In this post I discuss how a self-contained Web Site Viewing tool using a single Windows executable that that packages and runs an entire website locally. I discuss the use case and implementation of creating a self-contained Windows executable that can be generated to contain both the executable itself and the data - in this case a zipped up Web site - to use. ]]></abstract>
      <featuredImage>https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/Banner.jpg</featuredImage>
    </item>
    <item>
      <title>Lost ASP.NET Identity Cookies on IIS Application Pool Restarts</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Lost-ASP-NET-Cookies-on-IIS-Restarts/CookieMonsterAttackOnIis.jpg" alt="Cookie Monster Attack On Iis"></p>
<p>Last week I finally updated my blog and moved it to .NET 10 from the ancient WebForms based engine I built 20 years ago. The app is deployed onto a Windows server running IIS and I ran into a snag related to cookie authentication in ASP.NET.</p>
<p>The problem showed up in my Admin panel login where the login would persist across browser sessions, but would not persist across <strong>IIS or ASP.NET application restarts</strong>. In other words, I could sign in and the cookie worked fine for the current session and even in subsequent sessions after shutting down and restarting the browser, but it would eventually fail after an application update, or the nightly scheduled IIS recycle ignoring the full cookie persistence expiration.</p>
<p><a href="https://markdownmonster.west-wind.com?ut=weblog"  target="_blank"
	  title="Markdown Monster - Easy to use, yet powerfully productive Markdown Editing for Windows">
<img src="https://weblog.west-wind.com/images/sponsors/banner-example.png?v=1.2" class="da-content-image" />
</a></p>
<h2 id="encryption-keys">Encryption Keys</h2>
<p>The app uses Cookie Authentication for the administration backend using a custom identity implementation on top of the base ASP.NET Identity APIs. The base Identity implementation in ASP.NET handles the cookie creation and management using internal logistics to encrypt and decrypt the cookie data, which serves both to hide the data as well as ensuring the content is not tempered with.</p>
<p>In the scenario I mention above the problem is that Cookies are re-generating when the machine application or the Application Pool is restarting (which on IIS usually coincides). For the TLDR; crowd the short version is that the Encryption Keys for the application weren't persisting across Application Pool restarts.</p>
<p>But before I get into the why of that lets look at how the ASP.NET Cookie encryption works by default on Windows and IIS and locally in your development environment.</p>
<h3 id="cookie-authentication-101-in-aspnet-core">Cookie Authentication 101 in ASP.NET Core</h3>
<p>Here's a quick review of explicit (non-Identity-Provider) Cookie Authentication in ASP.NET Core, which thankfully has gotten a lot simpler over the many convulsions that were plaguing early Authentication schemes in ASP.Core. These days doing your own Cookie implementation on top of the Identity base layer is pretty easy.</p>
<p>It's a two step process:</p>
<ul>
<li>Configure the Cookie/Identity middleware in the Startup</li>
<li>Sign in and Sign out in your endpoints with a single method call</li>
</ul>
<p>In your app startup set up the auth middleware:</p>
<pre><code class="language-csharp">services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o =&gt;
    {
        o.LoginPath = &quot;/account/login&quot;;
        o.LogoutPath = &quot;/account/logout&quot;;
        o.SlidingExpiration = true;
        o.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0); // overridden by login 
        o.Cookie.Name = &quot;ww_wl&quot;;
    });
</code></pre>
<p>and to enable the middleware:</p>
<pre><code class="language-csharp">// in this order!
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();
</code></pre>
<p>Then in your authentication endpoint logic - a controller or minimal api endpoint or Page logic - you can sign in a user after you've validated their credentials (in my case <strong>not using the Identity provider</strong>) by adding the auth cookie:</p>
<pre><code class="language-csharp">// `user` comes from Db
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
identity.AddClaim(new Claim(&quot;Fullname&quot;, user.Fullname));
identity.AddClaim(new Claim(&quot;Username&quot;, user.Username));
identity.AddClaim(new Claim(&quot;UserId&quot;, user.Id.ToString()));

if (user.IsAdmin)                
    identity.AddClaim(new Claim(ClaimTypes.Role,&quot;Admin&quot;));

// Set cookie and attach claims
await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(identity), 
    new AuthenticationProperties
    {
        IsPersistent = true,
        ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7),
        AllowRefresh = true
    });
</code></pre>
<p>To sign out is also a one-liner:</p>
<pre><code class="language-cs">await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
</code></pre>
<p>With the sign in set you can now get a <code>Context.User.Identity</code> object when a cookie has been set, and you can examine <code>Identity.IsAuthenticated</code> and the individual <code>Claims</code> you added.</p>
<p>Here's the part that's relevant to this post and the lost cookies:</p>
<p>ASP.NET encodes all that information into an Identity cookie and encrypts that whole internally stored package with using a known encryption key when it it is created, and then uses that same key to decrypt the cookie value to restore the Identity associated data. More on this in a second as this is part of the problem I ran into...</p>
<p>Once signed in in my app, once the cookie is set, I can now access the admin panel. The cookie persists and should persist across browser sessions and  - in theory - across application shutdowns.</p>
<p>When I ran this on my local development setup with Kestrel and a regular logged in user everything is hunky dory. It works for all scenarios - including application restarts.</p>
<h2 id="on-server-on-iis-not-so-much">On Server on IIS: Not so much</h2>
<p>I then deployed the app to the server running IIS, and now the browser persistence was working fine even with browser restarts, but an application restart now forced a new login every time. Annoying!</p>
<p>So what gives?</p>
<h3 id="its-not-me---its-you-iis-you-old-bastard">It's not me - it's You! (IIS you old bastard!)</h3>
<p>Turns out I was looking in all the wrong places for the problem. I was looking at the ASP.NET Cookie configuration, which as a I showed above is pretty straight forward - not to many thing you can screw up there. I have several applications that use the <strong>exact same cookie auth set up</strong>, and they work just fine with cookies persisting across restarts. 🤔</p>
<p>After checking and checking and re-checking everything, and even pointing CoPilot at two projects and it confirmed that it couldn't spot a difference that would account for the different behavior either.</p>
<p>CoPilot turned out to be helpful after all, because in a small reasoning side note it mentioned the DataProtection API and key storage location. Although it didn't point at the exact cause - it made me review how encryption keys are generated and used and sure enough that's where the problem turned out to be!</p>
<h3 id="dataprotection-apis-for-cookie-encryption">DataProtection APIs for Cookie Encryption</h3>
<p>The cookies that ASP.NET writes are two-way encrypted and so the keys to read and write have to be available when the cookie is created and then also when it is read.</p>
<blockquote>
<p>More simply put: The underlying key can't change or be 'renewed' in any way between encryption and decryption.</p>
</blockquote>
<p>It turns out that the location where keys are stored is crucial. The location is configurable, but by default this location is stored in the active user's <strong>Windows User Profile</strong>. Aha! 💡</p>
<p><strong>Turns out when you create a new Application Pool in IIS, the User Profile activation is turned off by default!</strong>.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Lost-ASP-NET-Cookies-on-IIS-Restarts/InvalidLoadUserProfileSetting.png" alt="Invalid Load User Profile Setting"></p>
<p>Oddly the default of <code>False</code> shows as a non-default value (ie. it's bolded) in the Application Pool Admin panel.</p>
<blockquote>
<h5 id="--what-does-load-user-profile-do"><i class="fas fa-lightbulb" style="font-size: 1.1em"></i>  What does Load User Profile do?</h5>
<p><em>Load User Profile</em> effectively causes the Application Pool to map environment variables like <code>USERPROFILE</code> and registry keys like <code>HKEY_CURRENT_USER</code>, so that they work as expected against the specific Identity User Profile. For 'dynamic' accounts like ApplicationPoolIdentity a Profile folder is created the first time the AppDomain starts and that profile is persisted after that and behaves the same as a standard user account.</p>
</blockquote>
<p>Here's why this matters: By default, <strong>ASP.NET stores the DataProtection API encryption keys used for Cookie Encryption in the active User Profile</strong>. No user profile, no persistent encryption key storage.</p>
<p>Instead when no User Profile is mapped, a temporary, non-persistent user profile is created when the Application Pool instance is created (so that profile update operations can work without blowing up) which results in new encryption keys getting generated every time the Application Pool starts.</p>
<p>Now, when a previously created cookie comes in it and tries to validate against the new cookie encryption keys, the keys no longer match and the integrity check against the cookie fails, and a new sign in is required. And that's precisely what I saw happening in my Admin Panel access.</p>
<p>There are couple of ways to fix this:</p>
<ul>
<li>Re-enable the User Profile Mapping so your get a persistent User Profile</li>
<li>Explicitly store the encryption keys in a known location</li>
</ul>
<h4 id="enable-the-load-user-profile">Enable the Load User Profile</h4>
<p>The simplest fix then is to set the <strong>Load User Profile</strong> setting in the Application Pool  to <code>True</code> to force using a persistent user profile for the Application Pool Identity account. This works both with existing User and System accounts as well as with dynamic accounts like <code>ApplicationPoolIdentity</code>.</p>
<p>And voila - keys now work across application and IIS restarts.</p>
<p>When you enable the user profile and don't set an explicit DataProtection API location, keys are stored in:</p>
<pre><code class="language-ps">%LOCALAPPDATA%\ASP.NET\DataProtection-Keys
</code></pre>
<blockquote>
<p>On non-Windows platforms this works in a similar fashion and are stored under:
<code>~/.aspnet/DataProtection-Keys/key-*.xml</code></p>
</blockquote>
<p>For Windows 'Service' accounts like Network Service this ends up being in a special location that is not in <code>c:\Users</code> as you might expect but in <code>c:\windows\ServiceProfiles\</code>:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Lost-ASP-NET-Cookies-on-IIS-Restarts/ServiceProfileLocalAppDataFolder.png" alt="Service Profile Local App Data Folder"></p>
<p>For each account, each application gets its own set of keys based on some generated application id. You can see above several different applications that are all running under Network Service with their own dedicated encryption keys.</p>
<p>The Windows key files look like this in case you're interested:</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;key id=&quot;65d6b51f-5282-4065-a0e4-1681d6fc0096&quot; version=&quot;1&quot;&gt;
  &lt;creationDate&gt;2024-03-08T00:52:28.2113827Z&lt;/creationDate&gt;
  &lt;activationDate&gt;2024-03-08T00:52:28.2059047Z&lt;/activationDate&gt;
  &lt;expirationDate&gt;2024-06-06T00:52:28.2059047Z&lt;/expirationDate&gt;
  &lt;descriptor deserializerType=&quot;Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60&quot;&gt;
    &lt;descriptor&gt;
      &lt;encryption algorithm=&quot;AES_256_CBC&quot; /&gt;
      &lt;validation algorithm=&quot;HMACSHA256&quot; /&gt;
      &lt;encryptedSecret decryptorType=&quot;Microsoft.AspNetCore.DataProtection.XmlEncryption.DpapiXmlDecryptor, Microsoft.AspNetCore.DataProtection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60&quot; xmlns=&quot;http://schemas.asp.net/2015/03/dataProtection&quot;&gt;
        &lt;encryptedKey xmlns=&quot;&quot;&gt;
          &lt;!-- This key is encrypted with Windows DPAPI. --&gt;
          &lt;value&gt;AQBAANCMnd8BFdERjHoAwE/Cl+sBAAAA/13cneK0bkmbEII3g6oEDgAAAAACAAAAAAAQZgAAAAEAACAAAADxJcn0BIJYkGlTN...==&lt;/value&gt;
        &lt;/encryptedKey&gt;
      &lt;/encryptedSecret&gt;
    &lt;/descriptor&gt;
  &lt;/descriptor&gt;
&lt;/key&gt;
</code></pre>
<p>Using the profile option is easiest because it's automatic, but if you need to share keys between multiple machines or multiple applications, you need to use another approach using one of the other supported key storage mechanisms.</p>
<p>And remember that if you turn Load User Profile to <code>False</code> you don't get a peristant profile and keys won't survive an Application Pool shutdown.</p>
<h4 id="explicitly-provide-dataprotection-folder-location">Explicitly provide DataProtection Folder Location</h4>
<p>If you don't want to be tied to a Windows User Profile there's a more deterministic approach in ASP.NET that lets you specify where the DataProtection keys are stored explicitly in an explicitly specified known location or another storage API solution.</p>
<p>This is configured through ASP.NET's configuration and there's a middleware service that you can register for this via  <code>services.AddDataProtection()</code>:</p>
<pre><code class="language-csharp">// Key storage for cookies - so cookies can persist
if (env.IsProduction())
{
    services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(env.ContentRootPath, &quot;DataProtectionKeys&quot;)))
        .SetApplicationName(&quot;Weblog&quot;);
}
</code></pre>
<p>This example uses the File System provider to store the keys to a specified folder on disk, that contains the required key chain used for encryption using the DataProtection APIs. The layout for this folder is the same as shown in the Profile location, with the difference that you get to chose the location explicitly. Note that your Application Pool Identity account has to have read and write access in this location in order to create the keys written there.</p>
<p>You can store key - as I do here - in a folder below the app's content root (which is <strong>not</strong> Web accessible), or if you're overly paranoid, stuff it into a known, completely away-from-the-app location.</p>
<p>There are additional providers including persistence to the registry, AzureBlobStorage and you can also implement your own provider to store the key files.</p>
<p><a href="https://goldenbeartshirts.com" target="_blank">
<img src="https://weblog.west-wind.com/images/sponsors/BearingDownTheMountain-DisplayAd.jpg" class="da-content-image" />
</a></p>
<h2 id="summary">Summary</h2>
<p>In the end for my application, I opted for the simplest solution of just enabling the user profile to automatically let it do its thing to store the keys. When running on Windows I tend to have the User Profile enabled even though I don't explicitly access it, because there always can be odd APIs that require a profile user that you might not expect anyway - this has bitten me more than once so I tend to enable it on all app pools. There's no real overhead here as all that really does is map environment variables to point at file and registry keys that already exist. I've had apps break in the past with off the wall failures because profile access was not available. There's no real downside to leaving it on especially with an otherwise non-interactive account.</p>
<p>The alternative of using an explicit store makes sense if you need to share keys between multiple machines in a load balancing or other multi-server environment, or if you simply want more control over where the keys go. Just make sure your app can actually write out the new keys in the location you choose.</p>
<p>Writing this down mainly to remind myself in the future, because due to the default setting in the Application Pool to not load a User Profile I've run into this more than once. Hopefully spending a little time writing it down will have jogged my memory enough not to repeat that mistake in the future... 😀</p>
<div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;">
    <img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;">
    this post created and published with the 
    <a href="https://markdownmonster.west-wind.com" target="top">Markdown Monster Editor</a> 
</div>
]]></description>
      <link>https://weblog.west-wind.com/posts/2026/May/31/Lost-ASPNET-Cookies-on-IIS-Restarts</link>
      <guid isPermaLink="false">yd1j66azcyja</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/May/31/Lost-ASPNET-Cookies-on-IIS-Restarts#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/May/31/Lost-ASPNET-Cookies-on-IIS-Restarts</guid>
      <pubDate>Sun, 31 May 2026 08:49:58 GMT</pubDate>
      <abstract><![CDATA[If you find that your ASP.NET authentication cookies expire every time your IIS application pool restarts or recycles, the culprit is likely the DataProtection API not finding the previously stored keys.  This post describes one gotcha I ran into with the default storage location on IIS in the user profile due to a default Application Pool setting.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/imageContent/2026/Lost-ASP-NET-Cookies-on-IIS-Restarts/CookieMonsterAttackOnIis.jpg</featuredImage>
    </item>
    <item>
      <title>Running ASP.NET Core Applications as a Subfolder Application</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/PostBanner.jpg" alt="Post Banner"></p>
<p>ASP.NET Core applications by default want to run in a root folder - and to be fair that's the 99% use case. But there are those occasional situations where you want to run a Web site in a sub folder rather than on the root of the Web site.</p>
<p>In this post I review what's required to run ASP.NET with a <code>PathBase</code> - which works with any Web server - and then specifically discuss how to set this up on IIS, which is a little more complicated than it should be.</p>
<blockquote>
<p>Although I discuss IIS specifically here for the physical deployment part since that's what I'm running on, the majority of this this content concerning the ASP.NET set up and modifications, applies to any Web server hosting when using sub folder mapping.</p>
</blockquote>
<p><a href="https://markdownmonster.west-wind.com?ut=weblog"  target="_blank"
	  title="Markdown Monster - Easy to use, yet powerfully productive Markdown Editing for Windows">
<img src="https://weblog.west-wind.com/images/sponsors/banner-example.png?v=1.2" class="da-content-image" />
</a></p>
<h2 id="why-would-i-need-to-run-out-of-a-subfolder">Why would I need to run out of a SubFolder?</h2>
<p>The specific scenario that I ran into was that recently I updated my old custom Blog engine, and decided to pull all my secondary blogs onto my own server from various blog publishing sites. I've always run my blog on my own hosted server and the main blog runs of its own sub-domain. But the other two blogs are small barely used product related blogs that ran on different hosting sites which I never used because they were terrible and it would be a better fit to just run them off the same site in <code>/blog/</code> folder.</p>
<p>With my recent site blog update - I finally moved the old WebForms app to .NET Core - and the much simplified deployment set up that comes with it, I decided to just run everything in house for these relatively low volume sites and get better, more consistent theming and a much easier publishing pipeline using Markdown Monster (using a new custom protocol - more on that in another post).</p>
<p>In any case the scenario here is that I have a root product site:</p>
<ul>
<li><a href="https://markdownmonster.west-wind.com">https://markdownmonster.west-wind.com</a></li>
</ul>
<p>and I now want to run the blog site out of a <code>/blog/</code> sub-folder, rather than as a root site. So:</p>
<ul>
<li><a href="https://markdownmonster.west-wind.com/blog">https://markdownmonster.west-wind.com/blog</a></li>
</ul>
<p>rather than a separate new root Web site like:</p>
<ul>
<li><a href="https://markdownmonsterblog.west-wind.com">https://markdownmonsterblog.west-wind.com</a></li>
</ul>
<p>(which is what I started with then aborted)</p>
<p>For SEO it's often beneficial to run everything on the same site which matters more for the low volume sites, but it's also cleaner and more consistent with other sub folders like <code>/docs</code> and <code>/support</code>. Running a sub folder site also requires less setup - you don't need yet another certificate and custom bindings to manage and so on. So using a subfolder is effectively more lightweight from a config perspective. On the other hand, using a subfolder site requires that more care is given how urls are created in the application as we'll see in a minute.</p>
<h2 id="creating-an-application-in-a-subfolder">Creating an Application in a Subfolder</h2>
<p>There are three parts to the process of running an application in a subfolder:</p>
<ul>
<li>Configuring ASP.NET for running from a subfolder</li>
<li>Fixing up links so no hardcoded <code>/</code> references are used</li>
<li>Configuring the Web Server for a subfolder application</li>
</ul>
<h3 id="setting-up-aspnet-for-running-with-a-subfolder">Setting up ASP.NET For running with a Subfolder</h3>
<p>Turns out setting up ASP.NET to run from a subfolder is pretty easy to do as there's a dedicated middleware to set up a custom <code>PathBase</code> as ASP.NET likes to call it.</p>
<p>In <code>program.cs</code> add this:</p>
<pre><code class="language-cs">var app = builder.build();
...
app.UsePathBase(&quot;/blog/&quot;);
</code></pre>
<p>This code goes into <code>program.cs</code> after the builder has created an <code>app</code> instance using the provided services. You'll want to do this near the top of the app middleware declarations to ensure the folder is respected all the way through the middleware pipeline - you'll want this before authentication,  static files and certainly before any routing middleware. I have it at the very top of the pipeline immediately after the builder has created the <code>app</code> instance.</p>
<p>I tend to parameterize the PathBase parameter with a configuration value, because I'm  duplicating the same application in multiple folders. So, in my Weblog application it looks like this:</p>
<pre><code class="language-csharp">// config from DI initialization or wlApp.Configuration static
if (!string.IsNullOrEmpty(config.VirtualPath) &amp;&amp; config.VirtualPath != &quot;/&quot;)
{
	app.UsePathBase($&quot;/{config.VirtualPath}/&quot;);
}
</code></pre>
<p>I have 3 blog sites - one of which runs as root and two of which run in <code>/blog/</code> subfolders. By parameterizing I can customize whether they run out of a subfolder or not without recompilation.</p>
<p>So what does <code>.AddPathBase()</code> actually do?</p>
<p>It's used to resolve Urls internally, using the path specified in <code>AddPathBase()</code>. Anytime ASP.NET creates a path dynamically for routes, uses <code>~/</code> in Views or Pages, or via <code>Url.Content()</code> or other <code>IUrlHelper</code> the path is automatically fixed up with the provided path base.</p>
<p>So instead of returning <code>/images/someimage.png</code> which you'd get for a root site, you get <code>/blog/images/someimage.png</code> for example.</p>
<p>If you use implicit routing, Url helper methods,  or you stick to using <code>~/</code> paths in your Views/Pages, ASP.NET does most of the heavy lifting for you,  without having to do anything else.</p>
<h4 id="fixing-up-root-paths-with--and-an-applicationbasepath">Fixing up Root Paths with <code>~/</code> and an ApplicationBasePath</h4>
<p>All this means is that you need to be more vigilant about how you <strong>root</strong> any explicitly referenced Urls both in View markup and in your application code. Code fixups should be minimized as much as possible.</p>
<p>For Views:  Rather than  using <code>&lt;img src=&quot;/images/someimage.png&quot; /&gt;</code> you should use <code>&lt;img src=&quot;~/images/someimage.png&quot; /&gt;</code> to ensure the appropriate path is used.</p>
<p>If you didn't do this - and let's be honest most of us don't - you can quickly find and replace all instances of hard coded root paths by doing a <strong>Find in Files Search</strong> (Ctrl-Shift-F) in your IDE and doing a search and replace for <code>=&quot;/</code> and replacing with <code>=&quot;~/</code> in all your View files. This should capture most scenarios in any physical files.</p>
<p>In code you can access the <code>IUrlHelper</code> interface in Razor views or injected into controllers or methods.</p>
<p><strong>In a Razor Page or View</strong></p>
<pre><code class="language-cs">var rootPath = Url.Content(&quot;~/images/someimage.png&quot;);
</code></pre>
<p><strong>In Application Code (.cs files)</strong></p>
<p>You can also inject <code>IUrlHelperFactory</code> (WTF Microsoft?) and then retrieve the <code>IUrlHelper</code> in a somewhat convoluted way that only an ivory tower architect could love:</p>
<pre><code class="language-csharp">public class MyService
{
    private readonly IUrlHelper _url;

	public MyService(
    			IUrlHelperFactory factory,
    			IActionContextAccessor actionContextAccessor)
	{
	    _url = factory.GetUrlHelper(
	        actionContextAccessor.ActionContext);
	}
	
	public string Resolve()
	{
	    return _url.Content(&quot;~/images/logo.png&quot;);
	}

}
</code></pre>
<p>If you don't want to deal with this and just have a couple of generic methods that work anywhere, the <a href="https://github.com/RickStrahl/Westwind.AspNetCore">Westwind.AspNetCore</a> package has a couple of generic helpers:</p>
<ul>
<li><a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Extensions/HttpContextExtensions.cs#L88">HttpContext.ResolveUrl()</a> extension method</li>
<li><a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Utilities/WebUtils.cs#L209">WebUtils.ResolveUrl()</a> - you provide a base path as a string and it resolves with that (works without any Context)</li>
</ul>
<h3 id="running-locally-with-a-subfolder">Running locally with a SubFolder</h3>
<p>If you create your app this way, and fix up Urls you run it locally using the Kestrel Web server with <code>dotnet run</code> or from your IDE you will see the site come up in a subfolder:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/SubfolderInBrowser.jpg" alt="Subfolder In Browser"></p>
<p>The url includes the <code>/docs/</code> subfolder:</p>
<pre><code class="language-text">https://localhost:5001/docs/
</code></pre>
<p>If you stuck to using <code>~/</code> root paths everything is bound to just work the same as if you were running from the root folder.</p>
<p>Note that you should also change your dev <code>launchSettings.json</code> to reflect the new <code>launchUrl</code> that includes the subfolder:</p>
<pre><code class="language-json">&quot;Westwind.Weblog.MarkdownMonster&quot;: {
  &quot;commandName&quot;: &quot;Project&quot;,
  &quot;launchBrowser&quot;: true,
  &quot;environmentVariables&quot;: {
    &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
  },
  &quot;applicationUrl&quot;: &quot;https://localhost:5001&quot;,
  
  // THIS!
  &quot;launchUrl&quot;: &quot;https://localhost:5001/blog/&quot;
},
</code></pre>
<h3 id="configuring-iis-for-a-subfolder-application">Configuring IIS for a Subfolder Application</h3>
<p>While the <code>UsePathBase()</code> middleware handles the ASP.NET Core side seamlessly, IIS requires some extra work to create an <strong>Application</strong> under your root website.</p>
<p>IIS has Web Sites and Applications, which are very similar. Web Sites have extra configuration related to Host mappings and bindings, but otherwise Web Sites and Applications behave very similarly.</p>
<blockquote>
<h5 id="--one-aspnet-application-per-application-pool"><i class="fas fa-lightbulb" style="font-size: 1.1em"></i>  One ASP.NET Application per Application Pool</h5>
<p>IIS requires that each ASP.NET application uses <strong>its own dedicated ASP.NET Application Pool</strong>. Only a single ASP.NET Application can run inside of any given Application Pool, so unlike classic .NET sites, you can't share a single application pool for multiple Core apps.</p>
<p>It's possible to mix an ASP.NET Core app with a classic ASP.NET app or a static app in the same Application Pool, but you cannot have two or more ASP.NET Core Applications in an Application Pool. If you do only the first starts - the second one fails with a startup error.</p>
</blockquote>
<p>Here's what a folder based Application looks like in the IIS Manager:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/DedicatedApplicationPoolInIIS.png" alt="Dedicated Application Pool In IIS"></p>
<blockquote>
<h5 id="--application-vs-virtual-folder"><i class="fas fa-lightbulb" style="font-size: 1.1em"></i>  Application vs. Virtual Folder</h5>
<p>There are two options in IIS for a folder: Application or Virtual. In this post I'm only talking about Applications which are self contained apps that run in their own Application Pool.</p>
<p>You can also create a virtual which does not create or use a new Application Pool but simply provides a virtual folder mapping with the app running in the parents application scope. This might work in some scenarios where the parent site is not an ASP.NET Core app. A static site or an ASP.NET Classic site but I've not tried this out. What I describe here is the scenario of creating a new Application that is mapped to an Application Pool explicitly.</p>
</blockquote>
<p>Note that the physical folder doesn't have to live in the matching parent site folder  - it can point to any location on disk. However, in most cases I put the actual site content into the relative folder location in the parent Web site for consistency in finding it later 😄</p>
<p>For an Application you need to have a <strong>dedicated Application Pool</strong> for this application - or at least an AppPool that doesn't have another ASP.NET application in it. That AppPool should be configured for <strong>No Managed Code</strong>.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/IISApplicationPool.png" alt="IIS Application Pool"></p>
<p>Make sure you use an identity (user) that matches the rights that you need for your application. My apps tend to have a few configuration related settings that get written out so I have to use an account that has some additional rights.</p>
<p>Once this is set up and you've published your app, IIS will handle the initial request routing and pass the correct path information to the ASP.NET Core module to give you the same behavior that you see on your local install.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/RunningInIIs.png" alt="Running In IIS"></p>
<h2 id="recommendations">Recommendations</h2>
<p>If you're like me and you built this site with a Root Site in mind initially,  doing this switch to running a sub-folder - I'd highly recommend you take a breather 😄 and spend a bit of time testing locally with the subfolder.</p>
<p>There are bound to be lots of little edge cases that are hard to test and easy to miss when doing smoke testing.</p>
<p>I initially built the site for my main Weblog which runs on root, never giving any thought for running out of a sub-folder. Then when I created the other two sites I initially set them both up as root sites before realizing that I'd rather run them in sub folders.</p>
<p>I went through the initial steps of making the site work with subfolders, and at first glance everything worked. However I ran into lots of little edge cases with a couple of oddball links that used different quotes around links, and any links in Javascript code (see below). I managed to find most issue before uploading the site to production, but over the next day a whole bunch error log reports came in from things I had missed - mostly links.</p>
<p>So take the time to test your functionality, if not automated then manually excercising all the nooks and crannies of your site.</p>
<h3 id="summary-of-path-fixups">Summary of Path Fixups</h3>
<p>To summarize these are some of the things that you have to probably fix or at least check when going from Site to Folder based:</p>
<h3 id="all-cshtml-razor--pages">All .cshtml, .razor  Pages</h3>
<p>All view pages can automatically fix up pages that reference via &quot;~/&quot; paths. Maybe you were diligent and started doing this right way. I rarely ever do this right - I do some with <code>~/</code> and some not which is no better than not doing it all 😄.</p>
<p>Luckily fixing up View pages is pretty straight forward: You can do a simple search and replace:</p>
<ul>
<li>Select *.cshtml, *.razor etc.</li>
<li>Search for <code>=&quot;/</code></li>
<li>Replace with <code>=&quot;~/</code></li>
</ul>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/SearchInFiles.png" alt="Search In Files"></p>
<h3 id="code-fix-ups">Code Fix ups</h3>
<p>You can do something similar for your .NET code, but you have to be a lot more selective and you can't do a search and replace.</p>
<ul>
<li>Select *.cs, *.cshtml     (cshtml for code blocks that build Urls)</li>
<li>Search for <code>&quot;/</code></li>
</ul>
<p>You want to search code both in your CS files and any code snippets in your Views/Pages.</p>
<p>Hopefully there shouldn't be a lot of those in your code because generally hard coding Urls is a bad idea. Typically this only happens if Urls need to be built up based on a host of input parameters.</p>
<pre><code class="language-cs">var redirectUrl = &quot;/somedeeplink/in/the/site&quot;;
</code></pre>
<p>changed to:</p>
<pre><code class="language-cs">var redirectUrl = wlApp.Configuration.ApplicationBasePath + &quot;somedeeplink/in/the/site&quot;;
</code></pre>
<p>Again, this should be pretty rare but worth checking for. This search is likely to produce a lot of false positives that don't require any changes.</p>
<h3 id="javascript-code">Javascript Code</h3>
<p>Javascript code is little more tricky. If you have script code that's making server calls, you may have to fix up paths in your scripts. Scripts aren't executable code and the ASP.NET parser doesn't touch them as the files are served as static resources. It's also common that you provide Urls as strings.</p>
<p>It's a lesson I've learned a long time ago - almost every application that calls to the server requires a configurable value to for a base path to call the server. There are lots of ways to do this and in SPA applications I always have a config object to do this.</p>
<p>However, for traditional server based Web apps that have only a few isolated JavaScript callbacks to the server, there is no common entry point for scripts - they are just loaded randomly.</p>
<p>In this application I do this the brute force way by providing a base path in a global script variable that gets embedded into the <code>_Layout.cshtml</code> page. Since <code>_Layout.cshtml</code> touches every page this is as close as I get to a global client side entry point. You can also put this in a script file but you have to make sure it gets loaded early enough that all other scripts can see it.</p>
<p>The idea is that I create a global variable - or rather a global object with an embedded variable - that is accessible from any script.  The script is defined at the top of the page in the header so it's visible to any code that follows.</p>
<p>I use another helper class from <code>Westwind.AspNetCore</code> helper for this:</p>
<pre><code class="language-cs">@
{
	// at the top of _Layout.cshtml

	// creates an object with props for the value(s) below
	var scriptVars = new ScriptVariables(&quot;window.page&quot;);  
	scriptVars.Add(&quot;basePath&quot;, wlApp.Configuration.ApplicationBasePath);
}
&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head runat=&quot;server&quot;&gt;
    &lt;title&gt;@(ViewBag.Title ??  wlApp.Configuration.ApplicationName)&lt;/title&gt;
    &lt;script&gt;
        // expands into an object with props
        @scriptVars.ToHtmlString();
    &lt;/script&gt;
</code></pre>
<p><a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Utilities/ScriptVariables.cs#L100">ScriptVariables</a> is a helper class that makes it easy to create a Json safe object of values you want to embed into the page from your .NET code. You basically add variables which are then serialized - Javascript and Html safe into the document when you use <code>@scriptVars.ToHtmlString()</code>.</p>
<p>In this case it produces an object with a single variable, but typically I end up with a handful of 'global' page level properties that need to be passed through:</p>
<pre><code class="language-js">window.page = {
	basePath: &quot;https://markdownmonster.west-wind.com/blog/&quot;
};
</code></pre>
<p>You can decide whether you want to use just <code>/blog/</code> or the fully qualified path as I'm doing here.</p>
<p>I'm using:</p>
<pre><code class="language-cs">scriptVars.Add(&quot;basePath&quot;, wlApp.Configuration.ApplicationBasePath);
</code></pre>
<p>which is configured value that I store in the app to have quick and easy access to the full site url. In the case of this <code>_Layout.cshtml</code> page you could also use:</p>
<pre><code class="language-cs">scriptVars.Add(&quot;basePath&quot;, Url.Content(&quot;~/&quot;);
</code></pre>
<p>which produces the site relative <code>/blog/</code> base path.</p>
<p>Now any scripts on the page - both in the page itself or in any script files can look at <code>page.basePath</code> and use that to fix up any Urls as necessary:</p>
<pre><code class="language-js">deletePost = ()=&gt; {
	if (confirm('Are you sure you want to delete this post?')) {
		// THIS
		var url = page.basePath + &quot;posts/@post.Id&quot;;
		
		ajaxJson(url, null,
			(res) =&gt; { 
			    // AND THIS
			    location.href = page.basePath + &quot;posts&quot;;
			},
			(err) =&gt; {
			    alert(&quot;Error deleting post: &quot; + err.responseText);
			},
			{ HttpVerb: &quot;DELETE&quot; });                                                    
	}
}
</code></pre>
<p><a href="https://goldenbeartshirts.com"  target="_blank"
	  title="Bears and Beer">
<img src="https://weblog.west-wind.com/images/Sponsors/BearsBeer-Display.jpg" class="da-content-image" />
</a></p>
<h2 id="summary">Summary</h2>
<p>Conversions like this are always more time consuming than you think - getting to 80% is easy. And just as you're padding yourself on the shoulder for an easy job you find all the edge cases that you didn't test for.</p>
<p>The basics mentioned above should get you through most of the conversion pretty quickly. It takes a little bit of time to set up, and I always kick myself for not doing things like using <code>~/</code> paths and explicit base path fixups for any coded paths right from the start.</p>
<p>Running Web sites out of a virtual folder is not all that common, but for many low impact sites I'm actually finding myself using them more often than I would have thought. When you need to do it, it's good to know that it's possible and not as complicated as I thought it might be.</p>
<p>Now that I've gone through it I suspect I will be more diligent in the future with new and old sites to use proper pathing from the get go even when it seems overkill and you think you'd never run any other way than out of a root site... we shall see.</p>
<div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;">
    <img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;">
    this post was created and published with the 
    <a href="https://markdownmonster.west-wind.com" target="top">Markdown Monster Editor</a> 
</div>
]]></description>
      <link>https://weblog.west-wind.com/posts/2026/May/26/Running-ASPNET-Core-Applications-in-an-IIS-Subfolder-Application</link>
      <guid isPermaLink="false">qzzxfojpce9j</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/May/26/Running-ASPNET-Core-Applications-in-an-IIS-Subfolder-Application#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/May/26/Running-ASPNET-Core-Applications-in-an-IIS-Subfolder-Application</guid>
      <pubDate>Tue, 26 May 2026 14:16:20 GMT</pubDate>
      <abstract><![CDATA[While ASP.NET Core applications typically run from the root folder, some scenarios—such as hosting multiple blogs under a single domain—require running from a subfolder. This post explains how to configure ASP.NET Core with app.UsePathBase() for proper routing and ~/ path resolution, along with the IIS setup required for a dedicated Application Pool using "No Managed Code." It also covers key migration tips, including bulk updates for root-relative links and JavaScript adjustments to keep client-side functionality working in a subfolder environment.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/PostBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Getting the Client IP Address in ASP.NET Core</title>
      <description><![CDATA[When I need to pick up the client IP Address in ASP.NET Core I always forget where to find the connection information and/or forget about picking proxy forwarding instead of the actual IP address. To make things easy and reusable, here's a small HttpRequest extension method.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/May/13/Getting-the-Client-IP-Address-in-ASPNET-Core</link>
      <guid isPermaLink="false">dkvnwxdbh42n</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/May/13/Getting-the-Client-IP-Address-in-ASPNET-Core#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/May/13/Getting-the-Client-IP-Address-in-ASPNET-Core</guid>
      <pubDate>Wed, 13 May 2026 10:35:26 GMT</pubDate>
      <abstract><![CDATA[When I need to pick up the client IP Address in ASP.NET Core I always forget where to find the connection information and/or forget about picking proxy forwarding instead of the actual IP address. To make things easy and reusable, here's a small HttpRequest extension method.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Getting-the-Client-IP-Address-in-ASP-NET-Core/ClientIpBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Putting the Westwind.Scripting C# Templating Library to work, Part 2</title>
      <description><![CDATA[In part 2 of this post series I look at some of the issues you may have to deal with when using the Westwind.Scripting library as an offline document or Web site creation engine. While running simple templates is easy enough, when generating static output for Web site publishing or local preview requires some special considerations.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Apr/23/Putting-the-WestwindScripting-Templating-Library-to-work-Part-2</link>
      <guid isPermaLink="false">5314605</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Apr/23/Putting-the-WestwindScripting-Templating-Library-to-work-Part-2#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Apr/23/Putting-the-WestwindScripting-Templating-Library-to-work-Part-2</guid>
      <pubDate>Thu, 23 Apr 2026 16:04:43 GMT</pubDate>
      <abstract><![CDATA[In part 2 of this post series I look at some of the issues you may have to deal with when using the Westwind.Scripting library as an offline document or Web site creation engine. While running simple templates is easy enough, when generating static output for Web site publishing or local preview requires some special considerations.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/Part2-Banner.jpg</featuredImage>
    </item>
    <item>
      <title>Revisiting C# Scripting with the Westwind.Scripting Templating Library, Part 1</title>
      <description><![CDATA[The `Westwind.Scripting` library provides runtime C# code compilation and execution as well as a C# based Script Template engine using Handlebars style syntax with pure C# code. In this post I discuss use cases for script templating and some examples of how I use in real-world applications, followed by a discussion of the engine's features and the new Layout, Section and Partials feature that was added recently.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Apr/01/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1</link>
      <guid isPermaLink="false">5311031</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Apr/01/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Apr/01/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1</guid>
      <pubDate>Wed, 01 Apr 2026 10:35:37 GMT</pubDate>
      <abstract><![CDATA[The `Westwind.Scripting` library provides runtime C# code compilation and execution as well as a C# based Script Template engine using Handlebars style syntax with pure C# code. In this post I discuss use cases for script templating and some examples of how I use in real-world applications, followed by a discussion of the engine's features and the new Layout, Section and Partials feature that was added recently.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/Scripting-Banner.jpg</featuredImage>
    </item>
    <item>
      <title>Using .NET Native AOT to build Windows WinAPI Dlls</title>
      <description><![CDATA[Did you know that you can use .NET AOT compilation to create native Windows DLLs to potentially replace traditional C/C++ compiled DLLs? It's now possible to build completely native DLLs that can be called from external applications and legacy applications in particular using .NET AOT  compilation. In this post I show how this works and discuss the hits and misses of this tech.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Mar/21/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls</link>
      <guid isPermaLink="false">5321191</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Mar/21/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Mar/21/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls</guid>
      <pubDate>Sat, 21 Mar 2026 11:47:13 GMT</pubDate>
      <abstract><![CDATA[Did you know that you can use .NET AOT compilation to create native Windows DLLs to potentially replace traditional C/C++ compiled DLLs? It's now possible to build completely native DLLs that can be called from external applications and legacy applications in particular using .NET AOT  compilation. In this post I show how this works and discuss the hits and misses of this tech.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls/PostBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Azure Trusted Signing Revisited with Dotnet Sign</title>
      <description><![CDATA[In this follow-up post to my previous guide on Azure Trusted Signing, I  explore how the new `dotnet sign` tool significantly simplifies the code signing process compared to the traditional `SignTool` workflow. The post identifies `dotnet sign` using `artifact-signing` as a faster, more efficient alternative.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Mar/02/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign</link>
      <guid isPermaLink="false">5249291</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Mar/02/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Mar/02/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign</guid>
      <pubDate>Mon, 02 Mar 2026 10:10:23 GMT</pubDate>
      <abstract><![CDATA[In this follow-up post to my previous guide on Azure Trusted Signing, I  explore how the new `dotnet sign` tool significantly simplifies the code signing process compared to the traditional `SignTool` workflow. The post identifies `dotnet sign` using `artifact-signing` as a faster, more efficient alternative.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign/TrustedSigningBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Don't use the Microsoft Timestamp Server for Signing</title>
      <description><![CDATA[The default Microsoft timestamp server frequently causes intermittent failures during the code-signing process, particularly when processing many files or large binaries as part of a application distribution. These reliability issues can be resolved by replacing the Microsoft timestamp server with a more stable, compatible third-party alternative.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Feb/26/Dont-use-the-Microsoft-Timestamp-Server-for-Signing</link>
      <guid isPermaLink="false">5239628</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Feb/26/Dont-use-the-Microsoft-Timestamp-Server-for-Signing#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Feb/26/Dont-use-the-Microsoft-Timestamp-Server-for-Signing</guid>
      <pubDate>Thu, 26 Feb 2026 12:41:03 GMT</pubDate>
      <abstract><![CDATA[The default Microsoft timestamp server frequently causes intermittent failures during the code-signing process, particularly when processing many files or large binaries as part of a application distribution. These reliability issues can be resolved by replacing the Microsoft timestamp server with a more stable, compatible third-party alternative.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Dont-use-the-Microsoft-Timestamp-Server-for-Signing/SigningFailedBanner.png</featuredImage>
    </item>
    <item>
      <title>Reliably Refreshing the WebView2 Control</title>
      <description><![CDATA[The WebView2 control lacks a direct `Reload(noCache)` overload that forces a browser hard reload of the current page. Instead content is loaded with a soft refresh that - hopefully - reloads the current page and its dependencies dependent on WebView environment and server cache policies. In this post we'll look at how to work around this limitation and force a hard refresh in several different ways.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Feb/04/Reliably-Refreshing-the-WebView2-Control</link>
      <guid isPermaLink="false">5204283</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Feb/04/Reliably-Refreshing-the-WebView2-Control#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Feb/04/Reliably-Refreshing-the-WebView2-Control</guid>
      <pubDate>Wed, 04 Feb 2026 21:53:22 GMT</pubDate>
      <abstract><![CDATA[The WebView2 control lacks a direct `Reload(noCache)` overload that forces a browser hard reload of the current page. Instead content is loaded with a soft refresh that - hopefully - reloads the current page and its dependencies dependent on WebView environment and server cache policies. In this post we'll look at how to work around this limitation and force a hard refresh in several different ways.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Reliably-Refreshing-the-WebView2-Control/WebViewRefreshBanner.jpg</featuredImage>
    </item>
  </channel>
</rss>