Building a lightweight, controller-less, Markdown-only website in ASP.NET Core

Β· 1532 words Β· 8 minutes to read

In this blog post let’s have a look at building a lightweight site in ASP.NET Core.

In “classic” ASP.NET we had the WebPages framework - which allowed us to build sites composed only of views. This was perfect for lightweight projects, where we didn’t need the entire model-controller infrastructure.

At the moment, ASP.NET Core doesn’t have an equivalent yet (though it’s being worked on), but we have already provided a similar type of experience via the WebApiContrib project (you can read more about the project here). With the help of some of the libraries from there, we can build controller-less sites for ASP.NET Core already.

In addition to that, we can combine it with using Markdown tag helpers for content delivery - and it will result in a very cool experience - being able to author ASP.NET Core sites, without controllers, in Markdown. With Razor sprinkled on top of it, to provide dynamic data.

Let’s have a look - more after the jump.

Getting started πŸ”—

To get started, let’s create a new empty ASP.NET Core project and add references to the following WebAPiContrib.Core packages:

Here is my full project.json file. It seems quite large but it’s a typical ASP.NET Core app, created from the default template.

  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.0.0",
      "type": "platform"
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    "WebApiContrib.Core.WebPages": "1.0.1",
    "WebApiContrib.Core.TagHelpers.Markdown": "1.0.0"

  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": {
      "version": "1.0.0-preview2-final",
      "imports": "portable-net45+win8+dnxcore50"

  "frameworks": {
    "netcoreapp1.0": {
      "imports": [

  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true

  "runtimeOptions": {
    "gcServer": true

  "publishOptions": {
    "include": [

  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]

What’s worth mentioning, is that I also added Microsoft.AspNetCore.StaticFiles middleware, so that we can serve up some CSS. The rest is - again - pretty standard stuff from the default web application template, so Kestrel, IIS integration, as well as the default build and publish options.

Configuring WebApiContrib.Core.WebPages πŸ”—

In order to configure our web pages, we need to add the following to the Startup class:

    public class Startup
        public void ConfigureServices(IServiceCollection services)
            services.AddWebPages(new WebPagesOptions { RootViewName = "Index" });

        public void Configure(IApplicationBuilder app)

The pattern is the same as in adding/configuring i.e. fully fledged MVC - first we are “adding” our framework, and then we are “using” it.

Next step is to actually add some views - the views will serve as our pages, and there names will also define the routes - since we have no controllers in place, everything happens by convention. We’ll get back to this in a moment.

If you look again at the snippet above, we also configured the view called Index.cshtml to act as the root of the site (so our “homepage”) - it is the view that will be served when navigating to the root of the domain where the site will be deployed.

Adding layout πŸ”—

Now, let’s imagine that we are building a simple blog - this could be a good example for us.

The Index.cshtml view will be our root, listing all of the posts. Then each of the posts will be a separate view (separate physical file in the Views folder) and it will be authored as a Razor/Markdown combination.

However, before we get on to that, let’s first add all of the remaining bootstrapping that we need.

It’s quite typical that we’d want to have a shared layout for our site - this in Razor is normally represented by a _Layout.cshtml file. So let’s add that.

The responsibility of our _Layout.cshtml will be to provide the page title and load up the necessary CSS. This is shown below.

<html lang="en">


    @if (!ViewBag.HideBackLink)
            <a href="/">Back to Home</a>

Our layout file is using the classic Razor ViewBag to read some data from the views it will wrap around - each view will be able to pass a Title and a flag whether a link back to home page should be rendered or not.

Note: I am using a nice and elegant Markdown CSS file from here. This also means that the referenced CSS file exists in my wwwroot/css folder.

Configuring Markdown tag helper πŸ”—

Since we want to use Markdown to render our pages, and we already pulled the package for it, we just need to make it visible to our Razor views.

To do that, we need to add a _ViewImports.cshtml file, with the following content:

@addTagHelper "*, WebApiContrib.Core.TagHelpers.Markdown"

This will import the Markdown tag helpers (the package actually has two of them, we will be using just one though, the “basic” one).

The WebApiContrib.Core.TagHelpers.Markdown tag helpers (you can read more about it here) allows us to use an tag and write Markdown directly in our Razor views, and it will get auto-converted to HTML. The nice think about it is that we get a cool authoring experience - as writing Markdown files for content driven sites is usually more efficient than writing pure HTML.

Also, once a Razor view is rendered, it will be cached, so the fact that we will convert to Markdown on the fly doesn’t matter that much, as only the first hit will be slower.

Adding views πŸ”—

At this point we can start adding our views. Since we already established that Index.cshtml will be our root, let’s add it. It will act as our table of contents.

    Layout = "_Layout";
    ViewBag.Title = "Markdown ASP.NET Core site";
    ViewBag.HideBackLink = true;

# List of blog posts

 - [Announcing WebApiContrib](/WebApiContrib)
 - [Customizing FormatFilter behavior](/FormatFilter)

Notice that we set the layout file and interact with the ViewBag identically how you’d do it in a typical, fully-fledged MVC application. Since this is our root page, we can hide the “back to home” link and we do it via a relevant flag (we just added that logic to _Layout.cshtml moments ago).

The content of the page itself is a a simple Markdown header and a list with some links. We already mentioned in the beginning, that the names of the views will correspond to the links/routes that are available in our lightweight application. So - as a consequence - the above structure implies that we need to have WebApiContrib.cshtml and FormatFilter.cshtml in our Views folder.

So let’s add both of them. I am not gonna show their entire structure here - cause for demo purposes I used my old blog posts here (I just grabbed the last two: this one and this one) and they are fairly long (btw - I write the posts in Markdown, obviously). Instead in the snippets below, I will abbreviate their “content” part to save up on space.

But they are similar to our Index.cshtml - some Razor bootrsrapping on top, and then Markdown content.


    Layout = "_Layout";
    var title = $"Announcing WebApiContrib for ASP.NET Core!";

    ViewBag.Title = title;
    ViewBag.HideBackLink = false;
# @title

In the past, a [bunch of us]( from the ASP.NET Web API community worked together on a WebApiContrib project (or really, *projects*, cause there were many of them!).

The idea was to provide an easy to use platform, a one stop place for community contributions for ASP.NET Web API - both larger add ons, such as HTML/Razor support for Web API, as well as smaller things like i.e. reusable filters or even helper methods. This worked extremely well - [WebApiContrib packages]("WebApiContrib") were downloaded over 500k times on Nuget, and a nice community has emerged around the project on [Github](

(...) omitted for brevity (...)



    Layout = "_Layout";
    var title = "Customizing FormatFilter behavior in ASP.NET Core MVC 1.0";

    ViewBag.Title = title;
    ViewBag.HideBackLink = false;
# @title

When building HTTP APIs with ASP.NET Core MVC , the framework allows you to use *FormatFilter* to let the calling client override any content negotiation that might have happened on the server side.

This way, the client can for example force the return data to be JSON or CSV or any other format suitable (as long as the server supports it, of course).

(...) omitted for brevity (...)


One interesting note is that we can use the typical Razor mechanisms when authoring our posts. For example in the above snippets we defined the title as a local variable and used it both to set the title of the page, and to set the H1 of the Markdown content.

Similarly, you can make your life easier by injecting other dynamic content, leveraging loops or even accessing external services to pull some data.

You could even use the @inject directive to inject services into the views.

Source code & demo πŸ”—

So this is everything!

We now have a fully functional, controller-less, Markdown-driven, lightweight, ASP.NET Core site. We just have views, and all the content is written almost entirely in pure Markdown.

Of course this is a very basic implementation, but I hope it nudges you in a useful direction or inspires you to do cool things with ASP.NET Core.

The links for this post:


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP