Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Embedding a minimal ASP.NET Web Server into a Desktop Application


:P
On this page:

Web Server in a Box Banner

Have you ever wanted to embed a Web Server into another application like a Desktop app for example? It's not a common scenario but I've had a number of occasions where I've had a need for this:

  • Markdown Monster Markdown Editor
    Provide an application server REST API that can be used for Web and other desktop applications to communicate with the running app.

  • Html Help Builder (Documentation)
    Run a background Web Server to display rendered Http content for live previews of real time generated HTML content.

  • In-house Templates Generator
    Similar to the documentation app - run a Web Server to generate Razor templated content to static files.

Unusual requirements for sure, but it does come up from time to time. And I would argue that many applications could actually benefit from exposing some of their functionality via a Web automation interface.

There are a lot of ways to skin this cat too, depending on how much complexity you need in your server. If you are just after very simple static file hosting, or a few very simple commands that you need to handle (like in Markdown Monster's automation for example), it's pretty easy to hand roll a tiny TCP/IP server, or use one of several .NET packages that allow you to embed a small Web Server into your applications.

In this post I'll show you how you can use host an ASP.NET Web server inside of another application - I'll use a desktop application as an example here. I'll provide a sample and a tiny and self-contained class wrapper that makes adding an ASP.NET Web server a little easier for many scenarios and easily lets you extend customize what ASP.NET features you want to support with your own additions.

The sample WPF app and library described here are available on GitHub if you want to play around with what's described in this post:

and here is what the sample looks like:

Sample App With Local Server And Web Surge
Figure 1 - WPF Sample application hosting Web Server using WebSurge to test requests

ASP.NET inside of another Application

In this post I talk directly embedding an ASP.NET (Kestrel) Web server into your application. Although it's not really obvious how to do, setting an ASP.NET Server in the scope of a non mainline application is very simple and is not that different from a proper Web application.

Caveat: Requires the ASP.NET Runtime

Before I jump into the why and how, there's a big disclaimer that's a potentially important consideration on whether using ASP.NET inside of a non-Web application is the right choice for you.

Specifically you need make sure the the ASP.NET Runtime is available. ASP.NET is a .NET Core framework and as such is not part of the core .NET Runtime so like the Desktop Runtime, there's a separate framework reference and runtime that's required in order to use the ASP.NET related libraries needed for Web hosting and most other ASP.NET features.

Although it's possible to use individual libraries to perhaps whittle down the requirements considerably by using individual NuGet packages, this is a difficult to maintain solution. You first need to figure out what packages are required and you need to keep them in sync with the versions of other dependent libraries. Not recommended.

If you use the standard ASP.NET Framework reference you'll have to add the following to your non-ASP.NET project:

<ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

and insure that either the ASP.NET Runtime is globally pre-installed (shared mode), or alternately publish your application as self-contained which dumps all the dependent runtime files into your output folder (self-contained mode) along with the other base frameworks which means the distribution size gets larger.

So make sure this is acceptable before going down this path.

The simplest Thing with an ASP.NET Web Server

So if you want to do the absolute simplest thing you can do to host ASP.NET in another app you can create your server and run it like this:

var startupPath = Path.GetDirectoryName(typeof(WebServerTests).Assembly.Location);
var options = new WebApplicationOptions
{
    // binary location (execution folder)
    ContentRootPath = startupPath,
    // optional static file (ie. wwwroot)
    WebRootPath = startupPath,
};
var webBuilder = WebApplication.CreateBuilder(options);
webBuilder.WebHost.UseUrls("http://localhost:5003");
var app = webBuilder.Build();

app.MapGet("/test", async ctx =>
{
    ctx.Response.StatusCode = 200;
    ctx.Response.ContentType = "text/html";
    await ctx.Response.WriteAsync(
        $"<html><body><h1>Hello Test Request! {DateTime.Now.ToString()}</h1></body></html>");
    await ctx.Response.CompleteAsync();
});

app.MapGet("/api/test", async ctx =>
{
    ctx.Response.StatusCode = 200;
    ctx.Response.ContentType = "application/json";
    await ctx.Response.WriteAsJsonAsync(new { Message = "What's up doc?", Time = DateTime.UtcNow });
    await ctx.Response.CompleteAsync();
});

// optionally serve static files out of WebRootPath
app.UseStaticFiles();

// Run Fire and Forget
_ = app.RunAsync();

// ... (continnue to run your app)

// When ready to shut down
await app.StopAsync();

As you can see base hosting is pretty simple, especially using minimal APIs that provide basic routing with simple MapXXX() operations that make it easy to set up routes and handlers. You get access to the HttpContext so you have full control over requests and you can capture Request data, and write out Response output.

It's like ASP.NET in a box!

Watch Thread Synchronization

You need to be aware that any Web request in MapXXX() hits your app on a background thread, not your UI thread. So, any UI or other operations that require running on the main thread will have to be synchronized using a Dispatcher explicitly.

Creating a Small Reusable Library

Although the above is pretty easy, it's a little painful in that you have to explicitly add the runtime reference and ensure that the right namespaces are used which is not always easy due to a lot of the ASP.NET configuration functionality living in extension methods that aren't directly referenced - you don't notice this in ASP.NET projects because the namespaces are automatically imported but in a non-Web project you have to manually pull everything in.

IAC to facilitate this process and also provide a few additional small features like the ability to capture request start and completion here's a small wrapper library.

The steps are:

  • Create an instance
  • Configure the server
  • Start the Web Server
  • Stop the Web Server when done

Initialization

As in the raw code initialization means specifying how the Web Server should behave:

The configuration bits

  • Specify the host url(s)
  • Specify whether you want to serve Static files and where from
  • Provide Map handlers
  • Provide optional started/completed handlers
public MainWindow()
{
    InitializeComponent();

    InitializeWebServer();
    ... 
}

private void InitializeWebServer()
{         
    Server = new HostedAspNetWebServer();

    // set up routes/mappings or generic handling (fallback)
    Server.OnMapRequests = (app) =>
    {
        app.MapGet("/test", async ctx =>
        {
            ctx.Response.StatusCode = 200;
            ctx.Response.ContentType = "text/html";
            await ctx.Response.WriteAsync($"<html><body><h1>Hello Test Request! {DateTime.Now.ToString()}</h1></body></html>");
            await ctx.Response.CompleteAsync();
        });

        app.MapGet("/api/test", async ctx =>
        {
            ctx.Response.StatusCode = 200;
            ctx.Response.ContentType = "application/json";
            await ctx.Response.WriteAsJsonAsync(new { Message = "What's up doc?", Time = DateTime.UtcNow });
            await ctx.Response.CompleteAsync();
        });

        app.MapFallback(async ctx =>
        {
            // You can also use this fallback to generically handle requests
            // based on the incoming ctx.Request.Path
            string path = ctx.Request.Path;
            string verb = ctx.Request.Method;

            // In this case I just return a 404 error
            ctx.Response.StatusCode = 404;
            ctx.Response.ContentType = "text/html";
            await ctx.Response.WriteAsync($"<html><body><h1>Invalid Resource - Try again, Punk!</h1></body></html>");
            await ctx.Response.CompleteAsync();
        });
    };

    // Optionally Intercept to display completed requests in the UI UI 
    Server.OnRequestCompleted = (ctx, ts) =>
    {
        // Important: Request comes in on non-ui thread!
        Dispatcher.Invoke(() =>
        {
            var method = ctx.Request.Method.PadRight(8);
            var path = ctx.Request.Path.ToString();
            var query = ctx.Request.QueryString.ToString();
            if (!string.IsNullOrEmpty(query))
                path += query;
            var status = ctx.Response.StatusCode;


            var text = method + path.PadRight(94) +
                       " (" + status + ") " +
                       ts.TotalMilliseconds.ToString("n3") + "ms";

            Model.AddRequestLine(text);
            Model.RequestCount++;
        });
    };
}

The first step is mapping 'routes' which exposes the ASP.NET WebApplication object to allow you to add additional routes via the various Map() commands. These are the same methods you'd use in a standalone application using minimal APIs to route requests so you can use app.MapGet(), app.MapPost() etc. to map specific routes and from there handle the actual ASP.NET requests.

You can also use the generic app.MapFallback() which handles any requests that fall through the other maps, and doesn't contain a route. Here you can either handle the request as failed, or - if you want to go more low level manually parse the URL path and verb yourself and execute according to your own custom rules.

The OnRequestCompleted() (and OnRequestStarted()) handlers are optional. Here I'm using it in the WPF application to update the request panel to show all the individual requests as they are occurring.

Start and Stop the Server

The above code creates the Server instance, but it doesn't start it yet and the server has to be explicitly started.

You want to make sure you start the server asynchronously using _ = server.LaunchAsync() which amounts to a FireAndForget() operation.

To start remember to not wait (or await) completion, but let it continue to run in the background so your main thread can continue:

private async void Button_Start_Click(object sender, RoutedEventArgs e)
{
    Statusbar.ShowStatusSuccess("Server started.");
    Server.LaunchAsync(
        "http://localhost:5003", 
        System.IO.Path.GetFullPath("./wwwroot")
        ).FireAndForget();    // or just _ = Server.LaunchAsync()

    Model.RequestText = "*** Web Server started.";
    Model.ServerStatus = "server is running";
}

Note there's also a sync version of Server.Launch() but I don't think there's a use case for it, unless you want to wrap the task or thread operation yourself. It's there if you need it, but unlikely to be called by an application directly.

To stop you can just await Server.Stop():

private async void Button_Stop_Click(object sender, RoutedEventArgs e)
{
    await Server.Stop();
    Statusbar.ShowStatusSuccess("Server stopped.");
    Model.ServerStatus = "server is stopped";
    Model.RequestText = "*** Web Server is stopped. Click Start Server to run.";
}

You can try out this WPF sample app from the GitHub repo:

There's a WPF sample that you can run (shown in Figure 1 above), along with some HTTP request in both West Wind WebSurge and Visual Studio .http files so you can test the server...

And there you have it - ASP.NET easily hosted in a Desktop application with live interaction between incoming requests and the host application.

Alternatives

As nice as all of this is both in terms of functionality and ease of integration, the deployment issue related to the ASP.NET Runtime requirement rains on the parade of using ASP.NET embedded in a non-Web application.

If you have needs that require you to really take advantage of many of ASP.NET's advanced features for complex routing, JSON generation, easy request interceptions, middleware extensibility etc., using ASP.NET is definitely great choice and adding the runtime hit is well worth it in that case. I have a few scenarios where I can utilize Razor as part of my app, and using the full runtime is one way to get the full functionality of 'real' Razor instead of the dumbed down hosted versions that are available in some of the Razor standalone libraries that provide only a subset of features.

But if your needs are simple, the extra installation hit is likely overkill and you might be better of with other solutions that don't exert such a heavy footprint.

For example, in Markdown Monster's Web server integration which basically allows users to manually fire up the Web server from within a running MM application (and also optionally launch it via protocol handler) and then allow some automation commands to load documents edit them and return the edited content back. It's a great feature for providing an external, yet somewhat integrated Markdown editing experience where for example a REST opened document can automatically update the client application on edits or saves as you type in the editor (and vice versa). This interface literally only involves five simple handlers and in this case it was easy enough to build a small TCPListener based interface that can manually handle the incoming HTTP requests and write out simple JSON HTTP responses. It's very limited but for a local communications interface it's more than adequate and doesn't have any extra dependencies at all. You can check out the code here but be aware it's app specific since the server only has to process a few internal commands via JSON POST data sent to it and returned.

Another solution is to use another HTTP Server library like genHttp which provides a purely C# based solution in a single library with no other dependencies. The distribution footprint is small, and there are a host of options on how to serve content including simple controllers that are easy to use for REST requests.

Options are good.

Summary

Web Hosting in non-Web dedicated applications is a rare use case, although I'd argue a lot more applications should provide HTTP based interface to provide extensibility.

Using ASP.NET inside of a desktop application is not hard to do, but it does unfortunately require that the ASP.NET Runtime is installed in addition to the Desktop Runtime or if you build self contained, include all those files as part of your distribution. If that is not an issue, you do get the benefit of all of the features of ASP.NET inside of your non-Web specific application which can be very powerful. It's a good tool to have in your pocket when the need arises and it can be especially useful for tools and hybrid applications that need to mix Web and desktop content. Check it out...

Resources

this post created and published with the Markdown Monster Editor
Posted in ASP.NET  .NET  WPF  Windows  

The Voices of Reason


 

Fabio Silva
November 29, 2023

# re: Embedding a minimal ASP.NET Web Server into a Desktop Application

Nice article, Rick!

Regarding the "Caveat: Requires the ASP.NET Runtime". Doesn't compiling the server app with AOT solve (or alleviate) this problem?


Rick Strahl
November 29, 2023

# re: Embedding a minimal ASP.NET Web Server into a Desktop Application

I don't think you'd be able to compile a desktop application via AOT.

But to be fair haven't tried it. If never had apps that I could have used AOT on work with it - only thing it ever works for is overly simplistic stuff that's truly self-contained and has few or no outside dependencies.


Don Gunter
February 08, 2024

# re: Embedding a minimal ASP.NET Web Server into a Desktop Application

Outstanding. I missed the WebSurge part as i was speed reading the first time. Nice. You have quite a few goodies. I'm glad I've found this.

I also found your github before I found your site. The things I've discovered: my ignorance 😉

if you have a moment, would you please explain serving pages with images? I appear to have gotten far enough to make great progress on a project using your server. WOW.

I've come to something i've not been able to handle and have to go to work. Ive served a page with an image. set a breakpoint in your server MapFallback call and it's the image.

i suspect my ignorance is showing.

it's most likely, the <img src="./images/1537317.jpg" alt="This is a picture of produce" width="1024" height="150">

i have tried using "http://127.0.0.1:5003/images/1537317.jpg"

does anyone have a suggestion?

Regards, D.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024