<?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-30T20:44:39.6955053Z</pubDate>
    <lastBuildDate>2026-06-30T10:00:00Z</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>Getting Inherited Controller Routes to work in ASP.NET Core</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Getting-Inherited-Controller-Routes-to-work-and-ASP-NET-Core/Banner.jpg" alt="Banner"></p>
<p>By default ASP.NET applies Controller Attribute Routes on concrete types. If you create a Controller class, the class and its routes are automatically recognized by ASP.NET during the startup process.</p>
<p>ASP.NET scans the startup assembly for any instance controllers and adds the routes it finds on them to the route table. If you have Controllers that live in another assembly you can get those to register as well, but you have to explicitly add the assembly to be scanned to the MVC startup configuration in your startup code:</p>
<pre><code class="language-csharp">var mvcBuilder = services.AddControllersWithViews()
    // have to let MVC know we have an externally loaded controller
    .AddApplicationPart(typeof(QmmApiController).Assembly);
</code></pre>
<p>Things get more complicated when you want to <strong>inherit</strong> controllers as it's not always obvious how routed endpoints are making their routes available in an inherited class.</p>
<p>In this post I discuss some of the issues you have to watch out for if your want to inherit controllers with routes from base classes either in the same project or an external library/project.</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/MarkdownMonster-Display2.jpg" class="da-content-image" />
</a></p>
<h2 id="controller-inheritance">Controller Inheritance?</h2>
<p>Controller inheritance is not a common use case for most people, but yet I find myself frequently using it to provide default Admin APIs or default UI behavior for templating and error handling in various generic components and application templates.</p>
<p>For example, in my <a href="https://github.com/RickStrahl/Westwind.AspNetCore.Markdown">Westwind.AspNetCore.Markdown</a> library, a base Controller class provides the default template processing and styling for the default Markdown file rendering. In another project which is a messaging solution, a base controller provides an optional REST interface for the messaging APIs that otherwise use SignalR. In both of these cases,  Controllers are shipped as part of a separate library project that is referenced via NuGet in a top level project.</p>
<p>In both of these cases, you have the option of overriding both the functionality and in the case of the Markdown templating library provide a custom View to customize behavior and/or UI.</p>
<h3 id="controllers-vs-minimal-apis">Controllers vs. Minimal APIs</h3>
<p>I realize a lot of people will scoff at using Controllers as being old fashioned, but for packaged functionality like this, Controllers are way easier to manage and to extend than creating a slew of middleware related components along with complex configuration instructions to handle extensibility. Controllers provide a rich, extensive native interface, and you can easily subclass to extend functionality. All of that comes out of the box, so for components that need Http behavior <strong>generically</strong> provided for me Controllers are my go-to.</p>
<p>One additional reason very relevant for this post is that  using controllers it's possible to override routes. ASP.NET does not allow for duplicate routes to be defined and blows up one way or another if they are created, but with controllers there are ways that you can override this behavior,  that allow you to either inherit or override routes as I discuss in this post.</p>
<p>AFAIK, this is not really possible with <code>app.Map()</code> short of explicitly mucking with the route definitions.</p>
<h2 id="my-use-case-and-how-i-ended-up-here">My Use Case and how I ended up here</h2>
<p>As a result, I'm using Controllers for my generic API functionality in a messaging solution. As mentioned this is a REST API defined in a library class that is imported with:</p>
<pre><code class="language-csharp">var mvcBuilder = services.AddControllersWithViews()
    // have to let MVC know we have an externally loaded controller
    .AddApplicationPart(typeof(QmmApiController).Assembly);
</code></pre>
<p>The scenario I'm working with is the following:</p>
<ul>
<li>I have a base controller class <code>public class QmmApiController : BaseApiController</code><br>
in a separate assembly that provides a base admin interface. It has a bunch of <code>[Route()]</code> attributes attached to it provide base API functionality.</li>
<li>I then inherit from this controller <code>public class SampleAppQmmApiController : QmmApiController</code><br>
in the top level ASP.NET application</li>
</ul>
<p>When I try to access the routes defined in the base class as defined I end up with a <code>500 Internal Server Error</code>:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Getting-Inherited-Controller-Routes-to-work-and-ASP-NET-Core/RoutingErrorOnIheritedRoutes.png" alt="Routing Error On Iherited Routes"><br>
<small><strong>Figure 1</strong> - Routes from inherited controllers will throw a 500 error. API Access here via <a href="https://websurge.west-wind.com">WebSurge</a>.</small></p>
<p>Not a 404, but a 500... which is a <strong>pretty harsh</strong> result! 😄</p>
<p>If I take a closer look at the issue in the Console, I can see this log error output on the server:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Getting-Inherited-Controller-Routes-to-work-and-ASP-NET-Core/AmbigousRouteException.png" alt="Ambigous Route Exception"><br>
<small><strong>Figure 2</strong> - After inheriting from a concrete base class, I ended up with AmbiguousRoute exceptions</small></p>
<p>The <strong>ambiguous</strong> route exception is a clear hint that I - and also various LLMs - missed initially. Note that unfortunately it doesn't mention <strong>which routes</strong> are failing even though it looks like it's supposed to.</p>
<p>More on this failure a little bit later. But for now, realize that the routes defined on the base class are failing in the inherited class.</p>
<h2 id="controller-attribute-routes-and-inheritance">Controller Attribute Routes and Inheritance</h2>
<p>As it turns out using controllers with inherited Route definitions are tricky to work with. As you can see in Figure 1 and Figure 2, the routes defined on the base class are all failing. Any routes that are <strong>explicitly</strong>  defined on the concrete, top level controller work fine. Any routes defined on the concrete base class fail.</p>
<p>So what's going on here?</p>
<p>My initial thought was that ASP.NET <strong>wasn't picking up the base class routes</strong>. In fact, both I and various LLMs <strong>completely misdiagnosed the problem</strong> by assuming the <strong>routes were missing</strong> and trying to actually add the routes explicitly into the route table. That did not work!</p>
<p>After a lot of back and forth with various agents, and a lot of manual <code>Console.Writeline()</code> outputs trying to track down Routes and Endpoint mappings, it turns out that the problem is not missing routes but actually <strong>Route Duplication</strong>!</p>
<h3 id="the-problem-route-duplication">The Problem: Route Duplication</h3>
<p>When you subclass a Controller the Attribute Routes defined on it are inherited by the child class, you now effectively have two sets of Attribute Routes: One on the base class and one on the parent class! ASP.NET detects routes on the entire class inheritance structure (as it should), so for the concrete class it picks up any routes that are explicitly defined on the concrete class <strong>and</strong> the routes from the inherited class.</p>
<p>ASP.NET then also picks up the base class as a separate concrete class and maps the routes defined on it.</p>
<p><strong>And Bingo: You now have route duplication!</strong></p>
<p>This is why we're seeing the <strong>Ambiguous Route Error</strong> shown in <strong>Figure 2</strong>.</p>
<h3 id="controller-inheritance-its-complicated">Controller Inheritance... It's complicated</h3>
<p>So the problem is the inheritance, but it's not quite as cut and dried as that either. There's more nuance.</p>
<p>In the example above - which was my original use case - the base class was a concrete instance class:</p>
<p>I define my top level application class like this:</p>
<pre><code class="language-cs">public class SampleQmmApiController : QmmApiController {}
</code></pre>
<p>And I'm inheriting from this base class:</p>
<pre><code class="language-cs">public class QmmApiController : BaseApiController {}
</code></pre>
<p><strong>That doesn't work</strong> and gives the <strong>Ambiguous Route</strong> error.</p>
<p>But if I use an <code>abstract</code> base class to inherit from, Attribute Routes are not duplicated and an inherited controller can correctly see a single set of inherited routes projected by the inherited concrete instance:</p>
<pre><code class="language-cs">public abstract class QmmApiController : BaseApiController {}
</code></pre>
<p>If you inherit this abstract class <strong>the routes now work</strong>!</p>
<p>Here's a table that summarizes the various modes:</p>
<table>
<thead>
<tr>
<th style="text-align: left;">Library Inheritance</th>
<th style="text-align: left;">Base Class Type</th>
<th style="text-align: left;">Result</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;"><strong>No Inheritance</strong></td>
<td style="text-align: left;">Concrete</td>
<td style="text-align: left;">Works (Routes recognized on library class)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>No Inheritance</strong></td>
<td style="text-align: left;">Abstract</td>
<td style="text-align: left;">not picked up by ASP.NET</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Inherited</strong></td>
<td style="text-align: left;">Concrete</td>
<td style="text-align: left;"><strong>Doesn't work</strong> (Routes on base class are ignored)</td>
</tr>
<tr>
<td style="text-align: left;"><strong>Inherited</strong></td>
<td style="text-align: left;">Abstract</td>
<td style="text-align: left;">Works (Routes recognized on child class)</td>
</tr>
</tbody>
</table>
<p>The <strong>No Inheritance</strong> scenario works both for project local or imported from an external library controllers as long as <code>.AddApplicationPart()</code> has imported the assembly. With an instance class the routes are <strong>always-on</strong>. Makes sense - this is how controller routes are discovered by default. But in the case of an external library that exposes a controller it might be a little unexpected  that any public instance controller you have in the library automatically is available for processing.</p>
<p>For an external library the better path most likely is to use an <code>abstract class</code>, which ASP.NET's parser will not automatically add to the route table. This effectively hides the routing interface and lets you explicitly enable the routes by subclassing the controller in your top level project.</p>
<p>Which works best depends on the scenario. For example, in my Markdown template controller I want the routes to be always active so the Markdown processing can occur. As a result I use an instance class controller. But in my queue controller scenario I want the host application to decide whether the REST API is available to access, so I use an abstract class controller.</p>
<p>Subclassing in both of these use cases provides the abililty to override default behavior - for the Markdown component the template usage, for the API how authentication and tokens are handled - which are common customizations for these two.</p>
<h3 id="preserving-base-class-routes">Preserving Base Class Routes</h3>
<p>There are actually a couple of ways that you can subclass a controller and preserve the base class routes:</p>
<ul>
<li><p><strong>Define your base class as <code>abstract</code></strong><br>
Abstract base classes are not picked up by ASP.NET when enumerating routes, so an abstract class when inherited can safely bring in the base class routes and you can use the endpoints as is, or override them. <strong>It just works.</strong></p>
</li>
<li><p><strong>Inheriting Non-Abstract Controllers requires custom Route Manipulation</strong><br>
If you decide you can't use an abstract class because you want routes to be <strong>always-on</strong> by default and still have the ability to inherit, there's a way to make this work by intercepting and modifying routes ASP.NET has auto-discovered.</p>
</li>
</ul>
<h3 id="abstract-classes-are-easiest">Abstract Classes are Easiest</h3>
<p>If you build library components and you want to conditionally expose routes on controllers, using an abstract base class is the easiest way to do it. By marking the controller as <code>abstract</code> and creating a concrete implementation class you are delegating the routes explicitly to where they are required, bypassing potentially unintended conflicts.</p>
<p>The downside is that you have to explicitly implement a subclass and that requires some sort of documentation so that users know that this is necessary to enable the base behavior.</p>
<h3 id="creating-an-iactiondescriptorprovider-route-interceptor">Creating an IActionDescriptorProvider Route Interceptor</h3>
<p>In order to deal with non-abstract instance Controlller base classes, we need to manipulate the ASP.NET route table <strong>after</strong>  ASP.NET has picked up all the routes.</p>
<p>Recall that the issue is that routes get <strong>duplicated</strong> if you inherit and instance class that contains routes: You end up with the base class' routes added to the route table, and then again from the inherited class when ASP.NET scans for controller classes.</p>
<p>So to detect and remove the duplicated routes we can:</p>
<ul>
<li>Implement <code>IActionDescriptorProvider</code> to intercept completed Routes after auto-detection</li>
<li>Remove base class routes for all or specific classes</li>
</ul>
<p><code>IActionDescriptorProvider</code> lets you intercept the Controller route handling lifecycle, and it provides a <code>OnProvidersExecuted()</code> method that is fired after routes have been auto-detected.</p>
<p>We can implement that method like this, to remove inherited routes:</p>
<pre><code class="language-csharp">namespace Westwind.AspNetCore;

/// &lt;summary&gt;
/// Runs after the full route table is built. Removes descriptors for base
/// controller actions when a subclass is also registered, eliminating the
/// ambiguous-route conflict (duplicate routes).
///
/// Register in Program.cs:
///   var convention = new InheritedControllerRouteConvention();
///   services.AddSingleton&amp;lt;IActionDescriptorProvider&amp;gt;(convention);
/// &lt;/summary&gt;
public class InheritedControllerRouteConvention :  IActionDescriptorProvider
{
 
    /// &lt;summary&gt;
    /// Optionally restrict which inherited concrete controller base types 
    /// we want to allow base class routes to work on.
    /// If empty, all inherited concrete controller base types are processed.    
    /// &lt;/summary&gt;
    public List&lt;Type&gt; ChildControllerTypes { get; set; } = [];

    public int Order =&gt; 0;
    public void OnProvidersExecuting(ActionDescriptorProviderContext context) { }

    /// &lt;summary&gt;
    /// This method finds all base controller types or those of the type(s)
    /// specified in &lt;see cref=&quot;ChildControllerTypes&quot;/&gt;
    /// and removes any [Route()] attributes on the child controllers
    /// to fix the duplication of routes that break inherited controller routing.
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;context&quot;&gt;&lt;/param&gt;
    public void OnProvidersExecuted(ActionDescriptorProviderContext context)
    {
        var descriptors = context.Results
            .OfType&lt;ControllerActionDescriptor&gt;()
            .ToList();

        var controllerTypes = descriptors
            .Select(d =&gt; d.ControllerTypeInfo.AsType())
            .Distinct()
            .ToList();

        // Find child types to process (all, or the restricted list)
        var childTypes = ChildControllerTypes.Count &gt; 0
            ? controllerTypes.Where(t =&gt; ChildControllerTypes.Contains(t)).ToList()
            : controllerTypes;

        // Base types are those that a processed child inherits from and that are
        // also directly registered as controllers
        var baseTypesToSuppress = controllerTypes
            .Where(t =&gt; childTypes.Any(child =&gt; IsSubclassOf(child, t)))
            .ToList();

        var toRemove = descriptors
            .Where(d =&gt; baseTypesToSuppress.Any(bt =&gt; d.ControllerTypeInfo.AsType() == bt))
            .ToList();

        foreach (var d in toRemove)
            context.Results.Remove(d);
    }

    private static bool IsSubclassOf(Type child, Type parent) =&gt; child != parent &amp;&amp; parent.IsAssignableFrom(child);

}
</code></pre>
<p>This code looks at all the route descriptors captured, and then looks for our matching top level controllers (or all of them if not specified). It then finds all the routes defined on inherited types of the filtered list and removes them.</p>
<p>When running the code you end up with a set of routes to remove - in <strong>Figure 3</strong> the 7 routes are the base class routes in <code>QmmApiController</code> which is my filtered controller type that I specify.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Getting-Inherited-Controller-Routes-to-work-and-ASP-NET-Core/RoutesToRemoveInStartupProcessing.png" alt="Routes To Remove In Startup Processing"><br>
<small><strong>Figure 3</strong> - Inherited routes to remove in the <code>IActionDescriptorProvider</code> processing </small></p>
<p>The code to hook this up in the ASP.NET startup code in <code>program.cs</code> looks like this:</p>
<pre><code class="language-csharp">var inheritedRouteConvention = new InheritedControllerRouteConvention
{
    ChildControllerTypes = [typeof(SampleAppQmmApiController)]
};
// Oddly this is what triggers the convention to be used!
services.AddSingleton&lt;IActionDescriptorProvider&gt;(inheritedRouteConvention);

var mvcBuilder = services.AddControllersWithViews()
    // the base controller comes from an external library
    .AddApplicationPart(typeof(QmmApiController).Assembly)
</code></pre>
<p>You can find this component and its functionality pre-built in the <a href="https://github.com/RickStrahl/Westwind.AspNetCore">Westwind.AspNetCore NuGet package</a>.</p>
<p><a href="https://goldenbeartshirts.com" target="_blank">
<img src="https://weblog.west-wind.com/images/sponsors/BearingDownTheMountain-DisplayAd.jpg" alt="Mountain Bike TShirt at Goldenbear T-Shirts" class="da-content-image" />
</a></p>
<h2 id="llm-heaven-and-hell">LLM Heaven and Hell?</h2>
<p>As you might imagine, this is the kind of thing where I engaged with LLMs - specifically with <a href="https://github.com/features/copilot">GitHub CoPilot</a> and <a href="https://github.com/features/copilot">Claude Code</a> with various models.</p>
<p>I say <strong>and with various models</strong> for a reason here, because in this case the LLMs really did an absolute shit job in a) trying to identify the problem correctly and b) providing a workable solution. So much so I switched between various tools and models several times, yet all of them came up with the same incorrect conclusion at first.</p>
<p>It took some extensive troubleshooting and <strong>Console.WriteLineing</strong> of routes and endpoints to properly diagnose the problem which was duplicate routes, not missing routes as the original LLM diagnoses was.</p>
<p>In the end, Claude Code (with Sonnet 4.6) ended up giving me a working solution but only after several very long troubleshooting loops and walking through spitting out route mappings and feeding them back into Claude and finally identifying that routes were duplicated.</p>
<p>Once the duplication issue was clearly established the final solution of the <code>IActionDescriptorProvider</code> class was adeptly created by Claude.</p>
<p>I will plainly admit that I probably <strong>would have not figured this out on my own</strong>. ASP.NET is powerful in the extensibility it has built in but discovering that flexibility is nearly impossible. You would think LLMs help here, but apparently at the edges in scenarios that haven't been widely published even the LLMs can end up not being a big help without some serious hands-on nudging.</p>
<blockquote>
<p>Getting this done was a real slog and many, many false solutions were tried along the way trying to solve the wrong problem.</p>
</blockquote>
<p>I continue to marvel at people who claim that they are one-shotting solutions and fixing problems - that never, ever seems to happen to me. I can massage an LLM into providing a solution most times, but it's never just a matter of one or even a few prompts - it's always try something and then re-direct the LLM after it's made bad assumptions. This gets very tricky in situations like this where I often don't have the expertise to check the validity of the assumptions so the only way to check is to go with it and figure it out as I go along and hope for the best...</p>
<p>But in the end, the LLM did provide a working solution, which on my own I likely would not have found.</p>
<p>As it is I spent way too much time on this 😄.</p>
<p><a href="https://documentationmonster.com?ut=weblog"  target="_blank"
	  title="Documentation Monster - Creating documentation one Markdown at a time">
<img src="https://weblog.west-wind.com/images/Sponsors/DocumentationMonster-Display2.jpg" alt="Documentation Monster" class="da-content-image" />
</a></p>
<h2 id="summary">Summary</h2>
<p>ASP.NET Controller route inheritance is not a thing you commonly do, but if you do find a use case for it, there are a number of things to watch out for.</p>
<p>Here are the main points summed up:</p>
<ul>
<li>Routes cannot be duplicated</li>
<li>Instance classes automatically publish their routes including in external libs if registered</li>
<li>Inherited instance Controller classes will cause route errors due to route duplication on the child class</li>
<li>Abstract Controller classes do not duplicate routes and can be safely subclassed with route inheritance</li>
<li>For inheriting instance Controller class you can use <code>IActionDescriptorProvider</code> to remove base class routes</li>
</ul>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/RickStrahl/Westwind.AspNetCore">Westwind.AspNetCore Library on GitHub</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/30/Getting-Inherited-Controller-Routes-to-work-in-ASPNET-Core</link>
      <guid isPermaLink="false">jq09wrhriyz8</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Jun/30/Getting-Inherited-Controller-Routes-to-work-in-ASPNET-Core#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Jun/30/Getting-Inherited-Controller-Routes-to-work-in-ASPNET-Core</guid>
      <pubDate>Tue, 30 Jun 2026 00:00:00 GMT</pubDate>
      <abstract><![CDATA[Controller inheritance in ASP.NET Core is an edge case, but if you need it you have to be mindful of how route inheritance works in ASP.NET.  This post explores why concrete class inheritance causes route duplication and provides a couple of solutions.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/imageContent/2026/Getting-Inherited-Controller-Routes-to-work-and-ASP-NET-Core/Banner.jpg</featuredImage>
    </item>
    <item>
      <title>Creating Dual Use Windows GUI and Console Applications</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/GuiConsoleAppBanner.jpg" alt="Gui Console App Banner"></p>
<p>In <a href="https://weblog.west-wind.com/posts/2026/Jun/13/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable">my last post</a> I described a <a href="https://github.com/RickStrahl/WebPackageViewer">WebPackageViewer tool</a> tool that acts as a static Web site packager and self-contained Web site viewer all contained in a single Executable.</p>
<p>One feature of this single file tool is that it acts both as a GUI application (the Web site viewer) and as a CLI application (the package/unpackage tooling) from a single EXE. However, using both GUI and CLI interface from a single EXE is challenging and doesn't have a 100% clean solution. In this post I'll describe several options that make this dual mode operation work with an <strong>as clean as possible</strong> approach, as well as some alternatives that I've used in other applications.</p>
<p>If you think this is an off the wall concept, all of my commercial desktop applications <a href="https://markdownmonster.west-wind.com">Markdown Monster</a>, <a href="https://websurge.west-wind.com">WebSurge</a> and <a href="https://documentationmonster.com">Documentation Monster</a> have a support CLI for exposing common functionality in a scriptable way. But... in all of these latter cases I actually chose a different approach of using separate EXEs for the GUI app and CLI app with the CLI app driving the GUI app features. It's a very different use case than the simple single Exe WebPackager tool and it works well for full fledged applications that expose a CLI.</p>
<h2 id="cli-interfaces">CLI Interfaces</h2>
<p>In this post I'll discuss 3 different approaches. None of them are what I would consider perfect, which would be if Windows simply supported some way to cleanly service both GUI and Console apps from a single Exe.</p>
<p>But alas, it's Windows so we have to live with trade-offs. Here are the three I'll discuss:</p>
<ul>
<li>Create a GUI application with a Console interface</li>
<li>Create a Console Application and launch GUI from Console</li>
<li>Create separate CLI and GUI applications with shared functionality</li>
</ul>
<p>I've used all of these approaches now at some point or another, and it depends on the type of application or tool that you're building which makes the most sense.</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/MarkdownMonster-Display2.jpg" class="da-content-image" />
</a></p>
<h3 id="gui-application-with-cli-interface-option">GUI Application with CLI Interface Option</h3>
<p>Mixed mode GUI app that also supports CLI output is the most tricky of these approaches, because Windows makes this sort of thing difficult with <strong>choose-one-or-the-other</strong> situation. It's also the most likely situation you're going to find yourself in when you build a GUI app and then later decide that it would be nice to add some CLI functionality.</p>
<p>When you build a Windows .NET application (or any Windows application for that matter) you havet to specify whether it's a <code>WinExe</code> (GUI) or <code>Exe</code> (Console) application. The type of app is written into the PE header when the EXE is created.</p>
<p>In .NET you can specify this in the project's header:</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
	&lt;PropertyGroup&gt;
		&lt;!-- Windows GUI app - Exe for Console --&gt;
		&lt;OutputType&gt;WinExe&lt;/OutputType&gt;
		
		&lt;TargetFramework&gt;net472&lt;/TargetFramework&gt;
		&lt;UseWPF&gt;true&lt;/UseWPF&gt;
		&lt;Version&gt;1.1.5.2&lt;/Version&gt;
</code></pre>
<p>This affects the <strong>SubSystem</strong> value of the Windows PE header that gets generated into Windows Exes:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/ConsoleGuiPeHeader.jpg" alt="Console Gui Pe Header">
<small><strong>Figure 1</strong> - GUI and Console apps have different startup semantics in how they related to Consoles that are attached or loaded based on the SubSystem value in the PE header.</small></p>
<p>When you launch as a Console application a new Console is started if none is active. That's when you invoke via the Windows shell or Explorer. If launched from an existing Console in the Terminal the Console app attaches to the existing Console and pipes output into it including all the Console streams (Input/Output/Error).</p>
<p>When you launch a GUI app from the shell, no Console is launched, so by default there's no Console - the Console points at a null console if you write to it and that output goes nowhere.</p>
<p>Where it gets weird is if you launch a GUI app from a Console, and you then write to the console which is the exact scenario that I'm using in <strong>WebPackageViewer</strong>.</p>
<p>By default the launching Console and the GUI exe are not connected and so by default Console input and output are still not going anywhere. However, it's possible to attach to an existing Console using  a native <code>AttachConsole()</code> call from a GUI application when the application starts up, which effectively allows your GUI application to send Console output and input to and from the console.</p>
<p>You can use the following two P/Invoke calls (on Windows) to attach and release the Console:</p>
<pre><code class="language-cs">[DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
static extern bool FreeConsole();

[DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
static extern bool AttachConsole(int dwProcessId);
</code></pre>
<p>Then as part of your application's startup code you can attach the Console:</p>
<pre><code class="language-csharp">protected override void OnStartup(StartupEventArgs e)
{
    bool attached = AttachConsole(-1);  // -1 - active console
    ...
     
    if (attached)
		Console.Write(&quot;\n✅ Launching Web Viewer...&quot;);
	
	...
	
	// when done
	if(attached)
		FreeConsole();
</code></pre>
<p>This sorta works.</p>
<p>The problem is that Console output goes into the active console that the application was started from, but if the application runs for any sort of duration, the console output gets mixed up with the existing prompt:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/ConsoleFail.png" alt="Console Fail"><br>
<small><strong>Figure 2</strong> - Console Fail - In a GUI Application you can attach to a console, but the output gets very janky and the Console doesn't always return to the prompt after completion</small></p>
<p>The reason for this is that a console launched GUI application <strong>returns almost immediately to the console</strong> while the GUI app continues running in the background. The end result is that you get a new prompt that appears to be mixed into your Console output.</p>
<p>Depending on where the prompt occurs, it looks like the existing console's prompt is now overwriting your  content, but really the prompt is left behind while your output continued into no-man's land. It displays but no longer related to the now active prompt.</p>
<p>Furthermore, once the native prompt has completed if you call <code>FreeConsole()</code> the console has now <em>lost the prompt</em> and essentially hangs until you press a key. While it looks like the prompt is lost, what's really happening is that the cursor appears to be at the end of the output, but the actual prompt is the prompt above in the middle of the Console output.</p>
<p>Yeah, it's bloody mess!</p>
<p>To fix this somewhat I use a helper to release the console rather than just calling <code>FreeConsole()</code>:</p>
<pre><code class="language-csharp">[DllImport(&quot;user32.dll&quot;)] static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo);

const byte VK_RETURN = 0x0D;

static void ReleaseConsolePrompt()
{
    Console.WriteLine();  // force another line break so that the prompt is on a new line
    FreeConsole();
    
    // force a CR into the console
    keybd_event(VK_RETURN, 0, 0, 0);         // key down
    keybd_event(VK_RETURN, 0, 0x0002, 0);    // key up (KEYEVENTF_KEYUP)
}
</code></pre>
<p>which ends up showing a new prompt at the bottom with the cursor at the active new prompt!</p>
<h4 id="can-you-live-with-it">Can you live with it?</h4>
<p>In a pinch, this sort of funky output works for internal applications where you have a very short output, but for a commercial tool or something that has a lot of output that takes a bit to run, that's not really an option as it just looks like shit.</p>
<p>What's annoying is that the behavior of the prompt can vary. I can run the same command multiple times and sometimes it'll get interrupted by the prompt, sometimes not. If your app's CLI commands run very fast within a few milliseconds you can preempt the console prompt interference. If it's very slow you can be sure it'll show up in an unexpected location.</p>
<p>For some applications that only occasionally use the CLI or that are driven via automation this may not matter and is acceptable. It's the easiest path of providing a CLI interface to a GUI application and may just be <strong>Good Enough</strong>.</p>
<h4 id="hackery">Hackery!</h4>
<p>For slight performance trade off you can make this a little better by essentially <strong>forcing the Console to show the new prompt immediately</strong> by waiting for a brief period with <code>Thread.Sleep()</code>. The idle cycle in the app makes the Console show a new, second prompt immediately.</p>
<p>The idea is that you preemptively force the Console to show a new prompt so it doesn't overwrite your Console output in the middle of your content.</p>
<p>So in <code>WebPackageViewer</code> I do this:</p>
<pre><code class="language-csharp">protected override void OnStartup(StartupEventArgs e)
{            
    IsConsoleApp =  AttachConsole(-1);
    if (IsConsoleApp)
    {
        // delay slightly to let existing prompt finish and then show our prompt
        System.Threading.Thread.Sleep(20);
        
        // Insert a line to clear the &gt; prompt
        Console.WriteLine();
    }
</code></pre>
<p>Here's what that looks like in PowerShell with a custom OhMyPosh prompt:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/HackedGuiConsole.png" alt="Hacked Gui Console"><br>
<small><strong>Figure 3</strong> - A GUI app with Console output that delays slightly to force the completing existing Console prompt to display immediately, which avoids overwriting our Console output.</small></p>
<p>Here's what it looks like in a plain Command prompt:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/HackedGuiConsoleCommand.png" alt="Hacked Gui Console Command"><br>
<small><strong>Figure 4</strong> - Plain Command Prompt with the same delayed Console display. </small></p>
<p>If you look close you see the original Console prompt finish, and <strong>then</strong> our actual Console output is displayed. I added an extra <code>Console.WriteLine()</code> into the code to skip down over the prompt line (<code>&gt;</code>).</p>
<p>Is that perfect? No, but it looks a lot cleaner than having the prompt injected in the middle, or weird line breaks showing up in your Console output. While we still can't avoid the extra prompt, at least now it's in a predictable location at the very top where it's not interfering with our Console content. And if you don't pay close attention you may not even notice that it's happening... 😉</p>
<p>If you want to see this all working together you can take a look in <a href="https://github.com/RickStrahl/WebPackageViewer"><code>App.xaml.cs</code> in the  WebPackageViewer project</a>.</p>
<h4 id="is-console-interface-important">Is Console Interface Important?</h4>
<p>When building CLI tools, the use case is often about automation. For many CLI tools actual Console interaction may be rare anyway because the primary use case is automation.</p>
<p>For example, in Documentation Monster (a desktop application) I automate <code>WebPackageViewer</code> from the application via <code>Process.Start()</code>. I generate the Html Output in the application, package up the files explicitly into a Zip file, then use the packager to package the single file Exe. No Console interface accessed. I'm using the CLI interface, but I'm not ever actually looking at the Console so the output is not important - only the exit code.</p>
<p>In the end having a Console that doesn't behave 100% correctly may not be a big deal, especially when you can get it close enough as described above.</p>
<h4 id="when-to-use-this">When to use this</h4>
<p>If the primary application you are creating is a GUI app and the CLI functionality is secondary, then this approach can work very well for you.</p>
<p>For my use case in <code>WebPackageViewer</code> I used this approach as it was the best choice for this utility for the following reasons:</p>
<ul>
<li>I <strong>absolutely</strong> need a single Executable</li>
<li>The GUI interface is the primary feature users see</li>
<li>The CLI interface is likely  automated and not actually visibly running from the command line</li>
<li>The CLI output works well enough with the embellishments described in this post</li>
</ul>
<p>This integration ticks all of these points and the only real downside is the slightly messy CLI interface with the duplicated prompt line. I can live with that! In this case.</p>
<p><a href="https://documentationmonster.com?ut=weblog"  target="_blank"
	  title="Documentation Monster - Creating documentation one Markdown at a time">
<img src="https://weblog.west-wind.com/images/Sponsors/DocumentationMonster-Display2.jpg" alt="Documentation Monster" class="da-content-image" />
</a></p>
<h3 id="console-application-that-hosts-a-gui-interface">Console Application that hosts a GUI Interface</h3>
<p>As I've described above it's possible to create a GUI app that can interact with the Console, but you can also create the reverse: A Console application that can run a GUI application.</p>
<p>This might seem counter-intuitive and most likely this will come up <strong>after</strong> you've decided to build the entire application as a GUI app - as it did for me!</p>
<p>Realistically though the differences between a Windows GUI and Console application are relatively minor from an implementation perspective. Even for a WPF application it's possible to switch with a few lines of code. It comes down to specifying the Windows Subsystem used during build time and changing the startup code.</p>
<p>For .NET projects this means changing to <code>&lt;OutputType&gt;Exe&lt;/OutputType&gt;</code>. The following is from the <code>WebPackageViewer</code> which deliberately builds a .NET Framework Executable to remove any runtime requirements so it works on any recent Windows machine:</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
	&lt;PropertyGroup&gt;
		&lt;!-- Exe: Console - WinExe: GUI --&gt;
		&lt;OutputType&gt;Exe&lt;/OutputType&gt;
		&lt;TargetFramework&gt;net472&lt;/TargetFramework&gt;
		&lt;UseWPF&gt;true&lt;/UseWPF&gt;
		...
	&lt;/PropertyGroup&gt;		
&lt;/Project&gt;	
</code></pre>
<blockquote>
<p>If you're using ILMerge or ILRepack to package a single file binary, the final EXE and its PE Header is actually determined by those tools, not your project. In ILRepack this is the <code>/target:exe</code> or <code>/target:winexe</code> parameter. This means if you use these tools, it doesn't matter what you set for <code>&lt;OutputType&gt;</code> in the .NET project as the assignment overrides that value in ILMerge/ILRepack scripting!</p>
</blockquote>
<p>The other thing that has to change is that a Console application has to start with a <code>static int Main()</code> method, so <strong>you have to</strong> explicitly add this to your application, in this case for a WPF application that initializes and runs <code>app.xaml</code>:</p>
<pre><code class="language-csharp">using System;
using System.IO;
using System.Runtime.InteropServices;
using WebPackageViewer;

class Program
{
    [STAThread]
    static int Main(string[] args)
    {
        var app = new App();
        app.InitializeComponent();
        app.Run();

        return 0;
    }
}
</code></pre>
<p>and you should explicitly specify the startup class in the <code>.csproj</code> file:</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
	&lt;PropertyGroup&gt;
		&lt;StartupObject&gt;Program&lt;/StartupObject&gt;
	&lt;/PropertyGroup&gt;
&lt;/Project&gt;
</code></pre>
<p>Incidentally this also works for GUI mode applications and in fact I launch all of my GUI apps this way as I generally have a few startup checks that I include before the app actually gets launched. Also a great location for a launch screen that pops quicker here than in XAML code for WPF, WinForms or WinUi frameworks startup.</p>
<p>If you go this route, you don't need to <code>AttachConsole()</code> and <code>FreeConsole()</code>. Any Console commands work as you'd expect including having all the Console Streams hooked up.</p>
<h4 id="whats-the-downside-of-a-console-gui-application">What's the Downside of a Console GUI Application?</h4>
<p>Unfortunately, there is one downside to a Console application that doubles as a GUI application: A Console compiled application <strong>always</strong> displays a Console window and there's no good way to <strong>completely</strong> hide the window when the app starts up.</p>
<p>You can hide the Console window immediately after startup using code like this:</p>
<pre><code class="language-csharp">class Program
{
    [STAThread]
    static int Main(string[] args)
    {         
        if (!StartedFromConsole())
        {
            // hide the Console window immediately
            ShowWindow(GetConsoleWindow(), 0); // hide console flash
        }

        var app = new App();
        app.InitializeComponent();
        app.Run();

        return 0;
    }

    [DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
    static extern bool AttachConsole(int dwProcessId);

    [DllImport(&quot;kernel32.dll&quot;)]
    static extern bool FreeConsole();

    [DllImport(&quot;kernel32.dll&quot;)] static extern IntPtr GetConsoleWindow();
    [DllImport(&quot;user32.dll&quot;)] static extern bool ShowWindow(IntPtr h, int cmd);


    static bool StartedFromConsole()
    {
        if (AttachConsole(-1))
        {
            FreeConsole();
            return true;
        }

        // Already attached to a console also means console-launched.
        return Marshal.GetLastWin32Error() == 5;
    }
}
</code></pre>
<p>But even with this code at the very earliest possible point of .NET code entry, you'll get a brief flash or worse a slow animation of the Console Window disappearing.</p>
<p>The code above is also a bit simplistic as it universally hides the Console window. In reality you want to hide it selectively only when you're actually going to display UI, and leave it when you're working with the Console, which may cause you to delay the hiding a little bit longer yet, making the window flash even more pronounced.</p>
<p>In WebPackageViewer when I was experimenting with this, I ended up sticking the window hiding right before the Main Window of the Viewer is created:</p>
<pre><code class="language-csharp">// code in OnStartup that handles CommandLine Parsing and execution
// and UI operations

// check and hide only before actual GUI operation
if (!StartedFromConsole())
{
    // hide the Console window immediately
    ShowWindow(GetConsoleWindow(), 0); // hide console flash
}

MainWindow mainWindow = new MainWindow(config);
mainWindow.Show();
</code></pre>
<p>But even that may not be quite the right behavior because if you actually launch the UI application from the Console explicitly via keyboard, then you'd want the Console to stay active. There's no easy way to determine whether the Console was created by your application or an existing console in a Console app because a Console is always present.</p>
<h4 id="when-to-use-this-1">When to use this</h4>
<p>Functionally, a Console application with a GUI interface ticks all the boxes and gives you the best of both Console and GUI applications, except for the initial Console Window popup.</p>
<p>If you can live with the Console popup and it isn't a distraction to you, or if your app is primarily a Console interface, then using a Console app is the way to go.</p>
<p>But for me personally, I find the brief Console popup annoying and unprofessional for a GUI app, so other than for applications that are entirely CLI driven and use GUI only as adjunct, I would probably not opt for this choice.</p>
<h3 id="separate-cli-application">Separate CLI Application</h3>
<p>Another option for creating both a GUI and Console interface is to create two completely separate applications. As I've shown above both GUI app with CLI and CLI with GUI apps have quirks that make them behave not quite right for the CLI or UI scenario.</p>
<h4 id="avoiding-ambiguities">Avoiding Ambiguities</h4>
<p>By separating out the GUI and CLI into completely separate applications you can avoid any of these ambiguities by giving each it's dedicated target Subsystem type.</p>
<p>This works particularly well if your CLI interface is fairly sophisticated and requires the full Console experience including the ability to be driven through Console IO.</p>
<p>I'm using this approach of two separate EXE interface in Markdown Monster, WebSurge and Documentation Monster and it's a relatively simple set up in that the CLI project can simply import the GUI project (or 'business' project) as a reference and get all of the same functionality as the main GUI app without the overhead of all library requirements because it uses the same libraries as the main app.</p>
<h4 id="how-does-this-work">How does this work?</h4>
<p>So, using the <a href="https://markdownmonster.west-wind.com/docs/Startup-and-Command-Line-Options/Command-Line-Operations.html">Markdown Monster CLI Example</a> I have two .NET projects:</p>
<ul>
<li>MarkdownMonster <small><em>(GUI WPF app)</em></small></li>
<li>mmcli  <small><em>(Console App)</em></small></li>
</ul>
<p><code>mmcli</code> references <code>MarkdownMonster</code> which is a single assembly monolith. <code>mmcli</code> then calls into the <code>MarkdownMonster</code> and its internal libraries to use all of the built in operational logic to handle many system operations related to installation,  generate html and pdf output, start and stop the internal Web server and so on. In MM it's a single monolith that contains most of the logic, but in other tools like WebSurge, a separate business library that contains most of these operations is referenced. In either case, the CLI project references the 'parent' application's libraries that are effectively shared between both projects.</p>
<p>Here's an example of Markdown Monster's CLI generating Html output from Markdown:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/ConsoleOutputMarkdownMonsterCLI.jpg" alt="Console Output Markdown Monster CLI"><br>
<small><strong>Figure 5</strong> - Using the separate <code>mmCli</code> Console application in Markdown Monster to generate various types of Html output </small></p>
<p>In the project the setup looks something like this:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/MarkdownMonsterAndCLIProjectSetup.png" alt="Markdown Monster And CLI Project Setup"><br>
<small><strong>Figure 6</strong> - GUI and CLI projects are separate, with the small CLI project importing all operational functionality from the GUI project (or libraries)</small></p>
<h4 id="cli-projects-can-have-small-footprint">CLI Projects can have small Footprint</h4>
<p>The bottom line here is that the CLI project itself is very small and only implements the command line parsing + the logic to drive the application logic to perform the operations that are defined in the main application code base (either the main EXE or some business library). When the project builds, only the CLI executables (.exe and .dll + the runtime config files) are copied into the main GUI executable's folder:</p>
<pre><code class="language-xml">&lt;Target Name=&quot;PostBuild&quot; AfterTargets=&quot;PostBuildEvent&quot;&gt;
  &lt;Exec Command=&quot;copy $(TargetDir)mmcli.exe $(SolutionDir)MarkdownMonster\mmcli.exe&quot; /&gt;
  &lt;Exec Command=&quot;copy $(TargetDir)mmcli.dll $(SolutionDir)MarkdownMonster\mmcli.dll&quot; /&gt;
  &lt;Exec Command=&quot;copy $(TargetDir)mmcli.runtimeconfig.json $(SolutionDir)MarkdownMonster\mmcli.runtimeconfig.json&quot; /&gt;
  &lt;Exec Command=&quot;copy $(TargetDir)mmcli.deps.json $(SolutionDir)MarkdownMonster\mmcli.deps.json&quot; /&gt;
&lt;/Target&gt;
</code></pre>
<p>Because both projects share the exact same dependencies, the footprint of the CLI project is tiny - just the <code>.exe</code> and <code>.dll</code> file even though the CLI project's build output is huge - roughly the same as the main GUI project. So by simply copying the binaries into the main project folder, the CLI binaries have access to everything they need and can run just fine.</p>
<h4 id="when-to-use">When to use</h4>
<p>If you can live with multiple executables, separate CLI and GUI projects are the cleanest solution for dual interface applications. Each application can run in its intended environment without any hacks and workarounds.</p>
<p>The only downside is that it takes a little extra effort to set up two separate projects and ensure that the two binaries and runtime config files are available in the right places.</p>
<p><a href="https://open.spotify.com/track/174gSk2IavDs7h8d715468" target="top"
		  title="Anti-Trust - The Masters of Disaster">
<img src="https://weblog.west-wind.com/images/sponsors/TheMastersOfDisaster-Display.png" class="da-content-image" />
</a></p>
<h2 id="summary">Summary</h2>
<p>Mixed mode GUI and CLI applications are not very common. Most applications stick to a simple single interface and use that. GUI applications that have some command line switches typically don't use Console interfaces to display output but rather rely on GUI prompts or forms to display information, so those typically are not dual interface either.</p>
<p>But when you have true dual purpose use cases like I did with <code>WebPackageViewer</code> then a dual interface can be quite useful to provide both a nice GUI interface and a CLI to drive it.</p>
<p>In this post I've described various ways you can create dual interface GUI and CLI applications:</p>
<p><strong>1. GUI App with Console Interaction</strong></p>
<ul>
<li>use for UI primary application (ie. a Viewer)</li>
<li>easy to build</li>
<li><code>AttachConsole()</code> and <code>FreeConsole()</code></li>
<li>CLI is least desirable but works for 'quick and dirty use'</li>
</ul>
<p><strong>2. Console Application that Launches a GUI</strong></p>
<ul>
<li>use for Console centric applications that also have UI</li>
<li>requires <code>program.cs</code> launch for GUI (ie. a little non-standard)</li>
<li>easy to build but need to isolate GUI from Console commandlines</li>
<li>for GUI apps shows or at least flashes the Terminal Window</li>
</ul>
<p><strong>3. Separate GUI and CLI Application</strong></p>
<ul>
<li>two Separate Exes</li>
<li>no 'routing' required</li>
<li>Separates concerns</li>
<li>CLI can link in GUI app or library features</li>
<li>no UI oddities for either GUI and CLI</li>
</ul>
<p>For <code>WebPackageViewer</code> I opted for a GUI app as the Viewer is the primary interface that end-users see, and because the CLI automation most likely won't actually be done via a Console but through some sort of automation. Specifically in my case through I use a separate application. In that project the main reason is because of the single file requirement, so I had to choose between option 1 and 2, with 1 winning out because of the primarily GUI focus of the tool.</p>
<p>For most of my other much bigger application products I've chosen to use separate CLI and GUI projects because it's simply cleaner to separate out the CLI functionality into a separate binary. There are multiple reasons for this. For example, Markdown Monster already has a native command line interface that allows it to open files in various different ways, deal with single-instance limiting and other command line options that deal with application startup. In other words it already has a bunch of command line processing it has to do in various different modes. Adding CLI commands to that would actually be quite complicated. WebSurge and DocMonster have simpler command lines but there are still options to deal with and having separated, well-delineated CLI syntax is much easier to implement and also to use for users without ambiguities.</p>
<p>Beyond maintaining a separate project and setting up the initial build process to produce the binaries in the right place, there's virtually no downside to using separate projects as I get full access to all the functionality of the main application in the CLI project, with the clean separation of functionality. To me if this option is available, it's clearly the cleanest approach.</p>
<p>That leaves the Console Application with GUI launching (2), and although I was initially excited about it, the fact that the Console window pops up in GUI operation is major showstopper for me. If it weren't for that I think I would consider building every application using a Console application. It sure would be nice if Microsoft provided a way to initially hide the Console window on launch via a Manifest setting or something, but AFAIK no such thing exists - the best you can do is hide the window immediately and deal with the brief Console flash. For some situations that may not be a problem, but for me that's a non-starter. YMMV.</p>
<p><code>WebPackageViewer</code> was an interesting experiment in that it forced me to explore several of these approaches and try to make them work 'perfectly'. In the end I managed to get several 'good enough' approaches to work, with the only near perfect solution being separate EXEs.</p>
<p>Ah, good enough 😂</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/Jun/23/Creating-Dual-Use-Windows-GUI-and-Console-Applications</link>
      <guid isPermaLink="false">27v1kvj96o5y</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Jun/23/Creating-Dual-Use-Windows-GUI-and-Console-Applications#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Jun/23/Creating-Dual-Use-Windows-GUI-and-Console-Applications</guid>
      <pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate>
      <abstract><![CDATA[Building a tool that provides both a rich Windows GUI and a functional CLI from a single executable presents unique challenges due to how Windows handles subsystem types. This post explores three approaches for dual-mode apps: attaching to consoles from a GUI, launching UIs from a console app, and creating separate specialized EXEs. I’ll share the "clean as possible" workarounds for console jank and window flashing used in my own production tools.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/imageContent/2026/Creating-a-Windows-GUI-App-that-also-Doubles-as-a-CLI-App/GuiConsoleAppBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Creating a Packaged Windows 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/MarkdownMonster-Display2.jpg" 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>
<blockquote>
<p>This tool isn't limited to documentation however. In theory you can use it for any static Html Web site that you want to publish. This can be some other document only Web site, it could be a React, Vue, Angular app, it could be a Blazor WASM app - anything that can run off static Html basically.</p>
<p>For publishing a full application I actually think tools like <a href="https://www.tryphotino.io/">Photino</a> or <a href="https://github.com/InfiniLore/InfiniFrame">InfiniFrame</a> provide a more complete <strong>application centric packaging solution</strong> that also have cross-platform support. OTOH, these tools also require more setup and aren't a generic, 'just do it' tool like <code>WebPackageViewer.exe</code>, so they a target a different use case.</p>
</blockquote>
<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>Figure 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 a number of command line options that let you customize packaging and the viewer's behavior:</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">
<small><strong>Figure 6</strong> - Command line options for the packager, unpackager, and viewer</small></p>
<p><a href="https://amzn.to/4etxlQA"  target="_blank"
		  title="Sign up for an Amazon Visa - 5% cash back for Amazon and Whole Food Purchases">
<img src="https://weblog.west-wind.com/images/sponsors/AmazonPrimeVisa-Display.jpg" class="da-content-image" />
</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.
s
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>
</blockquote>
<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>This means the viewer application doesn't use a Web Server, but rather uses a form of virtual file mapping to execute requests locally.</p>
<p>The WebView2 Control 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. the base Url 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 base Url and so the file translation in a virtual folder doesn't work right.</p>
<p>So instead, 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>
<h6 id="creating-the-exe-and-zip-package">Creating the Exe and Zip Package</h6>
<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>
<h5 id="unpackaging-the-packaged-exe-into-exe-and-zipfile">Unpackaging the Packaged Exe into Exe and Zipfile</h5>
<p>Once the Exe has been created can then be unpackaged either by a packaged Exe 'running itself' and launching the Viewer, or explicitly unpacking the Exe into its components using the CLI:</p>
<pre><code class="language-ps">.\WebPackageViewer unpackage `
      --package .\WebViewer-Packaged.exe 
      --output .\DocMonsterDocs
</code></pre>
<p>This creates the folder containing the entire Zip file of Html files unzipped along with the unpackaged <code>WebViewPackager.exe</code> file that you can run out of that folder to run the Web site.</p>
<blockquote>
<p>Note: If the folder already exists, the unpackaging fails to avoid overwriting or wiping out data accidentally. If you need to replace existing content, delete the target folder first.</p>
</blockquote>
<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 mode Fluent UI 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 <strong>a lot</strong> 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>
<h3 id="console-and-wpf-ui-from-a-single-exe">Console and WPF UI from a single Exe</h3>
<p>The app supports two modes:</p>
<ul>
<li>CLI Mode for packaging and manual unpacking</li>
<li>WPF UI Mode for viewing the Help File</li>
</ul>
<p>Windows doesn't have direct support for 'mixed mode' executables - they are either marked as Command line or Windows UI applications. In order to make the UI bit work the application is compiled as a Windows UI application.</p>
<p>However, you can still provide output to the Console but it requires some extra work, and the result is typically not very clean due to the way that the UI application handles termination and wait states.</p>
<p>A desktop application can attach to a Console <strong>if the application is launched from a Console command line</strong> using <code>AllocConsole()</code> and <code>FreeConsole()</code> PInvoke calls:</p>
<pre><code class="language-csharp">[DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
    static extern bool FreeConsole();

[DllImport(&quot;kernel32.dll&quot;, SetLastError = true)]
static extern bool AttachConsole(int dwProcessId);
</code></pre>
<p>You can then attach to the 'default' console if one is available when the app starts and free the console when it's done.</p>
<pre><code class="language-csharp">protected override void OnStartup(StartupEventArgs e)
{
    
    bool attached  = AttachConsole(-1);

	// Processes command line operations
	// which all output to CLI
    CommandLine.Parse();

    if (!CommandLine.Unhandled)
    {
        if (attached)
            FreeConsole();
            
        // If Command Line is handled, exit the application
        Environment.Exit(0);
    }

    // unhandled: unpackage Web site for Viewer mode
    var pack = new FilePackager();
    var exeFile = Assembly.GetExecutingAssembly().Location;
    
    if (pack.FindMarkerOffset(exeFile, pack.SeparatorBytes) &gt; 0)
    {
        var outputPath = Path.Combine(Path.GetTempPath(), &quot;dm_&quot; + StringUtils.GenerateUniqueId(8));                
        TempUnpackDirectory = outputPath;
        
        if (!pack.UnpackageFile(exeFile, outputPath, true))
        {
            if (!attached)
            {
                MessageBox.Show(&quot;An error occurred unpacking the viewer app and Web site.\n&quot; +
                                pack.ErrorMessage, &quot;Web Viewer Error&quot;, MessageBoxButton.OK,
                    MessageBoxImage.Exclamation);
            }
            else
                Console.WriteLine(&quot;\n❌ Error:\n&quot; + pack.ErrorMessage);

            Environment.Exit(1);
        }
        Environment.CurrentDirectory = outputPath;

        var exe = Path.Combine(outputPath, &quot;WebPackageViewer.exe&quot;);
        var p = Process.Start(new ProcessStartInfo() { FileName = exe, WorkingDirectory = outputPath });

        Console.Write(&quot;\n✅ Launching Web Viewer...&quot;);

        if (attached)
            FreeConsole();

        Environment.Exit(0);
    }

    InitialStartDirectory = AppContext.BaseDirectory.TrimEnd('/');
    InitialUserStartedDirectory = Environment.CurrentDirectory;

    // Read configuration from Json and override with explicit values passed
    var config = WebViewerConfiguration.Read();
        
    if (attached)
        FreeConsole();
        
    MainWindow mainWindow = new MainWindow(config);
    mainWindow.Show();
    
   ...
</code></pre>
<p>The code attaches the console and if started from the Powershell or Command the <code>AttachConsole(-1)</code> call returns <code>true</code>. At that point <code>Console.Write()</code> now outputs to the existing console window instance.</p>
<p>Then when done <code>FreeConsole()</code> has to be called to release the Console and return control back to the console instance.</p>
<p>While this works, the Console behavior can be a bit quirky:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Creating-a-Packaged-Single-File-Web-Site-Viewer-Executable/ConsoleInUiApplications.png" alt="Console In Ui Applications"><br>
<small><strong>Figure 9</strong> - Console output in a UI compiled application can be a little awkward.</small></p>
<p>Not really sure what causes this behavior of prompts popping up in the middle of output and on occasion the final operation 'hanging' without a prompt as if you were call ReadKey(). Often also the terminal completes not at the last prompt but the one above it.</p>
<p>I'm not sure what causes this but it seems its a timeout of some sort - new prompts appear when operations take a bit of time like the packager on a large project for example.</p>
<p>It's not pretty, but for the most part it works and displays what you need to know. More often than not a <code>cls</code> is required to clean up the Console after the operation has completed.  It's not ideal, but still worth having compared to having to build a separate UI form to display the same information.</p>
<blockquote>
<p>I ended up implementing a fix for this by creating the application as a Console app and manually launching the WPF application. This allows Console access normally - including from the WPF interface. I'll have another post about that in the next days.</p>
</blockquote>
<p><a href="https://websurge.west-wind.com?ut=weblog"  target="_blank"
	  title="West Wind WebSurge - A powerful, yet easy to use REST Client and HTTP Load Testing tool for Windows">
<img src="https://weblog.west-wind.com/images/Sponsors/Websurge-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>
<li><a href="https://github.com/InfiniLore/InfiniFrame">InifiniFrame - Web Application Wrapper</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 00:00:00 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[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.]]></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[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.]]></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>
  </channel>
</rss>