EligereManage — Technical Breakdown
How (and why) I built www.eligere.app/manage, a graphical manager for Eligere (all-in-one Choral Music Management Platform)
How (and why) I built www.eligere.app/manage, a graphical manager for Eligere (all-in-one Choral Music Management Platform)
Why I built EligereManage
I was first approached by the Director of Music at my choir (during my A-Levels). He suggested that I create a digital inventory system to help the Choral Library manage and track pieces / items and physical copies. As it so happens, for my A-Level Computer Science course, it was required by my exam board to complete an ‘independent project’ — it therefore made sense to explore building this system for the choir.
Overview of EligereManage v1
I created a rudimentary version of what later became ‘Eligere’ using Python OOP (Object-Orientated Programming) and Tkinter (for graphics) using a Raspberry Pi-powered system with a web-camera attached (using Bluetac!) to the front. Originally, I designed this system to permanently reside in the Choral Library. The system utilised a PostgreSQL Database stored using the (discontinued since DataBricks acquisition) bit.io PostgreSQL Database hosting service.

As shown in the image above, this system was far from ideal but it functioned as a good proof-of-concept. In the end it served the role it was designed for (passing the assessment for the project!), however I was not satisfied at all to properly roll it out to my choir.
Since originally building the v1 project, I had learnt a lot about Project Management and Software Development.
During the Summer after A-levels, I set out to build EligereManage v2 …
How I built EligereManage v2 (current)
I stumbled across a new framework from Microsoft called “.NET MAUI” (MAUI standing for “Multi-platform App UI”) which uses C#. As it so happens, the language my institution decided for us to learn as our OOP language was C#. Therefore, by the end of my A-Levels, I was incredibly confident in developing software in it!
Perhaps acting more swiftly than I should have done for a new bleeding-edge Microsoft framework, I decided I would build my project in .NET MAUI to run locally on the clients’ (different choral librarians with the head choral librarian who was my primary client) mobile devices.
In addition to finding a new ‘front-end’ framework and language, I also had to now locate a new provider for my databasing as bit.io had just discontinued their in-house solution when acquired.
I set my sights onto neon.tech for my PostgreSQL needs (obligatory #notsponsored … obviously but I would quite frankly welcome it!). I will explain more in the next section, but a key criteria for me was a ’serverless’ PostgreSQL solution.
EligereManage’s Architecture — Technical
I have attached an image below of my Systems Architecture Diagram. I will break it down below into sections to explain my decisions.
You will notice due to unusual client requirements that the Architecture of EligereManage and its associated components is relatively unique.
Eligere Databases —Serverless PostgreSQL
I chose to use Neon.tech’s ’serverless’ PostgreSQL solution for hosting Eligere Databases (library data). I desired to use a ’serverless’ solution because the primary aim of Eligere is to save the Choral Librarians (users) time and there is significantly lower administrative overhead associated with ’serverless’. This is because the hosting provider is responsible for the upkeep and maintenance of the database. This leads me onto the drawbacks associated with this system… which mainly is that the upkeep and maintenance is the hosting provider’s responsibility. For large organisations with significant swathes of Personal Information on customers, this is a significant security risk and it may not be compliant with strict GDPR legislation in the United Kingdom / European Union.
If you were wondering why I was adamant on PosgreSQL for my databasing technology, there were several reasons. Mainly, incredible stability, advanced security features (SSL encryption, role-based access controls and more), cost effective, scalability and finally because of its support for user-defined functions (I personally find these very useful in my database projects).
However, the client’s organisation does not require uptime of 99.99% and does not store Personal Information other than minimal records (which can be secured through additional security measures).
On the note of what the Eligere Database doesn’t store, what it does store is a wide variety of tables relating to music pieces and choral books — all associated to a composer and / or arranger and includes physical library locations and QR codes where applicable. Finally, it has the ability for choir members to loan out music.
After being forcefully resettled from bit.io’s retiring, I settled on Neon.tech due to the availability of a free plan (which is useful for trialing) and the seemingly relatively robust infrastructure they provide. Being ‘serverless’ allows for scalability in the future if required (and it scales down to zero if not being used at all at any one time!)— although as of writing the client (pilot) organisation does currently not need to upgrade from the free tier!
Furthermore, Neon.tech offers simple user management and very developer friendly tools which made their offering very compelling.
Eligere Services — DigitalOcean ‘Droplet’ VPS
Eligere Services are split into two main types: Eligere Backup and Eligere AutoLoan Services, as shown in the diagram. Before we explore the individual services, let us first explore why the provider was chosen.
As per the diagram, Eligere Services are hosted using DigitalOcean. DigitalOcean is a prominent cloud infrastructure provider, often referred to as “the developer cloud” due to its focus on simplifying cloud computing for developers. Eligere Services uses a ‘droplet’ per organisation which DigitalOcean’s name for a VPS (Virtual Private Server). A Virtual Private Server is a virtual machine that provides a dedicated server environment on a physical server that is shared with other users.
This was decided because the Eligere Services consist of automation scripts — automation is the one area where you need an incredibly high availability to be useful (DigitalOcean offers a 99.99% guarantee!). Due to the generally low-cost and scalability of ‘droplets’, Eligere can be as cheap to organisations as possible only paying for what is used.
Eligere Backup Service
Accidents happen … production systems accidentally get wiped (or intentionally by malicious third-parties), ‘Acts of God’ happen … etc.
Backups are incredibly important. Whilst neon.tech internally creates its own backups of database data, that is not enough for a production system. This service consists of a Python script running regularly as a CRONJOB in Linux, on the ‘droplet’. This particular script uses the ’snapshot’ method of databasing backups, including all data from the database — tables and records alike. This provides reliable, timely ‘rollbacks’ if the worst were to happen to the production databasing infrastructure.
Eligere AutoLoan Service
Eligere allows for loans to be made out to choir members of pieces of music in the choral library — humans are notoriously forgetful. This consists of a Python automation script regularly using CRONJOB in Linux, on the ‘droplet’ (like the Backup Service). This script notifies the loanee when a loan is due soon and repeatedly after an overdue date. It also will send reports weekly to the administrator of the organisation about inventory that is out of the library from loanees. As per the diagram, this service uses ZohoMail API (Application Programming Interface) integrations. This is because ZohoMail is a very inexpensive and developer-friendly solution for automated emails, especially with Python.
EligereManage — Client Application
Introduction
As mentioned earlier, EligereManage is a multi-platform application written in C# with the .NET MAUI (MAUI standing for “Multi-platform App UI”) framework by Microsoft. My application uses a variety of libraries to manage PostgreSQL interfacing and several other libraries such as those used to create QR codes for use in the EligereQR Inventory System. An overview of the screens and components in the app hierarchy is available in the diagram above.
Key Packages / Libraries (NUGET), Frameworks Used
.NET MAUI, Camera.MAUI, CommunityToolkit.Maui (Microsoft)
Various important .NET MAUI packages for developing software with it. The Camera.MAUI is used to read the camera with EligereScan. More information about implementation and usage below in algorithm section.
AngleSharp
AngleSharp is used by EligereManage to programmatically create a new HTML document. For example, see below. More on algorithmic use in the project under the Generating, Sending Inventory Email Reports section below.
var context = BrowsingContext.New(AngleSharp.Configuration.Default);
var document = await context.OpenNewAsync();
Npgsql
Npgsql is a package that allows .NET-based projects to access and interface with PostgreSQL databases directly. It is fully open-source and all written in C#. More information about implementation and usage below in algorithm section.
QRCoder
Arguably the most important software component after .NET MAUI, QRCoder is ‘a pure C# Open Source QR Code implementation’ (according to their GitHub repository). This component is critical for interpreting and generating the choral library and item management codes which power EligereScan and the EligereQR Inventory System (see below section for more information on usage and implementation in these algorithmic components).
Key Algorithms, Algorithmic Components
EligereScan (and EligereQR Inventory System)
EligereScan, as shown in the diagram above, is an accessible sub-system that can be triggered through the library dashboard. When opened for the first time, as mandated by mobile device operating systems, the user is asked for the device camera permission to be granted. After this permission has been acquired, the screen will show a live camera feed from the main camera. The EligereScan algorithm is now actively scanning for QR codes in the field of view.
When a QR code is detected in a frame, a loading overlay is displayed to the user which indicates that the system is processing the code. This processing consists of the system validating that the code is an EligereQR — if not the user is displayed a pop-up message “INVALID QR CODE — TRY AGAIN” with an explainer. This explainer also has a reminder that expired EligereQR codes will be seen as invalid to the system (as the accompanying database records no longer exist).
If the QR code is a valid EligereQR, differing actions will become available via a pop-up window to the user. These are contextually linked to the type of EligereQR scanned.
For example, if a book item that was loaned out to a choir member was scanned subsequently again, the options that would be shown would be to do with returning the item to the library (as would be naturally expected in a library system).
The full C# algorithm can be found below — please note some methods have not been included that use the static EligereDatabase wrapper methods but this should not impact your understanding of the code.
EligereScan — Main Algorithm (C#)
/// EligereScan - Main Algorithm - C#
private async void cameraView_BarcodeDetected(object sender, Camera.MAUI.ZXingHelper.BarcodeEventArgs args)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
string currentQRCode = $"{args.Result[0].Text}";
await cameraView.StopCameraAsync();
LoadingOverlay.IsVisible = true;
// Check whether in EligereQR format (basic format validation)
bool isQRInEligereQRFormat = EligereDatabase.EligereFrameworkStaticFunctions.CheckTextInEligereQRFormat(currentQRCode);
if (isQRInEligereQRFormat)
{
// Passed Basic EligereQR Format Check
QRResult.Text = $"EligereQR detected";
// Extract ID from QR text contents string (uses static method from Eligere Database Wrapper class)
string justIDContainedInQRTextContentsWithoutQRType = EligereDatabase.EligereFrameworkStaticFunctions.ExtractNumberID(currentQRCode);
// Get EligereQR Type
string connectionString = await SecureStorage.Default.GetAsync("dbToken");
// Initialize database wrapper with connection string
EligereDatabase.DatabaseWrapper dbWrapper = new EligereDatabase.DatabaseWrapper(connectionString);
string EligereQRTypeOfCode = dbWrapper.CheckItemOrBookOrBox(currentQRCode);
if(string.IsNullOrEmpty(EligereQRTypeOfCode))
{
// Handle invalid QR code
QRResult.Text = $"INVALID QR CODE - TRY AGAIN";
LoadingOverlay.IsVisible = false;
await DisplayAlert("INVALID QR CODE","EligereScan could NOT interpret this QR Code- only EligereQR codes can be used here!\n\nPLEASE NOTE: Old EligereQR codes are 'dead' but will still contain data therefore this code may have been valid at one point but not now.", "OK");
await cameraView.StartCameraAsync();
}
Console.WriteLine("Type of EligereQR : " + EligereQRTypeOfCode);
LoadingOverlay.IsVisible = false;
string computedLoanStatus;
switch (EligereQRTypeOfCode)
{
case "Piece":
// Check if the piece is currently loaned out
bool isPieceLoanedOut = dbWrapper.IsItemLoanedOut(Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType));
switch (isPieceLoanedOut)
{
case true:
computedLoanStatus = "Return Current Loan";
break;
case false:
computedLoanStatus = "Takeout New Loan";
break;
}
string actionPiece = await DisplayActionSheet($"PIECE ITEM {justIDContainedInQRTextContentsWithoutQRType} - Action(s)","Cancel",null,"Piece ITEM Information", $"{computedLoanStatus}");
switch (actionPiece)
{
case "Takeout New Loan":
await Navigation.PushAsync(new TakeoutNewLoan(justIDContainedInQRTextContentsWithoutQRType));
break;
case "Return Current Loan":
// Process loan return
LoadingOverlay.IsVisible = true;
bool databaseReturnActionSuccess = dbWrapper.ReturnLoanedItem(Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType));
if (databaseReturnActionSuccess)
{
// Display success message and reset camera
await DisplayAlert("Success! :)", $"Your loan was RETURNED with SUCCESS. Please let a choral librarian know and place in the PROCESSING RETURNS tray.\n\nEligere Framework automatically processed the return of Loan (ID:{justIDContainedInQRTextContentsWithoutQRType}) on behalf of the choral library", "OK");
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
}
else
{
// Display error message and reset camera
await DisplayAlert("Error! :(", $"We are sorry but Eligere Framework could not process your RETURN (ID:{justIDContainedInQRTextContentsWithoutQRType}).\n\nPlease try again before contacting the choral library staff for help.", "OK");
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
}
break;
case "Piece ITEM Information":
// Fetch and display piece information
LoadingOverlay.IsVisible = true;
int itemID = Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType);
DatabaseWrapper.PieceInfo pieceInfo = dbWrapper.GetPieceInfoByItemID(itemID);
// Format date to remove time
string formattedDate = pieceInfo.CheckDate.ToString("yyyy-MM-dd");
LoadingOverlay.IsVisible = false;
await DisplayAlert($"PIECE ITEM {itemID} - Information", $"Title : {pieceInfo.PieceTitle}\nAlso Known As : {pieceInfo.Aka}\nText : {pieceInfo.PieceText}\nLanguage : {pieceInfo.PieceLanguage}\nTranslation : {pieceInfo.Translation}\nVoices : {pieceInfo.Voices}\nAttrib : {pieceInfo.Attrib}\nComposer : {pieceInfo.Composer}\nArranger : {pieceInfo.Arranger}\nType : {pieceInfo.PieceType}\nPublisher : {pieceInfo.PiecePublisher}\nCheck Date (US format): {formattedDate}\nNotes : {pieceInfo.Notes}\nPurchase cost / copy : {pieceInfo.PurchaseCost}\nNumber of items : {pieceInfo.NumberOfItems}\nIs VIP? {pieceInfo.IsVIP}\nISBN : {pieceInfo.ISBN}\n\n~ Eligere Framework","OK");
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
break;
case "Cancel" :
// Reset camera view
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
break;
}
break;
case "Book":
// Check if the book is currently loaned out
bool isBookLoanedOut = dbWrapper.IsItemLoanedOut(Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType));
switch (isBookLoanedOut)
{
case true:
computedLoanStatus = "Return Current Loan";
break;
case false:
computedLoanStatus = "Takeout New Loan";
break;
}
string actionBook = await DisplayActionSheet($"BOOK ITEM {justIDContainedInQRTextContentsWithoutQRType} - Action(s)","Cancel",null,"BOOK ITEM Information", $"{computedLoanStatus}");
switch (actionBook)
{
case "Takeout New Loan":
await Navigation.PushAsync(new TakeoutNewLoan(justIDContainedInQRTextContentsWithoutQRType));
break;
case "Return Current Loan":
// Process book return
LoadingOverlay.IsVisible = true;
bool databaseReturnActionSuccess = dbWrapper.ReturnLoanedItem(Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType));
if (databaseReturnActionSuccess)
{
// Display success message and reset camera
await DisplayAlert("Success! :)", $"Your loan was RETURNED with SUCCESS. Please let a choral librarian know and place in the PROCESSING RETURNS tray.\n\nEligere Framework automatically processed the return of Loan (ID:{justIDContainedInQRTextContentsWithoutQRType}) on behalf of the choral library", "OK");
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
}
else
{
// Display error message and reset camera
await DisplayAlert("Error! :(", $"We are sorry but Eligere Framework could not process your RETURN (ID:{justIDContainedInQRTextContentsWithoutQRType}).\n\nPlease try again before contacting the choral library staff for help.", "OK");
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
}
break;
case "BOOK ITEM Information":
// Fetch and display book information
LoadingOverlay.IsVisible = true;
int itemID = Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType);
DatabaseWrapper.BookInfo bookInfo = dbWrapper.GetBookInfoByItemID(itemID);
LoadingOverlay.IsVisible = false;
// Format date to remove time (US format)
string BookCheckDate = bookInfo.BookCheckDate.ToString("MM/dd/yyyy");
await DisplayAlert($"Book ITEM {itemID} - Information", $"Title : {bookInfo.BookName}\nPurchase cost / copy : £{bookInfo.BookPurchaseCost}\nNotes : {bookInfo.BookNotes}\nNumber of Book copies : {bookInfo.BookNOfItems}\nISBN : {bookInfo.BookISBN}\nCheck Date : {BookCheckDate}\nStorage Location: {bookInfo.BookStorageLocation}\n\n~ Eligere Framework","OK");
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
break;
case "Cancel" :
// Reset camera view
LoadingOverlay.IsVisible = true;
QRResult.Text = "";
await cameraView.StartCameraAsync();
LoadingOverlay.IsVisible = false;
break;
}
break;
case "Box":
string action = await DisplayActionSheet($"BOX {justIDContainedInQRTextContentsWithoutQRType} - Action(s)","Cancel",null,"Box Information");
// Fetch piece information associated with the box
(string pieceTitle, string storageLocation, int pieceID) = dbWrapper.GetPieceInfoByBoxID(Convert.ToInt32(justIDContainedInQRTextContentsWithoutQRType));
switch (action)
{
case "Box Information":
// Display box information
string formattedMessageHeader = $"BOX {justIDContainedInQRTextContentsWithoutQRType} - INFORMATION";
string formattedMessageBody = $"Box (ID) : {justIDContainedInQRTextContentsWithoutQRType}\nStorage Location : {storageLocation}\nContains : (ID:{pieceID}) {pieceTitle}\n\n~ Eligere Framework";
await DisplayAlert(formattedMessageHeader, formattedMessageBody, "OK");
QRResult.Text = "";
await cameraView.StartCameraAsync();
break;
}
break;
}
}
else if (!isQRInEligereQRFormat)
{
// Handle invalid QR code format
QRResult.Text = $"INVALID QR CODE - TRY AGAIN";
LoadingOverlay.IsVisible = false;
await DisplayAlert("INVALID QR CODE",
"EligereScan could NOT interpret this QR Code- only EligereQR codes can be used here!\n\nPLEASE NOTE: Old EligereQR codes are 'dead' but will still contain data therefore this code may have been valid at one point but not now.", "OK");
await cameraView.StartCameraAsync();
}
});
}
Generating, Sending Inventory Email Reports
When a new item for the choral library is created, various records are generated (depending on type). For example, for a new BOX, a large EligereQR code is created to be placed on the exterior for easy viewing and use by a librarian in the future. This is dependent on the type of new item or items; the number created depends on how many items there are. This is all context-dependent. When these are created, the Eligere Inventory System generates a printer-friendly document which is then automatically emailed to the configured librarian email address. This contains all EligereQR codes for items and / or boxes etc.
EligereHTML — GenerateInventoryPrintableForPieceItemsAndBox Method Code Snippet
A HTML printable report is created for a Piece, associated individual items of said piece and the accompanying box in this code snippet.
// NOTE: 'USING' BELOW CHOSEN FOR READABILITY'S SAKE
using Configuration = AngleSharp.Configuration;
// EligereHTML - GenerateInventoryPrintableForPieceItemsAndBox Method Code Snippet
public static async Task GenerateInventoryPrintableForPieceItemsAndBox(string pieceTitle, string htmlPath)
{
Console.WriteLine("Eligere Inventory Printable (Items + Box)...\n\n");
Console.WriteLine("Getting all .png (QR) images sorted by creation time...");
string currentDirectory = Directory.GetCurrentDirectory();
//string folderPath = Path.Combine(currentDirectory, "EligereCache");
string folderPath = Path.Combine(Microsoft.Maui.Storage.FileSystem.Current.AppDataDirectory, "EligereCache");
// Get all .png files in the specified folder, ordered by creation time
var imageFiles = Directory.GetFiles(folderPath, "*.png")
.Select(f => new FileInfo(f))
.OrderBy(f => f.CreationTime)
.ToList();
Console.WriteLine("imageFiles : " + imageFiles.Count);
if (imageFiles.Count == 0)
{
Console.WriteLine("No .png files found in the specified folder.");
return;
}
// Create a new AngleSharp document
Console.WriteLine("Creating new document...");
var context = BrowsingContext.New(Configuration.Default);
var document = await context.OpenNewAsync();
var body = document.Body;
Console.WriteLine("Adding PIECE TITLE information + BOX QR beneath");
// Add the first image with the specified text above it
var firstImage = imageFiles[0];
var paragraph = document.CreateElement<IHtmlParagraphElement>();
paragraph.TextContent = $"'{pieceTitle}' - Eligere Inventory Printout - BOX QR, ITEM QR(s)";
body.AppendChild(paragraph);
paragraph = document.CreateElement<IHtmlParagraphElement>();
paragraph.TextContent = $"BOX QR";
body.AppendChild(paragraph);
var image = document.CreateElement<IHtmlImageElement>();
image.Source = await GetImageDataUriAsync(firstImage.FullName);
image.SetAttribute("style", "width: 200px; height: 200px;");
body.AppendChild(image);
imageFiles.RemoveAt(0);
paragraph = document.CreateElement<IHtmlParagraphElement>();
paragraph.TextContent = $"ITEM QR(s)";
body.AppendChild(paragraph);
// Create a table element
var table = document.CreateElement<IHtmlTableElement>();
// Create a row for each pair of images
for (int i = 0; i < imageFiles.Count; i += 2)
{
var row = document.CreateElement<IHtmlTableRowElement>();
// Add the first image to the first cell
var cell = document.CreateElement<IHtmlTableCellElement>();
image = document.CreateElement<IHtmlImageElement>();
image.Source = await GetImageDataUriAsync(imageFiles[i].FullName);
image.SetAttribute("style", "width: 100px; height: 100px;");
cell.AppendChild(image);
row.AppendChild(cell);
// Add the second image to the second cell, if it exists
if (i + 1 < imageFiles.Count)
{
cell = document.CreateElement<IHtmlTableCellElement>();
image = document.CreateElement<IHtmlImageElement>();
image.Source = await GetImageDataUriAsync(imageFiles[i + 1].FullName);
image.SetAttribute("style", "width: 100px; height: 100px;");
cell.AppendChild(image);
row.AppendChild(cell);
}
// Add the row to the table
table.AppendChild(row);
}
EligereHTML — SendNewPrintableInventoryEmail Method Code Snippet
This code snippet uses ZohoMail to send the newly created printable as an attachment in a new email message to the choral librarian email address. I am currently working on building more resilience into this process in case of errors — however they are saved locally too to the device in another method unseen here.
//
public static async Task SendNewPrintableInventoryEmail(string htmlFilePath)
{
// Sender's Zoho Mail address and password
string systemEmailAddress = await Microsoft.Maui.Storage.SecureStorage.Default.GetAsync("systemEmailAddress");
string systemPasswordEmail = await Microsoft.Maui.Storage.SecureStorage.Default.GetAsync("systemEmailPassword");
string recipientEmailAddress = await Microsoft.Maui.Storage.SecureStorage.Default.GetAsync("recipientEmailAddress");
// Create a new SmtpClient with Zoho Mail's SMTP server and port
SmtpClient smtpClient = new SmtpClient("smtppro.zoho.eu")
{
Port = 587,
Credentials = new NetworkCredential(systemEmailAddress, systemPasswordEmail),
EnableSsl = true,
};
// Create a new MailMessage
MailMessage mailMessage = new MailMessage(systemEmailAddress, recipientEmailAddress)
{
Subject = "Library Inventory Printable - IMPORTANT",
IsBodyHtml = false, // Set to false for plain text content - content is not HTML as HTML printout file is attached!
};
string signoffDetails = await Microsoft.Maui.Storage.SecureStorage.Default.GetAsync("signoffDetails");
// EXAMPLE signoffDetails -> "Bob Chilcott,\nHead Choral Librarian,\nOxford Chamber Choir
string mistakeContactAddress = await Microsoft.Maui.Storage.SecureStorage.Default.GetAsync("mistakeContactAddress");
// EXAMPLE mistakeContactAddress -> "example@example.com"
// Set the plain text content as the email body
string plainTextBody = $"Dear Choral Library Staff,\n\nEligere has automatically generated a new printable inventory report which has been attached to this email.\nYou MUST print this as soon as possible and apply relevant EligereQRs to relevant copies and boxes, if applicable.\n\n\n--\nSent Automatically By Eligere On Behalf of {signoffDetails}\n\n\nIF THIS EMAIL HAS BEEN SENT TO YOU IN ERROR, YOU SHOULD INFORM THE CHOIRMASTER OR HEAD CHORAL LIBRARIAN AT: {mistakeContactAddress}\n";
mailMessage.Body = plainTextBody;
// Attach the HTML file as an attachment
Attachment htmlAttachment = new Attachment(htmlFilePath, MediaTypeNames.Text.Html);
mailMessage.Attachments.Add(htmlAttachment);
// Send the email
smtpClient.Send(mailMessage);
}
Databasing Component(s)
EligereDatabase is an organisation specific PosgreSQL database which contains all of the tables and records for the choral music library. EligereDatabaseWrapper is a C# class contained in the EligereManage application. Due to security with strict ‘parameterisation’, each method for the database in this class are strict with the data types and order they accept of parameters. As is industry practice, all database credentials are loaded from a (relatively) secure memory location (connection strings in this case). MAUI contains its own ready-to-use implementation of this called ’SecureStorage’. An example usage can be seen below:
MAUI SecureStorage Example Code Snippet For Reading Saved Connection String
// MAUI SecureStorage Example Code Snippet For Reading Saved Connection String
string connectionString = await SecureStorage.Default.GetAsync("connectionString");
EligereDatabaseWrapper — Example Database Method — CheckEligereDatabaseConnectionKeyIsWorkingAndUsableForEligereManageToUse
// EligereDatabaseWrapper - Example Database Method - CheckEligereDatabaseConnectionKeyIsWorkingAndUsableForEligereManageToUse
public bool CheckEligereDatabaseConnectionKeyIsWorkingAndUsableForEligereManageToUse()
{
/*
This method, CheckEligereDatabaseConnectionKeyIsWorkingAndUsableForEligereManageToUse,
establishes a connection to a PostgreSQL database and checks if it has the necessary
write permissions. Here's a breakdown of the implementation:
1. Establishes a connection to the PostgreSQL database using Npgsql.
2. Opens the database connection.
3. Defines a SQL query using the CASE statement to determine the existence of
required tables and privileges. The result is an 'OK' or 'BAD' string.
4. Creates an NpgsqlCommand object with the SQL query and the database connection.
5. Uses ExecuteScalar to retrieve a single value from the database, representing
the query result.
6. Converts the result to a string and checks if it equals "OK" to determine if
the database has the necessary permissions, access.
7. Returns true if the result is "OK", indicating the required permissions, access;
otherwise, returns false.
This method is designed to be used in the context of checking the eligibility of
a PostgreSQL database connection key for EligereManage usage.
*/
using (NpgsqlConnection connection = new NpgsqlConnection(connectionString))
{
connection.Open();
Console.WriteLine("Connection opened...");
// Query to check write permission on a table for a user
string query = "SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'person') THEN 'BAD' WHEN NOT EXISTS(SELECT 1 FROM information_schema.table_privileges WHERE table_schema = 'public' AND grantee = current_user AND privilege_type IN('INSERT', 'UPDATE', 'DELETE', 'SELECT')) THEN 'BAD' ELSE 'OK' END AS result;";
using (NpgsqlCommand command = new NpgsqlCommand(query, connection))
{
object result = command.ExecuteScalar();
bool isValid = result.ToString() == "OK";
return isValid;
}
}
}
Conclusion — What have I learnt and what would (will) I do differently?
Whilst I am still currently in the stages of piloting this platform at a choral organisation with an extensive choral repertoire in the library, the vast majority of this codebase was written over a year to, in some cases, two year ago! I have since learnt a significant amount relating to Cyber Security and ‘Professional Programming Practices’ at University; I will be in the process of updating vast amounts of the algorithms and classes in this program to reflect this knowledge.
For example, various elements of EligereManage’s architecture could be improved by utilising a central API service — ‘EligereAPI’ or something similar. This could drastically increase security (reducing attack vectors which need to be protected) whilst also providing greater granularity and control to Eligere over organisation systems.
This was originally avoided because one of my original requirements from the client was to have no external server infrastructure other than the database for the sake of handover purposes— at a further point in time the requirements desired more features which meant these ‘Eligere Services’ were bolted-on as an afterthought (e.g. automated email notifications for library loans). Furthermore, I have learnt many lessons which will / have helped me with other projects.
A Lesson In Bleeding-Edge (.NET MAUI)
One such example is Microsoft’s .NET MAUI. I do not recall exactly when I decided to utilise this framework for EligereManage v2; however, given that it was originally announced on 23 May 2022, I believe I started work on the new version almost immediately. This was primarily due to my comfort with programming languages consisting only of Python and C# at that point in time.
Furthermore, as .NET MAUI was based heavily on Xamarin, I assumed that it would be of the same level of general usability. Needless to say, I now understand why ‘bleeding-edge’ is quite so bluntly phrased: it was not painless from the start.
At no point did I write an ongoing list of issues, but I remember having to occasionally manually delete image files from certain locations due to strange artefacts created by the compiler, among all sorts of other quirks, such as the program compiling flawlessly until the next time you opened it (having made no changes at all), and then failing in some way.
To give credit to Microsoft, new frameworks are most definitely not advised for creating production apps; however, these sorts of issues continued to plague my afternoons and evenings while I was developing — the phantom bugs were the most infuriating (these did improve with more updates but still existed frequently).
MAUI’s dream state is that you develop one C# codebase, select the device type, and it runs flawlessly on said device across whichever operating system. This is an incredibly compelling ideal; however, in reality, I have not always found this to be the case. As mentioned above, there were a lot of very bizarre bugs in MAUI, but it is also worse because some platforms occasionally require workarounds on a platform-by-platform basis. I remember one day my Android app just decided that it would not remain open and would crash immediately upon opening.
Apple platforms worked fine with that same compiled code.
Because of the way the code is compiled differently on different platforms, certain bugs at a lower level than you could easily view were practically impossible to fix yourself without waiting for a patch in the next version of MAUI software or creating a new MAUI project and transferring pre-existing content over to it (and praying that it now works!).
A Lesson From Apple — Why Everything Is A Web App
Microsoft is not solely to blame for the ‘raw’ developer experience … although it is possibly worse with Apple as they consider this their desired developer experience. Let me explain, any developer that would like to create an app for any Apple platform (iOS, iPadOS, MacOS, etc.) has to do this through a Mac. I develop primarily on my trusty M1 MacBook so that was fine for me, however Apple also makes you pay a yearly subscription of £79 (in Great Britain where I reside). This seems a little steep for a hobby project someone may have but I decided that was a price I was willing to pay given that was a requirement as a mandatory platform for my client. I was not prepared for the amount of different ‘provisioning profiles’ and various types of certificates I had to manage in addition to distribution complexity … the process is complicated and seemingly far over-complicated in my opinion…
… and my app doesn’t even include in-app payments, something Apple is currently in hot water for due to alleged ‘harmful’ practices towards developers, around the world.
I will say that I do now understand why so many digital services and apps are built using web frameworks (such as React) and hosted online, however. Provided you scale to different screen sizes correctly and follow standard industry practices, there is no reason why your app shouldn’t work on any internet-connected device.
I will be far more inclined to build my projects as web-apps in the future…
This article contains technical details accurate of October 2024 of the architecture of EligereManage and assorted infrastructure, however this is subject to change as the entire architecture is updated and tweaked to better security, speed, robustness and utility according to client wishes and desires.
Thank you very much for reading this technical deep-dive on how and why I created EligereManage (and associated components). I hope this was of interest to you- please let me know if you have any thoughts … I would love to hear them.
Please note, there will / has been a second part of this published which will cover EligereAI as a technical breakdown as there are further unique system components that deserve their own article.
Hayden Williams — Creator of www.eligere.app