ASP.NET Core from Scratch

Introduction

There’s allot of articles on .NET Core and ASP.NET Core so I’m not going to be duplicating them. Here I’ll be taking a plain, empty .NET Core console application and building it up to a what you would expect an ASP.NET Core application to be like.

We’re not even going to use Visual Studio so we get to see everything.

Console application

You’ll need to install the .NET Core SDK to get very far, so go and do that if you haven’t. If you’re not sure whether you have or not then pop open a command window and enter:

dotnet --version

Hopefully you’ll get a response telling you that version 2.0.0.0 is installed.

Now, create a standard console application. Create a new folder, cd into it and create a new project with the .NET CLI:

C:\>mkdir BanksySan.BlogSite

C:\>cd BanksySan.BlogSite

C:\BanksySan.BlogSite>dotnet new console
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on C:\BanksySan.BlogSite\BanksySan.BlogSite.csproj...
  Restoring packages for C:\BanksySan.BlogSite\BanksySan.BlogSite.csproj...
  Generating MSBuild file C:\BanksySan.BlogSite\obj\BanksySan.BlogSite.csproj.nuget.g.props.
  Generating MSBuild file C:\BanksySan.BlogSite\obj\BanksySan.BlogSite.csproj.nuget.g.targets.
  Restore completed in 719.24 ms for C:\BanksySan.BlogSite\BanksySan.BlogSite.csproj.


Restore succeeded.

If we look at the project file we can see that there really isn’t much there:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

</Project>

The only code file is the Program.cs and it’s also pretty boring:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

We can build and run the project using the SDK as well:

NB: Your dll might be in a slightly different path, e.g. bin\Debug\...

    C:\BanksySan.BlogSite>dotnet build BanksySan.BlogSite.csproj
    Microsoft (R) Build Engine version 15.3.409.57025 for .NET Core
    Copyright (C) Microsoft Corporation. All rights reserved.

      BanksySan.BlogSite -> C:\BanksySan.BlogSite\bin\MCD\Debug\netcoreapp2.0\BanksySan.BlogSite.dll

    Build succeeded.
        0 Warning(s)
        0 Error(s)

    Time Elapsed 00:00:20.19

    C:\BanksySan.BlogSite>dotnet bin\MCD\Debug\netcoreapp2.0\BanksySan.BlogSite.dll
    Hello World!

Getting Kestrel

Kestrel is the default server that comes with ASP.NET Core. There is also HTTP.sys, but this only works for Windows and Kestrel is recommended unless you really, really need to use HTTP.sys.

Add a reference to Microsoft.AspNetCore to the project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" version="2.0.0.0" />
  </ItemGroup>
</Project>

Now we can start to build up the server. Change Program.cs so that it’s like so:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

static class Program
{
    static void Main(string[] args)
    {
        var webHostBuilder = WebHost.CreateDefaultBuilder(args);
        var webHost = webHostBuilder.Build();
        webHost.Run();
    }
}

The CreateDefaultBuilder method gives us a starting point with all the default configurations (all bar one, as we will see).

Is you build and run this (again using the .NET SDK CLI) then you’ll see that we get an exception.

Unhandled Exception: System.ArgumentException: A valid non-empty application name must be provided.
Parameter name: applicationName
   at Microsoft.AspNetCore.Hosting.Internal.HostingEnvironmentExtensions.Initialize(IHostingEnvironment hostingEnvironment, String applicationName, String contentRootPath, WebHostOptions options)
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.BuildCommonServices(AggregateException& hostingStartupErrors)
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
   at BanksySan.BlogSite.Program.Main(String[] args) in C:\BanksySan.BlogSite\Program.cs:line 11

You might be able to argue that this is a bug, maybe it could be as you’d think that the default builder would set this for you, it doesn’t though but the documentation does tell you how to in the (trouble shooting section)[https://docs.microsoft.com/en-us/aspnet/core/fundamentals/hosting?tabs=aspnetcore2x#troubleshooting-systemargumentexception]. We just need to add this setting to the builder:

static void Main(string[] args)
{
    var webHostBuilder = WebHost.CreateDefaultBuilder(args)
                                .UseSetting("applicationName", "BanksySan's Blog");
    var webHost = webHostBuilder.Build();
    webHost.Run();
}

Now when you run it, it still doesn’t work. The exception you get now is different though:

Unhandled Exception: System.InvalidOperationException: No service for type 'Microsoft.AspNetCore.Hosting.IStartup' has been registered.
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.AspNetCore.Hosting.Internal.WebHost.EnsureStartup()
   at Microsoft.AspNetCore.Hosting.Internal.WebHost.EnsureApplicationServices()
   at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
   at BanksySan.BlogSite.Program.Main(String[] args) in C:\BanksySan.BlogSite\Program.cs:line 12

This isn’t a configuration problem, this is actually because we haven’t added anything for it do. The error message isn’t too descriptive, but even though it’s not obvious all we have to do is add some middleware. We’ll just make it return `Hello World!".

To achieve this, we can use the Configure method (OK, I guess it is configuration).

using System.Text;
using System.Threading;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;

internal static class Program
{
    private static void Main(string[] args)
    {
        var webHostBuilder = WebHost.CreateDefaultBuilder(args)
            .UseSetting("applicationName", "BanksySan's Blog")
            .Configure(builder =>
            {
                builder.Run(async context =>
                {
                    var utf8Encoding = Encoding.UTF8;
                    var textBytes = utf8Encoding.GetBytes("Hello World!");
                    await context.Response.Body.WriteAsync(textBytes, 0, textBytes.Length, default(CancellationToken));
                });
            });
        var webHost = webHostBuilder.Build();
        webHost.Run();
    }
}

Now when we run we get the expected success message. The message tells us that it is listening on localhost:50000:

C:\BanksySan.BlogSite>dotnet bin\MCD\Debug\netcoreapp2.0\BanksySan.BlogSite.dll
Hosting environment: Production
Content root path: C:\BanksySan.BlogSite
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

If you send a GET request to that URL then you’ll get the response:

HTTP/1.1 200 OK
Date: Sun, 22 Oct 2017 15:37:01 GMT
Server: Kestrel
Content-Length: 12

Hello World!

Note that the server is Kestrel, even though we haven’t set it. Kestrel is the default server for ASP.NET Core.

The Startup Class

Using the Configure method above if functionally fine, but gets ugly fast, especially as configure isn’t the only method. We can create another class (with whatever name we want, but StartUp is the typical name) which can contain the logic for configuring the application and wiring up the middleware.

As per the documentation the Startup class must contain a Configure method, and may include a ConfigureServices method. We’ll be using ConfigureServices later. Create a new class Startup and move the delegate we already have in the existing Configure method into it so that the Startup class looks like this:

using Microsoft.AspNetCore.Builder;
using System.Text;
using System.Threading;

internal class Startup
{
    public void Configure(IApplicationBuilder builder)
    {
        builder.Run(async context =>
        {
            var utf8Encoding = Encoding.UTF8;
            var textBytes = utf8Encoding.GetBytes("Hello World!");
            await context.Response.Body.WriteAsync(textBytes, 0, textBytes.Length, default (CancellationToken));
        });
    }
}

Replace the (now empty and probably not compiling) Configure method with UseStartup<StartUp>() so that the we have:

private static void Main(string[] args)
{
    var webHostBuilder = WebHost.CreateDefaultBuilder(args)
                                .UseSetting("applicationName", "BlogSite")
                                .UseStartup<Startup>();

    var webHost = webHostBuilder.Build();
    webHost.Run();
}

Give that another run and you should see everything working as before.

Serving Files

The new ASP.NET Core has the notion of serving files from the file system just like in the days of WebForms. Not quite like WebForms, it has a folder (typically called wwwroot which will serve files from the files system and the routing you’d expect from MVC, so we have a hybrid system. wwwroot is there to deliver static content like images, scripts etc.

This is the directory that the static content of the site is served from, it defaults to wwwroot. Microsoft have kindly produced some middleware that handles serving files from this directory. Add Microsoft.AspNetCore.StaticFiles to your project file so it’s now:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" version="2.0.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
  </ItemGroup>
</Project>

NB: There is also Microsoft.Owin.StaticFiles which is the framework version, whilst it’s 100% compatible don’t use it as you’ll be bound unable to move the application to servers that don’t have the framework installed.

It’s very easy to use in it’s default form. Add a call to UseStaticFiles to the Configure method in the Startup class like this:

internal class Startup
{
    public void Configure(IApplicationBuilder builder)
    {
        builder.UseStaticFiles();
        builder.Run(async context =>
        {
            var utf8Encoding = Encoding.UTF8;
            var textBytes = utf8Encoding.GetBytes("Hello World!");
            await context.Response.Body.WriteAsync(textBytes, 0, textBytes.Length, default(CancellationToken));
        });
    }
}

NB: The call to UseStaticFiles needs to be before the call to Run. If it’s after it then the request "Hello World!" will be written as the response instead of the file’s content and HTTP headers.

Now create a wwwroot directory and put a file in it. I’ve created a text file and when I make a request to ~/public.txt I get the following response:

HTTP/1.1 200 OK
Date: Tue, 24 Oct 2017 13:42:16 GMT
Content-Type: text/plain
Server: Kestrel
Content-Length: 35
Last-Modified: Tue, 24 Oct 2017 12:51:11 GMT
Accept-Ranges: bytes
ETag: "1d34cc6c4a109a3"

This is some public static content.

If I make a request for a file that doesn’t exist then I get the Hello World! response defined in the Run method earlier. Likewise if I try to get a file that exists, but not in the wwwroot directory.

Add MVC

MVC doesn’t serve static content, it uses routing logic to pass a request into a controller action which then determines what to do with the request. We need to add this to the OWIN pipeline and again, Microsoft have given us some middleware that will achieve this. Add the Microsoft.AspNetCore.Mvc.Core Nuget package to the project file so it’s like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
	    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore" version="2.0.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
  </ItemGroup>
</Project>

This lets us use the UseMvc and UseMvcWithDefaultRoute methods. UseMvcWithDefaultRoute just calls UseMvc with the default route of {controller=Home}/{action=Index}/{id?} which is what’s in the ASP.NET Core MVC template.

UseMvc also need the MVC services added. We do this in the other method on the Startup class, ConfigureServices. There are quite a few services needed, but luckily there’s a nice extension method, also called UseMvc that will add them all for us.

We don’t need that Hello World! bit any more so we can delete it. Our Startup class now looks like:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

internal class Startup
{
  // Brand new maethod added here
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvc();
  }
  
  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
    app.UseStaticFiles();
    app.UseMvcWithDefaultRoute();
  }
}

Now that’s set up we need to add something for the MVC routing to route to. Here we have a choice, we could create a Razor Page and use the new style, or we can create a controller and view. We’ll start with the page, and then add the controller & view.

Razor Page

First, create a directory called Pages. This is the default location that ASP will look for all your pages (You can change is with the AddRazorPagesOptions or WithRazorPagesRoot extension methods off the services collection).

Inside the Pages directory, create a file called index.cshtml. Inside it add some basic Razor syntax, e.g.:

@page
<h1>This is a Razor page!</h1>
<p>The time is: @DateTime.Now.ToString("t")</p>

Now when we request the page it’s returned as expected. Note that we don’t need to specify the path at all because the default route is set for it.

Controller & View

Thanks for reading.

Give me a shout @BanksySan.

2 comments:

  1. I loved the simplicity of the code and examples. It was great fun to run the code and see the economy behind the DOTNETCORE. Thanks BanksySan!

    ReplyDelete
    Replies
    1. Thank you Burcu. Thanks for the proof reading too!

      Delete