Using Records in legacy .NET Frameworks

Recenly we’ve been seeing an increased activities on various blogs due to upcoming .NET 5 release date and one of it’s hottest feature - C# 9.0 records. A lot was written about this feature, starting with MSDN.

What was not clear however, was whether one can use records in older frameworks - like .NET 4.8.

Anatomy of init-only properties

Let’s settle our attention on the following example

record Vertebrate(string Name)
{
    public Vertebrate() : this("") { }
}

public enum Habitat { Terrestrial, Aquatic, Amphibian }

public record Reptile(Habitat Habitat) : Vertebrate { }

Upon compilation under net5.0 framework moniker, everything works as expected. Change it to however to net48 and you will not be able to compile it. This compiler feature fortunatelly works like opt-in member resolution (similarly to string interpolation). What compiler needs in this case is an accessible class of the following structure

//TODO: use appropriate compiler directives for legacy targets - it's not needed in net5.0
#if NETSTANDARD2_0
namespace System.Runtime.CompilerServices
{
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    internal static class IsExternalInit { }
}
#endif

Check if property is init-only

After quick research one can spot that init-only setter has special structure:

.set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)
  DotnetCommunityDemoNet5.Records/Vertebrate::set_Name(string)

To determine that setter is init-only one just needs to query the existence of required modifier initialized with aforementioned IsExternalInit type - this code helper should do the trick:

public static class RecordsHelper
{
    public static bool IsInitOnly(this PropertyInfo property) =>
        property.CanWrite && property.SetMethod is var setter
            && setter.ReturnParameter.GetRequiredCustomModifiers() is var reqMods 
            && reqMods.Length > 0
            && Array.IndexOf(reqMods, typeof(System.Runtime.CompilerServices.IsExternalInit)) > -1;
}

Consuming records in older frameworks

Fortunatelly, records are not any special types - they are, per se, not specially recognized by CLI/CLR. They are just spcially designed classes with:

  • init only properties (default behaviour for positional records)
  • free structural equality, IEquatable<> implementation, equality operators
  • positional deconstruction
  • printing/formatting

Due to this phenomenon, consuming records is quite straightforward - you are using them as normal classes. If you’d like to use with keyword then you need to use C# 9.0+ for instance by specifying that in *.csproj file:

<PropertyGroup>
    <LangVersion>9.0</LangVersion>
</PropertyGroup>

Sources

Read more...

Setting up a Personal or Organization Blog on GitHub Pages with Wyam

I’ve been trying to setup a static blog for Nemesis organization. There are many examples how to do it for project pages including this doc from Wyam author, Dave Glick, unfortunately according to this GitHub doc User and Organization pages are diffirent and can only be published from master branch top directory. There is no easy way to cram both sources and static site generated by Wyam into repository root directory.

Orphaned git branches to the rescue!

Orphaned git branch is esentially a branch that exists in the same repostory but has nothing in common with it’s master branch. Through orphaned branches, git allows multiple parallel unrelated histories.

So I decided to use master branch - the only legit source for User or Organization github page - as deployment target, and sources branch as orphaned branch with blog sources.

To create parallel history I did:

git checkout --orphan sources
git rm -rf .        #to remove all files
rm '.gitignore'
echo "# nemesissoft.github.io" > README.md
git add README.md
git commit -a -m "Initial Commit"
git push origin sources

Then you need to create the blog sources

dotnet tool install -g Wyam.Tool
wyam new -r Blog            #create new blog scaffold
wyam -r Blog -t CleanBlog   #build using specific theme

Run it within a test server and let it watch and auto reload on source file changes:

wyam  --recipe Blog --theme CleanBlog --watch --preview

Preview server listening at http://localhost:5080 ...

All you need then is a build process that will run Wyam and deploy your blog from sources branch and commit output to master branch to allow GitGub pages to host it: Add appveyor.yml file with content:

branches:
  only:
    - sources
    
environment:
  access_token:
    # EDIT the encrypted version of your GitHub access token
    secure: iIghceLJ...

install:
  - git submodule update --init --recursive
  - mkdir ..\Wyam
  - mkdir ..\output
  # Fetch the latest version of Wyam 
  - "curl -s https://raw.githubusercontent.com/Wyamio/Wyam/master/RELEASE -o ..\\Wyam\\wyamversion.txt"
  - set /P WYAMVERSION=< ..\Wyam\wyamversion.txt
  - echo %WYAMVERSION%
  # Get and unzip the latest version of Wyam
  - ps: Start-FileDownload "https://github.com/Wyamio/Wyam/releases/download/$env:WYAMVERSION/Wyam-$env:WYAMVERSION.zip" -FileName "..\Wyam\Wyam.zip"
  - 7z x ..\Wyam\Wyam.zip -o..\Wyam -r

build_script:
  - dotnet ..\Wyam\Wyam.dll --output ..\output

on_success:
  # Switch branches to master, clean the folder, copy everything in from the Wyam output, and commit/push
  # See http://www.appveyor.com/docs/how-to/git-push for more info
  - git config --global credential.helper store
  # EDIT your Git email and name
  - git config --global user.email $env:op_build_user_email
  - git config --global user.name $env:op_build_user
  - ps: Add-Content "$env:USERPROFILE\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n"
  - git checkout master
  - git rm -rf .
  - xcopy ..\output . /E
  # EDIT your domain name or remove if not using a custom domain
  #- echo wyam.io > CNAME
  # EDIT the origin of your repository - have to reset it here because AppVeyor pulls from SSH, but GitHub won't accept SSH pushes
  - git remote set-url origin https://github.com/nemesissoft/nemesissoft.github.io.git
  - git add -A
  - git commit -a -m "Commit from AppVeyor"
  - git push

Create your build pipeline on AppVeyor.com for free and watch your blog appear on {user or organization name}.github.io.

Read more...