In this tutorial, we will see how to create a hybrid desktop application that combines WPF, Webview2, ASP.NET, and React (no small feat!).
For many years, we have heard that the future of development is Web, and Desktop is doomed to disappear. In many ways, that is true; I won’t be the one to deny it.
However, although often ignored, desktop applications are still needed. Because yes, even if we ignore that elephant in 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 (the question is whether any of them really work well or have a future).
As if that weren’t enough options, today we will look at one of these solutions to generate Desktop applications using “modern” technologies. Specifically, we will create a hybrid application that combines the best of three worlds:
- .NET WPF or MAUI for the desktop interface. The example will be done with WPF (Windows). But it would basically be the same in MAUI (Android, Mac).
- Webview2 for rendering the UI (based on Chromium)
- ASP.NET Core as the backend (with local embedded or stand-alone option)
- React for the UI. But any Web framework or technology will work just as well for you.
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 with high performance. No dependencies, no libraries. And the development experience is very comfortable.
So, this tech stack can’t be that bad, as it is 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 thing, let’s go through it step by step 👇
Prerequisites
Before we start, I assume you have what you need.
- Visual Studio 2022 (or higher) with the .NET and desktop development workloads.
- Node.js and npm to manage React dependencies.
- WebView2 Runtime installed on your system.
- Basic knowledge of C#, WPF, ASP.NET, and React.
If you’re missing any of these, take a stroll through the web, as we have all of them 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.
- ClientReact: A React application that will run in the browser or within WebView2.
And here is 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 # Main window logic
│
├── ServerAspnet/ # ASP.NET Core Project (Backend)
│ ├── Controllers/ # ASP.NET Core Controllers
│ ├── wwwroot/ # Static files (React build)
│ ├── appsettings.json # Application configuration
│ └── ServerAspnet.csproj # ASP.NET Core project file
│
└── ClientReact/ # React Project (Web Frontend)
├── public/ # Static files (HTML, images, etc.)
├── src/ # React source code
│ ├── App.js # Main React component
│ └── index.js # React entry point
└── package.json # Node.js dependencies
I have only included the main files to understand the relationships. Of course, there will be many more files.
Create ASP.NET Project
Let’s start by creating the ASP.NET project, which is the easiest part. To do this, we create a folder for our project, for example, MyHybridApp
Inside this folder, we run,
dotnet new webapi -n ServerAspnet
Once the project is created, we open the Program.cs
file and replace it with this sample code I’m providing you.
using Microsoft.AspNetCore.Mvc.ApplicationParts;
namespace ServerAspnet
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(
"AllowAll",
policy =>
{
policy
#if DEBUG
// CORS resolver only in Debug
.WithOrigins("http://localhost:5173")
#endif
.AllowAnyMethod().AllowAnyHeader();
}
);
});
// This is needed for it to discover the controllers when invoked from WPF
builder.Services.AddControllers().PartManager.ApplicationParts.Add(new AssemblyPart(typeof(Program).Assembly));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("AllowAll");
app.UseHttpsRedirection();
app.MapControllers(); // configure Controllers
app.UseDefaultFiles(); // Serves index.html by default
app.UseStaticFiles(); // Serves files from wwwroot
app.MapFallbackToFile("index.html"); // For React routes
// Demo endpoint "without controller"
app.MapGet("/api/demo", () => new { message = "It works! 🎉" });
// This parameter will be passed when running from WPF
// because in that case it will need to be RunAsync instead of Run
if(args.Any(x=> x=="run_async")) app.RunAsync();
else app.Run();
}
}
}
Basically, what we have done is an ASP.NET API, which we will run either 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.
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 run
dotnet new wpf -n HybridAppWPF
cd HybridAppWPF
Next, we need to install the necessary packages and dependencies. To do that, 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 it will give you a NETSDK1080 warning that
PackageReference to Microsoft.AspNetCore.App is not necessary 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 must be replaced with frameworkReference.
AspNETCore.App doesn’t like us adding it this way. This is because it is a package meant to be used in Microsoft.NET.Sdk.Web
projects and we are using a WPF project, so it is Microsoft.NET.Sdk
.
We can fix it better if we do it this way in the project file
Add Webview2
To ensure our WPF application displays our web App in React (or any other thing we want to do), 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 HybridAppWPF;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly WebServer _server = new WebServer();
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
Closed += (sender, e) => _server.Stop();
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_server.Start();
await webView.EnsureCoreWebView2Async();
#if DEBUG
webView.CoreWebView2.Navigate("http://localhost:5173"); // React dev server
#else
webView.CoreWebView2.Navigate("http://localhost:5000");
#endif
}
}
That is,
- We have added the Webview2 control in the XAML
- We have set the logic so that in DEBUG mode, it shows the React 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, first we add the previously created project as a dependency.
On the other hand, we will add a file called WebServer.cs
that will act as an intermediary between WPF and ASP.NET.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Windows;
namespace HybridAppWPF
{
public class WebServer
{
private WebApplication? _app;
public void Start()
{
try
{
ServerAspnet.Program.Main(new string[] { "run_async" });
}
catch (Exception ex)
{
MessageBox.Show($"Server error: {ex.Message}");
}
}
public void Stop() => _app?.StopAsync();
}
}
What we are doing is creating an adapter in the WPF project that can launch and shut down (if you want) the ASP.NET project. When launched, it will remain “embedded” within WPF.
Again, you can add more logic here. This is the minimum example to make it work for you.
Create React Project
We can see the light at the end of the tunnel. Now it’s time to create our web application, which we will use as the UI. In our case, a simple app with React.
To create our project, we open another terminal at the root of the solution and do the following,
npm create vite@latest ClientReact -- --template react
cd ClientReact
npm install
Now, let’s configure the React App to work with our Backend. First, we create a file named .env
in ClientReact
with:
VITE_API_URL=http://localhost:5000
Additionally, we modify the vite.config.js
file like this.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": "http://localhost:5000", // Redirects requests to the C# API
},
},
});
Basically, we have configured the routes to work with our WPF App and its embedded ASP.NET, and to avoid CORS issues during development.
Create Our React App
Now we will create a simple App in React to check that it communicates correctly with the embedded ASP.NET backend.
import { useEffect, useState } from 'react'
function App() {
const [data, setData] = useState(null)
useEffect(() => {
fetch(`${import.meta.env.VITE_API_URL}/api/demo`)
.then(res => res.json())
.then(data => setData(data))
}, [])
return <h1>{data?.message || "Loading..."}</h1>
}
export default App
We simply make a call to the /api/demo
endpoint to check that everything works correctly.
Run the Application in Development
When we are developing with our application, we just need to run the WPF app in Debug mode.
We will see an error saying that there is no web
Now, on the other hand, we go to another terminal, open the React project, and run
cd ClientReact
npm run dev
We press F5 in our WPF App and voilà!… We see the message “It works! 🎉” coming from the ASP.NET server
Now we can develop our ASP.NET and React App as we would with any “normal” App, with Hot Reload, debugging, console, devtools, and all that jazz.
If we want, we can improve the process by creating a simple PowerShell script at the root of the solution (outside both projects), for example, named dev.ps1
# Start ASP.NET Core server
Start-Process dotnet -ArgumentList "run --configuration Debug --project AppWPF" -WindowStyle Hidden
# Start Vite
npm --prefix ClientReact run dev
So we only have to do
.dev
To start everything in development mode simultaneously.
Launch as a Web
Are you liking it? There’s more. The advantage of having our ASP.NET project separate is that we can easily launch everything as a “normal” web.
We simply change the configuration of the WPF project to the ASP.NET project, and everything will start as a “normal” web.
We can even use Swagger during development.
Furthermore, if needed, we can do a double build, one for our desktop app and another for our web app in a Docker container (as we would with any ASP.NET project, because… well, because it is what it is 😉).
Automated Build Configuration
Finally, as we deploy this, to make our hybrid WPF App self-contained. For this, in the WPF project file AppWPF.csproj
, we will add the following
<Target Name="BuildReactApp" BeforeTargets="BeforeBuild" Condition="'$(Configuration)' == 'Release'">
<Message Importance="high" Text="Building React App..." />
<Exec Command="npm install" WorkingDirectory="../ClientReact" />
<Exec Command="npm run build" WorkingDirectory="../ClientReact" />
<ItemGroup>
<ReactBuildFiles Include="../ClientReact/dist/**" />
</ItemGroup>
<Copy SourceFiles="@(ReactBuildFiles)"
DestinationFiles="@(ReactBuildFiles->'wwwroot\%(RecursiveDir)%(Filename)%(Extension)')" />
<ItemGroup>
<Content Include="wwwroot\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Target>
With this,
- We have configured it so that when we do a build in Release, it triggers the
npm run build
of the React app. - The files of the already “built” app will be copied to the
wwwroot
of our app.
Now, we simply do
dotnet build
dotnet run
Or if you prefer self-contained
dotnet publish -c Release -o publish -r win-x64 --self-contained true
And we can also create a PowerShell script at the root of the solution that performs the actions for us
# Publish
dotnet publish HybridAppWPF -c Release -o HybridAppWPF/publish
So publishing is as simple as doing
./publish.ps
Bonus
Final bonus pack. We can also establish bidirectional communication between the embedded web app in Webview2 and the host app in WPF / MAUI. For that, we would do
// WPF to JavaScript
webView.CoreWebView2.PostWebMessageAsString("Hello from WPF");
// JavaScript to WPF
webView.CoreWebView2.WebMessageReceived += (s, e) =>
MessageBox.Show(e.WebMessageAsJson);
But we will see that in another