Skip to content

Commit 53a7116

Browse files
Add param block to support conditional DLL loading (#34)
1 parent b7f8dae commit 53a7116

File tree

5 files changed

+343
-31
lines changed

5 files changed

+343
-31
lines changed

.github/workflows/build.yml

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,71 @@ jobs:
109109
Write-Host "All critical paths verified successfully!" -ForegroundColor Green
110110
}
111111
112-
- name: Test module import
112+
- name: Test module import (PowerShell Core)
113113
shell: pwsh
114114
run: |
115115
Import-Module ./artifacts/dbatools.library/dbatools.library.psd1 -Force
116116
$module = Get-Module dbatools.library
117117
Write-Host "Loaded dbatools.library version: $($module.Version)"
118118
Write-Host "Module path: $($module.Path)"
119119
120+
- name: Test AvoidConflicts with SqlServer module (Windows PowerShell 5.1)
121+
shell: powershell
122+
run: |
123+
Write-Host "=== Testing AvoidConflicts with SqlServer module on Windows PowerShell 5.1 ===" -ForegroundColor Cyan
124+
Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Yellow
125+
Write-Host "PowerShell Edition: $($PSVersionTable.PSEdition)" -ForegroundColor Yellow
126+
127+
# Install SqlServer module
128+
Write-Host "`nInstalling SqlServer module..." -ForegroundColor Cyan
129+
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
130+
Install-Module SqlServer -Force -AllowClobber -Scope CurrentUser
131+
Write-Host "SqlServer module installed successfully" -ForegroundColor Green
132+
133+
# Import SqlServer module first
134+
Write-Host "`nImporting SqlServer module..." -ForegroundColor Cyan
135+
Import-Module SqlServer -Force
136+
$sqlServerModule = Get-Module SqlServer
137+
Write-Host "SqlServer module version: $($sqlServerModule.Version)" -ForegroundColor Green
138+
139+
# Check what assemblies are loaded after SqlServer import
140+
$preLoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() |
141+
Where-Object { $_.GetName().Name -match 'Microsoft\.(Data\.)?SqlClient|Microsoft\.SqlServer' } |
142+
ForEach-Object { $_.GetName().Name }
143+
Write-Host "`nAssemblies loaded by SqlServer module:" -ForegroundColor Yellow
144+
$preLoadedAssemblies | ForEach-Object { Write-Host " - $_" }
145+
146+
# Now import dbatools.library with -AvoidConflicts
147+
Write-Host "`nImporting dbatools.library with -AvoidConflicts..." -ForegroundColor Cyan
148+
Import-Module ./artifacts/dbatools.library/dbatools.library.psd1 -ArgumentList $true -Force -Verbose
149+
150+
$dbatoolsLibrary = Get-Module dbatools.library
151+
Write-Host "`ndbatools.library version: $($dbatoolsLibrary.Version)" -ForegroundColor Green
152+
153+
# Verify both modules are loaded
154+
$loadedModules = Get-Module | Where-Object { $_.Name -in 'SqlServer', 'dbatools.library' }
155+
Write-Host "`nLoaded modules:" -ForegroundColor Yellow
156+
$loadedModules | ForEach-Object { Write-Host " - $($_.Name) v$($_.Version)" }
157+
158+
if ($loadedModules.Count -eq 2) {
159+
Write-Host "`n✅ SUCCESS: Both SqlServer and dbatools.library modules loaded successfully!" -ForegroundColor Green
160+
} else {
161+
Write-Host "`n❌ FAILURE: Expected 2 modules, got $($loadedModules.Count)" -ForegroundColor Red
162+
exit 1
163+
}
164+
165+
# Test that we can use types from both modules
166+
Write-Host "`nTesting assembly access..." -ForegroundColor Cyan
167+
try {
168+
$sqlClientType = [Microsoft.Data.SqlClient.SqlConnection]
169+
Write-Host "✅ Microsoft.Data.SqlClient.SqlConnection type accessible" -ForegroundColor Green
170+
} catch {
171+
Write-Host "❌ Failed to access SqlConnection type: $_" -ForegroundColor Red
172+
exit 1
173+
}
174+
175+
Write-Host "`n=== AvoidConflicts test completed successfully ===" -ForegroundColor Green
176+
120177
- name: Upload artifacts
121178
uses: actions/upload-artifact@v4
122179
with:

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,48 @@ Install-Module dbatools.library -Scope AllUsers
5454
Install-Module dbatools.library -Scope CurrentUser
5555
```
5656

57+
## Handling DLL Conflicts with SqlServer Module
58+
59+
If you need to use both the SqlServer module and dbatools.library in the same session, you may encounter DLL version conflicts. dbatools.library provides the `-AvoidConflicts` parameter to automatically skip loading assemblies that are already loaded by another module.
60+
61+
### Usage
62+
63+
Import the SqlServer module first, then import dbatools.library with `-AvoidConflicts`:
64+
65+
```powershell
66+
# Import SqlServer module first
67+
Import-Module SqlServer
68+
69+
# Then import dbatools.library with -AvoidConflicts
70+
Import-Module dbatools.library -ArgumentList $true
71+
```
72+
73+
When `-AvoidConflicts` is enabled, dbatools.library will:
74+
- Check if each assembly is already loaded in the current session
75+
- Skip loading any assemblies that are already present
76+
- Load only the assemblies that are missing
77+
78+
### Examples
79+
80+
**Basic usage with SqlServer module:**
81+
```powershell
82+
Import-Module SqlServer
83+
Import-Module dbatools.library -ArgumentList $true
84+
```
85+
86+
**See what's being skipped with -Verbose:**
87+
```powershell
88+
Import-Module SqlServer
89+
Import-Module dbatools.library -ArgumentList $true -Verbose
90+
# Output will show: "Skipping Microsoft.Data.SqlClient.dll - already loaded" etc.
91+
```
92+
93+
**Without AvoidConflicts (default behavior):**
94+
```powershell
95+
# Loads all assemblies, may cause conflicts if SqlServer module is already loaded
96+
Import-Module dbatools.library
97+
```
98+
5799
### ⚠️ Important: PowerShell Core + Credentials Issue
58100

59101
**If you plan to use SQL Server credentials with PowerShell Core (pwsh), you MUST install to AllUsers scope or grant appropriate permissions.**

dbatools.library.psm1

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
param(
2+
# Automatically skip loading assemblies that are already loaded (useful when SqlServer module is already imported)
3+
[Alias('SkipLoadedAssemblies', 'AllowSharedAssemblies')]
4+
[switch]$AvoidConflicts
5+
)
6+
17
function Get-DbatoolsLibraryPath {
28
[CmdletBinding()]
39
param()
@@ -54,29 +60,42 @@ if ($PSVersionTable.PSEdition -ne "Core") {
5460
"Microsoft.SqlServer.Rmo",
5561
"System.Private.CoreLib",
5662
"Azure.Core",
57-
"Azure.Identity"
63+
"Azure.Identity",
64+
"Microsoft.Data.Tools.Utilities",
65+
"Microsoft.Data.Tools.Schema.Sql",
66+
"Microsoft.SqlServer.TransactSql.ScriptDom"
5867
};
5968
60-
var name = new AssemblyName(e.Name);
61-
var assemblyName = name.Name.ToString();
69+
var requestedName = new AssemblyName(e.Name);
70+
var assemblyName = requestedName.Name;
71+
72+
// First, check if any version of this assembly is already loaded
73+
// This handles version mismatches (e.g., SMO requesting SqlClient 5.0.0.0 when 6.0.2 is loaded)
74+
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
75+
{
76+
try
77+
{
78+
if (assembly.GetName().Name == assemblyName)
79+
{
80+
return assembly;
81+
}
82+
}
83+
catch
84+
{
85+
// Some assemblies may throw when accessing GetName()
86+
}
87+
}
88+
89+
// Only load from disk if not already loaded and it's in our list
6290
foreach (string dll in dlls)
6391
{
6492
if (assemblyName == dll)
6593
{
6694
string filelocation = "$dir" + dll + ".dll";
67-
//Console.WriteLine(filelocation);
6895
return Assembly.LoadFrom(filelocation);
6996
}
7097
}
7198
72-
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
73-
{
74-
// maybe this needs to change?
75-
var info = assembly.GetName();
76-
if (info.FullName == e.Name) {
77-
return assembly;
78-
}
79-
}
8099
return null;
81100
}
82101
}
@@ -96,14 +115,24 @@ if ($PSVersionTable.PSEdition -ne "Core") {
96115
# REMOVED win-sqlclient logic - SqlClient is now directly in lib
97116
$sqlclient = [System.IO.Path]::Combine($script:libraryroot, "lib", "Microsoft.Data.SqlClient.dll")
98117

99-
try {
100-
Import-Module $sqlclient
101-
} catch {
102-
throw "Couldn't import $sqlclient | $PSItem"
118+
# Get loaded assemblies once for reuse (used for AvoidConflicts checks and later assembly loading)
119+
$script:loadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies()
120+
121+
# Check if SqlClient is already loaded when AvoidConflicts is set
122+
$skipSqlClient = $false
123+
if ($AvoidConflicts) {
124+
$skipSqlClient = $script:loadedAssemblies | Where-Object { $_.GetName().Name -eq 'Microsoft.Data.SqlClient' }
125+
if ($skipSqlClient) {
126+
Write-Verbose "Skipping Microsoft.Data.SqlClient.dll - already loaded"
127+
}
103128
}
104129

105-
if ($PSVersionTable.PSEdition -ne "Core") {
106-
[System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolveEventHandler)
130+
if (-not $skipSqlClient) {
131+
try {
132+
Import-Module $sqlclient
133+
} catch {
134+
throw "Couldn't import $sqlclient | $PSItem"
135+
}
107136
}
108137

109138
if ($PSVersionTable.PSEdition -eq "Core") {
@@ -114,6 +143,9 @@ if ($PSVersionTable.PSEdition -eq "Core") {
114143
'Microsoft.IdentityModel.Abstractions',
115144
'Microsoft.SqlServer.Dac',
116145
'Microsoft.SqlServer.Dac.Extensions',
146+
'Microsoft.Data.Tools.Utilities',
147+
'Microsoft.Data.Tools.Schema.Sql',
148+
'Microsoft.SqlServer.TransactSql.ScriptDom',
117149
'Microsoft.SqlServer.Smo',
118150
'Microsoft.SqlServer.SmoExtended',
119151
'Microsoft.SqlServer.SqlWmiManagement',
@@ -129,9 +161,11 @@ if ($PSVersionTable.PSEdition -eq "Core") {
129161
'Azure.Core',
130162
'Azure.Identity',
131163
'Microsoft.IdentityModel.Abstractions',
132-
'Microsoft.Data.SqlClient',
133164
'Microsoft.SqlServer.Dac',
134165
'Microsoft.SqlServer.Dac.Extensions',
166+
'Microsoft.Data.Tools.Utilities',
167+
'Microsoft.Data.Tools.Schema.Sql',
168+
'Microsoft.SqlServer.TransactSql.ScriptDom',
135169
'Microsoft.SqlServer.Smo',
136170
'Microsoft.SqlServer.SmoExtended',
137171
'Microsoft.SqlServer.SqlWmiManagement',
@@ -159,8 +193,8 @@ if ($PSVersionTable.OS -match "ARM64") {
159193
}
160194
#endregion Names
161195

162-
# this takes 10ms
163-
$assemblies = [System.AppDomain]::CurrentDomain.GetAssemblies()
196+
# Build string of loaded assembly names once for efficient checking
197+
$script:loadedAssemblyNames = $script:loadedAssemblies.FullName | Out-String
164198

165199
try {
166200
$null = Import-Module ([IO.Path]::Combine($script:libraryroot, "third-party", "bogus", "Bogus.dll"))
@@ -169,22 +203,34 @@ try {
169203
}
170204

171205
foreach ($name in $names) {
172-
# REMOVED win-sqlclient handling and mac-specific logic since files are in standard lib folder
173-
174206
$x64only = 'Microsoft.SqlServer.Replication', 'Microsoft.SqlServer.XEvent.Linq', 'Microsoft.SqlServer.BatchParser', 'Microsoft.SqlServer.Rmo', 'Microsoft.SqlServer.BatchParserClient'
175207

176208
if ($name -in $x64only -and $env:PROCESSOR_ARCHITECTURE -eq "x86") {
177209
Write-Verbose -Message "Skipping $name. x86 not supported for this library."
178210
continue
179211
}
180212

181-
$assemblyPath = [IO.Path]::Combine($script:libraryroot, "lib", "$name.dll")
182-
$assemblyfullname = $assemblies.FullName | Out-String
183-
if (-not ($assemblyfullname.Contains("$name,"))) {
184-
$null = try {
185-
$null = Import-Module $assemblyPath
186-
} catch {
187-
Write-Error "Could not import $assemblyPath : $($_ | Out-String)"
213+
# Check if assembly is already loaded (always check to avoid duplicate loads)
214+
if ($script:loadedAssemblyNames.Contains("$name,")) {
215+
if ($AvoidConflicts) {
216+
Write-Verbose "Skipping $name.dll - already loaded"
188217
}
218+
continue
219+
}
220+
221+
# Load the assembly
222+
$assemblyPath = [IO.Path]::Combine($script:libraryroot, "lib", "$name.dll")
223+
try {
224+
$null = Import-Module $assemblyPath
225+
} catch {
226+
Write-Error "Could not import $assemblyPath : $($_ | Out-String)"
189227
}
228+
}
229+
230+
# Keep the assembly resolver registered for Windows PowerShell
231+
# It's needed at runtime when SMO and other assemblies try to resolve dependencies
232+
# The resolver handles version mismatches (e.g., SMO requesting SqlClient 5.0.0.0 when 6.0.2 is loaded)
233+
if ($PSVersionTable.PSEdition -ne "Core" -and $redirector) {
234+
# Store the redirector in script scope so it stays alive and can be accessed if needed
235+
$script:assemblyRedirector = $redirector
190236
}

0 commit comments

Comments
 (0)