Building Jester - Continued

Nuh

The best way to learn something new in my field of web dev is to apply whatever you learn directly as soon as possible, and ask questions as early as possible. Mistakes are themselves learning points, bottlenecks in approaches or technologies chosen are a view into real-world cases you have to consider when proposing solutions.

Anyhow, I know I need to build something and carry myself with all the advantages gained being exposed to .NET apps from the lens of front end development and as a past student.

I intend to record in this post (and the series), as close as possible, the following:

  • Important decision points and reasons for any choice made
  • Errors encountered that are significant or present a new reality
  • Features added, and new things learnt
  • Milestones

You can catch up on the earlier post here Building Jester - Part 1

I will delete description of points that have been captured in the last post. Feel free to head there.

Decisions made

Decisions made in previous stage
  • Choosing a BE pattern: Standard .NET MVC

  • Choosing a ORM: Enitity Framework Core

  • Choosing a DB: PostgreSQL via Docker

  • Choosing or configuring routes: No decision

  • Choosing the product: Parody news web app

  • Choosing UI library / framework: Tailwind and typescript

  • Choosing to create a service class: A service for each business domain, a repository for each table or database

  • Reason(s) for creating a separate service class: Separation of concerns, reusability, testing

  • First unpleasant feeling of implementing repositories and services: See earlier post

  • First feeling of success: ViewComponents. See earlier post for details.

  • Shipping to prod 🚀 See earlier post for release steps before the one listed below. I had a blocker that wasted time but will not be prioritised.

    • App is live 🚀
      • Fix issue with seeding file not being available on the (railway.app) prod workdir (See note at the bottom of this section)

Front end setup

Setting up front end build structure and processes. I only needed tailwind and a bit of typescript but once I got started, I wanted to setup the whole thing once and for all. Little things without much thought add up quite quickly and become a mental burden down the line

Adding authentication and authorisation

I will now be adding authentication and authorisation before I demo the live site.

Authorisation and authentication is as old as the internet, with patterns developed over the years and books sold on it. However, it can still be somewhat of a headache to get started with … as you have to make choices from a pool of unfamiliar packages/approaches. I delved into this the last time I was building an app like this. Requires a very calm mind to collect and connect the pieces of information. I have asked AI and it is just regurgitating whichever it wants to promote, doesn’t ask questions at all.

My intention is again to keep it simple. I don’t need end to end authentication and authorisation. I just need login and logout capabilities, modify, reset and delete to go with it. And most importantly, a way to separate users (which accounts provide).

In a real business scenario, you’ll probably cover every case depending on the size and product of the business. A business can pay for the costs that I am keeping away from such as emails, 3rd party integrations other than social sites, single sign on, advanced security checks for password or account recovery etc.

It is in my interest to keep things simple, and to be content with touching each aspect of an enterprise requirement rather than.

I am going to start by researching the simplest approach to it. This authentication and authorisation in ASP.NET Core has been really helpful in explaining the hidden meanings of what every tutorial just tells you to follow blindly. Not only does he explain, and show how the various blocks connect together, but he also points out where he see flaws in the approach promoted by Microsoft and the rest of the crowd.

This is my Program.cs before the scaffolding.

using HealthChecks.UI.Client;
using Jester.DAL.NewsRepository;
using Jester.Data;
using Jester.Models;
using Jester.Services;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHealthChecks();
    //.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")); //Disabling as it could keep DB awake, will require further work
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<NewsContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<INewsRepository<NewsItem>, NewsRepository> ();
builder.Services.AddScoped<INewsService, NewsService>();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

//app.UseHttpsRedirection(); //TODO: needs re-enabling at the right time
app.UseStaticFiles();

app.UseRouting();

app.MapHealthChecks("/healthcheck", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
  • I wanted to scaffold the ASP.NET Core into the project, but faced a couple of hiccups

  • Scaffolding was easy through Visual Studio. It asks you what you want to override out of the pages it generates, and where your db context and user class are (or prompts you to give names if they don’t exist so it can create them). It modifies Program.cs to add to the service layers, adds db connection and instructs to use UseAuthentication middleware. It also adds a new connection string to app-settings.json. Pretty neat. It is essentially everything for you, except a few areas to modify. Isn’t it great?

    I want it to cover however much that it can, the less I have to do the better for me as Authentication is a bit of a hassle to build from scratch and maintain it.

  • Issue 1 introduced by the scaffolding was the connection string it created which assumes MSSQL db being available. I removed the whole connection string, and attached the IdentityContext to the existing postgreSQL database.

    builder.Services.AddDbContext<NewsContext>(options => options.UseNpgsql(ConnectionsString));
    builder.Services.AddDbContext<IdentityContext>(options => options.UseNpgsql(ConnectionsString));
  • Issue 2: The scaffolding created an Area folder and put everything there essentially (oages, components, data). Areas are familiar to me but I have never been the one to set them up in conjunction with everything else i.e. the route mapping.

    When I built the project and testing out a protected route, it correctly redirect to the login page. However, it returned a 404 (not found). My first assumption was to look for the controller, and investigate from there. To my surprise, there are no controllers scaffolded for Identity. I thought, “is my job now to create a controller for every page in this folder? thought I was saving time, it is dumb”.

    I couldn’t believe that as a possibility. I searched for clues to confirm whether I can avoid the pain of creating controllers and managing identity fully.

    I landed on a clue when searching through github. I found that identity scaffolded projects use app.MapRazorPages() and I had seen @page being referenced in the scaffolded razor views. I was curious about @page and its effect already given it hasn’t been used in the project yet, but in this context, it has prompted me to ponder the possibility that I need to add app.MapRazorPages() to Program.cs.

    However, I needed to confirm whether this works in conjunction with app.MapRouteControllers or if I needed to replace one with the other and the modifications needed to run.

    I threw the question at perplexity, it said “yes, you can use them together” confidently. And it was right, that was all that was missing. I have saved myself a lot of time. Mo worrying about controllers so far, not until this app grows to be more complicated.

Program.cs after adding Identity.

using HealthChecks.UI.Client;
using Jester.DAL.NewsRepository;
using Jester.Data;
using Jester.Models;
using Jester.Services;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.EntityFrameworkCore;
using Jester.Areas.Identity.Data;

var builder = WebApplication.CreateBuilder(args);
var ConnectionsString = builder.Configuration.GetConnectionString("DefaultConnection");

// Add services to the container.
builder.Services.AddHealthChecks();
    //.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")); //Disabling as it could keep DB awake, will require further work

builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<NewsContext>(options => options.UseNpgsql(ConnectionsString));
builder.Services.AddDbContext<IdentityContext>(options => options.UseNpgsql(ConnectionsString));
builder.Services.AddDefaultIdentity<User>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<IdentityContext>();

builder.Services.AddScoped<INewsRepository<NewsItem>, NewsRepository> ();
builder.Services.AddScoped<INewsService, NewsService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

//app.UseHttpsRedirection(); //TODO: needs re-enabling at the right time
app.UseStaticFiles();

app.UseRouting();

app.MapHealthChecks("/healthcheck", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});

app.UseAuthentication();
app.UseAuthorization();


app.MapRazorPages();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

The most surprising lesson in this section is not even the use of both MapRazorPages and MapRouteControllers, it is the @page directives I found in the scaffolded identity pages.

I am used to seeing @model ... but never saw @page and was immediately curious. It turns out that MapRazorPages turns these plain Razor pages into direct endpoints, essentially generating controllers in the background to its PageModel. Now what’s the page model, where is it and how do I take control of it?

Hunting for the answer led me to discover I have been lied to 🤣 I feel blind that I didn’t spot it early enough. Each such razor page has a code-behind file with the same name defining the actions on it like a good old controller (except it is scoped to this page). I was under the impression there was some magic going on but this is anything but magic as it is the same pattern that WPF (and WinForms?) used.

At least I now know how to take control, right? That’s progress.

Activating authentication

Errors

  1. Errors captured in previous stage 1. **C'mon Postgres**: DateTime type. A lot to wrangle with when deserializing json date. 1. **Why C#, why did you have to do this**: `List` vs `string[]` 1. **Where's this going?**: Continuation of Error #1 above 1. **You want to go your own way?**: Misremembering HTTP and Route attributes on Controller Actions 1. **Templates can be used only with ... ?**: A limitation of razor

Features

  • Features completed in previous stage
    • Getting data and showing all
    • Searching for particular news
    • Paginating results
    • Hyperlinking (for content, authors, tags, categories)
    • Content pages (to read full)
    • Adding “related articles” view component (delightful!) Learn about adding TagHelpers! +1
    • Dockerise so it can be deployed to free hosts other than Azure
    • Connecting to other docker a pain unless you can provide server ip directly (fine in prod as can be injected via env vars)
    • Update pagination count
    • Update pagination UI
    • Extend view component to accept author and maybe other tags
  • Setup front end assets, configs, and build pipeline including typescript, tailwindcss, sass, webpack, and gulp
  • Learn Docker to fix deployment issues (see my detour to Learn Docker)
    • Correct file paths
    • Add node build stage (using node:lts-alpine image) to build front end instead of coupling with dotnet
    • Add dev-cert in Dockerfile for demo purposes
    • Configure Healthcheck endpoints
  • Set up WYSIWYG editor page for each article
  • Authentication
  • [-] Authorisation
  • When editing / creating, enable option to dump json in one go
  • Reactions
  • Filtering results
  • Restful API endpoints
  • Add Swagger
  • Database indexes
  • Caching
  • Error handling and pages
  • Testing
  • Notifications
  • Analytics dashboard
  • Maybe Admin Panel
Nuh © 2024