IronPdf on .NET Framework 4.8 Windows Services - FileNotFoundException / StackOverflowException at Startup (packages.config projects)
Overview
In packages.config-style projects on .NET Framework 4.7 and later, MSBuild skips copying a small set of NuGet "framework facade" assemblies to bin\ because it treats them as redundant with the in-box framework versions — even when <Private>True</Private> is set. The build still succeeds, because it resolves against the framework reference assemblies, but at runtime IronPdf's dependency graph requests the specific NuGet-shipped versions named in the App.config binding redirects, and the .NET 4.8 in-box facades do not satisfy them. In a Windows Service host this surfaces as a FileNotFoundException when constructing ChromePdfRenderer or a StackOverflowException when setting the license key; copying the facade DLLs into the output or migrating to PackageReference resolves it.
Environment
- OS: Windows
- Hosting: Windows Service host (Console and WinForms hosts usually mask the issue via GAC fallback)
- Language/Runtime: .NET Framework 4.7 and later, reproduced on 4.8 —
packages.configproject style
Cause
MSBuild's facade-detection logic prevents these NuGet-supplied assemblies from being copied to bin\: System.ValueTuple, System.Buffers, System.Threading.Tasks.Extensions, System.Text.Json, System.Text.Encodings.Web, and System.Text.Encoding.CodePages. It assumes the in-box .NET 4.8 versions are equivalent, but IronPdf (via Ninject and IronSoftware.Common) requests the exact NuGet versions that the App.config binding redirects point to, and the in-box facades do not satisfy those redirects.
In Console and WinForms hosts the GAC fallback usually hides the mismatch. The Windows Service assembly resolver behaves differently, so the same lookup ends in one of two failures:
FileNotFoundExceptionwhen constructingChromePdfRenderer.StackOverflowExceptionwhen setting the license key — Fusion'sAssemblyResolvehandler recurses on the unsatisfied facade.
Solutions
1. Migrate packages.config to PackageReference — Recommended
This aligns with current .NET Framework tooling guidance and fixes the root cause rather than patching the output.
- In Visual Studio, right-click
packages.configand choose Migrate packages.config to PackageReference…. PackageReference resolves the full transitive dependency graph viaobj\project.assets.jsonand emits per-asset copy-local decisions, so the facade DLLs the runtime needs are shipped tobin\. - Confirm
<AutoGenerateBindingRedirects>is enabled (it already is in this project). It regenerates the redirects against the resolved graph. - Review hand-edited
<Reference>items and any custom imports after migration. The VS migration tool is not always perfect on .NET Framework 4.8 projects and may need a small amount of manual cleanup. - Rebuild and verify the facade DLLs are now present in
bin\.
2. Keep packages.config and copy the facade DLLs in an AfterBuild target
Use this when migrating is not an option. Add an AfterBuild target to the .csproj that copies the facade DLLs directly, bypassing MSBuild's facade detection:
<Target Name="CopyFacadeAssemblies" AfterTargets="Build">
<ItemGroup>
<FacadeAssemblies Include="..\packages\System.Buffers.4.5.1\lib\netstandard2.0\System.Buffers.dll" />
<FacadeAssemblies Include="..\packages\System.Text.Encodings.Web.6.0.1\lib\net461\System.Text.Encodings.Web.dll" />
<FacadeAssemblies Include="..\packages\System.Text.Json.6.0.11\lib\net461\System.Text.Json.dll" />
<FacadeAssemblies Include="..\packages\System.Text.Encoding.CodePages.5.0.0\lib\net461\System.Text.Encoding.CodePages.dll" />
<FacadeAssemblies Include="..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll" />
<FacadeAssemblies Include="..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll" />
</ItemGroup>
<Copy SourceFiles="@(FacadeAssemblies)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>
Adjust the version numbers in the paths to match the package versions restored in your packages\ folder.
Workarounds That Don't Work
- Setting
<Private>True</Private>(Copy Local) on the facade references — MSBuild's facade detection still skips copying them tobin\.