Let’s look at a series of tutorials aimed at seeing how to create a hybrid desktop application that combines WPF, Webview2, ASP.NET, and a Web application as UI (no small feat!).
For many years, we have heard that the future of development is Web, and Desktop is destined to disappear. In many ways, that’s true, and I won’t be the one to deny it.
However, although it is often ignored, there is still a need for desktop applications. Because yes, even if we ignore that elephant in the middle of the room, not everything can be web-based.
While we wait for the Desktop world to either collapse or reinvent itself, a good handful of technologies have emerged to fill the gap. Electron, Tauri, Flutter, Blazor hybrid, Reactive Native… everyone has their solution (whether any of them actually work well or have a future is another story).
Just in case those options weren’t enough, today we are going to see one of these solutions to generate Desktop applications using C# technologies for the backend, and Web for the UI.
In this first tutorial, we will see how to set up the backend (ASP.NET and WPF), and in the upcoming tutorials, we will see how to connect it with a web frontend (with React, Vue, or Astro).
Technological Stack of the Solution
The solution we propose is a hybrid application that combines the best of three worlds (desktop, backend, and web). Basically, we have,
- .NET WPF or MAUI for the desktop interface. The example will be done with WPF (Windows). But it would be basically the same in MAUI (Android, Mac).
- Webview2 for UI rendering (based on Chromium)
- ASP.NET Core as the backend (with local embedded or stand-alone option)
- Web frontend for the UI. Where we can use any Web framework or technology (like React, Vue, Astro, etc).
Why choose this combination over so many others? Mainly because it uses native .NET technologies (WPF/MAUI, Asp.NET) along with Webview2 as a native control.
In other words, proven, maintained, well-known technologies, and with high performance. No dependencies, no libraries. And the development experience is very comfortable.
So, this technological stack can’t be that bad, since it’s very similar to what Microsoft itself is using in its applications.
And since there isn’t a tutorial explaining how to set up this whole setup, let’s see it step by step 👇
Prerequisites
Before we begin, I assume you have what you need.
- Visual Studio 2022 (or later) with the .NET and desktop development workloads.
- Node.js and npm to manage the Web dependencies.
- WebView2 Runtime installed on your system.
- Basic knowledge of C#, WPF, ASP.NET, and web development.
If you are missing any of them, take a stroll through the web, as we have everything explained 😉.
Project Structure
Our hybrid application will have the following structure:
- AppWPF: A WPF project that will integrate the web application using WebView2.
- ServerAspnet: An ASP.NET Core project that will act as an embedded API server.
- ClientWeb: A Web application that will run in the browser or within WebView2.
And here’s a structure of the files, so we don’t get lost.
HybridApp/
│
├── AppWPF/ # WPF Project (Desktop Frontend)
│ ├── MainWindow.xaml # WPF User Interface
│ └── MainWindow.xaml.cs # Logic for the main window
│
├── ServerAspnet/ # ASP.NET Core Project (Backend)
│ ├── Controllers/ # ASP.NET Core Controllers
│ ├── appsettings.json # Application configuration
│ └── ServerAspnet.csproj # ASP.NET Core project file
│
└── ClientWeb/ # Web Frontend
├── public/ # Static files (HTML, images, etc.)
├── src/ # Frontend source code
└── package.json # Node.js dependencies
I have only included the main files for understanding the relationships. There will, of course, be many more files.
Create ASP.NET Project
Let’s start by creating the ASP.NET project, which is the easiest part. For that, we create a folder for our project, for example, MyHybridApp
Inside this folder, we execute,
dotnet new webapi -n ServerAspnet
Once the project is created, we open the file Program.cs
and replace it with this example code.
using System.Net;
using System.Net.NetworkInformation;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
namespace ServerAspnet
{
public class Program
{
public static int port = 0;
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(
"AllowAll",
policy =>
{
policy
.WithOrigins("http://localhost:5173") // Your frontend URL in Dev mode
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials(); // Allow sending credentials (cookies, authentication headers)
}
);
});
builder.Services.AddSignalR();
builder
.Services.AddControllers()
.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(Program).Assembly));
#if DEBUG
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
#endif
var app = builder.Build();
#if DEBUG
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
#endif
#if !DEBUG
// dynamic port (we'll see this in the next section)
port = GetAvailablePort(5000);
builder.WebHost.UseUrls($"http://localhost:{port}");
#endif
app.UseCors("AllowAll");
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.UseDefaultFiles(); // <-- Serves index.html by default
app.UseStaticFiles(); // <-- Serves files from wwwroot
app.MapGet("/api/demo", () => new { message = "It works! 🎉" });
app.MapHub<ClockHub>("/clockHub");
if (args.Any(x => x == "run_async"))
app.RunAsync();
else
app.Run();
}
}
}
Basically, what we have done is create an ASP.NET API, which we will run embedded in our WPF App, or independently.
Of course, you can adjust your configuration; this is an example to see how to run it in hybrid mode.
Vary Port
When we run a web application on a server (or in a container), we have control over the ports it uses. But when making a desktop app, we do not have control over whether the port we want to use is available.
For example, even launching two instances of your program would cause an error because both would use the same port.
One way to solve this is to make the program search for a free port. That’s why we’ve added these two lines.
#if !DEBUG
port = GetAvailablePort(5000);
builder.WebHost.UseUrls($"http://localhost:{port}");
#endif
Which use this function GetAvailablePort
that does exactly that… searches for an Available Port 😉.
public static int GetAvailablePort(int startingPort)
{
IPEndPoint[] endPoints;
List<int> portArray = new List<int>();
IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties();
//getting active connections
TcpConnectionInformation[] connections = properties.GetActiveTcpConnections();
portArray.AddRange(
from n in connections
where n.LocalEndPoint.Port >= startingPort
select n.LocalEndPoint.Port
);
//getting active tcp listeners - WCF service listening in tcp
endPoints = properties.GetActiveTcpListeners();
portArray.AddRange(from n in endPoints where n.Port >= startingPort select n.Port);
//getting active udp listeners
endPoints = properties.GetActiveUdpListeners();
portArray.AddRange(from n in endPoints where n.Port >= startingPort select n.Port);
portArray.Sort();
for (int i = startingPort; i < UInt16.MaxValue; i++)
if (!portArray.Contains(i))
return i;
return 0;
}
Integrate Controllers
In general, the preferred way to add logic to an ASP.NET app is through Controllers, which manage the endpoints.
For the example, in addition to the traditional WeatherForecastController (which is always used as an example) I have added this one
using Microsoft.AspNetCore.Mvc;
namespace ServerAspnet.Controllers;
[ApiController]
[Route("[controller]")]
public class FilesController : ControllerBase
{
private readonly ILogger<FilesController> _logger;
public FilesController(ILogger<FilesController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetFiles")]
public IEnumerable<string> Get()
{
var path = "C:\\temp";
return System.IO.Directory.GetFiles(path);
}
}
Which simply shows the files you have in the C:\temp
folder to demonstrate how the Desktop App will have the same access capabilities as a “normal” App.
If you do not have the C:\temp
folder, it will give you an error. If you want to run the example exactly as I present it, create this folder, or modify the path to another that exists.
On the other hand, in a “normal” ASP.NET application, it would automatically recognize the controllers during compilation.
In our case, it will work too. But since we can launch the backend from another assembly (by embedding it in the Desktop app), we have to specify the name of the assembly
builder
.Services.AddControllers()
.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(Program).Assembly)); // this is the added part
This does not interfere with the App when it runs in web mode; it is just a way to make it work in Desktop mode as well.
Integrate SignalR
SignalR is a real-time solution from Microsoft, which is very practical for bidirectional communication between server and client. We can also add it to our solution.
For that, we install the SignalR package.
dotnet add package Microsoft.AspNetCore.SignalR
For the example, I created a very simple Hub that simply sends the current time to the UI client.
using Microsoft.AspNetCore.SignalR;
namespace ServerAspnet
{
public class ClockHub : Hub
{
// This method is called when a client connects
public override async Task OnConnectedAsync()
{
// Start sending the time every second
await SendTime();
}
// Method that sends the time every second
private async Task SendTime()
{
while (true)
{
// Send the time to all connected clients
await Clients.All.SendAsync("ReceiveTime", DateTime.Now.ToString("HH:mm:ss"));
await Task.Delay(1000); // Wait 1 second before sending the time again
}
}
}
}
These lines in program.cs
are what activate SignalR in the solution and register the demo Hub that I created.
builder.Services.AddSignalR();
///....
app.MapHub<ClockHub>("/clockHub");
Create WPF Project
Now it’s time to create the skeleton of our desktop application by creating the WPF application. To do this, we open a terminal and execute
dotnet new wpf -n HybridAppWPF
cd HybridAppWPF
Next, we need to install the necessary packages and dependencies. To do this, we do the following,
dotnet add package Microsoft.AspNetCore.App
dotnet add package Microsoft.Web.WebView2
If you do it this way, it will work, but you will get a NETSDK1080 warning that
PackageReference to Microsoft.AspNetCore.App is not needed when the target is .NET Core 3.0 or later. If using Microsoft.NET.Sdk.Web, it will automatically reference the shared framework. Otherwise, PackageReference should be replaced with frameworkReference.
AspNETCore.App doesn’t like that we add it this way. This is because it is a package intended for use in Microsoft.NET.Sdk.Web
projects, and we are using a WPF project, which is therefore Microsoft.NET.Sdk
.
We can fix it better if we do it like this in the project file
Add Webview2
To make our WPF application display our web app, we need to modify the following files
<Window x:Class="HybridAppWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="Hybrid App" Height="600" Width="800">
<Grid>
<wv2:WebView2 Name="webView" />
</Grid>
</Window>
using System.Windows;
namespace AppWPF;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly WebServer _server = new WebServer();
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Console.WriteLine("Starting web server...");
var port = _server.Start();
await webView.EnsureCoreWebView2Async();
#if DEBUG
webView.CoreWebView2.Navigate("http://localhost:5173");
#else
webView.CoreWebView2.Navigate($"http://localhost:{port}");
#endif
}
}
That is,
- We have added the Webview2 control in the XAML
- We have set up the logic so that, in DEBUG mode, it shows the Web page (which we will launch in parallel with NPM for development).
- In RELEASE mode, we will host and serve the final web from WPF.
Embed the ASP.NET Project
Now, to create the ASP.NET Core server within WPF, we first add the previously created project as a dependency.
On the other hand, we are going to add a file called WebServer.cs
that acts as an intermediary between WPF and ASP.NET.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Windows;
namespace AppWPF
{
public class WebServer
{
private WebApplication? _app;
public int Start()
{
try
{
Console.WriteLine("Starting web server...");
ServerAspnet.Program.Main(new string[] { "run_async" });
var port = ServerAspnet.Program.port;
Console.WriteLine($"Started on {port}");
return port;
}
catch (Exception ex)
{
MessageBox.Show($"Error in the server: {ex.Message}");
return 0;
}
}
public void Stop() => _app?.StopAsync();
}
}
What we are doing is creating an adapter in the WPF project that launches and shuts down (if you want) the ASP.NET project. When launched, it will be “embedded” within the WPF.
Again, you can add more logic here. This is the minimum example for it to work for you.
Run the Application in Development
When we are developing with our application, we simply need to run the WPF app in Debug mode.
We will see an error saying there is no web (don’t worry, that’s what we wanted in Debug).
Now, on the other hand, we go to another terminal, open the web project, and run
cd ClientWeb
npm run dev
Press F5 in our WPF App and tada!… We see our application working “It works! 🎉” coming from the ASP.NET server.
Now we can develop our ASP.NET App and Web technologies as we would with any “normal”