Deploying an ASP.NET Core Application to IIS

by Michael Szul on

No ads, no tracking, and no data collection. Enjoy this article? Buy us a ☕.

I'll admit that I stayed away from .NET Core for quite some time. I've been working with .NET since 2001, and I've been building web applications even longer. Reading all the releases, there was a general theme of .NET Core just not being ready. From project.json back to *.csproj. From dnx, etc. to dotnet. From "forget about MSBuild" to "MSBuild is an integral part of our enterprise support." All of these flip flops made it hard to want to adopt the technology. Now that we're at .NET Core 3.0 and there is no going back, I decided that at work, all new .NET applications would be ASP.NET Core.

The problem? When you've worked in a Windows Server pipeline for a few decades, you get used to a few conventions, and with .NET Core being cross-platform, Microsoft certainly didn't go "Windows-first" for anything. In fact, it is much harder to get an ASP.NET Core application running under a Windows/IIS environment like most enterprise .NET developers are used to. You could even say that Microsoft sacrificed the good will of its most supportive developers to go cross-platform first, and the strain is noticeable.

I'm not saying that this is the only way to deploy an application, and I'm certainly not saying that ASP.NET Core is hugely difficult. What I am saying, is that given my company's current server architecture and pipeline, it's actually easier to deploy Node.JS and Python/Flask applications than it is to deploy ASP.NET Core ones.

Let's walk through the handful of difficulties.

First, you need to publish out your .NET Core files. This sounds simple and trivial, but bear in mind that you didn't have to actually "publish" anything in .NET Framework. Although best practices dictate that you should, .NET Framework files run when compiled. IIS' module and handler architecture just pick them up by virtual of the server seeing the web.config file.

Okay, that's not really a complaint. Just put in an extra build task for "publish" and output your data to a single folder. The Core framework has been "mature" long enough that dotnet publish has taken up the majority share of StackOverflow "how to publish an ASP.NET Core application" queries. You just need to make sure your command line switches are in order.

Unless you wanted to use dotnet msbuild… We'll save that thought for another time.

Okay, so back to deployment. Just pushing the publish folder over to the server with IIS on it gives me an error as if it can't find the files. The routes seem to be messed up, and none of my View pages are actually on the server. Looks like I need to make a few CSPROJ edits:

Let's add the following to a PropertyGroup under our main Project element:

<PreserveCompilationContext>true</PreserveCompilationContext>
      <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
      

Cool. Now I have my View files, but it's still not working. Let's make some IIS configuration changes.

Is the AspNetCoreModule installed? Yes? But actually, I need AspNetCoreModuleV2, so if that's not installed, I'll have to grab it. Luckily, I already have it, so I'm good there (small wins).

In setting up my Application Pool in IIS, I also have to set the .NET CLR to "No Managed Code."

Let's see if things are working. Great, the site comes up, but half of the JavaScript isn't working. Oh look, the node_modules folder is non-existent because .NET Core publishing won't publish empty folders, and the build steps take inventory prior to the npm install and npm build steps for front-end components. We need another change to the CSPROJ file:

<DefaultItemExcludes>$([System.String]::Copy($(DefaultItemExcludes)).Replace(';**\node_modules\**',''))</DefaultItemExcludes>
      <DefaultItemExcludes>$([System.String]::Copy($(DefaultItemExcludes)).Replace(';node_modules\**',''))</DefaultItemExcludes>
      

Now we've tricked the process into keeping an eye on the node_modules folder.

Great, all the NPM packages are where they should be. Everything works, right?

One small problem: The application needs to know which environment to run in order to swap out things like connection strings or application settings. No problem. I'll just edit the web.config.

crickets

So there isn't a web.config in the project, but there is in the publish folder. Turns out it gets auto-generated. All the settings are in the appSettings.json file, and Microsoft wants you to use a global environment variable to determine which environment you're in, and then you have some code hard-coded in the Startup.cs to parse out the environment variable and add extra JSON files.

Because we all need extra JSON configuration files.

Well, I happen to have an environment with multiple applications, and can't explicitly set an environment variable to key off of without side effects. I can just use a web.config transformation right? No. ASP.NET Core doesn't support it out of the box, and the little that was folded back in is incredibly cumbersome, and doesn't seem to work all the time.

What are your choices?

Well, you can use the aforementioned reconstituted transforms in .NET Core 2.2… if you can get them to work. Of course, the instructions aren't as easy to follow.

There is an external utility that replaces the old XDT tranforms for the web.config, but that would require you to add additional overhead to your dotnet CLI calls, as well as ensure that all of your environments have that utility.

Lastly, you could create multiple web.config files, and cheat with a file copy:

<Target Name="CopyFiles" AfterTargets="Build">  
          <Copy SourceFiles="web.$(Configuration).config" DestinationFiles="web.config" />  
        </Target>  
      

You could also use the previously mentioned dotnet msbuild to execute MSBuild on the CSPROJ; however, when I attempted this, it was having problems with namespaces and file locations, and I didn't have time to continue to dig.

Everything works now, right?

No. Not quite.

Locally, the application is using Kestrel, and it runs just fine, but deployed to IIS, there are quite a few caveats; One being that the content root is in wwwroot, while the application root is one folder outside. If you try to serve static files from IIS, while maintaining the same structure/pathing as your local development, you will need to add some additional web.config items. Thankfully, Rick Strahl wrestled with this a while back, and detailed some URL Rewrite and Static File Module configurations:

<rewrite>
        <rules>
          <rule name="wwwroot">
             <match url="([\S]+[.](jpg|jpeg|gif|css|png|js|ts|cscc|less|ico|html|map|svg|json))" />
             <action type="Rewrite" url="wwwroot/{R:1}" />
          </rule>
        </rules>
      </rewrite>
      
<handlers>
        <add name="StaticFilesJpg" path="*.jpg" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesJpeg" path="*.jpeg" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesGif" path="*.gif" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesCss" path="*.css" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesPng" path="*.png" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesTs" path="*.ts" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesCscc" path="*.cscc" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesLess" path="*.less" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesIco" path="*.ico" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesHtml" path="*.html" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesMap" path="*.map" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesSvg" path="*.svg" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesJs" path="*.js" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFilesJSON" path="*.json" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      

The moral of this story is that if you navigate the GitHub issues when it comes to ASP.NET Core and building with MSBuild or publishing under IIS, you're going to come across a lot of grumbling about the weird amalgamation of older .NET Framework/MSBuild ways of doing things, and the .NET Core way. Microsoft took a few steps back in order to bring their enterprise customers along, but I feel that the result is a more difficult IIS experience for deployments, and I'm not sure any of this helped move the dial for .NET Core users.