How to publish an ASP.NET Core & React SPA to IIS

Published: Tuesday 7 July 2020

Last week, we built an ASP.NET Core application with an integrated React SPA. This week, we did a live stream, where we published the application and set up an IIS website to run it.

We are going to have a look at the steps you need to take to publish an ASP.NET Core application. Then, we are going to have a look on how to set this website up in IIS.

Locating the SPA Static Files

In-order for the ASP.NET Core application to render the React static files, we need to specify the path for these files. When we say the React static files, we mean the CSS and JS files that it generates when the application is built.

To do that, we need to add a configuration to our Configure method in our Startup class. We added it just below the UseHttpsRedirection method, but I don't think it matters to much where it's located.

// Startup.cs
app.UseSpaStaticFiles(new StaticFileOptions { RequestPath = "/clientapp/build" });

So when we publish our ASP.NET Core application and run it in IIS, the application will know the folder location where to server the React CSS and JavaScript files.

The .csproj File

For publishing the ASP.NET Core application, we also need to build the React SPA. The way we can do that is to update the .csproj file for the application.

Updating the <ProjectFile> tag is required. Not only do we specify the .NET Core version number, but also a number of items. These include options related to TypeScript and where the React SPA is located.

<!-- RoundTheCode.ReactSignalR.csproj -->
<PropertyGroup>
	<TargetFramework>netcoreapp3.1</TargetFramework>
	<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
	<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
	<IsPackable>false</IsPackable>
	<SpaRoot>clientapp\</SpaRoot>
	<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
</PropertyGroup>

Next, we don't wish to publish the React SPA contained in the ASP.NET Core application. That's because we are going to build the React SPA separately. We don't want the TypeScript files included in our published solution. So, we need to exclude these files from publishing, but still need to list them, excluding the node_modules folder.

<!-- RoundTheCode.ReactSignalR.csproj -->
<ItemGroup>
	<Content Remove="$(SpaRoot)**" />
	<None Remove="$(SpaRoot)**" />
	<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
</ItemGroup>

After that, we need to focus on what happens when building the application. We need to make sure that NodeJS is installed. And when NodeJS is installed, it runs an "npm install" command to download all the modules required.

Now, this comes as the default if you create an ASP.NET Core app with the React Visual Studio template. However, one of the conditions for this rule is that it only runs if the "node_modules" folder doesn't exist. But that may cause an issue if you were to add a new module to your React application. Something to bare in mind.

<!-- RoundTheCode.ReactSignalR.csproj -->
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition="'$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
	<!-- Ensure Node.js is installed -->
	<Exec Command="node --version" ContinueOnError="true">
		<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
	</Exec>
	 
	<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />

	<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
	 
	<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
</Target>

Lastly, when publishing, we need to be able to build our React SPA. So, we run a couple of commands.

  • "npm install" - Installs the NodeJS modules
  • "yarn build:iis" - Builds the React app. We will set up the "build:iis" part in a bit

We also need to include the built files from our React SPA into our published ASP.NET Core application.

<!-- RoundTheCode.ReactSignalR.csproj -->
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
	<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
	<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
	<Exec WorkingDirectory="$(SpaRoot)" Command="yarn build:iis" />

	<!-- Include the newly-built files in the publish output -->
	<ItemGroup>
		<DistFiles Include="$(SpaRoot)build\**" />
		<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
		<RelativePath>%(DistFiles.Identity)</RelativePath>
		<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
		<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
		</ResolvedFileToPublish>
	</ItemGroup>
</Target>

So, the full RoundTheCode.ReactSignalR.csproj file:

<!-- RoundTheCode.ReactSignalR.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
 
	<PropertyGroup>
		<TargetFramework>netcoreapp3.1</TargetFramework>
		<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
		<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
		<IsPackable>false</IsPackable>
		<SpaRoot>clientapp\</SpaRoot>
		<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
	</PropertyGroup>
 
	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="3.1.5" />
		<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.1.5" />
		<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.5" />
	</ItemGroup>
 
 
	<ItemGroup>
		<!-- Don't publish the SPA source files, but do show them in the project files list -->
		<Content Remove="$(SpaRoot)**" />
		<None Remove="$(SpaRoot)**" />
		<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
	</ItemGroup>
 
	<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition="'$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">

		<!-- Ensure Node.js is installed -->
		<Exec Command="node --version" ContinueOnError="true">
			<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
		</Exec>
		 
		<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
		<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
		<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
	</Target>
     
	<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
		<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
		<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
		<Exec WorkingDirectory="$(SpaRoot)" Command="yarn build:iis" />
		 
		<!-- Include the newly-built files in the publish output -->
		<ItemGroup>
			<DistFiles Include="$(SpaRoot)build\**" />
			<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
				<RelativePath>%(DistFiles.Identity)</RelativePath>
				<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
				<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
			</ResolvedFileToPublish>
		</ItemGroup>
	</Target>
     
</Project>

Creating an Environment in React

We want to set up a configuration in our React SPA to support our publishing from our ASP.NET Core application. So, for this, we are going to set up a new configuration for React. We are going to call it "iis".

The reason why we need to do this is we need to tell our React SPA where the static CSS and JavaScript files are going to be built to.

First, we need to create a file in the "clientapp" folder. Lets call it ".env.iis". Inside this, we need to specify the location of the PUBLIC_URL:

PUBLIC_URL=/clientapp/build

Then we need to install the "env-cmd" npm module. This allows us to use configuration files like the one above.

yarn add env-cmd

It also requires the "@babel/runtime" npm module which you can do by running the following command:

yarn add -W @babel/runtime

Remember that we specified a "yarn build:iis" command in our RoundTheCode.ReactSignalR.csproj file? Well, if we were to publish the ASP.NET Core application now, we would get an error. So, we need to set up the "build:iis" script within the package.json file in our React app. You need to specify it within the "scripts" key.

"scripts": {
...
 
"build:iis": "env-cmd -f .env.iis react-scripts build",
 
...
}

What the "build:iis" command does is that it uses the configuration options set in ".env.iis" and builds the React app.

Publishing the ASP.NET Core Application

Now it's time to publish the application. In Visual Studio 2019, go the Build menu at the top and select the "Publish [Project].csproj" option.

You will be greeted with a menu similar to the following:

Publish an ASP.NET Core application in Visual Studio 2019

Publish an ASP.NET Core application in Visual Studio 2019

We are going to select Folder. You will need to specify the folder where you wish the application to be published to.

The publish can take a little bit of time, as it needs to install the node modules and build the React SPA.

But once it's finished, we will use the publish folder as our directory in our IIS website.

Setting up IIS Website

Now, before we go ahead and set up our IIS website, we need to make sure that the ASP.NET Core Runtime is set up. For ASP.NET Core 3.1, it recommends to install the Hosting Bundle for IIS support:

Download Windows Hosting bundle for ASP.NET Core Runtime 3.1.5

Download Windows Hosting bundle for ASP.NET Core Runtime 3.1.5

Then, all you need to do is to set up your IIS website. Set up a host for it, and point it to where you published the ASP.NET Core application. Then run the application from the host specified in your IIS website.

For our live stream demo, we set up a host record in our HOSTS file and then used that host for the IIS website.

Azure and Docker

As you would of seen from publishing the ASP.NET Core application in Visual Studio, you can use the same publishing methods if you are using Azure or publishing it to a Docker container. So it serves more than one purpose.