Saturday, October 3, 2026

C# Dependency Injection: What Happens When the Same Service Is Registered Twice?

In a .NET 10 web service, I recently found ILogger registered twice in Program.cs: 

services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetControlPlaneLogger()));
services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetDataPlaneLogger()));

No class in the project injected ILogger, so I wanted to know what happens when the same interface is registered more than once. The duplicate registrations in Program.cs did not throw an exception.

I unearthed the following from Microsoft's documentation (Service registration):

In plain English, the last registered ILogger is the one injected into a class constructor. In the original duplicate-registration example, the bolded registration is the one DI will resolve for ILogger:

services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetControlPlaneLogger()));
services.AddSingleton<ILogger>(sp =>
    new ControlPlaneLogger(
        sp.GetRequiredService<IArchitecturalPlanesFactory>()
          .GetDataPlaneLogger()));




Friday, October 2, 2026

Claude Code Blurts Out "Footgun": A Legitimate Software Anti-Pattern

While working with Claude Code, Sonnet 5.x diagnosed some C# I inherited as containing a "footgun." I had never heard the term footgun before. I mean, the most lethal footwork I'd seen was Rosa Klebb’s poison-tipped shoe blades immortalized in 'From Russia with Love.'

In modern computer science nomenclature, a footgun is a design that is technically valid but makes it unnecessarily easy for a developer to make a mistake.

A close-to-real-world example of this is as follows:

ILogger dataPlaneLogger =
           _architecturalPlanesFactory.GetDataPlaneLogger();

The factory method indicates that the logger is Data Plane specific. But the returned type is ILogger, which for all intents and purposes could be anything (log to a UDP port, log to Cloudflare R2 blob store, log to App Insights).

The factory makes the ambiguity explicit:

ILogger GetDataPlaneLogger();
ILogger GetControlPlaneLogger();

After that assignment, the type system no longer knows that this is the Data Plane logger. A variable named dataPlaneLogger cannot enforce that it was returned by method GetDataPlaneLogger. It is just another ILogger.

This is exacerbated by dependency injection. Consider a .NET 10 / C# class that requires a logging sink for the Control Plane and a logger sink for the Data Plane:

public class PlaneService(
                 ILogger controlPlaneLogger,
                 ILogger dataPlaneLogger)
{
}

The parameter names communicate intent to a human, but dependency injection resolves both services as the same type: ILogger. The primary constructor has two parameters of the same type, so there is no type-level distinction between logging to the Control Plane and logging to the Data Plane.

This ambiguity is an example of a footgun.


Thursday, October 1, 2026

LinkedIn Jobs: How to Sort by Most Recent When the Sort Option Is Missing

LinkedIn’s newer Jobs interface may not show a visible “Sort by Most Recent” option. What is the point of applying for an eleven-month-old job? Fear not! You can still favor more recently posted jobs by adding the following to the end of your LinkedIn Jobs search URL and reloading the page:

&sortBy=DD

However, LinkedIn does not appear to sort the results in strict chronological order. You may still see jobs posted 8 hours ago, 3 hours ago, 2 hours ago, and 10 hours ago mixed together.

Tuesday, April 14, 2026

Azure Pipelines: Not triggering a build when README.md is edited

I was updating a README.md and realized I was about to trigger a pointless pipeline build. Had I Googled it, it might have taken ten minutes to "StackOverflow" the answer. Instead, AI told me to add an exclusion to the YAML file’s trigger, and now I know:

trigger:
  batch: true
  branches:
    include:
    - master
  paths:
    exclude:
    - '*.md'

Wednesday, January 7, 2026

macOS: Capture Video's Frames using Bash

The blog will cover how to screenshot a video on macOS using Bash. Videos typically run at 24 FPS or 30 FPS, so taking a screenshot every second will likely capture a representative frame of the video. macOS provides a command-line utility, screencapture, that takes screenshots.

The man page for screencapture is viewed as follows:

man screencapture

Below is a screenshot showing the key command-line arguments of screencapture:

When running screencapture from a macOS terminal console, a window may be displayed by the system. Permission will have to be given for Terminal to access the computer’s screen and audio. Similarly, when running screencapture in a Bash script from Visual Studio Code, the same permissions will have to be granted to Visual Studio Code.

To screenshot a video, run the Bash script. Then display the video full screen. The script will capture an image each second. By default, the script runs for 26 minutes, as a great many shows are 24 minutes long. A command line is provided to change the script duration.

If you are reading this, you are a developer. The command line for the script is readable in the comments. The script is less than two hundred lines of code, but most of the work is processing the command line.

Here is the script in its entirety:

#!/usr/bin/env bash
# Version 0.0 January 1, 2025 
# orignal release
# Version 0.1 January 2, 2025
# Made screen capture loop deduct processing time to hit closer to 1 second
# 
# Screenshot Capture Tool (macOS)
#
# Purpose
# -------
# Captures full-screen screenshots at a fixed interval for a fixed duration and
# writes them into a deterministic, hierarchical folder structure suitable for
# show / episodic video processing workflows.
#
# The tool is designed to capture a complete episode into a "raw" frame set that
# can later be post-processed (pruning, cropping, encoding, etc.).
#
#
# Folder Structure
# ----------------
# The output directory structure is built from the provided parameters as follows:
#
#   <CONTENT_ROOT>/
#     <SHOW_NAME>/
#       Season<SS>/
#         EP<EE>/
#           raw/
#             000000.png
#             000001.png
#             000002.png
#             ...
#
# Where:
#   <SS> = zero-padded season number (2 digits)
#   <EE> = zero-padded episode number (2 digits)
#
#
# Example
# -------
# Given:
#   CONTENT_ROOT = ~/ShowContent
#   SHOW_NAME   = Frieren
#   SEASON       = 1
#   EPISODE      = 2
#
# The resulting output directory will be:
#
#   ~/ShowContent/Frieren/Season01/EP02/raw/
#
# With files:
#
#   000000.png
#   000001.png
#   000002.png
#   ...
#
#
# File Naming
# -----------
# - Frames are named sequentially using a zero-padded counter:
#     %06d.png
# - Example: 000000.png, 000001.png, 000002.png
# - Existing files are never overwritten; the script exits on collision.
#
#
# Required Parameters
# -------------------
#   -n <show name>  Show name (folder name, verbatim)
#   -s <season>     Season number (integer)
#   -e <episode>    Episode number (integer)
#
#
# Optional Parameters
# -------------------
#   -r <root>      Content root directory (default: ~/ShowContent)
#   -i <seconds>   Capture interval in seconds (default: 1)
#   -d <minutes>   Total capture duration in minutes (default: 26)
#
#
# Notes
# -----
# - Uses macOS `screencapture` (same pipeline as Cmd+Shift+3)
# - Requires screen recording permission for the shell/terminal
# - Intended for deterministic, repeatable frame extraction
#

set -euo pipefail

CONTENT_ROOT="$HOME/ShowContent"
CAPTURE_INTERVAL_SECONDS=1
RUN_DURATION_MINUTES=26
RUN_DURATION_SECONDS=$((RUN_DURATION_MINUTES * 60))

SHOW_NAME=""
SEASON_NUMBER=""
EPISODE_NUMBER=""
RAW_FOLDER_NAME="raw"

usage() {
  echo "Usage: $0 -n <show name> -s <season> -e <episode> [-r <root>] [-i <interval>] [-d <duration>]"
  exit 1
}

while getopts "n:s:e:r:i:d:" opt; do
  case "$opt" in
    n) SHOW_NAME="$OPTARG" ;;
    s) SEASON_NUMBER="$OPTARG" ;;
    e) EPISODE_NUMBER="$OPTARG" ;;
    r) CONTENT_ROOT="$OPTARG" ;;
    i) CAPTURE_INTERVAL_SECONDS="$OPTARG" ;;
    d) RUN_DURATION_MINUTES="$OPTARG"
      RUN_DURATION_SECONDS=$((RUN_DURATION_MINUTES * 60))
      ;;
    *) usage ;;
  esac
done

if [[ -z "$SHOW_NAME" || -z "$SEASON_NUMBER" || -z "$EPISODE_NUMBER" ]]; then
  usage
fi

CONTENT_ROOT=$(cd "$(dirname "$CONTENT_ROOT")" && pwd)/$(basename "$CONTENT_ROOT")
if [[ "$CONTENT_ROOT" == /Volumes/* ]]; then
  VOLUME_ROOT="/Volumes/$(basename "$(dirname "$CONTENT_ROOT")")"

  if [ ! -d "$VOLUME_ROOT" ]; then
    echo "External drive not mounted: $VOLUME_ROOT" >&2
    exit 1
  fi
fi

if ! mkdir -p "$CONTENT_ROOT"; then
  echo "Failed to create content root: $CONTENT_ROOT" >&2
  exit 1
fi

SEASON_TAG=$(printf '%02d' "$SEASON_NUMBER")
EPISODE_TAG=$(printf '%02d' "$EPISODE_NUMBER")

RAW_DIR="$CONTENT_ROOT/$SHOW_NAME/Season$SEASON_TAG/EP$EPISODE_TAG/$RAW_FOLDER_NAME"

mkdir -p "$RAW_DIR"

printf 'Starting capture\n'
printf '  Show        : %s\n' "$SHOW_NAME"
printf '  Season       : %02d\n' "$SEASON_NUMBER"
printf '  Episode      : %02d\n' "$EPISODE_NUMBER"
printf '  Content root : %s\n' "$CONTENT_ROOT"
printf '  Output dir   : %s\n' "$RAW_DIR"
printf '  Interval (s) : %d\n' "$CAPTURE_INTERVAL_SECONDS"
printf '  Duration (m) : %d\n' "$RUN_DURATION_MINUTES"
printf '\n'

START_TIME_SECONDS=$SECONDS
END_TIME_SECONDS=$((START_TIME_SECONDS + RUN_DURATION_SECONDS))
FRAME_COUNTER=0
LAST_STATUS_SECONDS=0

while (( SECONDS < END_TIME_SECONDS )); do
  ITERATION_START=$SECONDS
  FRAME_NAME=$(printf '%06d.png' "$FRAME_COUNTER")
  FILE_PATH="$RAW_DIR/$FRAME_NAME"

  if [ -e "$FILE_PATH" ]; then
    echo "Frame file already exists: $FILE_PATH" >&2
    exit 1
  fi

  screencapture -x "$FILE_PATH"
  if [ ! -s "$FILE_PATH" ]; then
    echo "Screenshot failed or file not written: $FILE_PATH" >&2
    exit 1
  fi

  FRAME_COUNTER=$((FRAME_COUNTER + 1))

  if (( SECONDS - LAST_STATUS_SECONDS >= 30 )); then
    ELAPSED=$((SECONDS - START_TIME_SECONDS))
    printf '[%02d:%02d] frames=%d last=%s\n' \
      $((ELAPSED/60)) $((ELAPSED%60)) "$FRAME_COUNTER" "$FRAME_NAME"
    LAST_STATUS_SECONDS=$SECONDS
  fi

  ITERATION_END=$SECONDS
  PROCESSING_TIME=$((ITERATION_END - ITERATION_START))
  SLEEP_TIME=$((CAPTURE_INTERVAL_SECONDS - PROCESSING_TIME))

  if (( SLEEP_TIME > 0 )); then
    sleep "$SLEEP_TIME"
  fi
done

Tuesday, December 23, 2025

macOS: Configure Visual Studio Code to Run the Bash Debug Extension

The Bash Debug Extension, as of version 0.3.9, requires at least Bash 4.* or Bash 5.* to be installed. macOS natively has Bash 3.2 installed.

A convenient way to install Bash is with Homebrew. If Homebrew has not previously been installed, it can be installed as follows. From a terminal session, invoke the following to install Homebrew:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

The Homebrew installer provides the following steps to finalize the installation for the current user:

- Run these commands in your terminal to add Homebrew to your PATH:
    echo >> /Users/<username>/.zprofile
    echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/<username>/.zprofile
    eval "$(/opt/homebrew/bin/brew shellenv)"

Install Bash (currently 5.3.9) using Homebrew:

brew install bash

To run the latest Bash, specify the pathBash attribute in launch.json as follows:

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "bashdb",
            "request": "launch",
            "name": "Bash-Debug (simplest configuration)",
            "program": "${file}",
            "pathBash": "/opt/homebrew/bin/bash"
        }
    ]
}




Monday, September 29, 2025

Git Command-Line: Listing All Untracked Files

When a new folder is added to a local Git repo, "git status" only shows the folder name:

C:\repo>git status

On branch bug/jdnark/1234408-Fix-Automation-Unzip

Untracked files:

  (use "git add <file>..." to include in what will be committed)

        CICDAutomation/

To see all the files that are untracked, and not just the folder, use the following:

git status --untracked-files=all

An example of the files that are untracked is as follows:

C:\repo>git status --untracked-files=all
On branch bug/jdnark/1234408-Fix-Automation-Unzip
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Automation/.gitignore
        Automation/Readme.md
        Automation/get_logs.py

nothing added to commit but untracked files present (use "git add" to track)