CSS Positioning Schemes

The Positioning Schema

Introduction

It took me a good while (and some down-voted StackOverflow i.e. questions) for me to understand what I was asking exactly. I'd heard the term Normal Flow used when describing how HTML layout engines position elements on the screen. I'd also read that there are many of these flows, which was confusing because e couldn't find any mention of any flow other than the normal one.

After a lot of Binging and Googling and shouting into a bucket I did finally find what I was looking for, and understood why people were looking at me like I was dense when I was asking what the non-normal flows might be.

The critical bit of information I was missing was in the CSS 2.2 specification in Section 9: Visual formatting model which talks about positioning schemes. it say that there are three kinds:

  1. Normal flow
  2. Float
  3. Absolute

That explains why I was getting no luck looking for flows that weren't normal. Firstly, they're called positioning schemes, not flows and secondly, none of the others members of it even have the work flow in their name.

Normal Flow

The default positioning scheme.

Normal flow is concerned with whether an element is block or inline. inline elements are positioned along a line until it reaches the end and can't fit another, then it starts a fresh line and carries on. block elements won't share a line with anything else, as such they will always create a new line appear one after another down the page at the start of each line.

The following shows only inline elements.

There was a table set out under a tree in front of the house, and the March Hare and the Hatter were having tea at it: This doesn't work if you can't see the image. a Dormouse was sitting between them, fast asleep, and the other two were using it as a cushion, resting their elbows on it, and talking over its head. 'Very uncomfortable for the Dormouse,' thought Alice; 'only, as it's asleep, I suppose it doesn't mind.'

The text formatting elements and the image flow across the line, starting a new one as needed. Even though the image is larger than the text it still does it and will increase the height of the line to accommodate it.

Compare this with block elements:

  1. 'Have some wine,' the March Hare said in an encouraging tone.
  2. Alice looked all round the table, but there was nothing on it but tea. 'I don't see any wine,' she remarked.
  3. 'There isn't any,' said the March Hare.
  4. 'Then it wasn't very civil of you to offer it,' said Alice angrily.
  5. 'It wasn't very civil of you to sit down without being invited,' said the March Hare.

Each list item is on it's own line, regardless of whether there was room for it next to the existing text. in fact, block elements won't share the line with trailing inline items as well as previous ones.

  1. 'Have some wine,' the March Hare said in an encouraging tone
Alice looked all round the table, but there was nothing on it but tea. 'I don't see any wine,' she remarked.

Relative positioning

Consider this inline set on boxes all in the normal flow:

A
B
C
D
E
F
G
H

We can move elements around within the normal flow if we give their position property the value of relative. This doesn't affect the normal flow at all, but does cause the element to be displayed offset to its original position.

A
B
C
D
E
F
G
H

Importantly notice that the space it would have taken up is still being reserved for it. Even though it's visual properties have changed it's affect on the normal flow has not changed. This is very different to the other two positioning schemes, both of which remove the element from the normal flow.

Floating

We float an element by setting its float is calculated as something other than none. It will cause the element to go as far to the edge of it's containing element as it can, stacking next to any other floated elements that might be there. The elements remaining in the normal flow then try to fit themselves around it.

Left float

A
B
C
D
E
F
G
H

Right float

A
B
C
D
E
F
G
H

Left and Right Floats

Here we've floated boxes C, E and F to the left. Notice that each goes to the left of it's own line, E and F stack onto each other

A
B
C
D
E
F
G
H

Floating larger objects

So far floats are boring, their power becomes apparent when we mix floats with smaller elements like text. Here we're floating two images of the the March Hare.

'Not the same thing a bit!‘ said the Hatter. 'You might just as well say that 'I see what I eat' is the same thing as 'I eat what I see'!&'

'You might just as well say,' added the March Hare, 'that 'I like what I get' is the same thing as 'I get what I like'!‘

'You might just as well say,' added the Dormouse, who seemed to be talking in his sleep, 'that 'I breathe when I sleep' is the same thing as 'I sleep when I breathe'!';

The text just flows within its new boundaries.

Absolute positioning

For an element to be absolutely positioned the position property must have a value of either:

  1. absolute
  2. fixed

both of these values will remove the element from the normal flow, though in slightly different ways.

Absolute position

(The word 'absolute' can refer to both the category of properties that remove an element from the normal flow or the specific value 'absolute', a potential value of position. It's a confusingly overloaded word).

This will position an element relative to the top left corner of the first parent container who's position value except static. It's a bit of a strange one conceptually and I admit that I don't quite know why it was designed this way (if anyone knows, please let me know @BanksySan).

I've added a div around boxes B to D and set its position value to relative. Just to highlight it I've shifted the container down and to the left and made the contained boxes green.

A
B
C
D
E
F
G
H

If I now set box C to have a position of absolute then we can see the effect.

A
B
C
D
E
F
G
H

Two important things have happened here, box firstly C has been removed from the normal flow so box D has shifted up next to B; secondly box C has moved down and right. What's particularly important to notice though is that it has moved relative to the container box. It's relative to this because we changed the container box's position from static. If we removed that property then the box would position itself relative to the next parent it found that wasn't statically positioned or, if nothing's found, relative to the top left of the document itself.

Fixed position

Fixed positioning is very similar to absolute positioning with one important difference. Rather that positioning the element in relation to any other element it instead positions it in relation to the view port itself.

The box above is actually an iFrame, so is an isolated viewport. Try scrolling down and up, you'll notice that box C remains where it is and the elements in the normal flow ignore it.

Conclusion

There are three positioning schemes. These are:

  1. Normal Flow
  2. Absolute positioning
  3. Floating

Normal flow involves two categories of element.

Inline
Will layout vertically (left‑to‑right or right‑to‑right depending on your culture settings), starting a new line once the current line can't fit the next token on it. Line height expends per line as required to accommodate everything on the line.
Block
Will always use an entire line (i.e. will not share a line with either another block or inline element) and will appear at the start of the line (left or right depending on culture). A block element has a height and width which can be manipulated via CSS.

Both absolute positioning and floating will remove the element from the normal flow. In the case of absolute positioning (position: absolute or position: fixed) the normal flow elements are no longer affected by the element at all and so will flow as if the element didn't exist. Floated elements are removed from their position in the normal flow and shifted to the left or right, elements in normal flow do interact with them and will attempt to flow around them. Floated elements will also interact with each other, stacking up next to each other.

The question you're probably asking now (and rightfully so) is how do elements removed from the normal flow interact with each other?. The keyword to Bing Google with is context, which I'll be looking into in the next post.

Thanks for reading.

Give me a shout @BanksySan.

Sub-Arrays with the ArraySegment Class

Introduction

Sometimes we want to pass a subsection of an array to a method. It’s surprising that this functionality isn’t hanging off either the static Array class or an array instance.
Until recently I thought that there were only two solutions to this problem. Either create a new array with the elements you want exposed or pass a start index and size and hope the consumer honours them:

Copy array example

public void ArrayCopyExample()
{
    var sourceArray = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    var subArray = new int[5];
    Array.Copy(sourceArray, 4, subArray, 0, 5);
    WriteLine($"Source: {string.Join(", ", sourceArray)}");
    DoSomething(subArray);
}

private void DoSomething(int[] integers)
{
    WriteLine($"Integers: {string.Join(", ", integers)}");
}
Outputs:
Source: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Integers: 4, 5, 6, 7, 8

Passing the index and length example

public void PassingAnIndexExample()
{
    var sourceArray = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    WriteLine($"Original array: {string.Join(", ", sourceArray)}");
    DoSomething(sourceArray, 4, 5);
}

private void DoSomething(int[] integers, int index, int length)
{
    var subArray = integers.Skip(index).Take(length);
    WriteLine($"Integers: {string.Join(", ", subArray)}");
}
Output:
Original array: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Integers: 4, 5, 6, 7, 8

ArraySegment

C# 2 introduced the ArraySegment structure. On the surface it looks allot like this class is intended to be the solution to this problem. Unfortunately, it’s pretty bad and not close to being a genuine solution as I’ll demonstrate here.
You can find the documentation on MSDN.
Here’s an example of how it’s used. I create three different ArraySegment structures. Firstly I use string.Join() to enumerate them then I write out the value at each ordinal position on the middleSegment.
public void ArraySegmentBasicUsage()
{
    var array = "abcdef".ToCharArray();
    WriteLine($"Original array: {string.Join(", ", array)}");

    var headSegment = new ArraySegment<char>(array, 0, 3);
    var tailSegment = new ArraySegment<char>(array, 3, 3);
    var middleSegment = new ArraySegment<char>(array, 1, 4);

    WriteLine("Head:  {0}", string.Join(", ", headSegment));
    WriteLine("Tail:  {0}", string.Join(", ", tailSegment));
    WriteLine("Middle: {0}", string.Join(", ", middleSegment));

    WriteLine("Ordinarily access elements: ");
    
    for (var i = 0; i < middleSegment.Count; i++)
    {
        _outputHelper.WriteLine("{0,4}:  {1}", i, middleSegment[i]);
    }
}
The output looks promising at first:
Original array: a, b, c, d, e, f
Head:  a, b, c
Tail:  d, e, f
Middle: b, c, d, e
Ordinarily access elements: 
   0:  b
   1:  c
   2:  d
   3:  e
We’ll use it in the example above now.
public void ArraySegmentExample()
{
    var sourceArray = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    WriteLine($"Source: {string.Join(", ", sourceArray)}");
    var arraySegment = new ArraySegment<int>(sourceArray, 4, 5);
    DoSomething(arraySegment);
}

private void DoSomething(ArraySegment<int> integers)
{
    WriteLine($"Integers:  {string.Join(", ", integers)}");
}
We can’t use the overload that accepts an array as ArraySegment isn’t an array. ArraySegment is IEnumerable though, so we could call the ToArray() extension method. Calling this method will create us a new copy of the array though. Something we were trying to avoid.
We can prove that it is creating a new array using Jetbrain’s dotMemory Unit
 [Fact]
 public void ArraySegmentExample()
 {
     var sourceArray = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
     WriteLine($"Source: {string.Join(", ", sourceArray)}");
     var arraySegment = new ArraySegment<int>(sourceArray, 4, 5);
     var memoryCheckPoint = dotMemory.Check();
     var subArray = arraySegment.ToArray();
     dotMemory.Check(memory =>
         WriteLine(
             $"New integer arrays:  {memory.GetDifference(memoryCheckPoint).GetNewObjects(property => property.Type.Is<int[]>()).ObjectsCount}"));

     DoSomething(arraySegment);
 }
The output shows that we have a new array.
Source: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
New integer arrays:  1
Integers:  4, 5, 6, 7, 8
Not only that, but the original array is still exposed via the ArraySegment.Array property, along with the offset and size used to create the ArraySegment.

It’s not that bad.

OK, I’m exaggerating a bit. If you have a massive array of value types then duplicating it en masse might be a problem, but if you are doing that then you should probably be having a more bespoke solution anyhow.
If, instead of thinking of passing an array about, we instead use an IEnumerable<T> then we’re in good stead. True, if we have a method that’s expecting an array then we’re forced to use Array.Copy, but if we’re able to write the consuming method ourselves then using the IEnumerable<T> will only cause the allocation of an enumerator. In reality, using the Linq methods Skip() and Take() will more likely be what you use (though it does make two more allocations).

Conclusion?

Use ArraySegment as an IEnumerable and you won’t get bitten. Don’t use it as it if you don’t want the full array to be accessible to the called method.
Thanks for reading.
Give me a shout @BanksySan.

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.