Multiple ASP.NET apps under the same host and port in Visual Studio with IIS Express and Webpack Dev Server Proxy

The scenario is I have a ASP.NET application (full framework) as the main application that is hosted in IIS. During development, it is hosted in IIS Express under http://localhost:51077. I also have a ASP.NET Core application that is used as the API backend that is hosted in IIS Express under http://localhost:56030. On the production system these are going to run in the same IIS Website. One as the root application, and the other as a sub-application. In this way, the site and API are both accessible at http://localhost and http://localhost/api respectively using the virtual application and port sharing features built into IIS.

This is a simple setup and can be demonstrated by the following IIS configuration in the applicationHost.config file.

applicationHost.config

<sites>
  <site name="Default Website" id="1">
    <application path="/MyApp" applicationPool="AspNetAppPool">
      <virtualDirectory path="/" physicalPath="C:\inetpub\wwwroot" />
    </application>
    <application path="/MyApp/Api" applicationPool="AspNetCoreAppPool">
      <virtualDirectory path="/" physicalPath="C:\inetpub\wwwroot" />
    </application>
    <bindings>
      <binding protocol="https" bindingInformation="*:443:localhost" />
      <binding protocol="http" bindingInformation="*:80:localhost" />
    </bindings>
  </site>

Problem

During development however, this is a major issue. If you have landed on this post, hopefully it is because like me, you exhausted researching StackOverflow posts. There are several posts and attempts from people claiming they have a working configuration by manipulating the .vs\applicationHost.config file for a sub-application setup, but my attempt at following all examples I found was while I could eventually get both sites to launch, I could only get the main site to be attached to the debugger. Obviously this is a major problem.

This got me thinking that in a production system its a trivial matter because IIS makes this nice and simple, except that we need to be able to pull our code repository down, build, and hit F5, and simply be up and running. While we could use full IIS and solve the problem that way (which I have not tried), it requires each developer to setup their machine. I prefer to simply have the project build and run fresh from the repo with zero or minimal setup if possible.

Now, IIS supports a Reverse Proxy using the Rewrite module and Application Request Routing (ARR) module. IIS Express does not. So setting that up would also not be an option for local development.

However, the idea of using a proxy is still valid, and that got me thinking...

Goal

Could we use another type of proxy that we can run locally? One that came to mind was Webpack Dev Server, because I am using this in a personal project, and plan to use it for a professional project at work for an enterprise application (gasp! we're not using Angular!). I wondered if Webpack Dev Server, which is pretty fast and easy to configure once you hurdle over the initial learning curve, if we could use it only as a reverse proxy.

Turns out, the answer is yes!

The goal is simple:

  1. Be able to run both ASP.NET and ASP.NET Core web applications by just hitting F5 in Visual Studio.
  2. Be able to access both through the same authority (scheme, host, port) for local development, so the main site is accessible at http://localhost:8080, and the API is accessible at http://localhost:8080/api.
  3. No custom configuration of either project.
  4. It just "works"

Solution

I'm going to walk through this from a completely empty solution, start to finish.

Prerequisites

  • You need to have Node Package Manager installed (e.g. npm), and be familiar with running npm commands.
  • This will use Webpack Dev Server 5. For version 4, see their documentation and their migration guide. It works for v4, but the schema for the proxy and server configuration has changed slightly.

Create the Solution and Projects

  1. Create a new Empty Solution and name it Cohosted-SameSite.
  2. Add a new ASP.NET Web Application (.NET Framework) project to the solution, and name it MyApp. The .NET Framework version does not matter, but I use .NET 4.8. For the sake of the demo, choose the Single Page Application template, although the template doesn't matter either.
  3. Add a new ASP.NET Core Web API project to the solution and name it Api. You can choose your choice of .NET version, and you can leave Configure for HTTPS checked.
  4. Change the launch profile for the Api project and set it to IIS Express.
  5. Run the Api project by itself and make sure it launches successfully and that you can see it is running using IIS Express.
  6. Configure the Startup Projects for the solution and set both the MyApp and Api projects as startup projects to Debug.
  7. Press F5 to make sure both applications launch and attach to the debugger out of the box.

During my initial setup the ASP.NET (.NET Framework) application would not start on the HTTPS url and port. It turned out that the port it selected must have been in use. If this happens to use, simply go into the project properties and change the SSL port number in the launch URL and accept the reconfiguration of the virtual application directory, then re-run both projects.

Setup Webpack Dev Server

  1. Open the Developer Powershell window in Visual Studio
  2. Run npm init and initialize the package.json with whatever values you see fit.
  3. Open the package.json that was created in the Solution Directory.
  4. Modify the scripts section by adding build and start commands.

package.json

{
  "name": "cohosted-samesite",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "start": "webpack serve"
  },
  "author": "",
  "license": "ISC"
}
  1. In the Developer Powershell window, run npm install webpack.
  2. Run npm start and when prompted to install webpack-cli type yes and hit enter. It will prompt you again to install webpack-dev-server. Type y and hit enter. Here it will generate some errors that it could not resolve ./src. This is OK right now.
  3. Press Ctrl+C and type Y and press enter to terminate the Webpack Dev Server command.
  4. Run New-Item webpack.config.js -type file. This will create the file in the current directory, which should be your solution directory.
  5. Open webpack.config.js in Visual Studio.
  6. Add the following configuration to the file.

webpack.config.json

module.exports = {
    mode: 'development',
    entry: ['./webpack.entry.js'],
    devServer: {
        open: true,
        server: {
            type: 'http'
        },
        proxy: [
            {
                context: ["/api"],
                target: "http://localhost:{PORT}",
                changeOrigin: false
            },
            {
                context: ["/"],
                target: "http://localhost:{PORT}",
                changeOrigin: false
            }
        ]
    }
};
  1. Change {PORT} for both /api and / to the proper ports for each of the ASP.NET and ASP.NET Core applications and save the file.
  2. In the Developer PowerShell, run New-Item webpack.entry.js -type file. Leave this file empty.
  3. Press F5 and run the ASP.NET applications.
  4. Run npm start in the Developer PowerShell.
PS C:\Users\*****\source\repos\Cohosted-SameSite> npm start

> cohosted-samesite@1.0.0 start
> webpack serve

<i> [webpack-dev-server] [HPM] Proxy created: /api  -> http://localhost:47329
<i> [webpack-dev-server] [HPM] Proxy created: /  -> http://localhost:54163
asset main.js 260 KiB [emitted] (name: main)
runtime modules 27.4 KiB 12 modules
modules by path ./node_modules/ 172 KiB
  modules by path ./node_modules/webpack-dev-server/client/ 69.7 KiB 16 modules
  modules by path ./node_modules/webpack/hot/*.js 5.18 KiB
    ./node_modules/webpack/hot/dev-server.js 1.94 KiB [built] 
    ./node_modules/webpack/hot/log.js 1.74 KiB [built] 
    + 2 modules
  modules by path ./node_modules/html-entities/lib/*.js 78.9 KiB
    ./node_modules/html-entities/lib/index.js 4.84 KiB [built] 
    ./node_modules/html-entities/lib/named-references.js 73.1 KiB [built] 
    ./node_modules/html-entities/lib/numeric-unicode-map.js 389 bytes [built] 
    ./node_modules/html-entities/lib/surrogate-pairs.js 583 bytes [built] 
  ./node_modules/ansi-html-community/index.js 4.16 KiB [built] 
  ./node_modules/events/events.js 14.5 KiB [built] 
./webpack.entry.js 1 bytes [built] 
webpack 5.91.0 compiled successfully in 240 ms
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8081/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.81:8081/
<i> [webpack-dev-server] Content not from webpack is served from 'C:\Users\*****\source\repos\Cohosted-SameSite\public' directory

It works!

If you did everything right, webpack should have launched a new browser window to the default port it chooses and you should see the ASP.NET (.NET Framework) application's login page. In my case, it chose port 8081, but I'm not sure if that would be the same for you.

Now let's test the API.

  1. Change the browser url from http://localhost:8081/Account/Login to http://localhost:8081/api/weatherforecast and press enter.

Sort Of...

Oops! We have not yet addressed the default HTTPS Redirection that is configured in the ASP.NET Core startup. Currently, we're going to get this up and running on HTTP before we visit a HTTPS configuration.

  1. Open the Program.cs of the ASP.NET Core application, and modify the call to app.UseHttpsRedirection() into an if-statement to check if we're in development mode.

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

// * Add check for local dev *
if (!app.Environment.IsDevelopment())
{
    app.UseHttpsRedirection();
}

app.UseAuthorization();

app.MapControllers();

app.Run();
  1. Restart the debugger and refresh the page to http://localhost:8081/api/weatherforecast

Oops! There is one more problem. The ASP.NET Core web application currently is serving requests at the root of its url. It doesn't know about the /api base path.

  1. Modify the Program.cs one more time, adding in a call to app.UsePathBase(string).

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

// * Add check for local dev *
if (!app.Environment.IsDevelopment())
{
    app.UseHttpsRedirection();
}
else{
    app.UsePathBase("/api");
}

app.UseAuthorization();

app.MapControllers();

app.Run();
  1. Restart the application yet again and refresh the browser window for the API, and observe success!.

Polish

Alright. We accomplished the goal, but now we need to get rid of some annoyance. Let's disable the launching of the browser for each of the ASP.NET applications, because we only need webpack dev server to launch a browser since that's how we're going to access our site now.

  1. In the ASP.NET Core project, go to the project properties page, and under the Debug section, open the debug launch profiles UI, and uncheck Launch browser. You can also do this by modifying the launchSettings.json file directly and setting launchBrowser: true to false.
  2. In the ASP.NET (.NET Framework) project, go to the project properties page, and under the Web tab, set the Start Action to Don't open a page. Wait for a request from an external application.

Now at this point if you followed this to the letter, you only will have a browser launch when you run npm start. Your developer workflow is now to F5 Debug the .NET projects, and npm start to run the proxy and launch a browser, and wha-la!, we have perfecto!

What about HTTPS?

Well, luckly webpack dev server supports HTTPS too! You can change your configuration to the following:

module.exports = {
    mode: 'development',
    entry: ['./webpack.entry.js'],
    devServer: {
        open: true,
        server: {
            // type: 'http'
            type: 'https'
        },
        proxy: [
            {
                context: ["/api"],
                //target: "http://localhost:47329", // HTTP Port
                target: "https://localhost:44302", // HTTPS Port
                changeOrigin: false
            },
            {
                context: ["/"],
                //target: "http://localhost:54163", // HTTP Port
                target: "https://localhost:44368", // HTTPS Port
                changeOrigin: false
            }
        ]
    }
};

It is incredibly important to note however there are two initial problems. One, the browser, especially Chromium browsers like Google Chrome or Edge will give you a nice STOP warning when visiting the page because there is no valid SSL certificate at the proxy url. Secondly, if you continue forward, webpack dev server will complain as well giving a DEPTH_ZERO_SELF_SIGNED_CERT error that you can observe in the Developer PowerShell window.

However, given that this is just a development server for local dev and test, we can simply disable this check via the webpack configuration.

...
        proxy: [
            {
                context: ["/api"],
                ...
                secure: false // Add secure: false
            },
            {
                context: ["/"],
                ...
                secure: false // Add secure: false
            }
        ]
...

Of course, you could also use a proper self-signed certificate, or a real certificate, and configure that too. But for local development this is generally unnecessary, and when at that point of the SDLC you're likely going to want to test that in a real environment, even if locally, but likely full-blown IIS.

Other Solutions

Webpack Development Server is the solution that came to mind for me because I have experience with it, it works relatively well as a proxy, and it is also used for, well, actual bundle and pack creation for front-end development. It has the benefit that when changing your TypeScript, JavaScript, SCSS, or styling it will auto refresh the browser and reload the application for you. In this instance, I just needed an easy solution to solve this use-case.

I'm sure there are other solutions that could work well to, given pros and cons of each:

  • Angular CLI
  • NodeJS Proxy
  • Full IIS with Reverse Proxy
  • Full IIS with a subapplication
  • NGINX
  • Half a dozen others

I would be very interested if someone has solved this problem another way that might even be easier. For me, I use Kendo UI for jQuery with their MVVM framework, which works well with TypeScript, SCSS, and integrates well with webpack, generally.

Unfortunately I don't have nearly as much experience with Angular and the Angular CLI, but that would be an area I should spend some time on next.

Final Thoughts

In this, you will have noticed that we setup npm and our package.json in the solution directory. Do what works for you, that is just the case for this sample project. Typically, this would be setup in your client UI application, in my case, the ASP.NET (.NET Framework) application since that provides the client UI, and the ASP.NET Core project is simply the API backend.

Leave a Comment