Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats โ€“ natively work with DOCX, XLSX, PPT, PDF, images and more

More on ASP.NET Core Running under IIS


My last post on ASP.NET Core and IIS generated a lot of interest and also a lot of questions and discussion in the commment section to warrant a follow up post. In this post I address a few common themes that keep coming up in regards to IIS hosting of ASP.NET Core applications.

A quick Review of IIS Hosting for ASP.NET Core

When using ASP.NET Core on Windows, your most likely setup involves using IIS as a front end to the ASP.NET Core Kestrel Web Server that actually runs your ASP.NET Core application.

ASP.NET Core Hosting with IIS

IIS acts as a front end proxy to the backend Kestrel Console application that hosts the .NET based Kestrel Web server.

In this scenario, IIS uses a very low level and early pipeline AspNetCoreModule that intercepts all requests pointed at it (via a module mapping) and then forwards those requests to Kestrel on a different port. Requests come in on standard HTTP ports (80 and 443 for SSL) and IIS proxies the incoming requests to a different port that Kestrel is listening on.

By default the module configuration forwards all requests to Kestrel, but you have some very limited control over what gets routed to the module.

Why do you need a proxy? Kestrel is a raw Web server that knows how to serve HTTP requests, but it doesn't include support for hosting multiple services on the same port, host headers, easy SSL configuration and any number of other useful features that a full featured Web service like IIS provides. Some of these features may come in the future, but today it's very likely that if you have a public facing Web site or service you'll need some sort of front end Web server that proxies relevant requests to Kestrel.

Process Identity

One important aspect of running a Web application is process identity. When using IIS the process identity is determined through IIS Application Pools, and that IIS identity is carried forth to the ASP.NET Core Kestrel process. This means if you set the Launch Identity for the IIS Application Pool to a specific account, the ASP.NET Core Module launches the Kestrel process under that same account.

It's easy to see if you open Task manager and look at the w3wp.exe process (there may be more than one for each application pool so you have find the right one which you can do by looking at the command line aruments in Process Explorer or Task Manager) and the dotnet.exe process. In this example, I'm using my local desktop machine and I've set the Application Pool identity to Network Service:

You can see that both w3wp.exe and dotnet.exe - which runs the Kestrel Web server process - are using the same NETWORK SERVICE account. Keep in mind that you may have multiple application pools, and multiple instances of .NET Core Application's running at the same time in which case each application pool and kestrel process will launch in their associated security context.

In the example above, I only have the IIS AppPool active to demonstrate the Identity is indeed transferred to the the Kestrel process.

Both of these will run using the same User Identity so both should have the same set of permissions to access local disk resources, network resources and access to connections over the network.

If you need to use Windows credentials for things like SQL Server Authentication, then you can choose to use an account that has the necessary access right on the domain or local machine to make that happen.

IIS Application Pool Lifetime

One important feature you gain by using IIS as a front end to your ASP.NET Core application is that you get to take advantage of IIS's lifetime management features. The IIS Admin service can ensure that your application gets launched when the AppPool starts, and gets shut down when the AppPool shuts down.

If Kestrel crashes the Admin features provided by the ASP.NET Core Module also trigger for dotnet.exe to get relaunched.

If you make changes to the Application Pool and end up changing the Identity or any other feature, the Application Pool is shut down and Kestrel with it, and then restarted with the new Application Pool and settings. IIS handles draining of the request pipeline keeping the old application pool alive and requests are completed while routing new requests to a new Application Pool. This is the same behavior IIS has always provided but it essentially extends that same behavior to Kestrel.

So even though you're running a separate Web Server in Kestrel, your ASP.NET Core application as a whole behaves pretty much the same way ASP.NET Applications have always behaved in IIS when it comes to application life time.

Performance

Ah yes, performance is a big issue. The ASP.NET team has done a phenomenal job of creating an extremely fast Web server in Kestrel. At a very low level, Kestrel's throughput is roughly 20x faster than classic ASP.NET on Windows. It's also nearly 5x faster at serving content natively vs. serving it through IIS:

based on official ASP.NET benchmark results

The difference is ridiculously large, but in real world applications you are unlikely to see that kind of performance difference for all but very, very lean microservices, as heavy processing for database access, disk and network access kick in and negate just about all of this raw throughput performance.

Still low level performance at the protocol and service level is significant as it also means better resource usage which can lead to better scalability even for heavier Web applications.

It might seem that if you're using IIS, you're ham stringing yourself to a slow Web server compared to Kestrel, right?

It's not quite so simple.

I set up some local load test with West Wind Web Surge and when hitting a simple, static test.html page with both IIS and then with Kestrel I'm seeing that IIS is vastly faster at serving this content.

Here's a quick short run test against both with WebSurge (all running through a published app in a local folder in release mode, with server GC enabled and all logging turned off):

  • Kestrel: 13,750+ req/sec
    This serves an index page from disk using the static file middleware.

  • Kestrel (Memory Served Index): 20,950 req/sec
    This serves a static HTML string from a raw controller action. No DI no other code than the method returning static html.

  • IIS: 54,000+ req/sec
    IIS running a static index page in a published application.

Whoa, what?

IIS is faring much better here. My guess is it has to do with caching as IIS is surely serving the content entirely from cache while Kestrel is reading and writing out the static content each time which would account for some of this large difference.

To be fair the Kestrel application (my albumviewer) isn't optimized for this low impact use case and the entire pipeline for the Web API and MVC is fired up. Still - I would have expected much better performance and I'm not sure why the performance is practically the same as what I see with classic ASP.NET applications. I've not seen any run away performance numbers with Kestrel in my own testing.

And yes I made sure I'm running a Release build and I don't have console logging enabled ๐Ÿ˜„.

Curious to hear what some of you are seeing on Windows in your testing or what special steps you're taking to optimize your ASp.NET Core based applications.

Rather than dwell on Kestrel and ASP.NET Core performance, I think it's more telling how efficiently IIS is able to serve static content.

Bottom line - IIS is plenty fast for static content and it provides automatic content compression for static content. It's a good idea to use IIS for static file serving.

Unfortunately that's not as easy as one might hope out of the same application.

You can try these out yourself in my ASP.NET Core AlbumViewer project - it includes the West Wind WebSurge .websurge file to quickly test running against both IIS hosted and raw Kestrel. It'd be good to see this tested on a beefy Windows Server install which I have not done.

Mixing IIS and Kestrel Content

The ASP.NET Core module is hooked up in web.config and takes a filter path to determine what it should look at and pass to Kestrel for processing.

By default the configuration globs everything and passes everything to Kestrel for processing. This includes static content.

Here's what the aspnetcore module configuration looks like by default:

<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*." verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="dotnet" arguments=".\AlbumViewerNetCore.dll" stdoutLogEnabled="false" stdoutLogFile="..\logs\stdout" forwardWindowsAuthToken="false" />
  </system.webServer>
</configuration>

The path="*" setting means that all requests to the server are forwarded to Kestrel.

If you want to have Kestrel only handle requests in an api folder you can do:

path="api/*"

or if you only want to handle only extensionless urls:

path="*."

That works beautifully to filter requests and handle only requests in the API folder for the first and only extensionless URLs in the second case.

Fail

As nice as that is, the second part needed to let IIS serve everything else falls flat on its face because an ASP.NET Core Web site has to be rooted in the binary folder (the Content Root) rather than the wwwroot (Web Root) folder.

What this means is that IIS's root folder is the Content Root not the Web Root which is not what you'd want.

For example, I can't say:

http://localhost/index.html 

which is a 404. Instead I'd have to do:

http://localhost/wwwroot/index.html

Aargh. Seriously? Yup and I still don't understand why this couldn't have been cleaner where the Site/virtual could be configured on the wwwroot folder.

But today this approach simply doesn't work. Even if you were willing to explicitly reference the wwwroot folder, this leaves the Content Root with all of its binary files and web.config exposed to the Web which is definitely not a good idea!

Do it yourself!

One alternative is that you explicitly map static file extensions to the IIS StaticFileModule in IIS before the ASP.NET Core module and use some custom rewrite rules to route all requests to the wwwroot folder.

I can't take credit for this, the following comes courtesy of Imran Baloch who has a post on how to set up the Static file handlers and rewrite rules in a post.

The following is pretty much lifted directly from Imran's post with the addition of html file handling by IIS:

<configuration>
  <system.webServer>  
    <rewrite>
        <rules>
            <rule name="wwwroot">
                <match url="([\S]+[.](html|htm|svg|js|css|png|gif|jpg|jpeg))" />
                <action type="Rewrite" url="wwwroot/{R:1}" />
            </rule>
        </rules>
    </rewrite>

    
    <handlers>      	
      	<add name="StaticFileModuleHtml" path="*.html" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
      	<add name="StaticFileModuleHtm" path="*.htm" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModuleSvg" path="*.svg" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModuleJs" path="*.js" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModuleCss" path="*.css" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModuleJpeg" path="*.jpeg" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModuleJpg" path="*.jpg" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModulePng" path="*.png" verb ="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="StaticFileModuleGif" path="*.gif" verb="*" modules="StaticFileModule" resourceType="File" requireAccess="Read" />
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>

    <aspNetCore processPath="dotnet" arguments=".\AlbumViewerNetCore.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
  </system.webServer>
</configuration>

As you can see you explicitly map each static resource explicitly to the IIS StaticFileModule. You may be able to get away with using any file with an extension (.) as well, but it's probably better to be very explicit. The rewrite URL then basically looks for specific extensions in a URL and if it finds it automatically prepends the wwwroot base url to the url. The URL is rewritten without affecting the browser's url, so all relative paths continue to work as you would expect on the front end in the browser.

It's a shame that that the AspNetCoreModule doesn't provide this functionality out of the box for all requests that are not handled by the ASP.NET Core module (ie. when you put a stricter filter on the core module like *. or api/*). At the very least the default template should add the configuration settings above as a comment to allow quickly setting up IIS for what it is really good for in a local site.

But for now the above is a great starting point for providing drop in static file features from IIS.

Static and Dynamic Sites/Virtuals

It's also possible to work around this by creating two web sites or a web site and a virtual. You run all of your static content out of the root Web site and you can then set up a child directory as an IIS Application/Virtual where you run your ASP.NET Core application.

This is not a uncommon approach as it also helps in building optimized server instances that can focus on only serving dynamic content and static content which often require different sets of optimization settings.

If you're building a rich client application for your front end - like an Angular or React app for example - you are probably already developing your applications as separate sites anyway with the development server running on a different port than your Web backend.

You can do the same with IIS and Kestrel.

The simplest thing is to use two separate domains:

  • web.mysite.com (or just mysite.com) - static content
  • api.mysite.com - API

If two host header domains are overkill for you, you can also accomplish the same thing with IIS Virtual Directories or a site and virtual directory:

  • Set up the IIS Site root in the wwwroot Web Root Folder
  • Create a sub Application in the Content Root Folder

Here's what this looks like:

The api folder now points at the ASP.NET Core application, while the site root points at the wwwroot folder where the actual files live.

So now I can navigate to:

http://localhost:81/index.html

and

http://localhost:81/api/api/albums

Note the double api/ reference because my API in this case was set up to use an API prefix.

For this to work properly I would probably want to change the routing in the application to remove the /api prefix in the route and just handle the requests directly off the root.

So:

[HttpGet]
[Route("api/albums")]

to:

[HttpGet]
[Route("albums")]

which then lets me use this URL:

http://localhost:81/api/albums

and now the routing works.

This works fairly well for modern JavaScript applications that only load up static content, but it's much more difficult to do the same with a server rendered MVC application because there you mix static and dynamic content much more liberally.

Whether you choose to use the Rewrite URL approach and run everything in a single site, or you break out static and dynamic content into separate sites really depends on the type of application you are building. If you're building a rich JavaScript front end application the separate site approach is very easy to work with. However, if you are building a server rendered MVC application that approach doesn't really work so well because you want your static content to be part of your application's folder structure rather than pointing to an external Url.

Summary

The current state of the hosting environment on windows sure seems like it is a lot more complicated than what we had previously with ‘classic’ ASP.NET that 'just works'. By using IIS as a front end to the Kestrel Web Server as is recommended on Windows the complexity of setting up and running a site is more involved as you are essentially configuring two separate environments and as you've seen with the static content configuration it can be a bit messy to get content separated between the dynamic and static content.

Although this post points out some of the pain points I've run into, it's clear that ASP.NET Core offers many improvements and a lot of potential for improving the hosting process in the future. It's also quite likely that Kestrel will eventually become a more full featured Web Service (as opposed to a raw Web Server) to provide some of the Infrastructure features that we need to use IIS or some other front Web server for today.

I hope this post has been useful in covering some issues that come up frequently. I'd also be curious how you use IIS and Kestrel together and make the most of the strengths of each of the platforms. Chime in if you have a good integration story to share…

Resources

this post created with Markdown Monster
Posted in ASP.NET Core   IIS  

The Voices of Reason


 

Thiago Henrique da Silva
March 17, 2017

# re: More on ASP.NET Core Running under IIS

Great post Rick ! I think that the worker process name is wrong in some places. Was wrote w3p.exe but it's w3wp.exe


Paul Hunt
March 20, 2017

# re: More on ASP.NET Core Running under IIS

Really interesting article, love the performance work that's been done by the team.

p.s. there's a small typo in the opening paragraph "commment"


Ian
March 20, 2017

# re: More on ASP.NET Core Running under IIS

Great post. One minor correction to the intro information - Kestrel supports HTTPS according to https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel


Andrei Rรฎnea
March 21, 2017

# re: More on ASP.NET Core Running under IIS

Rick, I believe IIS has by default kernel caching enabled and IIS has also static content caching by default. This probably leads to IIS serving the contents of the static file directly from kernel memory space (via HTTP.sys) which should explain the high throughput that you're seeing.


Mark
March 23, 2017

# re: More on ASP.NET Core Running under IIS

Hi Rick, Great article as always. I have struggled to get IIS to talk to Kestrel, seems to be getting mixed up with port numbers. My IIS site is running on port 50888 and I have setup Kestrel (via .UseUrls on the host builder) to use port 50889. When I hit the IIS hosted site it starts dotnet.exe, hosting the .NET Core app on port 50889 as expected. However I eventually get an error in the Application Event Log stating:

"Application 'MACHINE/WEBROOT/APPHOST/CORETEST2' with physical root 'C:\Temp\Coretest2' created process with commandline '"dotnet" .\CoreTest2.dll' but either crashed or did not reponse or did not listen on the given port '12555', ErrorCode = '0x800705b4'"

Where is port 12555 coming from? Is there some config for this in IIS?

Thanks.


Rick Strahl
March 30, 2017

# re: More on ASP.NET Core Running under IIS

@Andrei - yup Kernel caching is a beautiful thing for static content :-). Still I would have expected Kestrel to be as fast given the supposed performance advantages that ASP.NET Core is supposed to have. But as was pointed out by the ASP.NET team - to date the StaticFile middleware is not optimized or well featured. There's no support for caching nor any capability to manage content compression, which both are big reasons to me to not use Kestrel for static content.


Andy
April 08, 2017

# re: More on ASP.NET Core Running under IIS

Thanks for providing the stats.

It strikes me that they confirm what I felt was a bit weird. That the MS line has been to trumpet how much faster ASP.NET Core is than its predecessor while at the same time adding an extra process hop!

One option I am considering for my production ASP.NET Core apps is to use Http.sys via WebListener.

Another option would be to break out old skills and write an ISAPI module to host Core with a smaller number of hops so that the benefits of IIS are not lost. But why haven't MS done this?

Is there a compelling explanation anywhere as to why the proposed use of IIS as a reverse proxy is an improvement?


Rick Strahl
April 08, 2017

# re: More on ASP.NET Core Running under IIS

IIS is very fast for request routing. If you never hit the main pipeline or .NET in the pipeline, the routing features are very fast, so I doubt you're going to do any better than what IIS already does. The extra process hop isn't an issue - the network hit maybe but I doubt it since this goes over a local connection.

As I pointed out - this is all splitting hairs unless you are running requests that are actually fast enough to matter that the request overhead is a factor in the response time. For anything that touches a database, or does any sort of IO that's unlikely to be the case.


Pieter van Kampen
April 17, 2017

# re: More on ASP.NET Core Running under IIS

Thanks Rick, you mention you can run asp.net core in a virtual, below a static IIS web site. I am looking how to this the other way round:

www.testme.com: asp.net core web site www.testme.com/soap: asmx web services pages

What I have right now, is that the pages under /soap are handled by the asp.net core executable.

Any suggestions?


Haik
May 03, 2017

# re: More on ASP.NET Core Running under IIS

I would serve static content form CDN


Amar
June 05, 2017

# re: More on ASP.NET Core Running under IIS

Hosted my .NetCore web application under IIS and now I have to get the client credentials to pass it TFS REST API services via httpClient. Can anyone know how to do this in .Netcore?


James
June 12, 2017

# re: More on ASP.NET Core Running under IIS

If you have the time, it would be interesting to rerun the test with static file middleware sending the Cache-Control response headers as shown here:

https://github.com/aspnet/Docs/blob/8a10c9800d79cb2a88a6e9bdb9cd4a27f8379d48/aspnetcore/fundamentals/static-files/sample/StartupAddHeader.cs#L26

IIS Caching is still faster, but I would be interesting to see if this reduces some of the performance overhead.


Rick Strahl
June 13, 2017

# re: More on ASP.NET Core Running under IIS

@James - that isn't going to matter since those are client side cache values that a Web test client isn't going to respect.

And that's not really the point in a load test ๐Ÿ˜ƒ You want the server to have to serve a large number of requests so you can see what it's capable of client caching or not. Relying on client cache just hides to perf characteristics.

 

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2017