CheapASPNETHostingReview.com | Best and cheap ASP.NET Core 2.0 hosting. At Build 2017, there were a lot of new features announced for ASP.NET Core 2.0, .NET Core 2.0 and .NET Standard 2.0.
Today, we’re going to look at a few of the changes, specifically: the new configuration model and Razor Pages
Configuration
A lot of the changes that the ASP.NET Core team have brought to ASP.NET Core 2.0 are all about taking the basic application setup and making it as automatic, and quick and easy to change as possible. The first and easiest way that they have done this is by creating the AspNetCore.All package.
AspNetCore.All Package
In previous versions of ASP.NET Core when we’ve created an application and wanted to add in functionality, we’ve had to search on NuGet or using the Package Manager to find the NuGet packages for the functionality that we want.
This lead to a csproj which looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <Description>A .NET Core WebApi project, utilizing SqlLite and EF Core, for searching Discworld Books and Characters.</Description> <VersionPrefix>1.0.0.0</VersionPrefix> <Authors>Jamie Taylor</Authors> <TargetFramework>netcoreapp1.0</TargetFramework> <AssemblyName>dwCheckApi</AssemblyName> <OutputType>Exe</OutputType> <PackageId>dwCheckApi</PackageId> <RuntimeFrameworkVersion>1.1.0</RuntimeFrameworkVersion> <PackageTargetFallback>$(PackageTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8</PackageTargetFallback> </PropertyGroup> <ItemGroup> <Content Update="wwwroot;Views;appsettings.json;web.config"> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </Content> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Routing" Version="1.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="1.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="1.1.*" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.*" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.*" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="1.1.*" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.*" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.*" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.*" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.*" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="1.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.1"></PackageReference> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.0"></PackageReference> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" /> </ItemGroup> </Project> |
Re-targeting this project as a netcoreapp2.0 (.NET Core 2.0) application with ASP.NET Core 2.0 libraries, we get the following csproj file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <Description>A .NET Core WebApi project, utilizing SqlLite and EF Core, for searching Discworld Books and Characters.</Description> <VersionPrefix>1.0.0.0</VersionPrefix> <Authors>Jamie Taylor</Authors> <TargetFramework>netcoreapp2.0</TargetFramework> <AssemblyName>dwCheckApi</AssemblyName> <OutputType>Exe</OutputType> <PackageId>dwCheckApi</PackageId> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0-*" /> </ItemGroup> </Project> |
The AspNetCore.All package is a meta package which pulls down all of the relevant (Anti Forgery, Auth, Entity Framework Core, MVC, Static files, etc.) packages to our application when package restore happens.
Because we no longer have to track down each of these individual packages, our job is made easier. Also, when the packages within the AspNetCore.All package are updated, the updated versions will be included in the AspNetCore.All meta package.
The AspNetCore.All package is included in .NET Core 2.0’s Runtime Store, and is compiled to native code, rather than IL. This means that all of the libraries included in the AspNetCore.All package are pre-compiled as native binaries for the Operating Systems that .NET Core 2.0 supports.
Boot Time Improvements
Dan and Scott were able to show that ASP.NET Core 2.0 applications can cold boot in less than a second, versus up to 7 seconds for ASP.NET Core 1.0 applications.
The ASP.NET Core team have achieved this by shipping the AspNetCore.All package in native code for each platform, and by enabling view pre-compilation. By pre-compiling the views, they no longer have to be compiled at start up.
View pre-compilation is a trick that has been around in .NET Framework for a while, but it isn’t a default build action.
New Program Setup
This leads me nicely onto the new program setup model.
In ASP.NET Core 1.0 the program.cs file contained a single method for configuring and running the server, and there was a lot of manual configuration required. As in the following code block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Builder; namespace aspNetCoreOneDemo { public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .Build(); host.Run(); } } } |
To enable server features, you had to know what those features where called or rely on intellisense in order to find the right methods.
But in ASP.NET Core 2.0, a lot of the configuration is taken care of for us. So much so that the following code snippet is the default program.cs for an ASP.NET Core 2.0 application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; namespace AspNetCoreTwoDemo { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .Build(); } } |
From the off, you can see how much simpler the new program.cs file is. The new program.cs goes hand in hand with the new startup.cs
First a refresher on what the ASP.NET Core 1.0 startup.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace AspNetCore1Demo { public class Startup { public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } } |
Configuration is handled by us developers and we have to explicitly list all configuration files and enable logging.
Compare this to the new startup.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace AspNetCore2Demo { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } } |
There’s a lot that’s changed here, so let’s look at the changes in turn.
The Constructor and DI
Taking a look at the constructor, we can see that the configuration is Dependency Injected in for us.
1 2 3 4 5 6 7 8 9 10 | namespace AspNetCore2Demo { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } |
This is because all of the explicit configuration that we had to do in ASP.NET Core 1.0 is done automatically for us. ASP.NET Core 1.0 will look for any and all relevant json/ini files and attempt to deserialise them to objects for us and inject them into the IConfiguration object.
The ConfigureServices method is pretty much the same, but the Configure method has been very simplified:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } |
In the ASP.NET Core 1.0 Configure method, we had to inject the ILoggerFactory in order to enable logging:
1 2 3 4 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); |
However, the ASP.NET Core 2.0 Configure method doesn’t have the ILoggerFactory injected in:
1 2 3 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) |
This is because the contents of the appsettings.json are parsed and added into the IConfiguration object which is injected in at the constructor level of the class:
1 2 3 4 5 6 7 8 | public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } |
If we take a look at the appsettings.json, we can see that the logging is set up for us there:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" } }, "Console": { "LogLevel": { "Default": "Warning" } } } } |
And looking at the highlighted lines, we’ll see that logging is set up so that we’ll only get warnings. This can be proven by running the application from the terminal. Doing so, and navigating around in the application, you won’t receive any messages in the terminal other than warnings:
1 2 3 4 5 | $ dotnet run Hosting environment: Production Content root path: /AspNetCore2Demo Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. |
However, if we edit the appsettings.json file to match the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Information" } }, "Console": { "LogLevel": { "Default": "Information" } } } } |
Then re-run the application and click around, we’ll see the familiar log messages again:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | $ dotnet run : Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0] User profile is available. Using '/.aspnet/DataProtection-Keys' as key repository; keys will not be encrypted at rest. Hosting environment: Production Content root path: /AspNetCore2Demo Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] Request starting HTTP/1.1 GET http://localhost:5000/ info: Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker[2] Executed action (null) in 532.612ms info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 2961.046ms 200 text/html; charset=utf-8 info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] Request starting HTTP/1.1 GET http://localhost:5000/favicon.ico info: Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware[2] Sending file. Request path: '/favicon.ico'. Physical path: '/AspNetCore2Demo/wwwroot/favicon.ico' info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 17.479ms 200 image/x-icon info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1] Request starting HTTP/1.1 GET http://localhost:5000/About info: Microsoft.AspNetCore.Mvc.RazorPages.Internal.PageActionInvoker[2] Executed action (null) in 10.589ms info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 318.834ms 200 text/html; charset=utf-8 |
Its entirely up to the developer and their needs as to which level of logging they require. I prefer information logging when I’m developing and to switch to warnings once I’ve published, but your requirements may be different.
Razor Pages
The other big new thing in ASP.NET Core 2.0 is the concept of Razor Pages. Razor Pages are enabled by default, as they are a feature of MVC, thus the following line in the startup.cs enables them:
1 2 3 4 | public void ConfigureServices(IServiceCollection services) { services.AddMvc(); } |
Razor Pages cover the situations when creating a full blown Controller, View and a Model for a single or small number of pages seem a little over kill. Take for instance a simple homepage with no controller required, presumably something which could be handled by a static page, but which should have a simple model.
An example of this can be seen in the ASP.NET Core Web App (Razor Pages) template, which is installed as part of the .NET Core 2.0 preview1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Templates Short Name Language Tags ---------------------------------------------------------------------------------------------------- Console Application console [C#], F# Common/Console Class library classlib [C#], F# Common/Library Unit Test Project mstest [C#], F# Test/MSTest xUnit Test Project xunit [C#], F# Test/xUnit ASP.NET Core Empty web [C#] Web/Empty ASP.NET Core Web App (Model-View-Controller) mvc [C#], F# Web/MVC ASP.NET Core Web App (Razor Pages) razor [C#] Web/MVC/Razor Pages ASP.NET Core Web API webapi [C#] Web/WebAPI Nuget Config nugetconfig Config Web Config webconfig Config Solution File sln Solution Razor Page page Web/ASP.NET MVC ViewImports viewimports Web/ASP.NET MVC ViewStart viewstart Web/ASP.NET |
Taking a look at the directory structure for this new template, we can see that the new Razor Pages are located within the Pages directory.
Routing
Before we take a look at the contents of one of the Razor Pages, it will be worth covering how the routing for Razor Pages works. The request URL for a Razor Page is mapped to it’s path within the Pages directory – the Pages directory being the default location which the Runtime checks for any Razor Pages which could match the requested URL.
The following table shows a few examples of how the location of Razor Pages maps to requests: