I am writing a custom C# console program targeting .net 7.0 and running on a Raspberry Pi 4B with Rasbian after being published self-contained targeting ARM64. There is 1 property of my configuration that seemingly refuses to deserialize automatically.
The LedDefinitions property deserializes perfectly when I debug it on my desktop (windows or wsl), but when I run in on the Pi, it won't deserialize and I get an empty array. I have hacked it with a PostConfigure setup like below, but I don't understand why I have to do this.
I have projects at my job that are setup similarly and they seem to work without any hackery, although they run inside of containers based on the mcr.microsoft.com/dotnet/aspnet:6.0 docker image.
Any insight as to what I need to look at or tweak is much appreciated.
Program.cs snippet
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((bc, s) =>
{
s.Configure<LightingConfiguration>(bc.Configuration.GetSection("LightingConfiguration"));
s.AddTransient<LightingExecutor>();
s.PostConfigure<LightingConfiguration>(x =>
{
var b = bc.Configuration
.GetSection("LightingConfiguration")
.GetChildren()
.Where(y => y.Path.EndsWith("LedDefinitions"))
.SelectMany(y => y.GetChildren())
.ToArray();
var propIndex = typeof(LedRange).GetProperty("Index");
var propLedStart = typeof(LedRange).GetProperty("LedStart");
var propLedEnd = typeof(LedRange).GetProperty("LedEnd");
var propPatternStart = typeof(LedRange).GetProperty("PatternStart");
var newLedRanges = new List<LedRange>();
foreach (var q in b)
{
var obj = new LedRange();
foreach(var p in q.GetChildren())
{
var prop = p.Key switch
{
"Index" => propIndex,
"LedStart" => propLedStart,
"LedEnd" => propLedEnd,
"PatternStart" => propPatternStart,
_ => null,
};
prop?.SetValue(obj, int.Parse(p.Value));
}
newLedRanges.Add(obj);
}
x.LedDefinitions = newLedRanges.ToArray();
});
});
var host = builder.Build();
var config = host.Services.GetRequiredService<IOptionsMonitor<LightingConfiguration>>();
Configuration class
class LightingConfiguration
{
public int NumberOfLEDs { get; init; }
public bool DoFire { get; init; }
public bool HorizontalFire { get; init; }
public int PatternOffsetPerFrame { get; init; }
public int TargetFps { get; init; }
public string[] PatternToRepeat { get; init; }
public LedRange[] LedDefinitions { get; set; }
public int Cooling { get; init; } // Rate at which the pixels cool off
public int Sparks { get; init; } // How many sparks will be attempted each frame
public int SparkHeight { get; init; } // If created, max height for a spark
public int Sparking { get; init; } // Probability of a spark each attempt
public bool Reversed { get; init; } // if reversed, we draw from 0 outwards
public bool Mirrored { get; init; } // if mirrored, we split and duplicate the drawing
}
class LedRange
{
public int Index { get; init; }
public int LedStart { get; init; }
public int LedEnd { get; init; }
public int PatternStart { get; init; }
}
Example appsettings.json file.
{
"LightingConfiguration": {
"NumberOfLEDs": 60, //300
"DoFire": false, //true
"HorizontalFire": true, //true
"PatternOffsetPerFrame": 1,
"TargetFps": 5,
"PatternToRepeat": [
"00800000",
"00008000",
"00000080"
],
"LedDefinitions": [
{
"Index": 0,
"LedStart": 1,
"LedEnd": 29,
"PatternStart": 0
},
{
"Index": 1,
"LedStart": 31,
"LedEnd": 58,
"PatternStart": 0
}
],
"Cooling": 30,
"Sparking": 100,
"Sparks": 5,
"SparkHeight": 300,
"Reversed": false,
"Mirrored": false
}
}
$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<PlatformTarget>AnyCPU</PlatformTarget>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
<Platforms>AnyCPU;ARM64</Platforms>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Iot.Device.Bindings" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
</ItemGroup>
</Project>
Publishing properties
Configuration: Debug | ARM64
Target Framework: net7.0
Deployment mode: Self-contained
Target runtime: linux-arm64
Options: Produce single file
Trim unused code
I found a solution, but I am not sure why it is required. I changed the host builder to the below code.
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((bc, s) =>
{
s.Configure<LightingConfiguration>(bc.Configuration.GetSection("LightingConfiguration"));
s.Configure<LedRange>(bc.Configuration.GetSection("LightingConfiguration").GetSection("LedDefinitions"));
s.AddTransient<LightingExecutor>();
});
I'm posting this as an answer in case someone else runs into this.