A bee is visiting a purple flower.

Hosting ASP.NET Core RC2 in an Azure Web App

 

This is how we publish our ASP.NET Core RC2 application from GitHub to an Azure Web App. It’s an algae monitoring API, which we were running with the pre-release of RC2 in an virtual machine on Azure. There were only a few changes to make in order to move to an Azure Web App and use the go love RC2 release.

Prerequisites

  1. An Azure Web App.
  2. A GitHub account.
  3. An ASP.NET Core RC 2 project.

General Process

Push the ASP.NET Core project to a GitHub repository. Then in the Azure Portal, create a Web App and set its deployment source to the repository’s master branch. On each push to GitHub, Azure will pull from the repo, restore the dependencies, and build. Then you can take the rest of the day off and garden.

Settings > Deployment Source

ASP.NET Core Rc2 Repository Structure

This is what’s in our app’s repository. It’s a fairly simple API with controllers, extensions, services, and a Sqlite database. You’ll notice there is no wwwroot because we do not have any static files.

AlgaeApi       
    Controllers         
    Extensions           
    Migrations          
    Models                  
    Services            
    ViewModels        
    Algae.db            
    hosting.json        
    project.json      
    Startup.cs          
    web.config          < ----- Look mom, no wwwroot!
.git
.gitignore
.deployment
Nuget.config

project.json

To host in an Azure Web App, we needed the win8-x86 runtime instead of the win10-x64 runtime. We didn’t need to change anything else in our project.json. Well done MSFT.

{
    "compilationOptions": {
        "emitEntryPoint": true,
        "preserveCompilationContext": true
    },
    "dependencies": {
        "Microsoft.AspNetCore.Mvc.Core": "1.0.0-*",
        "Microsoft.AspNetCore.Diagnostics": "1.0.0-*",
        "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.0-*",
        "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*",
        "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*",
        "Microsoft.EntityFrameworkCore.Sqlite": "1.0.0-*",
        "Microsoft.EntityFrameworkCore.InMemory": "1.0.0-*",
        "Microsoft.EntityFrameworkCore.Commands": "1.0.0-*",
        "Microsoft.AspNetCore.Cors": "1.0.0-*",
        "Microsoft.AspNetCore.StaticFiles": "1.0.0-*",
        "Microsoft.Extensions.Logging.Console": "1.0.0-*",
        "Microsoft.Data.Sqlite": "1.0.0-*",
        "System.Xml.XDocument": "4.0.11-rc2-*",
        "Microsoft.NETCore.App": "1.0.0-rc2-*"
    },
    "tools": {
        "dotnet-watch": {
            "version": "1.0.0-*",
            "imports": [
                "portable-net45"
            ]
        },
        "dotnet-ef": {
            "version": "1.0.0-*",
            "imports": [
                "portable-net45"
            ]
        },
        "dotnet-publish-iis": {
            "version": "1.0.0-*",
            "imports": [
                "portable-net45"
            ]
        }
    },
    "frameworks": {
        "netcoreapp1.0": {
            "imports": [
                "dnxcore50",
                "portable-net45"
            ]
        }
    },
    "runtimes": {
        "win8-x86": {}      < ---- For Azure, we needed win8-x64 instead of win10-x64.
    },
    "content": [
        "web.config",
        "algae.db",
        "logs"
    ]
}

Startup.cs

To move from RC1 to RC2, we stopped using IApplicationEnvironment and instead used IHostingEnvironment. The appEnv.ApplicationBasePath is now hostingEnv.ContentRootPath. This is well documented here. We also took out the call to UseDefaultHostingConfiguration() because it was breaking our build and didn’t seem necessary anymore.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.IO;
using AlgaeApi.Models;
using AlgaeApi.Services;

namespace AlgaeApi.CloudComputer
{
    public class Startup
    {
        private readonly IHostingEnvironment _hostingEnv;

        public Startup(IHostingEnvironment hostingEnv)
        {
            _hostingEnv = hostingEnv;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            #region Comment out when running migrations.

            services
                .AddMvcCore()
                .AddJsonFormatters();

            services.AddCors();

            #endregion

            services
                .AddEntityFramework()
                .AddEntityFrameworkInMemoryDatabase()
                .AddEntityFrameworkSqlite()
                .AddDbContext<AlgaeDbContext>(options =>
                {
                    var basePath = _hostingEnv.ContentRootPath;
                    var dbPath = Path.Combine(basePath, "algae.db");
                    options.UseSqlite($"Filename={dbPath}");
                });

            services.AddDirectoryBrowser();

            services.AddSingleton<IComponentHistoryRepository, ComponentHistoryRepository>();
        }

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(LogLevel.Debug);

            // PowerShell> $env:ASPNETCORE_ENVIRONMENT = "Development"
            // PowerShell> $env:ASPNETCORE_ENVIRONMENT = "Staging"
            // PowerShell> $env:ASPNETCORE_ENVIRONMENT = "Production"
            if (!_hostingEnv.IsProduction())
            {
                using (var serviceScope =
                    app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
                {
                    serviceScope.ServiceProvider.GetService<AlgaeDbContext>().EnsureSeedData();
                }
            }

            // UseCors must come before UseMvc!                             
            app.UseCors(policy =>
            {
                policy.AllowAnyOrigin();
                policy.AllowAnyHeader();
                policy.AllowAnyMethod();
            });

            app.UseDeveloperExceptionPage();
            app.UseRuntimeInfoPage(); // default path is /runtimeinfo

            app.UseStaticFiles();

            app.UseMvc();
        }

        public static void Main(string[] args)
        {
            var contentRoot = Directory.GetCurrentDirectory();

            var webRoot = Directory.GetCurrentDirectory();
            if (!contentRoot.Contains("wwwroot"))
            {
                webRoot = Path.Combine(contentRoot, "wwwroot");
            }

            var host = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(contentRoot)
                .UseWebRoot(webRoot)
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}

web.config

The main change here is the use of the new AspNetCoreModule instead of HttpPlatformHandler.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add 
        name="aspNetCore" 
        path="*" verb="*" 
        modules="AspNetCoreModule"        < ------ A new handler just for AspNetCore
        resourceType="Unspecified" />
    </handlers>
    <aspNetCore
        processPath=".\AlgaeApi.exe"
        arguments=""
        stdoutLogEnabled="true"
        stdoutLogFile=".\logs\stdout" />
  </system.webServer>
</configuration>

.deployment

This tells Azure the folder in which our ASP.NET Core project runs. The Kudu documentation on this is here.

[config]
project = AlgaeApi

Nuget.config

We’re using the aspnetrelease feed though others would work too.

<configuration>
  <packageSources>
    <clear />
    <add key="AspNetCI" value="https://www.myget.org/F/aspnetrelease/api/v3/index.json" />
    <add key="NuGet.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

References

NuGet Feeds: https://github.com/aspnet/Home/wiki/NuGet-feeds

Migration from RC1 to RC2: https://docs.asp.net/en/latest/migration/rc1-to-rc2.html

Time to dig in the garden!

Light reflecting off the roof in the background of our garden.