Blog
Add EF Core Migration Checks to CI

Add EF Core Migration Checks to CI

Have you ever had a deploy succeed but then your website error because the code didn't match the database? With Entity Framework Core using migrations this had started to become a thing of the past. The database model is defined in code and with a setup which checks for migrations needing to be applied, the database was always kept up to date. However this is exactly what has happened twice in the last few months in one of our dev environments, and the reason is AI.

EF Core Migrations are scripts that are generated based on your code models. A developer makes code changes to a model, runs the add migration script and the next time apply migrations is run (either through the app startup or another process), the migrations get applied to the DB.

1dotnet ef migrations add AddBlogCreatedTimestamp

However, if an AI tool like Copilot or Claude is being used by the developer you can run into a scenario where a migration script is attempted to be written by the AI, but isn't quite right. In our case it didn't add the .design file.

Now one solution to this is to update your AGENTS.md file to include instructions specifying that your migrations folder should only be updated via the dotnet ef migrations tools, but it would still be advantageous to have your CI tests detect the error and automatically fail the status checks on your PRs. After all this could happen for many other reasons like the changes being missed from a git commit.

Automatically detect model / migration inconsistencies

Fortunately the migration tools have a command to do this exact thing.

1dotnet ef migrations has-pending-model-changes

The has-pending-model-changes command will compare your migration code with the model and return true if changes exist.

If you already have unit tests in your code this can easily be added as a test.

1using MyApp.Infrastructure;
2using Microsoft.EntityFrameworkCore;
3using Npgsql;
4
5namespace Tests;
6
7public class HasEfModelChangesTests
8{
9 [Fact]
10 public async Task TestHasEfModelChanges()
11 {
12 await using var dataSource = CreateDataSource();
13 await using var dbContext = CreateDbContext(dataSource);
14
15 var hasChanges = dbContext.Database.HasPendingModelChanges();
16
17 Assert.False(hasChanges, "The EF model has pending changes. Add a new migration.");
18 }
19
20 // Mirrors the Program.cs setup so the model matches production. No connection is opened.
21 private static NpgsqlDataSource CreateDataSource()
22 {
23 var builder = new NpgsqlDataSourceBuilder("Host=localhost;Database=model_check");
24 builder.EnableDynamicJson();
25 builder.UseVector();
26 return builder.Build();
27 }
28
29 private static AppDbContext CreateDbContext(NpgsqlDataSource dataSource)
30 {
31 var options = new DbContextOptionsBuilder<AppDbContext>()
32 .UseNpgsql(dataSource, o => o.UseVector())
33 .Options;
34
35 return new AppDbContext(options);
36 }
37}
38

One thing to note in my example is that I am using Npgsql rather than an in memory database. Typically for unit tests I'd be using an in memory db and set the on model creating function to ignore any tables not relevant for the test.

In this case we are testing the whole model schema and my model includes data types such as jsonb and pgvector which aren't supported, so instead npgsql is used. No actual DB is required and the connection string is just a dummy connection string, it's just used to access the model building functionality.

Rendering Storyblok images with Next.Js

Rendering Storyblok images with Next.Js

Like all CMS's Storyblok provides content editors with the facility to upload images to an asset library (or digital asset manager for a more current term). This asset library gives users the ability to organise all their assets into folders, assign tags, as well as more advanced tools like setting expiration dates and setting focal points.

For developers the asset library provides a CDN for serving images in way of AWS Cloudfront to minimise latency. There is also an image service with functionality to crop images as well as adjust the quality level and apply filters.

Storyblok Assets

Rendering Images with Next.Js

To render images in Next.Js you should use Next's Image component. This extends the standard HTML <img> element to provide automatic optimisation.

1import Image from 'next/image'
2
3export default function Page() {
4 return (
5 <Image
6 src="/profile.png"
7 width={500}
8 height={500}
9 alt="Picture of the author"
10 />
11 )
12}

The automatic optimisation will resize images to the desired height and width at a particular quality level.

As well as this the Image component offers other properties such as specifying responsive sizes.

Using next/image with Storyblok

To use the Next.js image component with Storyblok I create my own Image component that wraps next/image.

As you can see in the code below, this accepts properties for the StoryblokAsset as well as any other properties I might want to pass through such as className or the loading setting.

The component then extracts the filename from the StoryblokAsset to set the src property on the Next.Js Image component.

1import { StoryblokAsset } from "@/types/storyblok";
2import Image from "next/image";
3import React from "react";
4
5export interface ImageProps {
6 image: StoryblokAsset;
7 width?: number;
8 height?: number;
9 className?: string;
10 loading?: "lazy" | "eager";
11}
12
13export const ImageComponent: React.FC<ImageProps> = ({
14 image,
15 width,
16 height,
17 className,
18 loading = "lazy",
19}) => {
20 if (image.filename == null || image.filename.length === 0) {
21 console.error("Image filename is null");
22 return null;
23 }
24
25 return (
26 <Image
27 src={image.filename}
28 alt={image.alt || ""}
29 className={className}
30 width={width}
31 height={height}
32 loading={loading}
33 />
34 );
35};
36

However there's an improvement that could be made to this. Storyblok and Next.Js both provide the ability to optimise images and by directly using Next.Js's image component we will be using Next.Js over Storyblok.

This isn't a massive issue, but by using Next.Js the image request will now go through these steps:

  1. The browser will request the Image from the website host (not Storyblok). e.g. Vercel or Netlify
  2. The host will request the image from Storyblok's CDN
  3. Storybloks CDN will return the full size image
  4. The host will resize the image
  5. The host will return the image to the browser

Not only is this extra steps than is really needed, it also means we're using bandwidth on the website host. So wouldn't it be better if the client could just directly load the image from Storyblok and have Storyblok resize it to begin with?

Next.Js Image Loaders

Next.Js provides a property on the Image component to specify a loader, which is a function to generate the image url. When a loader is set images will be set to use this URL rather than Next.Js's own url with the Next.Js image optimisation.

The loader can accept parameters for the original src, a width and quality level so that a URL can be constructed to a different image service. Note: Image loaders do not support a height parameter.

In the code below I have created a loader that will return the URL for Storybloks image service with the width and quality parameters being set.

1const storyblokLoader: ImageLoader = ({
2 src,
3 width,
4 quality,
5}: ImageLoaderProps): string => {
6 return `${src}/m/${width}x0/filters:quality(${quality || 75})`;
7};

The image loaders also work in conjunction with the sizes property meaning the correct responsive size urls will be populated using the loader.

The code for my complete ImageComponent is below.

1import { StoryblokAsset } from "@/types/storyblok";
2import Image, { ImageLoader, ImageLoaderProps } from "next/image";
3import React from "react";
4
5export interface ImageProps {
6 image: StoryblokAsset;
7 width?: number;
8 height?: number;
9 className?: string;
10 loading?: "lazy" | "eager";
11}
12
13function isStoryblokImage(src: string): boolean {
14 return src.includes("a.storyblok.com");
15}
16
17const storyblokLoader: ImageLoader = ({
18 src,
19 width,
20 quality,
21}: ImageLoaderProps): string => {
22 return `${src}/m/${width}x0/filters:quality(${quality || 75})`;
23};
24
25export const ImageComponent: React.FC<ImageProps> = ({
26 image,
27 width,
28 height,
29 className,
30 loading = "lazy",
31}) => {
32 if (image.filename == null || image.filename.length === 0) {
33 console.error("Image filename is null");
34 return null;
35 }
36
37 return (
38 <Image
39 loader={isStoryblokImage(image.filename) ? storyblokLoader : undefined}
40 src={image.filename}
41 alt={image.alt || ""}
42 className={className}
43 width={width}
44 height={height}
45 loading={loading}
46 />
47 );
48};
49
Auto Format C# Code

Auto Format C# Code

When you're reading code there are a few things which make the process a lot easier, particularly when it was code written by other people.

  1. It's commented well.
  2. It's written in a logical way.
  3. Lines of text aren't really really long meaning you have to side-scroll.
  4. The formatting is consistent.

There's not a lot we can do about the first two other than having a good team, but the second two are something that can be automated and enforced.

In many languages (particularly front-end code) there's a tool named prettier that can configure everything from tab spacing, line length and even if you end a line with a semi-colon or not. I've used it for years and it's brilliant. Unfortunately, it doesn't support C#.

A tool I have found though is CSharpier (https://csharpier.com/), and like Prettier it is an opinionated code formatter.

Add CSharpier to your project

To get started with CSharpier first create a tool-manifest file in the root of your project and then run the install command.

You can do this with the following terminal commands.

1# if you don't yet have a .config/dotnet-tools.json file
2dotnet new tool-manifest
3
4dotnet tool install csharpier

Next add a configuration file to your project so that all team members will produce the same results.

To do this add a JSON file named .csharpierrc.json to the root of the solution.

1{
2 "printWidth": 100,
3 "useTabs": false,
4 "tabWidth": 4,
5 "endOfLine": "auto"
6}

After you add CSharpier you will want to reformat every file in one go, otherwise your going to spend a year going through PRs mostly containing format changes as every file gradually gets reformated.

Do this with the following command.

1dotnet csharpier .

Configure Visual Studio to auto format on save

There's lots of options for triggering the format; You can do it manually, on Pre-commit hooks, or within CI tools, but I find the best is to have a file format on save. This way not only is it making your code readable to everyone else, it also makes it easier to read as your working on it. Plus when tab spacing doesn't fix itself correctly its a good indication there's something wrong with your code.

To do this you will need to install the Visual Studio extension https://marketplace.visualstudio.com/items?itemName=csharpier.CSharpier

Once installed you then need to configure it to format on save. This is located under Tools | Options | CSharpier | General and can either be configured at a global and project level.

CSharpier Visual Studio Config Dialog

And that's it. Now whenever you save a file it will automatically get reformated.

Creating Multiple Dynamic Routes with NextJs

Creating Multiple Dynamic Routes with NextJs

If you’ve used NextJs then you will probably be familiar with how it handles routing using the file system.

Files named page.tsx are organized into folders and the name of the folder becomes the name of the route. E.g. If the folder were named about-us then the route on the site will be /about-us. To create a page under another page, simply create a folder within a folder. E.g. company/about-us/page.tsx becomes /company/about-us.

NextJs Routing Structure

Page names can also be dynamic, to do this you use a catch-all route which is defined with square brackets in the folder name. e.g. [slug]/page.tsx.

NextJs Routing Structure with dynamic route

Now all requests to the route of the site which don’t match a fixed path will be picked up by this page and will include a parameter matching the name within the square backets (in this example “slug”), which you can then use to render the page dynamically.

1export default function Page({ params }: { params: { slug: string } }) {
2 return <div>My Page Name: {params.slug}</div>
3}

Dynamic pages can also be nested. E.g. [category]/[product]/page.tsx. In this example a request to /electronics/50-inch-tv will route to the page including the parameters category=electronics, and product=50-inch-tv.

By using triple dots in-front of the parameter name NextJs will catch all requests to subpages and provide the hierarchy as an array to the parameter. E.g. […slug]/page.tsx will capture requests to /company/about-us and have a parameter named slug with the values [‘company’, ‘about-us’].

This is all explained on the NextJs site in detail https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#catch-all-segments

However, the one thing you can’t do is have two or more dynamic paths at the same level. E.g. Creating…

/[category]/page.tsx

/[slug]/page.tsx

Will lead to an error saying "Error: You cannot use different slug names for the same dynamic path".

The official way to have more than one dynamic path is to separate them by placing one into a subfolder, so in my scenario I could have…

/products/[category]/page.tsx

/[slug]/page.tsx

Now any routes starting /products/ will be routed to the category page rather than the slug page. However this typically doesn’t match the requirement and hasn’t been needed for the majority of sites being built with a CMS.

Creating multiple dynamic paths in NextJs using one catch all page

The first option for enabling multiple dynamic paths at the same level is to have one catch-all routes and within that file figure out what the page should be. E.g. /[…slug]/page.tsx

Now all requests to the site will be captured by this route and within the page you can write custom logic to determine if the first value is a category or generic page.

To keep code clean the actual rendering of the different page types can be split into separate components with page.tsx handling the logic for which to include.

This setup works well, however it has a tradeoff that a lot of functionality provided by NextJs through it’s folder structure is lost. E.g. NextJs support layout files at different levels of the file system but here you would be restricted to just the one.

Creating multiple dynamic paths in NextJs using rewrites

An alternative method is to structure the dynamic pages into separate subfolders as per the official way and then hide this using Rewrites.

What is a Rewrite?

A rewrite is like a redirect, but one that happens server side and is therefore invisible to the user. When a request goes into the server it is first checked against the rewrite rules and if it matches one, the request is rewritten to that URL rather than being passed as it to the application.

Anyone old enough to remember programming in Classic ASP may remember this being one of the methods first used to implement friendly URLs rather than having query strings for everything dynamic.

How to setup a rewrite in NextJs

Rewrites are configured in the next.config.mjs file.

1module.exports = {
2 async rewrites() {
3 return {
4 beforeFiles: [
5 {
6 source: '/electronics/:slug*',
7 destination: '/products/electronics/:slug*',
8 },
9 ]
10 },
11 },
12}

In this example I am creating a re-write for anything going to a path starting /electronics/ to route the request to the path /products/electronics/

Hardcoding the list of categories as re-writes isn’t much better than creating a lot of hard coded sub-folders, but this is easily fixed by dynamically writing the list which will happen when the site is built.

Creating a redirect in NextJs

The solution now works, however any requests to the /products/ url will also resolve as duplicate pages. To overcomes this we can create a standard redirect to put people back on our preferred path.

1module.exports = {
2 async redirects() {
3 return [
4 {
5 source: '/products/electronics/:slug*',
6 destination: '/electronics/:slug*',
7 permanent: true,
8 },
9 ]
10 },
11}

One thing to be aware of

While this is a solution that will work, one thing to note is that there may be a limit to how far it will scale. If you hit the point of creating over 1000 redirects or rewrites the NextJs build script will start warning of potential performance hits from there being to many redirects.

This isn’t something I’ve experienced, but each rewrite is effectively another check that needs to be performed against each request.