Over the last couple of days, I've been working to make my e-mail package Newsletter Studio run well on Linux and Mac. The codebase for the current iteration of the package was created for Umbraco 8 while trying to keep in mind that .NET Core-support for Umbraco was around the corner. Technically, most of the core C# code is in a .NET Standard 2.0 library that is shared between the different versions of the package.
I have a very Windows-heavy background and back when the initial code for the package was created I did not really take into account the massive opportunities that the cross-platform support for modern .NET brings - I was mainly focused on getting it to run in .NET Core on a Windows box.
More and more clients have asked for official Linux support, to be able to run the package e.g. in containers or on bare metal Linux servers. Over the last few days, I've put some effort into this and wanted to share some of the "gotchas" I've encountered along the way.
Expect general tips and trips on how to write C# code that runs cross-platform and some Umbraco-package specific gotchas for packages targeting Umbraco v9-v13.
Case is king
On Unix-like operating systems like Linux and MacOS, the casing of a path or filename matters a lot since the filesystem is case-sensitive. This means that all code-references to folders and files need to match in case as well. This was one of the biggest problems with the code base when I started. The casing was mixed and there were no real standards for the casing. To make the code run well cross-platform I would propose a naming standard like this:
For folder names
- Always use lowercase names and kebab-case
For client-side files (HTML, TypeScript, Javascript, CSS, SCSS, LESS)
- Always use lowercase names and kebab-case
For C# files and cshtml
- Always use CamelCase
Working with Paths
Windows and Unix-like systems use different characters to separate paths. On Windows we use \ and on Unix /. C# has a built-in feature that helps with this:
string myPath = "folder" + Path.DirectorySeparatorChar + "sub-folder";
The .NET-runtime will make sure that Path.DirectorySeparatorChar contains the right character based on the OS where the code is running.
One note here is that you can also use Path.Combine(), but be aware of how it works:
[Test]
public void Test1()
{
// Note that both path starts and ends with \\
string path1 = "\\foo\\";
string path2 = "\\bar\\";
var merged = Path.Combine(path1, path2);
// Fails, merged = \\bar\\
Assert.That(merged,Is.EqualTo("\\foo\\bar\\"));
}
[Test]
public void Test2()
{
string path1 = "foo";
string path2 = "bar";
var merged = Path.Combine(path1, path2);
// Success, merged = foo\\bar
Assert.That(merged, Is.EqualTo("foo\\bar"));
}
The takeaway here is that you need to make sure not to try to build a path with input strings that have both leading and trailing slashes - find a way to be consistent.
Umbraco backoffice extensions
Newsletter Studio is built as a custom section with a custom tree, it's important to keep in mind that the alias of the section and trees will be used in the URLs when Umbraco loads the HTML views.
In the image above we see the URL to a view in the package.
- Alias of the Section (as in Content, Media etc)
- Alias of the Tree
- View name
In this scenario, Umbraco will look for a view here: <siteroot>\App_Plugins\newsletterStudio\backoffice\section\campaigns.html. When running on a Unix-like operating system the whole path to that file needs to match in case was well.
Different code on different platforms
Sometimes you need to do different things based on the current platform/OS, there are many ways to do this, and here are some of them.
Check OS from C# during runtime
In the package, we have a default path for the "SMTP Pickup"-configuration used during development and testing. We can use the RuntimeInformation-class to know what platform we are running on and use different defaults.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// Do something
}
Checking OS from MSBuild during build
It might be that you need to execute different scripts depending on the build platform etc.
<Exec Command="./foo.sh" Condition=" '$(OS)' != 'Windows_NT' " />
Razor Class Libraries and cross-platform development
In the package, all of the built-in views and assets are shipped as files inside a Razor Class Library (RCL), this way we don't have to ship individual files that need to be copied/synced inside the consuming projects.
I had major issues with this on the Ubuntu-machine I was working on. I kept getting errors that the Razor view did not exist, but I was 100% sure that the file existed in the RCL and that the casing of the path/filename was correct.
After hours of debugging, I tried to upgrade the .Net SKD. When I was working on my PC I was on 7.0.400 but the version shipped in the Ubuntu package repository at the time was 7.0.113 so I used that. After manually installing 7.0.400 on the Ubuntu-machine the error was gone.
I was really surprised about this. Another thing that surprised me even more was when I opened the solution on my MacBook and started it using the same .NET SDK (7.0.400) - I got the same error as with 4.0.117 on Ubuntu!
My objective was to test the package with an Umbraco site running on MacOS, so I tried to work around the error by building the NuGet-package files on my Windows-machine and then copying them over to the Mac. I created a new Umbraco project, installed the NuGet packages from a local package source, and started the website. Now the views worked!
I have not had a closer look at why this is happening but it seems like the SKD builds for the different platforms might produce slightly different builds that work slightly differently. Just keep this in mind when strange things happen, try on another OS, try another SKD version - that might save you a couple of hours.
Development environment
I'm working on Ubuntu Desktop but I'm guessing that a MacOS environment would work just as well. I've found that the following tools are very helpful:
- Visual Studio Code - Runs cross-platform
- DB Browser for SQLite - To debug the SQLite database if used with Umbraco.
- Docker + SQL Server for Linux - If you need to run "full-blown" SQL Server.
- Powershell - Also cross-platform, great to reuse scripts for all OS versions
- Rider IDE - As an old Resharper fanboy, Rider is great and provides a consistent development experience on all platforms.