For the past several months we have been working hard to provide full feature parity accross all Habitat components on Windows. We often use an ASP.NET Core plan to test Windows functionality because building and running a .NET Core application is very similar to developing a Node application not to mention many other runtimes that others use Habitat for building and running their applications. The .NET Core runtime is extremely portable and easy to isolate within a Habitat environment. It does not require machine scoped configuration like its "full framework" cousin. I'm personally excited about .NET Core and really hope others come to adopt it.
However, today, the world's .NET applications run on the .NET full framework and ASP.NET applications run inside IIS and not the lightweight Kestrel web server. How do we build and run these inside of Habitat? How would we create packages for the .NET full framework or IIS like we do for Node or nginx? These are truly challenging scenarios because the .NET full framework needs to reside in a specific location on disk and IIS is a feature of the Windows operating system.
This post will walk you through how to work around these challenges and create a plan that builds and runs an ASP.NET full framework application inside of an IIS application pool.
You can find a complete Github repo of this application along with its Habitat plan, hooks and config here.
.NET Full Framework and IIS as Infrastructure
First and foremost, we will not be creating Habitat packages for the .NET framework or IIS. We are going to let our configuration management or provisioning system of choice ensure that the .NET framework is installed and that the appropriate IIS features are enabled. When we author our Habitat plan, we will assume that the .NET framework and IIS pieces are already in place. Our plan will focus on our individual .NET application. It will build the application on any machine where the .NET 4.0 CLR is installed, it will setup the IIS application pool and web site, and deploy our app binaries and web artifacts to our nodes where the Habitat Supervisor will ensure it is running and update it as updates are available.
So lets get started!
An ASP.NET application
The first thing we need is an actual ASP.NET application. You may already have one and if so, that's great. Because I don't right now (but I did spend 10 years of my career building them), I'm gonna cheat. I'll open Visual Studio 2017 and select file/new/project and then create the stock ASP.NET MVC application template. Earlier Visual Studio like 2015 should work too.
Finally, and mainly to illustrate how to adjust our Habitat plan to target different .NET framework versions, we will target version 4.7.
Building with a plan.ps1
Right in Visual Studio I'll add a
habitat folder to my project and then right click on that folder and select Add/New Item. Because I have the PowerShell Tools Visual Studio Extension, I have the option to add a PowerShell script and I will add a
Now I'll add the codes for building this project:
This bit of PowerShell script essentially leverages
msbuild to grab dependencies, compile our code and publish it to our Habitat
pkgs directory which will be archived to a Habitat
hart package. It should also be noted that in order for Habitat to build this plan, one does NOT need to have Visual Studio installed. Rather you just need a recent framework version of .NET installed which ships by default on Windows Server 2012R2 and forward.
First, enter a Windows Habitat Studio:
Within the studio, lets build and package our
hart for this application:
This should compile and package our
hab-sln project. Lets drill in a bit closer on a few of the moving parts of the plan.
The Nuget Command Line Utility
Note that our plan takes a build dependency on
core/nuget. That is a core Habitat plan that packages the
nuget.exe utility. Our build will use that to do a "package restore" of all packages declared in our project's
packages.config file. Visual Studio would essentially do the same if we were building inside the IDE.
Using the Right .NET Reference Assemblies
When you build a .NET project,
msbuild searches for reference assemblies matching the framework version you are targeting. You can build a .NET assembly against any framework version compatible with the CLR version you are using regardless of the version of the .NET framework you have installed. You just need the right reference assemblies. We use the
core/dotnet-47-dev-pack because we are targeting 4.7 and set the
TargetFrameworkRootPath to its contents. This may be optional since if the reference assemblies are not present, the build will use the assemblies in your Global Assembly Cache. However, the build could fail if there are APIs used that are not in your locally installed assemblies so it is good practice to reference the appropriate targeting pack.
Getting the VisualStudio.Web.targets
Most of the
.prop files, and the
msbuild.exe needed to build .NET assemblies can be found in your local
c:\windows\Microsoft.NET folder. However, the web targets are not. The easiest way to get those without installing Visual Studio is to use
nuget to install the
MSBuild.Microsoft.VisualStudio.Web.targets package and then point
VSToolsPath to its location.
Using Compatible MSBuild Tooling
Because we are targeting the v4.7 framework, our project will take a dependency on the v4.7
Microsoft.Net.Compilers Nuget package. This is going to require us to use the Visual Studio 2017 MSBuild tooling in our
Invoke-Build. So we declare a build time dependency on
core/visual-build-tools-2017 which places its version of
msbuild on the path.
Configuring IIS Pools, Sites and Applications
As mentioned earlier, we won't rely on Habitat for the installation of .NET or the right IIS features. However, it makes sense that Habitat should be able to configure IIS so that the application can run and behave as expected. We will leverage our run hook to do this.
There are two approaches we can take in our hook. One is a bit dated and clunky, but works everywhere. The other is more elegant, but limited to machines where WMF 5 (or higher) is installed.
This is the old-fashioned
appcmd.exe of yore. For our purposes in our little sample app, it can get the job done:
This uses values in our configuration to create the app pools, site and application folders needed to run our site.
If you are familiar with DSC (particularly the resources in
xWebAdministration), then using Habitat to configure IIS should come naturally. We will create a
config folder in our
habitat directory and a
website.ps1 inside of that. Here you might have the following configuration:
This uses Habitat's templating capabilities to ensure that IIS points to where our application is stored in the Habitat
svc folder and uses the configured port.
run hook will apply this configuration whenever we start our ASP.NET application and make sure everything is configured according to our DSC or
Running our Application in the Habitat Supervisor
In full framewoerk apps, IIS is often in charge of the process lifecycle of our app, unlike Node or .NET Core where we would simply invoke
dotnet.exe to run our app in a lightweight web server. So instead of handing off our run hook to call into a runtime, we just need to make sure IIS is configured and the site and app pool are both in the "running" state. We already covered IIS configuration above. However if the
run hook terminates, the Supervisor assumes our service has terminated. As a result, we will have PowerShell loop and check that our application endpoint is responsive:
As you can see, we simply call
Invoke-Webrequest to send a
HEAD request to our app. If the response is not a
200, then we assume things have gone horribly wrong and exit the loop. Furthermore, if the Supervisor stops our service (
hab svc stop mwrock/hab-sln), execution will move to the
finally block, where we tell IIS to shut down our application.
So let's run our application. If you are not already in a Windows Studio, enter
hab studio enter -w from the root of our solution.
Assuming that the plan has been built as shown above, We can stream the Supervisor log and start our service:
Our Supervisor log should look something like this:
and browsing to
http://localhost:8099/hab_app should bring up our ASP.NET web site.
Now let's go crazy and change the web site port:
and our Supervisor log reads:
Our port change forces an application restart and now we can reach our application on
Of course changing a port is just one small example of a multitude of possible configuration change scenarios where we can inject a configuration change into our Habitat ring and every service can respond.
We'd love to hear what you think! Do you have ASP.NET applications that you'd like to build and run with Habitat? Do you think this pattern would work for your applications and tour team's work flows? Do you think it would need some tweaking. We'd love to hear! You can talk with us in the #windows Habitat slack channel.