Sometimes your code needs to work with secrets that you don’t want to risk accidentally leaking on the internet. There are many strategies for solving this problem, and here I share my preferred approach. I see a lot of articles about how to manage user secrets in ASP.NET and other web applications, but not many focusing on console or desktop applications.
Secrets are stored as plain-text key/value pairs in JSON format in %AppData%\Microsoft\UserSecrets
This isn’t totally secure, but may be an improvement over .env and .json files stored inside your project folder which can accidentally get committed to source control if your .gitignore file isn’t meticulously managed
Cloud platforms (GitHub Actions, Azure, etc.) often use environment variables to manage secrets. Using local user secrets to populate environment variables is a useful way to locally develop applications that will run in the cloud.
This example shows how to populate environment variables from user secrets:
GitHub Actions makes it easy to load repository secrets into environment variables. See GitHub / Encrypted secrets for more information about how to add secrets to your repository.
This example snippet of a GitHub action loads two GitHub repository secrets (USERNAME and PASSWORD) as environment variables (username and password) that can be read by my unit tests using Environment.GetEnvironmentVariable() as shown above.
steps:...- name:🧪 Run Testsenv:username:${{ secrets.USERNAME }}password:${{ secrets.PASSWORD }}run:dotnet test ./src
Using these strategies I am able to write code that seamlessly accesses secrets locally on my dev machine and from environment variables when running in the cloud. Since this strategy does not store secrets inside my project folder, the chance of accidentally committing a .env or other secrets file to source control approaches zero.
.NET MAUI (Multi-Platform Application User Interface) is a new framework for creating cross-platform apps using C#. MAUI will be released as part of .NET 6 in November 2021 and it is expected to come with Maui.Graphics, a cross-platform drawing library superior to System.Drawing in many ways. Although System.Drawing.Common currently supports rendering in Linux and MacOS, cross-platform support for System.Drawing will sunset over the next few releases and begin throwing a PlatformNotSupportedException in .NET 6.
By creating a graphics model using only Maui.Graphics dependencies, users can share drawing code across multiple rendering technologies (GDI, Skia, SharpDX, etc.), operating systems (Windows, Linux, MacOS, etc.), and application frameworks (WinForms, WPF, Maui, WinUI, etc.). This page demonstrates how to create a platform-agnostic graphics model and render it using Windows Forms and WPF. Resources in the Maui.Graphics namespace can be used by any modern .NET application (not just Maui apps).
⚠️ WARNING:Maui.Graphics is still a pre-release experimental library (as noted on their GitHub page). Although the code examples on this page work presently, the API may change between now and the official release.
For this example I will start by creating a a .NET 5.0 WinForms project from scratch. Later we will extend the solution to include a WPF project that uses the same graphics model.
We need to get the windows forms MAUI control and all its dependencies. At the time of writing (September, 2021) these packages are not yet available on NuGet, but they can be downloaded from the Microsoft.Maui.Graphics GitHub page.
💡 Tip: If you’re developing a desktop application you can improve the “rebuild all” time by editing the csproj files of your dependencies so TargetFrameworks only includes .NET Standard targets.
Add the Microsoft.Maui.Graphics project to your solution and add a reference to it from your project.
Windows Forms: Add the Microsoft.Maui.Graphics.GDI.Winforms project to your solution and add a reference to it in your WinForms project. A GDIGraphicsView control should appear in the toolbox.
WPF: Add the Microsoft.Maui.Graphics.Skia.WPF project to your solution and add a reference to it in your WPF project. A WDSkiaGraphicsView control should appear in the toolbox.
A drawable is a class that implements IDrawable, has a Draw() method, and can be rendered anywhere Maui.Graphics is supported. By only depending on Maui.Graphics it’s easy to create a graphics model that can be used on any operating system using any supported graphical framework.
usingMicrosoft.Maui.Graphics;
This drawable fills the image blue and renders 1,000 randomly-placed anti-aliased semi-transparent white lines on it. Note that the location of the lines depends on the size of the render field (passed-in as an argument).
Windows Forms applications may also want to intercept SizeChanged events to force redraws as the window is resized.
publicpartialclassForm1 : Form
{
public Form1()
{
InitializeComponent();
gdiGraphicsView1.Drawable = new RandomLines();
}
privatevoid GdiGraphicsView1_SizeChanged(object? sender, EventArgs e) =>
gdiGraphicsView1.Invalidate();
privatevoid timer1_Tick(object sender, EventArgs e) =>
gdiGraphicsView1.Invalidate();
}
I found performance to be quite adequate. On my system 1,000 lines rendered on an 800x600 window at ~60 fps. Like System.Drawing this system slows down as a function of image size, so full-screen 1920x1080 animation was much slower (~10 fps).
Since our project is configured to display the same graphics model with both WinForms and WPF, it’s easy to edit the model in one place and it’s updated everywhere. We can replace the random lines model with one that manages randomly colored and sized balls that bounce off the edges of the window as the model advances.
To build this project from source code you currently have to download Maui.Graphics source from GitHub and edit the solution file to point to the correct directory containing these projects. This will get a lot easier after Microsoft puts their WinForms and WPF controls on NuGet.
Can you recreate this classic screensaver using Maui.Graphics? Bonus points if the user can customize the number of shapes, the number of corners each shape has, and the number of lines drawn in each shape’s history. It’s a fun problem and I encourage you to give it a go! Here’s how I did it: mystify-maui.zip
Microsoft’s System.Drawing.Common package is commonly used for cross-platform graphics in .NET Framework and .NET Core applications, but according to the dotnet roadmap System.Drawing will soon only support Windows. As Microsoft sunsets cross-platform support for System.Drawing they will be simultaneously developing Microsoft.Maui.Graphics, a cross-platform graphics library for iOS, Android, Windows, macOS, Tizen and Linux completely in C#.
The Maui.Graphics library can be used in any .NET application (not just MAUI applications). This page documents how I used the Maui.Drawing package to render graphics in memory (using a Skia back-end) and save them as static images from a console application.
I predict Maui.Graphics will eventually evolve to overtake System.Drawing in utilization. It has many advantages for performance and memory management (discussed extensively elsewhere on the internet), but it is still early in development. As of today (July 2021) the Maui.Graphics GitHub page warns “This is an experimental library … There is no official support. Use at your own Risk.”
This program will create an image, fill it with blue, add 1,000 random lines, then draw some text. It is written as a .NET 5 top-level console application and requires the Microsoft.Maui.Graphics and Microsoft.Maui.Graphics.Skia NuGet packages (both are currently in preview).
We use SkiaSharp to create a canvas, but importantly that canvas implements Microsoft.Maui.Graphics.ICanvas (it’s not Skia-specific) so all the methods that draw on it can be agnostic to which rendering system was used. This makes it easy to write generic rendering methods now and have the option to switch the rendering system later.
This page demonstrates how to continuously monitor microphone input using C#. Code here may be a helpful reference for developers interested in working with mono or stereo data captured from an audio device in real time. This project uses NAudio to provide simple access to the microphone on Windows platforms.
Mono
Stereo
Full source code is available on GitHub (Program.cs)
This program starts by creating a WaveInEvent with a WaveFormat that specifies the sample rate, bit depth, and number of channels (1 for mono, 2 for stereo).
We can create a function to handle incoming data and add it to the DataAvailable event handler:
var waveIn = new NAudio.Wave.WaveInEvent
{
DeviceNumber = 0, // customize this to select your microphone device WaveFormat = new NAudio.Wave.WaveFormat(rate: 44100, bits: 16, channels: 1),
BufferMilliseconds = 50};
waveIn.DataAvailable += ShowPeakMono;
waveIn.StartRecording();
This method is called when the incoming audio buffer is filled. One of the arguments gives you access to the raw bytes in the buffer, and it’s up to you to convert them to the appropriate data format.
This example is suitable for 16-bit (two bytes per sample) mono input.
privatestaticvoid ShowPeakMono(object sender, NAudio.Wave.WaveInEventArgs args)
{
float maxValue = 32767;
int peakValue = 0;
int bytesPerSample = 2;
for (int index = 0; index < args.BytesRecorded; index += bytesPerSample)
{
intvalue = BitConverter.ToInt16(args.Buffer, index);
peakValue = Math.Max(peakValue, value);
}
Console.WriteLine("L=" + GetBars(peakValue / maxValue));
}
This method converts a level (fraction) into bars suitable to display in the console:
privatestaticstring GetBars(double fraction, int barCount = 35)
{
int barsOn = (int)(barCount * fraction);
int barsOff = barCount - barsOn;
returnnewstring('#', barsOn) + newstring('-', barsOff);
}
When the WaveFormat is configured for 2 channels, bytes in the incoming audio buffer will have left and right channel values interleaved (2 bytes for left, two bytes for right, then repeat). Left and right channels must be treated separately to display independent levels for stereo audio inputs.
This example is suitable for 16-bit (two bytes per sample) stereo input.
privatestaticvoid ShowPeakStereo(object sender, NAudio.Wave.WaveInEventArgs args)
{
float maxValue = 32767;
int peakL = 0;
int peakR = 0;
int bytesPerSample = 4;
for (int index = 0; index < args.BytesRecorded; index += bytesPerSample)
{
int valueL = BitConverter.ToInt16(args.Buffer, index);
peakL = Math.Max(peakL, valueL);
int valueR = BitConverter.ToInt16(args.Buffer, index + 2);
peakR = Math.Max(peakR, valueR);
}
Console.Write("L=" + GetBars(peakL / maxValue));
Console.Write(" ");
Console.Write("R=" + GetBars(peakR / maxValue));
Console.Write("\n");
}
Scientific image analysis frequently involves working with 12-bit and 14-bit sensor data stored in 16-bit TIF files. Images commonly encountered on the internat are 24-bit or 32-bit RGB images (where each pixel is represented by 8 bits each for red, green, blue, and possibly alpha). Typical image analysis libraries and documentation often lack information about how to work with 16-bit image data.
This page summarizes how I work with 16-bit TIF file data in C#. I prefer Magick.NET (an ImageMagick wrapper) when working with many different file formats, and LibTiff.Net whenever I know my source files will all be identically-formatted TIFs or multidimensional TIFs (stacks).
ImageMagick is a free and open-source cross-platform software suite for displaying, creating, converting, modifying, and editing raster images. Although ImageMagick is commonly used at the command line, .NET wrappers exist to make it easy to use ImageMagick from within C# applications.
ImageMagick has many packages on NuGet and they are described on ImageMagick’s documentation GitHub page. TLDR: Install the Q16 package (not HDRI) to allow you to work with 16-bit data without losing precision.
ImageMagick is free and distributed under the Apache 2.0 license, so it can easily be used in commercial projects.
An advantage of loading images with ImageMagick is that it will work easily whether the source file is a JPG, PNG, GIF, TIF, or something different. ImageMagick supports over 100 file formats!
// Load pixel values from a 16-bit TIF using ImageMagick (Q16)MagickImage image = new MagickImage("16bit.tif");
ushort[] pixelValues = image.GetPixels().GetValues();
That’s it! The pixelValues array will contain one value per pixel from the original image. The length of this array will equal the image’s height times its width.
Since the Q16 package was installed, 2 bytes will be allocated for each pixel (16-bit) even if it only requires one byte (8-bit). In this case you must collect just the bytes you are interested in:
For this you’ll have to install the high dynamic range (HDRI) Q16 package, then your GetValues() method will return a float[] instead of a ushort[]. Convert these values to proper pixel intensity values by dividing by 2^16.
MagickImage image = new MagickImage("32bit.tif");
float[] pixels = image.GetPixels().GetValues();
for (int i = 0; i < pixels.Length; i++)
pixels[i] = (long)pixels[i] / 65535;
LibTiff is a pure C# (.NET Standard) TIF file reader. Although it doesn’t support all the image formats that ImageMagick does, it’s really good at working with TIFs. It has a more intuitive interface for working with TIF-specific features such as multi-dimensional images (color, Z position, time, etc.).
LibTiff gives you a lower-level access to the bytes that underlie image data, so it’s on you to perform the conversion from a byte array to the intended data type. Note that some TIFs are little-endian encoded and others are big-endian encoded, and endianness can be read from the header.
LibTiff is distributed under a BSD 3-clause license, so it too can be easily used in commercial projects.
// Load pixel values from a 16-bit TIF using LibTiffusingTiff image = Tiff.Open("16bit.tif", "r");
// get information from the headerint width = image.GetField(TiffTag.IMAGEWIDTH)[0].ToInt();
int height = image.GetField(TiffTag.IMAGELENGTH)[0].ToInt();
int bytesPerPixel = image.GetField(TiffTag.BITSPERSAMPLE)[0].ToInt() / 8;
// read the image data bytesint numberOfStrips = image.NumberOfStrips();
byte[] bytes = newbyte[numberOfStrips * image.StripSize()];
for (int i = 0; i < numberOfStrips; ++i)
image.ReadRawStrip(i, bytes, i * image.StripSize(), image.StripSize());
// convert the data bytes to a double arrayif (bytesPerPixel != 2)
thrownew NotImplementedException("this is only for 16-bit TIFs");
double[] data = newdouble[bytes.Length / bytesPerPixel];
for (int i = 0; i < data.Length; i++)
{
if (image.IsBigEndian())
data[i] = bytes[i * 2 + 1] + (bytes[i * 2] << 8);
else data[i] = bytes[i * 2] + (bytes[i * 2 + 1] << 8);
}
Routines for detecting and converting data from 8-bit, 24-bit, and 32-bit TIF files can be created by inspecting bytesPerPixel. LibTiff has documentation describing how to work with RGB TIF files and multi-frame TIFs.
I often prefer to work with scientific image data as a 2D arrays of double values. I write my analysis routines to pass double[,] between methods so the file I/O can be encapsulated in a static class.
// Load pixel values from a 16-bit TIF using ImageMagick (Q16)MagickImage image = new MagickImage("16bit.tif");
ushort[] pixelValues = image.GetPixels().GetValues();
// create a 2D array of pixel valuesdouble[,] imageData = newdouble[image.Height, image.Width];
for (int i = 0; i < image.Height; i++)
for (int j = 0; j < image.Width; j++)
imageData[i, j] = pixelValues[i * image.Width + j];
According to ImageProcessor’s GitHub page, “ImageProcessor is, and will only ever be supported on the .NET Framework running on a Windows OS” … it doesn’t appear to be actively maintained and is effectively labeled as deprecated, so I won’t spend much time looking further into it.
Although this library can save images at different depths, it can only load image files with 8-bit depths. System.Drawing does not support loading 16-bit TIFs, so another library must be used to work with these file types.