Skip to content

Port Lambda Boss to .NET Framework 4.8 (spec 0007)#199

Merged
jimmytacks merged 5 commits into
mainfrom
issue-198-net-framework-port
May 21, 2026
Merged

Port Lambda Boss to .NET Framework 4.8 (spec 0007)#199
jimmytacks merged 5 commits into
mainfrom
issue-198-net-framework-port

Conversation

@jimmytacks
Copy link
Copy Markdown
Collaborator

Implementation of spec 0007 — .NET Framework 4.8 port.

Closes #198.

What's in here

Two commits:

  1. Port projects from net6.0-windows to net48 — retargets all three csproj files, inlines Taglo.Excel.Common, wires up polyfills (PolySharp, Microsoft.Bcl.HashCode, hand-rolled KeyValuePair.Deconstruct), fixes ~15 small net48 BCL gaps. 900/900 unit tests pass.
  2. Replace installer flow with zip-only distribution — deletes the installer/ directory entirely, adds release/README.txt + release/unblock.cmd + scripts/build-release-bundle.ps1, rewrites scripts/publish-release.ps1 to drop InnoSetup steps.

Notable findings during execution

  • gong-wpf-dragdrop 3.2.* and Ookii.Dialogs.Wpf 5.0.1 already support net48 at the existing version floors. No pinning needed.
  • ExcelDNA on net48 has no deps.json to read, so it can't auto-enumerate managed dependencies for packing. Each runtime dep needs an explicit <Reference Path="..." Pack="true" /> entry in lambda-boss.dna. Captured in CLAUDE.md as a "when adding NuGet dependencies" convention.
  • Microsoft.Bcl.HashCode on net48 calls StringComparer.GetHashCode(null) directly and throws, where .NET 6's built-in HashCode tolerates null. Coalesce nullable strings to "" in any GetHashCode() using StringComparerCellRef.GetHashCode() needed it, and was the entire cause of 158 test failures on first run before the fix.
  • <ImplicitUsings>enable</ImplicitUsings> is silently a no-op on net48 — added explicit <Using Include="…"> items in Directory.Build.props covering the .NET 6 implicit-using set so all 69 files using file-scoped namespaces still compile.

Release artifact

The packed XLL is lambda-boss64-packed.xll at 4.3 MB with all 13 managed deps embedded as Win32 resources. Wrapped in LambdaBoss-<version>.zip (~1.2 MB compressed) with README + unblock.cmd.

Test plan

  • dotnet build addin/lambda-boss.slnx -c Release — 0 errors, 20 pre-existing warnings.
  • dotnet test addin/lambda-boss.Tests/lambda-boss.Tests.csproj -c Release — 900/900 pass.
  • dotnet build addin/lambda-boss.AddinTests/lambda-boss.AddinTests.csproj — 0 errors.
  • scripts/build-release-bundle.ps1 -Version <test> — produces a valid zip with the three expected files.
  • Tim: smoke test the packed XLL in real Excel — double-click load, popup opens, evaluate a LAMBDA, run /Gather and /Edit slash commands. Path: addin/lambda-boss/bin/Release/net48/publish/lambda-boss64-packed.xll.
  • Tim: final acceptance verification on a clean Windows 11 + Office 365 VM with no .NET 6 runtime installed (AddIns folder, arbitrary folder, double-click, USB / Zone.Identifier paths — checklist in #198).

Core retargeting work for spec 0007:

- All three projects target net48
- Inline Taglo.Excel.Common (Logger, NativeMethods, WindowPositioner,
  UpdateChecker) into addin/lambda-boss/Common/ under LambdaBoss.Common
  namespace; drop the PackageReference
- Add PolySharp for compiler-required polyfills (IsExternalInit,
  Index, Range)
- Add Microsoft.Bcl.HashCode for the HashCode struct
- Add System.Text.Json explicit PackageReference (was framework-bundled
  on net6)
- Add System.Net.Http + Microsoft.CSharp framework references (SDK-style
  net48 projects need these explicit)
- Replace IReadOnlySet<T> with ISet<T> (net48 has no IReadOnlySet)
- Add KeyValuePair Deconstruct extension polyfill
- Rewrite 8 ArgumentNullException.ThrowIfNull calls inline (PolySharp
  doesn't polyfill static methods)
- Guard CellRef.GetHashCode against null strings (net6 HashCode tolerates
  null; Microsoft.Bcl.HashCode polyfill does not)
- Replace string.StartsWith(char) with the (string, StringComparison)
  overload
- Polyfill Path.GetRelativePath in LambdaFormatTests via Uri.MakeRelativeUri
- Update Directory.Build.props to add explicit <Using> items for the
  net6 implicit-using set (ImplicitUsings is a no-op on net48)

Build: 0 errors, 900/900 tests passing.

Refs #198
- Flip lambda-boss.dna Pack="true" and add explicit <Reference> entries
  for each runtime dep (net48 has no deps.json so ExcelDNA's pack step
  can't auto-enumerate)
- Make NativeMethods class + extern methods internal (silences 18
  CA1401 warnings now that callers all live in the same assembly)
- Delete installer/ entirely: lambda-boss.iss, bundled-runtime/, logo.ico,
  wizard-*.bmp
- Drop installer paths from .gitignore; add release/output/
- Add release/README.txt and release/unblock.cmd as checked-in source
  files for the zip contents
- Add scripts/build-release-bundle.ps1 — stages signed XLL + README +
  unblock.cmd into LambdaBoss-<version>.zip
- Rewrite scripts/publish-release.ps1: drop InnoSetup compile and
  installer signing steps, drop bundled-runtime preflight, call the
  new bundle script, attach the zip to the GitHub Release
- Update CLAUDE.md: tech stack to net48, polyfill notes, NuGet
  dependency convention now points at lambda-boss.dna, publishing
  section reflects zip-only output

Packed XLL size: 4.3 MB (64-bit) with all 13 managed deps embedded.
Build: 0 errors, 20 warnings (all pre-existing); tests: 900/900 pass.

Refs #198
- Bump setup-dotnet to 8.0.x SDK (any modern SDK can build net48; net6
  is no longer relevant)
- Drop the GitHub Packages authentication step and the
  `permissions: packages: read` block — Taglo.Excel.Common was the only
  GH-Packages dependency and is now inlined
- Drop the TagloGit feed from addin/nuget.config for the same reason
After inlining Taglo.Excel.Common into lambda-boss.dll, ExcelDNA's
auto-registration started scanning Logger, UpdateChecker, and
WindowPositioner public static methods as candidate Excel UDFs. Logger
and UpdateChecker both expose `Initialize(...)`, which triggered:

  Registration [Error] Repeated function name: 'Initialize' -
  previous registration will be overwritten.

Also silently registered Logger.Info, Logger.Error, UpdateChecker.ParseVersion
and WindowPositioner.CenterOnExcel as Excel functions, which we never wanted.

On net6 these methods lived in a separate Taglo.Excel.Common.dll referenced
as a dependency (not as an ExternalLibrary), so ExcelDNA didn't scan them.

Marking the three helper classes `internal static` keeps the registration
scan out — ExcelDNA only picks up public static methods in public classes.
NativeMethods is already internal.
CI failed on .NET SDK 8.0.x with:
  error CS1501: No overload for method 'Contains' takes 2 arguments

The `string.Contains(string, StringComparison)` overload is from
netstandard 2.1, not net48. Local builds (running .NET SDK 10) accepted
it because that SDK's net48 reference assemblies are looser. .NET SDK 8
on CI uses stricter net48 reference assemblies.

Switch the two call sites in LambdaPopup.xaml.cs to
`IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0`, which is
universally available.
@jimmytacks jimmytacks merged commit f51fb44 into main May 21, 2026
1 check passed
@jimmytacks jimmytacks deleted the issue-198-net-framework-port branch May 21, 2026 13:52
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.

Port Lambda Boss to .NET Framework 4.8 (spec 0007)

1 participant