Skip to content

Migrate to .NET 10 with NativeAOT - #219

Merged
einsteinx2 merged 10 commits into
masterfrom
net10-migration
Jul 26, 2026
Merged

Migrate to .NET 10 with NativeAOT#219
einsteinx2 merged 10 commits into
masterfrom
net10-migration

Conversation

@einsteinx2

Copy link
Copy Markdown
Owner

Summary

Migrates WaveBox from .NET Framework 4.5 / Mono to .NET 10 with NativeAOT publishing for all six platform targets (win-x64/arm64, osx-x64/arm64, linux-x64/arm64). The published binary is fully self-contained — no .NET runtime install needed (~27 MB on osx-arm64, ~48 MB RSS vs ~110 MB on CoreCLR).

Major changes, in commit order

  • Dead code removal — Bass audio (unused since 2013), the native WaveBoxFSEvents dylib/Xcode project (modern .NET's FileSystemWatcher is FSEvents-backed on macOS), OWIN/SignalR/MvvmCross DLLs, ImageMagick interop
  • SDK-style projectsnet10.0, NuGet packages replacing all checked-in DLLs
  • DI — Ninject → Microsoft.Extensions.DependencyInjection (static locator shim keeps 300+ call sites unchanged)
  • sqlite-net — vendored ORM kept and modernized; natives via SQLitePCLRaw (e_sqlite3), database backup rewritten against sqlite3_backup_*
  • HostingServiceBase/Mono.Posix/Thread.Abort → .NET Generic Host (Windows service + systemd support built in)
  • HTTP — hand-rolled TcpListener server → Kestrel, with an adapter layer that preserves the exact legacy wire protocol (HTTP-200-with-error-JSON envelope, raw undecoded query params, sliding session cookie, PUT/DELETE action overrides)
  • JSON — Newtonsoft → System.Text.Json source generation (AOT-compatible)
  • Media — art resizing on SixLabors.ImageSharp, ffmpeg transcoder flags modernized (-b:a, built-in aac), file watching consolidated onto hardened FileSystemWatcher
  • Logging — log4net-shaped shim over Microsoft.Extensions.Logging
  • AOT + CIModelTypeRegistry roots ORM-mapped types (zero IL warnings), end-to-end smoke test with a generated MP3 fixture, GitHub Actions matrix building all six RIDs

Intentional wire deltas vs. the old server

  • No UTF-8 BOM in text responses (the old +3 Content-Length was the BOM)
  • Keep-alive instead of Connection: close; gzip via middleware

Known follow-ups

  • Rolling-file logging (old wavebox_log.txt) not yet reimplemented — console only
  • Transcoding runtime-untested locally (no ffmpeg on the dev machine); aac encoder switched from libfaac to ffmpeg's built-in aac
  • Web-client submodule still points at a broken path

Test plan

  • Local: Scripts/smoke-test.sh against the osx-arm64 AOT binary — login/session auth, status, albums, media scan + TagLib parse of generated MP3, 206 range request all pass
  • NativeAOT publish with zero IL2xxx/IL3050 warnings
  • CI matrix on this PR: publish all 6 RIDs, smoke-test osx-arm64, linux-x64, linux-arm64

🤖 Generated with Claude Code

https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT

einsteinx2 and others added 10 commits July 25, 2026 23:58
- Bass.Net and the BASS native libraries were unused since the jukebox
  service was deleted in 2013; all audio work goes through external ffmpeg
- The WaveBoxFSEvents Xcode project and dylib existed to work around
  Mono's kevent-based FileSystemWatcher exhausting file descriptors on
  macOS; modern .NET's FileSystemWatcher is FSEvents-backed natively
- The OWIN/SignalR/MvvmCross/System.Windows assemblies were an abandoned
  experiment with zero code references
- ImageMagick interop and its x86-only native builds are replaced by
  ImageSharp in a later commit
- Drop the stale Mono mkbundle packaging scripts, nested solutions, and
  the System.Configuration.Install-based Windows service installer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- Replace the VS2010-era csproj files with SDK-style projects targeting
  net10.0; WaveBox.Server uses the Web SDK with NativeAOT publishing
  enabled for win/osx/linux on x64 and arm64
- All checked-in assemblies in lib/ are replaced by NuGet packages:
  TagLibSharp, Mono.Nat 3.x, SQLitePCLRaw.lib.e_sqlite3, ImageSharp,
  Microsoft.Extensions.*
- Delete the Mono-only <dllmap> config files; native library resolution
  now comes from per-RID NuGet runtime assets
- Delete AssemblyInfo files (the SDK generates assembly metadata, and a
  BuildDate assembly attribute replaces the old PE-header timestamp hack)
- Regenerate the solution in the new slnx format

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- Injection becomes a static locator over IServiceProvider; the 300+
  Injection.Kernel.Get<T>() call sites become Injection.Get<T>() (the
  mechanical rewrite rides along with each file's other changes)
- CoreModule/ServerModule become AddWaveBoxCore()/AddWaveBoxServer()
  IServiceCollection extensions with the same explicit singletons
- The platform-conditional web clients (curl-shelling LinuxWebClient and
  WebClient-based TimedWebClient, both Mono-era workarounds) collapse
  into one HttpClient-backed implementation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- Rename DllImport("sqlite3") to "e_sqlite3" so the native library ships
  per-RID from SQLitePCLRaw.lib.e_sqlite3 (the Mono dllmap is gone)
- Rewrite DatabaseBackup against the vendored SQLiteConnection's public
  Handle, deleting the System.Data.SQLite dependency and its
  private-field reflection into the connection internals; check the
  sqlite3_backup_step/finish results
- Replace the Newtonsoft query-log serialization with a hand-rolled
  Utf8JsonWriter scalar-array writer producing identical output
- Add trim-warning suppressions justified by ModelTypeRegistry, which
  preserves all ORM-mapped types' reflection metadata under AOT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- WaveBoxService (ServiceBase dual console/service mode, Kernel32
  SetConsoleCtrlHandler, Mono.Posix signal thread) is replaced by
  WaveBoxLifecycleService, an IHostedService; Windows service and
  systemd support come from the hosting packages
- Static server metadata moves to ServerInfo; build date now comes from
  an assembly attribute instead of reading the PE header (which breaks
  under deterministic builds)
- DetectOS drops its uname P/Invoke for OperatingSystem.IsX(); RootPath
  uses UserProfile because SpecialFolder.Personal means ~/Documents on
  modern .NET, which would have abandoned existing installs' config
- DelayedOperationQueue's Thread.Abort (PlatformNotSupportedException on
  modern .NET) becomes cooperative CancellationTokenSource shutdown
- NatService ports to Mono.Nat 3.x's async API; ZeroConfService becomes
  a stub (Mono.Zeroconf died with Mono) that still accepts the conf name
- Status CPU metric samples Process.TotalProcessorTime instead of the
  Windows-only PerformanceCounter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- The TcpListener-based HttpService and vendored HttpProcessor parser
  are deleted; Program.cs hosts a WebApplication with a single terminal
  handler covering both /api dispatch and the web UI
- ApiDispatcher reproduces the legacy dispatch flow exactly: raw
  undecoded parameter parsing, POST body appended to the query string,
  session-cookie auth with sliding refresh, and HTTP 200 + error JSON
  for all failure modes
- HttpContextProcessor implements the legacy IHttpProcessor contract
  over HttpContext, so all 26 API handlers run unchanged, including the
  transcode file-tailing stream path
- Response compression middleware replaces the hand-rolled gzip; wire
  deltas vs the old server: correct Content-Length (no more +3-byte
  UTF-8 BOM), HTTP keep-alive, spec-compliant range handling
- ServiceFactory and ApiHandlerFactory use explicit registration lists
  instead of Assembly.GetTypes() scans (required for NativeAOT); the
  listen port is pre-read from wavebox.conf because Kestrel's options
  are constructed before the database-dependent settings load

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
…-gen

- [JsonProperty] becomes [JsonPropertyName] across all response DTOs and
  models; every serialized type is registered in the source-generated
  WaveBoxJsonContext (required for NativeAOT), including the runtime
  types placed in object-valued status/stats dictionaries
- IItem/IMediaItem get [JsonPolymorphic]/[JsonDerivedType] (without a
  discriminator) so interface-typed members keep serializing the runtime
  type's full shape, matching Newtonsoft's behavior
- ServerSettings drops its hand-rolled JSON comment stripper: STJ reads
  wavebox.conf's comments and trailing commas natively, so the file
  format is unchanged; the settings API's partial-update write-back and
  the Last.fm response handling move from AOT-fatal `dynamic` to JsonNode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- Art resizing moves to ImageSharp (pure managed, AOT-safe): Lanczos3
  aspect-fit resize plus optional Gaussian blur, always JPEG output,
  replacing the System.Drawing/ImageMagick dual path
- Transcoders drain ffmpeg's stderr asynchronously (pipe-buffer deadlock
  risk), kill the entire process tree on cancel, and use current ffmpeg
  flags: -b:a/-q:a/-c:a, the built-in aac encoder instead of the removed
  libfaac, and aresample=async=1 instead of the deprecated -async; the
  transcode service probes for ffmpeg at startup with per-OS install hints
- One cross-platform FileManager (FSEvents-backed on macOS via the
  built-in watcher): watchers are strongly referenced, use the maximum
  buffer size, recover from overflow via the Error event with a rescan,
  and renames also rescan the old parent directory

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- A ~90-line shim (WaveBox.Core.Logging.ILog/LogManager) keeps every
  logger call site's shape while writing through the host's logging
  pipeline; log4net 3.x itself was not AOT-safe (XML-configured
  reflection-instantiated appenders), and MethodBase.GetCurrentMethod()
  logger naming is unreliable under NativeAOT, so categories come from
  [CallerFilePath] or an explicit type
- The remaining files in this commit carry the mechanical sweeps: logger
  declaration rewrites, Injection.Kernel.Get<T>() -> Injection.Get<T>(),
  and removal of dead Ninject/Newtonsoft/log4net usings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
- ModelTypeRegistry roots every ORM-mapped model type so the vendored
  sqlite-net's reflection-based mapping keeps full metadata under
  trimming/NativeAOT; new mapped types must be added there
- Enum.GetValues(Type) becomes the AOT-friendly generic overload
- Scripts/smoke-test.sh runs the server against an isolated data
  directory and verifies login, status, a media scan of a generated MP3
  fixture (tests/fixtures/make_fixture.py, no encoder needed), TagLib
  metadata parsing, and a 206 range request
- GitHub Actions builds NativeAOT binaries for win/osx/linux x64+arm64
  (AOT cannot cross-compile between OSes, hence the four runner types)
  and smoke-tests the three natively-executable ones

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjfMF1zrb6Z7KzexmTNjXT
@einsteinx2
einsteinx2 merged commit b023ec2 into master Jul 26, 2026
6 checks passed
@einsteinx2
einsteinx2 deleted the net10-migration branch July 26, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant