Category Archives: ASP.NET Core

From IoT to the Cloud: A .NET Ecosystem Showcase with GitHub, Raspberry Pi, and Azure

As part of a demo to show a complete interconnected .NET ecosystem from IoT to the Cloud, I assembled a demo with the following components:

Raspberry Pi 3b (ARM32 processor, Wifi, 4GB RAM)
Touchscreen case from SmartPi-Touch
Azure Container Registry
– GitHub Repository
– GitHub action
– Docker
– Docker-Compose
– .NET 7 / ASP.NET Core / SignalR Core

The goal of this demo is to show how a connected IoT device, a Raspberry Pi, can run unattended and receive automatic updates from GitHub, Azure, refresh .NET content on a screen with no interruptions.

Prior to writing the software for this demo, I followed all of the instructions for the touch-screen and mounted the Raspberry Pi inside the case it provides.  This allows me to connect a keyboard and mouse as well as a power cable and work on the Raspberry Pi as a typical desktop workstation.

Note: This blog post are the notes from my construction of this demo.  I typically present and show this content on-stage and it is easily my most complex demo with moving pieces on a Raspberry Pi device that I show in my hands, Azure Container Registry, GitHub Codespaces and GitHub actions.  To make the demo a little more interesting, I’ll update some CSS (sometimes suggested by the audience) in the GitHub repository using GitHub codespaces on stage and the Raspberry Pi will update with the new look automatically.  I’d like to record a video showing this demo and update this post with a link to that demo

Step 0: Configure an Azure Container Registry

I already have one of these at ``. It was easy enough to configure and deploy with credentials required to access the content.  Alternatively, you can also use the GitHub package repository features and store containers there.

Step 1: Configured Docker on the Raspberry Pi

These instructions originally appeared at:

On the Raspberry Pi, I downloaded Docker with this command when running as root:

curl -sSL | sh

I then added myself to the `docker` group by running this as my standard user `jfritz`:

sudo usermod -aG docker $USER

To get connected with the Azure registry, I logged in with this command and specified the registry name, the user id and password displayed on the Access Keys panel:

Azure Portal and identifying the Access Keys for a container registry

Azure Portal and identifying the Access Keys for a container registry

I could then login to the registry with these credentials on my Raspberry Pi by executing

docker login

This generated a `/home/.docker/config.json` file that will be used to login to my private Azure registry.

Step 2: Configured Docker-compose

For this system, I want to use Watchtower to monitor the container registry and install updates automatically.  In order to configure this with those dependencies, I needed Docker-componse. The prerequisites for Docker-Compose are installed with these commands:

sudo apt-get install libffi-dev libssl-dev
sudo apt install python3-dev
sudo apt-get install -y python3 python3-pip

I completed the installation of Docker-Compose with a pip3 install command:

sudo pip3 install docker-compose

Step 3: Configure Watchtower

Watchtower is a container that will watch other containers and gracefully update them when updates are available. I grabbed the watchtower image for ARM devices using this docker command on the Raspberry Pi:

docker pull containrrr/watchtower:armhf-latest

I built a `docker-compose.yml` file with my desired watchtower configuration:

The `command` argument instructs watchtower to check for updated images every 30 seconds, and the `restart` argument instructs the container to start at startup and always restart when the watchtower stops.  More details about how to configure a Docker-compose file are available in their documentation.

Step 4 : Customize the Dockerfile to build for ARM32

The device I have for this scenario is a Raspberry Pi 3B and has an ARM32 processor.  That can throw a little wrinkle into things because most systems now target ARM64 processors by default.  Not a problem, because there is still support for ARM32 available and we just need to specify it in our deployment scripts.

The application that I will be running on the device is a simple ASP.NET Core application that counts the number of times the screen has been touched.  In a more complete scenario, there might be a sensor connected or some kiosk screen wired up that presents information and collects data.

I wrote a Dockerfile for ARM is called `Dockerfile-ARM32` with the following content:

There’s an interesting bit in the middle where I copied in the `.git` folder. This allows my application to grab the latest git SHA hash as a bit of a version check for the source code. That SHA is made availalble on the assembly’s `AssemblyInformationalVersionAttribute` attribute value.

We can then build the container for my ASP.NET Core application to run on the Pi from my Windows workstation using this command:

docker build --platform linux/arm -f .\Fritz.DemoPi\Dockerfile-ARM32 . /
-t fritz.demopi:4 /
-t fritz.demopi:latest /
-t /

and then push to my remote registry with:

docker push -a

Step 5: Configure Docker on the Pi to run the website

By default ASP.NET Core configured port 8080 for the website inside the container. I wrote a quick `docker-compose.yml` script to run my .NET application with all of the configuration I would need:

To ensure that the SignalR bits of my demo would work, I removed a privacy extension from the Chromium browser that comes with the Raspberry Pi device.

Step 6: Configure the Pi to boot into Chromium for the website

In order to configure the Pi device to boot directly into a browser and my application running in the container, I added a file at `~/.config/lxsession/LXDE-pi` called `autostart` with this configuration:

@lxpanel --profile LXDE-pi
@pcmanfm --desktop --profile LXDE-pi
#@xscreensaver -no-splash
@chromium-browser --start-fullscreen --start-maximized http://localhost/

Additional options and the instructions I started with are at where I learned how to write this script.

Step 7: Prepare a GitHub action

I configured a GitHub action to checkout my code and build in ARM32 format with the Dockerfile established previously.  This would also publish the resultant container to my Azure container registry:



As changes are made to the GitHub repository, the GitHub action will rebuild the image and deploy it to the Azure Container Registry. Watchtower identifies the update and automatically stops the existing application on the Pi and then deploys a new copy with the same settings. With a little SignalR work, the UI updates seamlessly.

Lesson Pager on the C# in the Cards website

Blazor and .NET 8: How I Built a Fast and Flexible Website

I’ve been working on a new website for my series CSharp in the Cards. I built this website in a way that was easy to maintain, flexible and most importantly would respond quickly to requests from visitors.  I knew that Blazor with .NET 8 had a static server rendering feature and decided that I wanted to put it to the test. I recently published a new lesson to the website and included a web assembly component to allow for paging and filtering the list of lessons I was pleasantly surprised when I saw the performance dashboards on azure showing that it was handling requests and responding very very quickly.

Response times of C# in the Cards after the new episode

In this blog post, let’s talk about how I’ve optimized the website for speed and some of the finishing touches that you can put on your Blazor website to make it screaming fast running on a very small instance of Azure App Service.

Static Site Rendering – Its Blazor, but easier

With .NET 8 there’s a new render mode for Blazor and it’s called static site rendering or SSR. This new render mode ditches all interactivity that we previously had with Blazor server side and Blazor Web Assembly and instead favors delivering HTML and other content from the server to browsers in a high speed manner. We can bolt on other techniques that we know from SEO and website optimization to make this even faster and deliver a great experience for our visitors.

The About page is configured to output a bunch of HTML headers for the SEO folks and the social media pages to be able to present good information about the site.  Notice the headers that are added to satisfy the search engines:

  • a canonical link element that identifies where the page should be served from
  • a keywords meta element with information about what you can find here
  • a robots element that tells the search engine crawlers what they can do with the page
  • open graph and Twitter meta tags that instruct Twitter, Facebook, LinkedIn, Discord, and other sites about the images, titles, and description of the page

That’s fine… but there are two other features to notice:

  1. This is a static page with no data being presented.  I’ve tagged it on line 2 with an attribute to allow output caching for 600 seconds (10 minutes).  This way the web server doesn’t have to render a new copy when its requested within 10 minutes of a previous request.
  2. The images references are in webp format.  Let’s not overlook this super-compressed format for displaying high-quality images.  It might be 2024, but every bit we deliver over the network still matters for performance and the 600×600 portrait picture of myself on this page was compressed nicely:
Original Compressed # Difference
450kb 30kb -93.3%

93% savings…  that’s crazy good and means that your browser is not downloading an extra 420kb it doesn’t need.

Data is stored in static files on disk

For this simple website I don’t need a big fancy database like SQL Server or Postgres or even MySQL. For this site, I’ve stored all of the data in a simple CSV file on disk.  That means that I can edit the list of articles that are available and the metadata that goes with them by just opening the file in Excel and writing new content. This means that when it comes time for me to read data about the list of content that’s available I’m only reading from a very small file on disk and I don’t need to worry about any kind of contention. I also don’t need to worry about any service that’s running to deliver that data because it’s only coming out of a small file on disk that’s read only.

In this repository class I use the LinqToCSV library to open and read all of the content from the file into a Post object in the first method, GetPostsFromDisk.  Later, in a public method called GetPosts, you see where I use the in memory cache feature of ASP.NET Core to fetch data from the cache if its available or get it from disk and store it in cache for 30 minutes.  I could probably extend this timeout to several hours or even days since the website doesn’t get any new content without uploading a new version of the site.

The key here is that the meta data about the lessons on the site is loaded and kept in memory.  As of episode 9 of the series, the posts.csv file is only 1.4kb so I have no worries about loading its entire contents into memory.

Don’t forget, in order to add the MemoryCache to your ASP.NET Core application, you need to add this line to your site configuration in the Program.cs file:


I could add other cache options like Redis to the site, but with how small the data I want to cache is, I don’t need that sophistication at this point.

Pre-rendered Interactive Web Assembly Content is fast… REALLY fast

I wanted to add a subset of the lessons to the front page of the website so that you could see the latest six episodes in the video series and scroll back and forth to the other episodes. This should be an interactive component but I still wanted the home page to render quickly and have a fresh speedy response time as you page through and look at the various episodes that are available. The natural way to do this with Blazor is to build a web assembly component that will run on the client and render data as users click on the buttons for that collection of articles.

I wrote a simple pager component that would receive a collection of lesson data and render cards for each lesson.  Since we already know that the collection of lesson data is less than 2kb in size I don’t have a problem sending the entire collection of data into the browser to be rendered.

When I use the @rendermode attribute in this code, it forces the render mode to web assembly and ASP.NET will pre-render as well as cache a version of that component’s resultant HTML with the home page. After viewers download the Web Assembly content it will hand control over to web assembly and it will be a fully interactive component for them to be able to work with.

Lesson Pager on the C# in the Cards website

Lesson Pager on the C# in the Cards website

Blazor lets me build content to be rendered on the web and I get to choose where exactly it should run. It can run in the browser with web assembly it can run statically on the server it can run interactively on the server if I want it to. In this case running as web assembly gives a really nice usability effect that makes it easy for viewers to locate the content they want to watch.

Compress the Content from Kestrel

By default content that’s delivered from the ASP.NET kestrel web server is uncompressed. We can add brotli compression to the Web server and deliver content in a much smaller package to our visitors with just a few simple lines of code in program.cs. This is something that I think everybody should do with their Internet facing websites:

#if (!DEBUG)
builder.Services.AddResponseCompression(options =>
options.EnableForHttps = true;

Add response compression configures the server so that it will deliver broadly compressed content. In this application I wrap it with the conditional debug detection because hot reload does not work with compression enabled.  When we deliver the website to the production web host it will be running in release mode and compression will be enabled.

Optimize all the JavaScript and CSS

CSS AND JavaScript can be minified and combined to reduce the number and size of downloads for this static content that makes our websites look good.  For this website I installed and used the WebOptimizer package available on NuGet.  My configuration for this looks like the following:

This script bundles the CSS files that were delivered with my website template and minifies the one JavaScript file that I manage with my project.

Set long cache-control headers for static content

The last thing that I did was set long duration cash dash control headers for static content like images CSS and Javascript files. This is easy to do with just a few more lines of optional configuration when I configure the static file feature inside of ASP.NET Core:


This website’s been easy for me to build because I can rely on my normal HTML skills and the plethora of HTML templates and CSS libraries out there to make my website look good. Blazor helps me to make it interactive render quickly and grow as I add more content to it. my cost in interaction with azure is minimal, as I’m using a Basic-2 instance of Azure App Service running Linux to deliver this site.

How I built the first PDF report for KlipTok using IronPDF

An important goal for me in building the KlipTok web application is to be able to deliver reports that can be downloaded and referenced by streamers and their support teams to help them learn how to grow their online presence.  I investigated a few tools and landed on IronPDF and started building an initial and simple report showing the dashboard content from KlipTok.  In this post, I’m going to show you how I took a Blazor Web Assembly page and re-used it so that the content could be printed with IronPDF. Continue reading

Investigating PDF Generation Software for C# and Azure

Over the last month or two, I’ve been investigating various tools that help .NET developers generate PDF documents.  In particular, I want to be able to create a few PDF reports for the KlipTok application. I compared a set of four popular tools (IronPDF, iText, SyncFusion, and Aspose) and published a YouTube video recently to show off the results:

In this post, I want to show the code I wrote for the winning IronPDF renderer and explore some of the features as I prepare to connect it to render a report from the Streamer Dashboard page of KlipTok.

Why not just print a PDF of the dashboard page?

The dashboard on KlipTok is rendered by JavaScript and WebAssembly.  In the tests from the comparison (source code on GitHub) I originally loaded the full-featured sports website with code like this:

When I attempt to simply update this code to render KlipTok at, I get the following result:

Simple KlipTok Rendering - only seeing splashscreen

Simple KlipTok Rendering – Only the splash screen renders

Clearly, I need to dig in a little further and just attempting to render the Blazor Webassembly site is not going to work.  I also tried a few configuration options:

No.. no dice. I kept getting the same splashscreen again and again. I tried extending the timeout periods, thinking I had a race condition where the page wasn’t finished rendering. Let’s take a look at some of the IronPDF features that I can use after I do a little refactoring of KlipTok to get this rendering.

Additional IronPDF features are going to allow me to format the new KlipTok dashboard page nicely for printing.  Let’s take a look at some of these features and apply them to the static KlipTok page used for SEO.

IronPDF document rendering options

The first feature I activated was visible in the test code I used above, EnableJavaScript.  This feature is intended to allow websites that layout their pages with a framework like React, Angular, or Vue to perform their layout before the PDF is rendered.

I also attempted to use the RenderDelay feature to render KlipTok.  RenderDelay directs IronPDF to wait a specific number of seconds to wait before rendering a PDF.  Usually, IronPDF will render immediately after the HTML is loaded.

I also added a TextHeader to the code block above.  This is very cool because we can add some information about the content of the page inside the header.  In this case, I added an “As Of” date.  I could also add a TextFooter with similar LeftText, RightText and CenterText options to place content in the footer.  Even BETTER, I can use an HtmlFooter or HtmlHeader to place an HTMLFragment in the header or footer.  I’m going to use a little bit of HTML with the {page} and {total-page} macros to place a pager in the footer of the pages.

There are MarginLeft, MarginRight, MarginTop, and MarginBottom properties that you can use to set those margins around your content in millimeters.  I found a setting of 10mm in IronPDF with a margin of 0px in my HTML content achieved a nice balance between stretching the content to fill the paper and some reading room around the edge of the content.

By default, IronPDF attempts to render a print media version of a website.  You can force the CSS media to screen mode by setting the CSSMediaType to PdfCssMediaType.Screen and you should render the same content in your PDF that you see on the screen.

Additionally, you can force IronPDF to render a smaller version of the website by setting the ViewPort size in pixels with the ViewPortHeight and ViewPortWidth options.  It’s a good idea to tinker with these two settings and the FitToPaperMode option which lets you specify how you would like IronPDF to place content on the page.  I liked setting a large ViewPort and setting FitToPaperMode to FitToPaperModes.AutomaticFit so that the page content will be stretched to fill the ViewPort and then rendered on the page.

Finally, you’re going to want to specify the paper size to render the HTML content to.  With a PaperOrientation setting for landscape vs. portrait, and a PaperSize property as well, you have a wealth of options for the format you wish to deliver.

The SEO-optimized version of KlipTok rendered as a PDF with margins, header, and footer.

The SEO-optimized version of KlipTok rendered as a PDF with margins, header, and footer.

Just the beginning…

I had my problems getting the KlipTok content that was built with Blazor Web Assembly to render using IronPDF.  Fortunately, I’m in a process of refactoring the location and delivery of Blazor components in the application and I can probably move that dashboard content to server-size Blazor and render it in the SEO version of the website.

In my next blog post, I’ll move those components and configure IronPDF to render content as an ASP.NET Core Minimal API running on Azure using the format I just defined in the code above.

Introducing KlipTok – Social Media fun for Twitch Clips

For the last year, I’ve struggled with a pair of questions I get from my Twitch and developer communities:

  • How do I make my Twitch channel more discoverable?
  • What is an enterprise scale Blazor application that I can find on the internet?

… and since November 2020, I have been answering both of these questions with the same application, and its called KlipTok.

Fritz introduces KlipTok on the first stream in November 2020

KlipTok is an application that indexes Twitch clips from streamers and presents them in a social media friendly way so that you can catch up on your favorite content, discover new content, and discuss with friends your favorite clips.  KlipTok makes video more fun.

Continue reading