Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

- Working on updates to replace PlatyPS documentation creation with the new Microsoft.PowerShell.PlatyPS module. (PRs and other help would be welcomed!)
- CI: Release-and-Publish now auto-triggers only on changes to the published module bundle (`src/DLLPickle/**`, `src/DLLPickle.Build/DLLPickle.csproj`, `src/DLLPickle.Build/packages.lock.json`). CI-, policy-, docs-, test-, and tooling-only changes no longer publish a new PowerShell Gallery version; release a genuine packaging-logic change via a manual `workflow_dispatch` run.

## [2.2.0] - 2026-06-02

> Adds proactive detection of the Az.Storage + ExchangeOnlineManagement OData incompatibility (#174) and a public conflict check, plus release-pipeline and CI hardening. The bundled assemblies are **unchanged** from 2.1.2.

### Added

- **`Test-DPLibraryConflict`** — a public cmdlet that reports known-incompatible module combinations loaded in the current session (with the reason, the separate-process workaround, and the issue link) and returns the active conflicts.
- **`Import-DPLibrary` now warns** when a known-conflicting module pair is — or later becomes — co-loaded: immediately if both are already imported, otherwise via a best-effort, idempotent, self-unregistering `AssemblyLoad` handler that fires only once the whole pair is co-loaded (armed only when both modules are installed). Advisory: it never throws and is skipped under Constrained Language Mode.
- Data-driven **`knownConflicts`** in `build/dependency-policy.json`, shipped into the module as `KnownConflicts.json` (adding a future conflict is a data edit, not a code change).
- Runtime ALC-ownership probe tooling under `tools/` (`Get-DLLPickleLoadedTrackedAssembly.ps1`; `Get-DLLPickleRuntimeAssemblySnapshot` now sources its filter from `trackedAssemblies`, so it captures the OData stack) used to adjudicate #174.

### Changed

- **Release-and-Publish auto-triggers only on changes to the published bundle** (`src/DLLPickle/**`, `src/DLLPickle.Build/DLLPickle.csproj`, `src/DLLPickle.Build/packages.lock.json`). CI-, policy-, docs-, test-, and tooling-only changes no longer publish a new PowerShell Gallery version; release a genuine packaging-logic change via a manual `workflow_dispatch` run.
- The Upstream-Compatibility gate workflows now **always report** (a lightweight change-detection job skips the heavy work on non-bundle PRs), so they can be configured as **required status checks** without deadlocking docs-/CI-only PRs; the matrix build's required-check target is the always-present aggregate **`Build gate`**.

### Documentation

- Documented the **Az.Storage + ExchangeOnlineManagement known limitation** (#174): the two modules bundle incompatible strong-named `Microsoft.OData.Core` versions (7.6.4 vs 7.22.0) into the default `AssemblyLoadContext` and **cannot share one process** in either import order. DLLPickle cannot resolve this by preloading (preloading either version breaks the other), so the OData stack stays `block` — now runtime-confirmed. Use the two modules in separate PowerShell processes; #174 remains open as an upstream incompatibility.
- Synced `docs/Architecture.md` (ALC self-isolation model, preload/block taxonomy, the required-checks model, and the multi-TFM / Windows PowerShell 5.1 notes).

## [2.1.2] - 2026-06-01

Expand Down Expand Up @@ -304,7 +324,8 @@ Full Changelog: [v0.2.5...v0.2.6](https://github.com/SamErde/DLLPickle/compare/v

- Initial release.

[Unreleased]: https://github.com/SamErde/DLLPickle/compare/v2.1.2...HEAD
[Unreleased]: https://github.com/SamErde/DLLPickle/compare/v2.2.0...HEAD
[2.2.0]: https://github.com/SamErde/DLLPickle/tag/v2.2.0
[2.1.2]: https://github.com/SamErde/DLLPickle/tag/v2.1.2
[2.1.1]: https://github.com/SamErde/DLLPickle/tag/v2.1.1
[2.1.0]: https://github.com/SamErde/DLLPickle/tag/v2.1.0
Expand Down
10 changes: 10 additions & 0 deletions build/DLLPickle.Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,16 @@ Add-BuildTask CopyModuleFiles -After RestoreDependencies -Before Build {
} #CopyModuleFiles


# Synopsis: Ship the policy's knownConflicts list into the module for the runtime conflict warning
Add-BuildTask ExportKnownConflicts -After CopyModuleFiles {
Write-Build Gray ' Exporting knownConflicts to the module output...'
$PolicyPath = Join-Path -Path $script:ProjectRoot -ChildPath 'build/dependency-policy.json'
Comment thread
SamErde marked this conversation as resolved.
$OutputPath = Join-Path -Path $script:ModuleOutputPath -ChildPath 'KnownConflicts.json'
Comment thread
SamErde marked this conversation as resolved.
& (Join-Path -Path $PSScriptRoot -ChildPath 'Export-DLLPickleKnownConflicts.ps1') -PolicyPath $PolicyPath -OutputPath $OutputPath
Write-Build Gray ' ...knownConflicts exported.'
}


# Synopsis: Copies module assets to Artifacts folder
Add-BuildTask AssetCopy -After CopyModuleFiles -Before Build {
Write-Build Gray ' Copying assets to Artifacts...'
Expand Down
31 changes: 31 additions & 0 deletions build/Export-DLLPickleKnownConflicts.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<#
.SYNOPSIS
Extracts the knownConflicts array from the dependency policy into a standalone JSON file shipped
with the module, so the runtime conflict-warning can read it (the full policy is not shipped).
.PARAMETER PolicyPath
Path to dependency-policy.json.
.PARAMETER OutputPath
Path to write the extracted knownConflicts JSON (an array).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$PolicyPath,

[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string]$OutputPath
)

$ErrorActionPreference = 'Stop'

$Policy = Get-Content -LiteralPath $PolicyPath -Raw | ConvertFrom-Json
$Conflicts = @($Policy.knownConflicts)

$OutputDirectory = Split-Path -Path $OutputPath -Parent
if ($OutputDirectory -and -not (Test-Path -LiteralPath $OutputDirectory -PathType Container)) {
$null = New-Item -Path $OutputDirectory -ItemType Directory -Force
}

ConvertTo-Json -InputObject $Conflicts -Depth 20 | Set-Content -LiteralPath $OutputPath -Encoding utf8NoBOM
34 changes: 31 additions & 3 deletions build/dependency-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@
]
]
},
"knownConflicts": [
{
"id": "174-odata-azstorage-exo",
"modules": [ "Az.Storage", "ExchangeOnlineManagement" ],
"assembly": "Microsoft.OData.Core",
"issue": "174",
"reason": "Az.Storage force-loads Microsoft.OData.Core 7.6.4 at import; ExchangeOnlineManagement's Get-EXO* cmdlets require 7.22.0. Both target the default ALC and are strong-named, so the two versions cannot coexist in one process - both import orders fail.",
"workaround": "Use Az.Storage and ExchangeOnlineManagement (Get-EXO* cmdlets) in separate PowerShell processes - for example a background job (Start-Job) or a second pwsh. A separate runspace in the same process does NOT help: the conflict is process-wide (one default AssemblyLoadContext per process).",
"evidence": {
"versions": { "Az.Storage": "7.6.4", "ExchangeOnlineManagement": "7.22.0" },
"alc": "Default",
"runtimeProbe": "2026-06-01 scenarios 1-4: both load OData into the Default ALC; Az.Storage-first -> EXO REF_DEF_MISMATCH (0x80131040); EXO-first -> Az.Storage 'same name already loaded'.",
"decidedOn": "2026-06-01"
}
}
],
"baseline": {
"capturedOn": "2026-05-31",
"moduleVersions": {
Expand Down Expand Up @@ -362,32 +378,44 @@
{
"packageName": "Microsoft.OData.Core",
"assemblyName": "Microsoft.OData.Core",
"classification": "block",
"sourceModules": [
"ExchangeOnlineManagement",
"Az.Storage"
],
"updateMode": "reportOnly",
"reason": "Issue #174 showed that default OData preloading can break Az.Storage when ExchangeOnlineManagement and Az.Storage require incompatible OData identities."
"reason": "Issue #174 showed that default OData preloading can break Az.Storage when ExchangeOnlineManagement and Az.Storage require incompatible OData identities.",
"evidence": {
"basis": "Az.Storage 9.6.1 ships Microsoft.OData.Core 7.6.4; ExchangeOnlineManagement 3.9.2 requires 7.22.0. Both load into the default ALC; no preload version can satisfy both. See knownConflicts 174-odata-azstorage-exo for the runtime adjudication."
}
},
{
"packageName": "Microsoft.OData.Edm",
"assemblyName": "Microsoft.OData.Edm",
"classification": "block",
"sourceModules": [
"ExchangeOnlineManagement",
"Az.Storage"
],
"updateMode": "reportOnly",
"reason": "Keep the OData family out of the default preload set unless a future isolation strategy is implemented."
"reason": "Keep the OData family out of the default preload set unless a future isolation strategy is implemented.",
"evidence": {
"basis": "OData family dependency paired with Microsoft.OData.Core; same default-ALC conflict as Core. See knownConflicts 174-odata-azstorage-exo for the runtime adjudication."
}
},
{
"packageName": "Microsoft.Spatial",
"assemblyName": "Microsoft.Spatial",
"classification": "block",
"sourceModules": [
"ExchangeOnlineManagement",
"Az.Storage"
],
"updateMode": "reportOnly",
"reason": "Keep the OData family out of the default preload set unless a future isolation strategy is implemented."
"reason": "Keep the OData family out of the default preload set unless a future isolation strategy is implemented.",
"evidence": {
"basis": "OData family dependency paired with Microsoft.OData.Core; same default-ALC conflict as Core. See knownConflicts 174-odata-azstorage-exo for the runtime adjudication."
}
}
]
}
20 changes: 20 additions & 0 deletions docs/Deep-Dive.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ exclusion for environment-specific troubleshooting.
- [Module reference](DLLPickle.md)
- [Dependency policy and compatibility notes](../DEPENDENCIES.md)

## Known limitation: Az.Storage + ExchangeOnlineManagement (issue #174)

`Az.Storage` and `ExchangeOnlineManagement` bundle **incompatible, strong-named versions of
`Microsoft.OData.Core`** (7.6.4 and 7.22.0 respectively) and both load it into the default
`AssemblyLoadContext`. Only one version can exist per process, and **neither import order works**:

- Import `Az.Storage` first, then run `Get-EXO*` → fails (`Could not load … Microsoft.OData.Core,
Version=7.22.0.0 … manifest definition does not match`).
- Import `ExchangeOnlineManagement`/`Connect-ExchangeOnline` first, then import `Az.Storage` → fails
(`Microsoft.OData.Core, Version=7.6.4.0 … assembly with same name is already loaded`).

This is an upstream incompatibility between the two modules; **DLLPickle cannot fix it by preloading**
(preloading either version breaks the other module), which is why the OData assemblies are
classified `block`. DLLPickle warns when it detects both modules loaded (see `Test-DPLibraryConflict`).

**Workaround:** use the two modules in **separate PowerShell processes** — for example, run `Get-EXO*`
work in one `pwsh` (or a `Start-Job` background job) and `Az.Storage` work in another. A separate
**runspace** in the *same* process does **not** help: the conflict is process-wide (one default
`AssemblyLoadContext` per process).

## Audio Discussion

[![Listen](https://raw.githubusercontent.com/SamErde/DLLPickle/main/assets/DLL_Pickle__A_Clever_Fix.png)](https://raw.githubusercontent.com/SamErde/DLLPickle/main/assets/DLL_Pickle__Interactive_Deep_Dive_audio.mp4)
Expand Down
Loading
Loading