C# nullable refs and virtual vs abstract properties

Consider:

public class InitialStep2Sqlite : ScriptMigration
{
    protected override string UpPath => "Sqlite2.Up.sql";
    protected override string DownPath => "Sqlite2.Down.sql";
}

public abstract class ScriptMigration : Migration
{
  protected virtual string UpPath {get;}
  protected virtual string DownPath {get;}

  protected override void Up(MigrationBuilder mb) 
    => mb.RunSqlFile(UpPath);
  protected override void Down(MigrationBuilder mb) 
    => mb.RunSqlFile(DownPath);
}

which, compiled under nullable enable, says “[CS8618] Non-nullable property 'UpPath' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.”

Changing the property's virtual to abstact removes the warning.

The lesson: If your non-null proof of a property depends crucially on the subclass making it non-null, then making the parent property abstract not virtual is the correct statement of intent.

Generally, if a parent Member depends on the subclass for correctness – if the parent can't provide a correct implementation itself – then, logically, its correctness requires it to be abstract not virtual.

Model Binding an Array from a Form Post | Asp.Net MVC & Asp.Net Core

if you form post html form fields:

<li>
  <label for="name">Your name<span>*</span></label>
  <input type="text" id="name" name="Name" />
</li>
<li>
  <label for="phoneNumber">Telephone<span>*</span></label>
  <input type="tel" id="phoneNumber" name="PhoneNumber" />
</li>

to a Controller using the [FromForm] attribute, AspNet will modelbind the form fields to your parameters by matching the name= attribute of the input elements to Property names on the parameter class:

class MyController : Controller
{
    public IActionResult Index([FromForm]Contact contact){ ... }
}

// ----
public class Contact
{
    public string Name {get;set;}
    public string PhoneNumber {get;set;}
}

But what about model binding an array — for instance, if you have a list of question inputs, and want to store the answers in a list?

If you name each field as if it were an array element:

 <fieldset id="q1">
     <label class="question">Question 1</label>
     <ul class="form-style">
        <li>
            <input type="radio" id="c1" name="answers[0]" value="@c1"/>
            <label for="c1">@c1</label>
        </li>
        <li>
            <input type="radio" id="c2" name="answers[0]" value="@c2"/>
            <label for="c2">@c2</label>
        </li>
    </ul>
  </fieldset>
    <fieldset id="q2">
        <label class="question">Question 1?</label>
        <ul class="form-style">
            <li>
                <input type="radio" id="b1" name="answers[1]" value="@b1"/>
                <label for="b1">@b1</label>
            </li>
            <li>
                <input type="radio" id="b2" name="answers[1]" value="@b2"/>
                <label for="b2">@b2</label>
            </li>
            <li>
                <input type="radio" id="b3" name="answers[1]" value="@b3"/>
                <label for="b3">@b3</label>
            </li>
            <li>
                <input type="radio" id="b4" name="answers[1]" value="@b4"/>
                <label for="b4">@b4</label>
            </li>
        </ul>
    </fieldset>

then AspNet will bind the submitted fields named answers[0], answers[1], ... to an array Property in your class with the matching name:

public class Questions
{
    public string[] Answers {get;set;}
}

.Net Core Strong Typed Configuration Binding for Arrays | Microsoft.Extensions.Configuration

The .Net Core Microsoft.Extensions.Configuration package includes the ConfigurationBinder.Get<>() method which can read a config file and bind name-value entries to a class. This gives you strongly typed configuration:

public class FromTo
{
    public string From { get; init; }
    public string To { get; init; }
}

The method is an extension method for Configuration so you can call it straight off a Configuration instance. In an Asp.Net Startup.cs class, for instance:

services.AddSingleton(
    s=>Configuration
            .GetSection("fromto").Get<FromTo>());

will result in your services collection knowing to provide an FromTo class with the Properties populated from the configuration entries, matched by section:propertyname :

{
  "fromto:from": "[email protected]",
  "fromto:to": "[email protected]"
}

or if you use secrets:

cd <projectdirectory>
dotnet user-secrets init
dotnet user-secrets set fromto:from [email protected]
dotnet user-secrets set fromto:to [email protected]

That works great for the usual primitives - string, numbers, boolean — but what about binding an Array?

public class FromTo
{
    public string From { get; init; }
    public string To[] { get; init; }
}

the From field is still bound but the To field silently fails and results in a null value.

The magic trick is to add a colon-separated number suffix to each setting name:

{
  "fromto:from": "[email protected]",
  "fromto:to:0": "[email protected]",
  "fromto:to:1": "[email protected]",
  "fromto:to:2": "[email protected]",
}

Now Configuration.GetSection("fromto").Get<FromTo>() will successfully bind all 3 rows for "fromto:to into the public string To[] array.

Time Machine Backup to a Windows Share — Do it More Reliably

You can find plenty of instructions on the interwebs for setting up time machine to a network share, even a Windows share.

What you can't easily find is, how to do it reliably.

I have some recommendations.

1. Use cron scripts to just-in-time mount and dismount the time machine share

This is my first and biggest point. Dismounting the backup drive after each backup removes most of the reliability problem of network backup. Before doing this, I rarely got through 3 months without some kind of “the network share got dismounted uncleanly and now it won't mount until I run Disk First Aid on it”.

2. When it comes to data security, if you don't have 3 copies you aren't being serious.

This is a lesson you can take from cloud computing. Both Microsoft and Amazon clouds treat 'at least 3 copies' as the basic level for storing data. That means you want at least 2 independent backup systems for anything on your own machine.

If you combine this thought with the standard “don't put all your eggs in one physical location” motto of backup, you realise that you need a cloud or offsite backup as well as your time machine backup. The simplest free solution for your third copy, if 5GB is enough, is to use iDrive or OneDrive.

3. Buy a copy of Alsoft Disk Warrior

This is optional, and certainly less important than the first two points but, running Disk First Aid or fsck doesn't always work. Sad but true. I typically got a “fsck can't repair it properly” incident about once a year. I had a growing stack of hard disks with a year's worth of backup each, all only mountable readonly.
DiskWarrior has so far been reliable in restoring broken volumes back to fully working state. NB as of 2021 DiskWarrior can't yet repair APFS volumes so stay with HFS+ volumes for your time machines.

Help with cron scripts and multiple backups

Cron Scripts

Here are my script and cron table for mounting a TM drive from the network, requesting a backup, and dismounting the TM drive. It uses wakeonlan to wake the server from sleep, and ping to confirm it's up before trying to mount. It uses osascript to mount the volume because that deals with saving the network password in your keychain.

#! /usr/bin/env sh
#
smbServer=NameOfYourServer.local
smbServerMacAddress='00:00:00:00:00:00' # fill in server mac address to be able to wake it from sleep
smbVolumeUrl="smb://$(whoami)@$smbServer/D"
smbMaybeMountedAtPath1="/Volumes/$smbServer/Backups"
smbMaybeMountedAtPath2="/Volumes/D/Backups"
smbMaybeMountedAtMore(){
    dirname $(find /Volumes -iname $tmDiskImageName -maxdepth 3 2>/dev/null | head -n 1) 2>/dev/null
}
tmDiskImageName='TM3.21.sparsebundle'
tmVolumeMountedAtPath='/Volumes/TM3.21'

# wakeonlan
# I got mine from https://github.com/jpoliv/wakeonlan/blob/master/wakeonlan
# otherwise try https://ddg.gg/bash%20script%20wakeonlan
#

#cron jobs get a very truncated path and can't find ping, diskutil, hdiutil, tmutil ...
PATH="$PATH:/sbin:/usr/sbin:/usr/bin:/usr/local/bin"

echo '#-------------------------------------------------------'
date

echo "$0 [email protected]"
if [[ -z "$1" && -x $(which tmux) ]]
then
    echo "tmuxing ..."
    tmux new-session $0 tmuxed
    exit
elif [[ "$1" == *unmount* ]] ; then
    tmutil status
    if [[ ! -d "$tmVolumeMountedAtPath" ]] ; then 
        echo "$tmVolumeMountedAtPath is already unmounted"
    elif [[ $(tmutil status | grep 'Running = 0') ]] ; then 
        echo "unmounting ..."
        /usr/sbin/diskutil unmount $tmVolumeMountedAtPath
    else
        echo "not unmounting $tmVolumeMountedAtPath because tm status says still running."
    fi
    exit
fi

#-------------------------------------------------------
if [[ -x $(which wakeonlan) ]] ; then   wakeonlan $smbServerMacAddress ; fi
for tried in {1..50} ; do ping -c 1 -t 5 $smbServer 2>&1 && break ; done
if (( $tried == 50 )) ; then
    echo "failed to ping $smbServer. Exiting."
    exit
fi
sleep 10
if (( $tried > 2 )) ; then 
    echo "waiting in case it was a cold-ish start..."
    sleep 20
fi
echo 'mounting ...'
osascript -e 'mount volume "'$smbVolumeUrl'"'
echo 'attaching ...'
hdiutil attach $smbMaybeMountedAtPath1/$tmDiskImageName \
    || hdiutil attach $smbMaybeMountedAtPath2/$tmDiskImageName \
    || hdiutil attach $(smbMaybeMountedAtMore)/$tmDiskImageName \
    || echo \
       "Didn't find $tmDiskImageName at $smbMaybeMountedAtPath1 or $smbMaybeMountedAtPath2 or $(smbMaybeMountedAtMore)"
[ $? -eq 0 ] && echo "attached OK"
echo 'requesting backup.'
  tmutil startbackup --auto
  echo 'Done.'

And the crontab lines:

5  10-23/4 * * * /Users/chris/Applications/tmbackupnow.sh >> /Users/chris/Applications/Logs/tmbackupnow.log 2>&1
20,35 10-23/4 * * * /Users/chris/Applications/tmbackupnow.sh -unmount >> /Users/chris/Applications/Logs/tmbackupnow.log 2>&1

This schedule does 3 or 4 backups per working day on top of the local snapshots that time machine does anyway. Possibly this is overkill if you are squeezed for disk space. It tries to mount the TM machine and kick off a backup at 5 minutes past 10am,2pm,6pm,10pm and then tries to dismount the backup disk 15 minutes later and again another 15 minutes later. Adjust the timing to the size & speed of your backup.

At least 3 copies

My third copy, on top of time machine, is syncthing “continuous file synchronization”, which is great. It's like being able to set up a load of open source CloudDrives but using your local network too.

My fourth copy is either github or bitbucket for code; and iDrive or OneDrive for documents and graphics.

My fifth copy will be scripted backups to Azure storage, which seems like the cheapest way to do cloud backups. Meanwhile I'm paying Apple or Microsoft each month for big enough cloud storage.