Blog-post Thumbnail
My Blog Building Journey

The Quest

I want to create a blog and learn ASP.Net C# 7.0 MVC Core 2.0 and Entity Framework Core. I don't have time to start from complete scratch to so I plan on using a couple blog engines as starting points.


  • ASP.Net MVC C# 7.0 / Core 2.0
  • Entity Framework Core
  • SQL Server
  • Bootstrap Template (Titan)

The Story

I considered a handful of blog templates but am currently looking at:

  • Mads Kristensen ASP.Net Core Template Pack 2017.3
    • Pro - Appears to be very latest UI and client-side tech (especially edit textarea)
    • Pro - Minimalist MVC code
    • Con - No advanced search/viewing
    • Con - XML data layer
  • Tyler Rhodes' ASP.Net Core Blog Engine
    • Pro - Beginner Introduction to MVC Core
    • Pro - Entity Framework data layer
    • Pro - Separate Data layer project
    • Pro - Advanced Search/Sort
    • Pro - SQL Server Backend
    • Con - Gruntfile.js minifier

I've gone back and forth but currently plan on copying Kristensen's (K) sweet web/ui to Rhode's (R) powerful EF/SQL backend.

First Snag - Moving UI Web files

I really want to move the cool tinymce editor from K to R site but do not know how to do this. R uses LigerShark WebOptimizer.Core and LigerShark.WebOptimizer.Sass NuGet packages, neither of which I'm familiar with. Compound that with the fact that I'm struggling with basic understanding of appsettings.json and Startup.cs.

R uses Grunt for minification/optimization, which apparently requires npm with Node.js to run, neither of which I am familiar with. Awesome.

My main problem is the blasted 404s on the UI files. Grunt programatically clears the wwwroot folder at startup and moves/minifies client files there via the Gruntfile.js settings. I've manually added a few entries there to remove the 404s but the 

First Big Break

I installed npm and Node.js but doesn't appear I need those, at least not yet.

Removing Grunt was much easier than I thought, simply right click the grunt packages located under Project>Dependencies>npm and [Uninstall Package]. I also removed gruntFile.js from Project root.

I've already added both LigerShark NuGet packages to the project so I consulted the following sites:

I surmised I needed to add some code to Startup.cs.


using WebMarkupMin.AspNetCore2;

using WebMarkupMin.Core;

using IWmmLogger = WebMarkupMin.Core.Loggers.ILogger;

using WmmNullLogger = WebMarkupMin.Core.Loggers.NullLogger;

using Microsoft.Net.Http.Headers;

public void ConfigureServices(IServiceCollection services) {

// HTML minification (https://github.com/Taritsyn/WebMarkupMin)

services .AddWebMarkupMin(options => { options.AllowMinificationInDevelopmentEnvironment = true; options.DisablePoweredByHttpHeaders = true; }) .AddHtmlMinification(options => { options.MinificationSettings.RemoveOptionalEndTags = false;

options.MinificationSettings.WhitespaceMinificationMode = WhitespaceMinificationMode.Safe; });

services.AddSingleton<IWmmLogger, WmmNullLogger>(); 

// Used by HTML minifier 

// Bundling, minification and Sass transpilation (https://github.com/ligershark/WebOptimizer) services.AddWebOptimizer(pipeline => { pipeline.MinifyJsFiles(); pipeline.CompileScssFiles().InlineImages(1); });


// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {


app.UseStaticFiles(new StaticFileOptions() { 

OnPrepareResponse = (context) => { 

var time = TimeSpan.FromDays(365); 

context.Context.Response.Headers[HeaderNames.CacheControl] = $"max-age={time.TotalSeconds.ToString()}"; context.Context.Response.Headers[HeaderNames.Expires] = DateTime.UtcNow.Add (time).ToString("R"); 





I moved all relevant tinymce files to the wwwroot directory and away went all the 404s!

The tinymce editor is still displaying as a simple textarea so this blog helped me understand I just needed to reconfigure the EditPost.cshtml view by setting the form id and add a delete button (null listener error) and the form displays and functions beautifully. Multi-floor happy dancing ensued.

Rolling Along

Now that I've successfully merged the tinymce plugin and replaced Grunt with LigerShark, I want to apply the Tital Bootstrap Template. This was pleasingly straighforward.

  1. I copied the [assets] folder in to the wwwroot folder
  2. I created a BootstrapTitan Controller with an Index View
  3. I copied the raw sample bootstrapper blog page HTML into the View
  4. I started bringing over the working Razor code from the Home/View until I had a decent amount of data populating my new cool bootstrap home page
  5. I created a new Shared _Layer_new.schtml partial view and divied up the code between this and the Bootstrap/Index view.
    1. Admin/Author navigation
    2. List of Recent Public Blogs
    3. List of Categories
    4. List of Tags/Topics

Change of plan. Instead of using a throwaway controller BootstrapTitalController, I ditched that and re-named the controller methods <method>_old and copy/pasted the original and began my edits. I did the same with razor pages. This worked infinitely better than managing separate controllers. Work went pretty fast and I had a working:

  • Main Page of recent Blog Posts
  • Blog Page
  • Blog Edit Page

I had a few re-naming issues with the Blog Edit Page, it's corresponding .js files and a few element names like the form id="".

Publish to Web Server

I learned I needed to install the .Net Core Windows Server Hosting Bundle (2.0.3) on a Windows Server 2008 so I left that to my awesome hosting partner, Tim Cheek.

Rick Strahl has an excellent blog highlighting the important parts.

Configuring my Application

Through trial and error, I learned I needed to fight through these errors

HTTP Error:   502.5Misconfigured web.config replace aspNetCore processPath and arguments values

stdOut Log Error: 


 I forgot to populate the SQL DB with ASP net identity tables and my application tables, doh! Populate needed tables.

stdOut Log Error:

System.Data.SqlClient.SqlException (0x80131904): Incorrect syntax near 'OFFSET'. Invalid usage of the option NEXT in the FETCH statement.

SQL 2008 does not allow .Skip in LINQ from EF Core. 

Removed the only instance of .Skip() (PaginatedList.cs) understanding that I'll take a slight memory hit.

An alternative is to add this option to my DBcontxext instances...

builder.UseSqlServer(ConnectionString, b => b.UseRowNumberForPaging());

...but this broke my local SQL 2014 so I took it out.

I didn't notice this earlier as I'm running SQL 2014 locally. Pitchforks down, please.

 It works from my hosting Windows 2008 / SQL 2008 environment!  Woohoo!

Google Authentication

This was tricker than expected. My copy of Pro ASP.NET Core MVC 2 arrived so I used the sample code from Chapter 30, Advanced ASP.NET Core Identity, Using Third-Party Authentication. The steps are as follows:

1. Create a Google API Console Project 

You'll get Client ID and Client Secret. You provide the authorized redirect URIs


2. Enable Google+ API

This is needed so you can query the users Google's email


3. Copy Code from Pro ASP.NET Core MVC 2

  • Add Google ClientId and Secret to appsettings.json
  • Modify Account Controller as instructed
  • Modify Account/Login.schtml View as instructed

I won't list all code here but the lion's share is in the Account Controller

    public async Task LoginOAuth(string returnUrl = "/account/GoogleLogin")
        await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = returnUrl });
public async Task&lt;IActionResult&gt; Login(AccountLoginViewModel alvm, string returnUrl)
  if (!ModelState.IsValid) return View(alvm);

var au = await _userManager.FindByEmailAsync(alvm.Email);

if (au != null) { await _signInManager.SignOutAsync(); var result = await _signInManager.PasswordSignInAsync(au, alvm.Password, alvm.RememberMe, false); if (result.Succeeded) { _logger.LogInformation("Info - Successful login for {0}", au.Email); return Redirect(returnUrl ?? "/"); } }

_logger.LogWarning("Warning - unsuccessful login attempt for {0}", au?.Email); ModelState.AddModelError(nameof(AccountLoginViewModel.Email), "Invalid user or passsword"); return View(alvm); }

public async Task&lt;IActionResult&gt; Logoff() { var curUser = await _userManager.GetUserAsync(HttpContext.User); _logger.LogInformation("Info - Logout for {0}", curUser.Email); await _signInManager.SignOutAsync(); return Redirect("/"); }<br /><br /> [AllowAnonymous] public IActionResult GoogleLogin(string returnURL) { string redirectUrl = Url.Action("GoogleResponse", "Account", new { ReturUrl = returnURL }); var properties = _signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl); return new ChallengeResult("Google", properties); }

[AllowAnonymous] public async Task&lt;IActionResult&gt; GoogleResponse(string returnUrl = "/") { ExternalLoginInfo info = await _signInManager.GetExternalLoginInfoAsync(); if (info == null) { return RedirectToAction(nameof(Login)); }

var result = await _signInManager.ExternalLoginSignInAsync(
    info.LoginProvider, info.ProviderKey, false);

if (result.Succeeded)
    return Redirect(returnUrl);
    ApplicationUser user = new ApplicationUser
        Email = info.Principal.FindFirst(ClaimTypes.Email).Value,
        UserName = info.Principal.FindFirst(ClaimTypes.Email).Value

    // Does a User exist with this email?
    var existingUser = await _userManager.FindByEmailAsync(user.Email);
    if (existingUser != null)
        await _signInManager.SignInAsync(existingUser, false);
        return Redirect(returnUrl);

    IdentityResult identResult = await _userManager.CreateAsync(user);            
    if (identResult.Succeeded)
        identResult = await _userManager.AddLoginAsync(user, info);
        if (identResult.Succeeded)
            await _signInManager.SignInAsync(user, false);
            return Redirect(returnUrl);
    return AccessDenied();


4. Allow existing ASP.NET Identity Users to log in with matching Google+ email.

The security risk here is minimal as someone would have to know an app user's email account, create or log in to their Google+ account to be able to use this app.

Now that this is working, I can see my API activity in the API Dashboard



Cleaning Up

I now have a functioning Data-driven Blog with Authentication supported by Google+. I quickly and easily set the remaining pages, mostly Admin and Author, to the new Bootstrap design by setting their layout to _Layout_New and wrapping the HTML with <div class="container">. I removed about half the main menu elements and added a [Login] menut item and new separator/Logout menu items under Author and Admin Tools.

Phase I is complete!

The next blog will be about adding new features such as extending the Model and public registration for new Authors.

There are 3 comments


The European languages are members of the same family. Their separate existence is a myth. For science, music, sport, etc, Europe uses the same vocabulary. The European languages are members of the same family. Their separate existence is a myth.

Today, 14:55 - Reply

Europe uses the same vocabulary. The European languages are members of the same family. Their separate existence is a myth.

Today, 15:34 - Reply

The European languages are members of the same family. Their separate existence is a myth. For science, music, sport, etc, Europe uses the same vocabulary. The European languages are members of the same family. Their separate existence is a myth.

Today, 14:59 - Reply

Add your comment

About Chris Acree

The languages only differ in their grammar, their pronunciation and their most common words.

Phone: +1 234 567 89 10

Fax: +1 234 567 89 10


Popular Posts