ASP.NET Core 2.0 Configuration and Razor Pages

Today’s header image was created by NastyaSensei Sens, the original source for the image is available here

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.

Take a look at last weeks’s post, it contains a high level overview of some of the changes

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:

<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>

The above is the real csproj for my dwCheckApi project, which is a netcoreapp1.0 (.NET Core 1.0) application with ASP.NET Core 1.0.* libraries.

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:

<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>

This is due to the magic of the AspNetCore.All package.

Take a look at that link to see the full list of the packages included

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.

One dependency to rule them all

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.

Gotta love fast and optimised code in your libraries

Not only does AspNetCore.All include all of the packages required for ASP.NET Core, but it’s incredibly fast. So much so, that the speed of cold boots of ASP.NET Core applications have increased by a huge amount.

This was shown off at Build 2017 during a talk called “Introducing ASP.NET Core 2.0“ which was given by Scott Hanselman and Dan Roth. During the talk, Dan Roth showed the cold boot time differences between ASP.NET Core 1.0 and ASP.NET Core 2.0.

ASPNETCore 1.0 start up time va ASPNET Core 2.0 start up time
I left the timestamp on the screen shot. Please go over to the video and watch it for yourself, it’s really impressive
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:

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:

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:

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.

I’ve highlighted the relevant areas in the code snippet above

Compare this to the new startup.cs:

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.

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:

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:

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:

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:

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:

{
"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:

$ 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:

{
"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:

$ 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:

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:

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.

ASP.NET Core 2.0 Razor Pages directory structure
I’ve already expanded 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:

Razor Pages Request Mapping Example
Source: https://docs.microsoft.com/en-us/aspnet/core/razor-pages/#getting-started
Razor Page Content

We’ll start with the simplest example within the ASP.NET Core 2.0 Razor Pages template: the About page.

technically the simplest example is the index page, but that’s a massive page. The About page is nice and short, but covers the core stuff.

ASP.NET Core 2.0 Razor Pages directory structure

In the above screen shot, we can see an About.cshtml and an About.cs file – these are the files we’ll be looking at for our example.

First the About.cshtml:

@page
@model AboutModel
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@Model.Message</h3>
<p>Use this area to provide additional information.</p>

It looks pretty much like any other Razor cshtml file. We’re informing the compiler what out model is going to be, and adding some inline view data:

@model AboutModel
@{
ViewData["Title"] = "About";
}

And we’ve even got some standard html mixed in with our Razor syntax, too:

<h2>@ViewData["Title"].</h2>
<h3>@Model.Message</h3>
<p>Use this area to provide additional information.</p>

But the biggest change is the @page directive:

@page

The @page directive informs the compiler that we want to take the contents of this file, the associated cs file, and any inline c# code

because we can inline everything that we’re about to cover in an area called functions. More on those in a moment

and treat it as a single MVC action. This means that ASP.NET Core can handle requests to the About URL with this new action without having to use a controller.

To do this, however, the @page directive must be the the first line of the Razor Page. If the @page directive was placed anywhere else in this file, it would not work.

If we moved the @page directive around in the About.cshtml file, like the following:

@model AboutModel
@{
ViewData["Title"] = "About";
}
@page
<h2>@ViewData["Title"].</h2>
<h3>@Model.Message</h3>

the About.cshtml file would no loner bee seen by the compiler as a Razor Page, thus it would not be used to handle the request to /About. This would mean that the request is not handled and we would be re-routed to the Index page.

That’s not technically true, since there is no action which matches the /About request,  ASP.NET Core defaults to the /Index or root (i.e. /) action within the Razor Pages directory.

note: I am specifically discussing the behaviour of the Razor template. If you add Razor to the MVC template (or any other template), then standard error handling will take over and handle the request for any missing or badly formatted Razor Page

Razor Page Controller

Alongside the About.cshtml Razor Page is the About.cshtml.cs file, and it looks like this:

using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AspNetCore2Demo.Pages
{
public class AboutModel : PageModel
{
public string Message { get; set; }
public void OnGet()
{
Message = "Your application description page.";
}
}
}

The PageModel (which we’re inheriting from on line 5) provides us with a lot of the features that we would expect from a “traditional” MVC controller, and the source for it can be found here.

remember, .NET Core is completely open source and fully hosted on GitHub.

Within our AboutModel, we’re handling a single HTTP Verb (i.e. Get) in the OnGet method:

public class AboutModel : PageModel
{
public string Message { get; set; }
public void OnGet()
{
Message = "Your application description page.";
}
}

The OnGet method is used to handle GET Verb requests.

thank you, Captain Obvious 😛

You can add methods for any other HTTP Verb and they will be picked up by ASP.NET Core 2.0:

You can add handler methods for any HTTP verb. You most frequently use an OnGet handler to initialize any state a needed to show the HTML and OnPost to handle form submissions.

Source: https://docs.microsoft.com/en-us/aspnet/core/razor-pages/

This works exactly the same way as the the About controller method in a non-Razor Pages application:

namespace AspNetCore2Demo.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}

Razor Pages acts exactly like MVC does; this is because Razor Pages is part of it.

[Razor Pages] is MVC. It is itself MVC. Razor Pages is MVC

But it’s C#. It’s Razor, it’s stuff that you already know. It’s fast, and it’s self contained. It’s a page model for MVC.

Scott Hanselman: https://channel9.msdn.com/Events/Build/2017/B8048

The ASP.NET Core team are hoping that RazorPages will reduce the barrier to entry into the MVC world. As Scott Hanselman points out:

You said, “new project web”. You got a startup, like two lines of code.
And then there’s two options, you could say make a controllers folder, make a models folder, make a views folder, populate it with those things, and then hello world.
Or you can make a Pages folder and put a page in it. Just add a page and then start writing.
So think about this from a learning perspective, this is great.
Then if you start saying I’ve outgrown pages, you don’t move to MVC. You just move things to a different way of thinking.
The razor page exists, it can become a view. The code that you had in functions can go into a controller.

Scott Hanselman: https://channel9.msdn.com/Events/Build/2017/B8048

Functions

You don’t have to have the C# code separated from the Razor Page itself, as you can build up everything you need in a single file.

it’s beginning to look a little classic ASP.NET here, isn’t it?

Just as Scott Hanselman says in the above linked talk (Introducing ASP.NET Core 2.0), this can lead to fully dynamic pages which are completely self contained.

Taking the About.cshtml example from above, we can add the contents of the controller to the page by doing the following:

@page
@functions{
public string Message { get; set; }
public void OnGet()
{
Message = "Your application description page.";
}
}
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>Message</h3>
<p>Use this area to provide additional information.</p>

What we’ve done here is taken the contents of the About.cs file and added it to the Razor Page.

I’ve even highlighted the lines where we did that, for you

We’ve taken a reverse approach to the way that Scott Hanselman recommends, because we know MVC so well but we don’t know Razor Pages that well yet.

Caveat

We also have to either delete the About.cs file or comment out its contents, otherwise the contents of the About.cs file will be chosen over the Razor Pages @functions block.

You can see this by adding a break point to the following lines (I’ll highlight them for you):

In About.cshtml

@page
@functions{
public string Message { get; set; }
public void OnGet()
{
Message = "Your application description page.";
}
}
@{
ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>Message</h3>
<p>Use this area to provide additional information.</p>

and in About.cs

using Microsoft.AspNetCore.Mvc.RazorPages;
namespace AspNetCore2Demo.Pages
{
public class AboutModel : PageModel
{
public string Message { get; set; }
public void OnGet()
{
Message = "Your application description page.";
}
}
}

Running the application with the debugger attached (i.e. in VS Code or full Visual Studio), and requesting the About page. Every time that you request the About page, the break point in About.cshtml.cs will fire, whereas the break point in the @functions block of the About.cshtml file will not fire.

A Bug With Razor Pages

So Razor pages work like any other MVC view, except that they’re found (by default) in a Pages directory, as we’ve seen.

Because they work with standard MVC, we can use them when we have a _Layout.cshtml file which has a @RenderBody method within it.

This is pretty standard, cookie cutter, stuff with MVC. If you’re not sure about the @RenderBody method, then I would recommend taking a look at this page on docs.microsoft.com

However a few days before I wrote this blog post, Damien Edwards set about the task of converting the code behind live.asp.net to .NET Core 2.0 and to Razor Pages, which he live streamed on Google Hangouts and uploaded to YouTube. During this live stream, he encountered a bug with using Model members in Razor Pages which were being consumed in a _Layout.cshtml with an @section Scripts block.

you expected a preview build of something to be free of bugs?!

An issue has been raised, and you can track it’s progress here. Looking through the conversation on the issue, it looks like it’s been confirmed as working in ASP.NET Core 2.0 preview 2.

These Microsoft engineers don’t mess about, by the looks of it

And in case you wanted to watch the uploaded version of the live streamed coding session, I’ll leave an embedded version of it here:

jumping to 47:05 will take you to where Damien found and reported the bug

Conclusion

We’ve seen some of the new features of ASP.NET Core 2.0, including the cold boot speed increases and Razor Pages. We’ve also seen how the preview build of ASP.NET Core 2.0 has, at least, one pretty serious bug in it.

We’ve also seen, by checking the status of the bug that Damien Edwards reported on GitHub, how quickly issues in the ASP.NET Core (or indeed .NET Core) stack can be investigated and solved.

Razor Pages, at least, seems like an interesting addition to the ASP.NET Core stack. One that will find its niche once developers start playing with it, I’m sure.

Have you used Razor Pages for any test applications yet?

as long as they’re not being used in a page with a @section Scripts block, remember

If so, are they open source and can we all take a look at them?

If not, it’s worth checking out the docs.microsoft.com page for Razor Pages, maybe it’ll give you some ideas for where and how you can use them.

Related Posts

A .NET developer specialising in ASP.NET MVC websites and services, with a background in WinForms and Games Development. When not programming using .NET, he is either learning about .NET Core (and usually building something cross platform with it), speaking Japanese to anyone who'll listen, learning about languages, writing for his non-dev blog, or writing for a blog about video games (which he runs with his brother)