Publishing Razor Pages Applications - Gotchas

I have just completed an article covering publishing a Razor Pages application to IIS on my Razor Pages dedicated site: learnrazorpages.com. While writing it (and publishing revised versions of the site) I uncovered a few stumbling blocks that might catch others out. So I thought they were worth highlighting here in their own post.

Locked Assemblies and App_Offline Files

When a Razor Pages site is running on the web server, the application's .dll file is loaded by the IIS process along with the .dll file that holds the precompiled page content and are locked. If you use FTP to upload the published content, you will need to stop the process in order release the .dll files so that they can be overwritten. This is handled automatically by the traditional ASP.NET framework but needs to be done manually with ASP.NET Core applications.

There are a number of ways to accomplish this. If you have access to the web server, you can stop the site's application pool and then restart it once the new files have been uploaded. However, this won't provide the visitor with a particularly good experience. A cleaner alternative is to use an App_Offline.htm file. When placed in the root folder of the site (i.e. the location returned by the IHostingEnvironment.ContentRootPath property), the presence of a file name App_Offline.htm results in any further request to the application being served with the content of the file. This file is most commonly used to display a message explaining the the site is currently undergoing maintenance.

The name of the file is important, although case-insensitive. The easiest way to switch this feature on and off is to use an FTP client to rename the file to something else when the site should be live, and then alter it back when the site should be offline.

Since requests for other files will be ignored, you should not link to local resources such as style sheets, javascript, images etc in your App_Offline.htm file. You should embed these in the file itself. Images can be embedded as Base64 encoded strings. There are a number of online services for converting image files to a Base64 encoded string. Or you can do this yourself:

var imagePath = <full path to image file>
var bytes = File.ReadAllBytes(imagePath);
var base64Image = Convert.ToBase64String(bytes, 0, bytes.Length);

Once you have the value of base64Image, set it as the src of an <img> element using a data URI:

<img src="data:image/png;base64, <base64Image output>" />

Including Static Content in the Root Folder

Static content added to the root of a Razor Pages application is not automatically included in the published output. You might create folders to contain MarkDown or data files, for example, that you don't want your visitors to be able to browse directly, or you might add an app_offline.htm file to the root of your application. If you want to ensure that such contents are included in the published output, you must add specific entries to the application's .csproj file.

<ItemGroup>
    <Content Include="MyFolder\**\*" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

If you want to include only certain items, you can use file matching patterns. This example uses the recursive wildcard file matching pattern to specify that all contents of the given directory and its subdirectories should be included. The next example specifies that only MarkDown files in the specified directory hierarchy should be included:

<ItemGroup>
    <Content Include="MyFolder\**\*.md" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

You can also choose to exclude items. This example results in all items in the MyFolder structure except .txt files being published:

<ItemGroup>
    <Content Include="MyFolder\**\*" CopyToPublishDirectory="PreserveNewest" />
    <Content Remove="MyFolder\**\*.txt" />
</ItemGroup>

You can include multiple items by either creating a separate entry for each, or by providing a comma-separated list:

<ItemGroup>
    <Content Include="MyFolder\**\*;MySecondFolder\**\*;MyThirdFolder\**\*.db" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

Finally, this example includes a file named app_offline.html in the published output:

<ItemGroup>
    <Content Include="app_offline.html" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

Note that the file extension has an additional l appended. This has the effect of ensuring that the app_offline file doesn't actually take the application offline when it is uploaded as part of the application files (or indeed when you try to run the site locally against IIS [Express]).

Non-supported framework versions

One of the USPs offered by .NET Core is the fact that you do not have to rely on your hosting company installing the latest version of the framework on the hosting server in order to take advantage of it. This means that you can target any version of the framework you like. So when a newer version is released that contains features or bug fixes that you need, you can simply target it. However, if you deploy an application that targets a version that isn't supported by the hosting server, you will get errors along the lines of:

Error: An assembly specified in the application dependencies manifest (xxx.deps.json) was not found: package: 'Microsoft.ApplicationInsights.AspNetCore', version: 'xxx' ... This assembly was expected to be in the local runtime store as the application was published using the following target manifest files: ...

By default, the publish operation employs a process known as Runtime Store Trimming, whereby dependencies that form part of the .NET Core runtime are not included in the published output. The error therefore arises because items specified in the MyApp.deps.json file are not present on the hosting server.

There are four possible solutions to this problem:

  1. Install the relevant version of the SDK on the server (which also installs the runtime store package)
  2. Install just the Runtime Store Package on the target server (you can get that by locating the correct version of .NET Core here)
  3. Amend the .csproj file to include the following entry which disables runtime store trimming:
    <PropertyGroup>      
        <PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>  
    </PropertyGroup>  
  4. Use the following (undocumented) property option when publishing via the CLI:
    dotnet publish -o c:\publish\myApp -c release /property:PublishWithAspNetCoreTargetManifest=false  

The third and fourth options will result in the entire contents of the runtime store being included with the published output - about 200 files and folders at around 40MB. All of this will need to be included in the files uploaded to the hosting server.

Summary

The items covered here are just some of the things that can catch the unwary out when publishing a Razor Pages application to IIS using the Folder publishing option. The article on www.learnrazorpages.com goes into more detail about the actual process, and will be updated as more commonly encountered Gotchas come to light.