diff --git a/build.psm1 b/build.psm1
index 3e33ed87de..2dacee9744 100644
--- a/build.psm1
+++ b/build.psm1
@@ -1046,9 +1046,10 @@ function Publish-PSTestTools {
Find-Dotnet
$tools = @(
- @{Path="${PSScriptRoot}/test/tools/TestExe";Output="testexe"}
- @{Path="${PSScriptRoot}/test/tools/WebListener";Output="WebListener"}
- @{Path="${PSScriptRoot}/test/tools/TestService";Output="TestService"}
+ @{ Path="${PSScriptRoot}/test/tools/TestAlc"; Output="library" }
+ @{ Path="${PSScriptRoot}/test/tools/TestExe"; Output="exe" }
+ @{ Path="${PSScriptRoot}/test/tools/WebListener"; Output="exe" }
+ @{ Path="${PSScriptRoot}/test/tools/TestService"; Output="exe" }
)
$Options = Get-PSOptions -DefaultToNew
@@ -1069,12 +1070,19 @@ function Publish-PSTestTools {
Remove-Item -Path $objPath -Recurse -Force
}
- if (-not $runtime) {
- dotnet publish --output bin --configuration $Options.Configuration --framework $Options.Framework --runtime $Options.Runtime
- } else {
- dotnet publish --output bin --configuration $Options.Configuration --framework $Options.Framework --runtime $runtime
+ if ($tool.Output -eq 'library') {
+ ## Handle building and publishing assemblies.
+ dotnet publish --configuration $Options.Configuration --framework $Options.Framework
+ continue
}
+ ## Handle building and publishing executables.
+ if (-not $runtime) {
+ $runtime = $Options.Runtime
+ }
+
+ dotnet publish --output bin --configuration $Options.Configuration --framework $Options.Framework --runtime $runtime --self-contained
+
if ( -not $env:PATH.Contains($toolPath) ) {
$env:PATH = $toolPath+$TestModulePathSeparator+$($env:PATH)
}
diff --git a/src/System.Management.Automation/engine/ExecutionContext.cs b/src/System.Management.Automation/engine/ExecutionContext.cs
index 18ba461a68..d73a1e97eb 100644
--- a/src/System.Management.Automation/engine/ExecutionContext.cs
+++ b/src/System.Management.Automation/engine/ExecutionContext.cs
@@ -1269,55 +1269,150 @@ namespace System.Management.Automation
internal PSTransactionManager transactionManager;
- internal Assembly AddAssembly(string name, string filename, out Exception error)
+ ///
+ /// This method is used for assembly loading requests stemmed from 'InitialSessionState' binding and module loading.
+ ///
+ /// Source of the assembly loading request, should be a module name when specified.
+ /// Name of the assembly to be loaded.
+ /// Path of the assembly to be loaded.
+ /// Exception that is caught when the loading fails.
+ internal Assembly AddAssembly(string source, string assemblyName, string filePath, out Exception error)
{
- Assembly loadedAssembly = LoadAssembly(name, filename, out error);
-
- if (loadedAssembly == null)
- return null;
-
- if (AssemblyCache.ContainsKey(loadedAssembly.FullName))
+ // Search the cache by the path, and return the assembly if we find it.
+ // It's common to have two loading requests for the same assembly when loading a module -- the first time for
+ // resolving a binary module path, and the second time for actually processing that module.
+ //
+ // That's not a problem when all the module assemblies are loaded into the default ALC. But in a scenario where
+ // a module tries to hide its nested/root binary modules in a custom ALC, that will become a problem. This is
+ // because:
+ // in that scenario, the module will usually setup a handler to load the specific assemblies to the custom ALC,
+ // and that will be how the first loading request gets served. However, after the module path is resolved with
+ // the first loading, the path will be used for the second loading upon real module processing. Since we prefer
+ // loading-by-path over loading-by-name in the 'LoadAssembly' call, we will end up loading the same assembly in
+ // the default ALC (because we use 'Assembly.LoadFrom' which always loads an assembly to the default ALC) if we
+ // do not search in the cache first. That will break the scenario, because the module means to isolate all its
+ // dependencies from the default ALC, and it failed to do so.
+ //
+ // Therefore, we need to search the cache first. The reason we use path as the key is to make sure the request
+ // is for exactly the same assembly. The same assembly file should not be loaded into different ALC's by module
+ // loading within the same PowerShell session (Runspace).
+ //
+ // An example module targeting the abovementioned scenario will likely have the following file structure:
+ // IsolatedModule
+ // │ IsolatedModule.psd1 (has 'NestedModules = @('Test.Isolated.Init.dll', 'Test.Isolated.Nested.dll')')
+ // │ Test.Isolated.Init.dll (contains the custom ALC and code to setup 'Resolving' handler)
+ // └───Dependencies (folder under module base)
+ // Newtonsoft.Json.dll (version 10.0.0.0 dependency)
+ // Test.Isolated.Nested.dll (nested binary module referencing the particular dependency)
+ //
+ // In this example, the following events will happen in sequence:
+ // 1. PowerShell is able to find 'Test.Isolated.Init.dll' under module base folder, so it will be loaded into
+ // the default ALC as expected and setup the 'Resolving' handler via the 'OnImport' call.
+ // 2. PowerShell cannot find 'Test.Isolated.Nested.dll' under the module base folder, so it will call the method
+ // 'FixFileName(.., bool canLoadAssembly)' to resolve the path of this binary module.
+ // This particular overload will attempt to load the assembly by name, which will be served by the 'Resolving'
+ // handler that was setup in the step 1. So, the assembly will be loaded into the custom ALC and insert to the
+ // assembly cache.
+ // 3. Path of the nested module 'Test.Isolated.Init.dll' now has been resolved by the step 2 (assembly.Location).
+ // Now it's time to actually load this binary module for processing in the method 'LoadBinaryModule', which
+ // will make a call to this method with the resolved assembly file path.
+ // At this poin, we will have to query the cache first, instead of calling 'LoadAssembly' directly, to make sure
+ // that the assembly instance loaded in the custom ALC in step 2 gets returned back. Otherwise, the same assembly
+ // file will be loaded in the default ALC because 'Assembly.LoadFrom' is used in 'LoadAssembly' and that API will
+ // always load an assembly file to the default ALC, and that will break this scenario.
+ if (TryGetFromAssemblyCache(source, filePath, out Assembly loadedAssembly))
{
- // we should ignore this assembly.
- return loadedAssembly;
- }
- // We will cache the assembly by both full name and
- // file name
- AssemblyCache.Add(loadedAssembly.FullName, loadedAssembly);
-
- if (AssemblyCache.ContainsKey(loadedAssembly.GetName().Name))
- {
- // we should ignore this assembly.
+ error = null;
return loadedAssembly;
}
- AssemblyCache.Add(loadedAssembly.GetName().Name, loadedAssembly);
+ // Attempt to load the requested assembly, first by path then by name.
+ loadedAssembly = LoadAssembly(assemblyName, filePath, out error);
+ if (loadedAssembly is not null)
+ {
+ AddToAssemblyCache(source, loadedAssembly);
+ }
+
return loadedAssembly;
}
- internal void RemoveAssembly(string name)
+ ///
+ /// Add a loaded assembly to the 'AssemblyCache'.
+ /// The is used as a prefix for the key to make it easy to remove all associated
+ /// assemblies from the cache when a module gets unloaded.
+ ///
+ /// The source where the assembly comes from, should be a module name when specified.
+ /// The assembly we try to cache.
+ internal void AddToAssemblyCache(string source, Assembly assembly)
{
- Assembly loadedAssembly;
- if (AssemblyCache.TryGetValue(name, out loadedAssembly) && loadedAssembly != null)
- {
- AssemblyCache.Remove(name);
+ // Try caching the assembly by its location if possible.
+ // When it's a dynamic assembly, we use it's full name. This could happen with 'Import-Module -Assembly'.
+ string key = string.IsNullOrEmpty(assembly.Location) ? assembly.FullName : assembly.Location;
- AssemblyCache.Remove(loadedAssembly.GetName().Name);
+ // When the assembly is from a module loading, we prefix the key with the source,
+ // so we can remove it from the cache when the module gets unloaded.
+ if (!string.IsNullOrEmpty(source))
+ {
+ // Both 'source' and 'key' are of the string type, so no need to specify 'InvariantCulture'.
+ key = $"{source}@{key}";
+ }
+
+ AssemblyCache.TryAdd(key, assembly);
+ }
+
+ ///
+ /// Remove all cache entries that are associated with the specified source.
+ ///
+ internal void RemoveFromAssemblyCache(string source)
+ {
+ if (string.IsNullOrEmpty(source))
+ {
+ return;
+ }
+
+ var keysToRemove = new List();
+ string prefix = $"{source}@";
+
+ foreach (string key in AssemblyCache.Keys)
+ {
+ if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ keysToRemove.Add(key);
+ }
+ }
+
+ foreach (string key in keysToRemove)
+ {
+ AssemblyCache.Remove(key);
}
}
- [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadWithPartialName")]
- [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")]
- internal static Assembly LoadAssembly(string name, string filename, out Exception error)
+ ///
+ /// Try to get an assembly from the cache.
+ ///
+ private bool TryGetFromAssemblyCache(string source, string filePath, out Assembly assembly)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ assembly = null;
+ return false;
+ }
+
+ // Both 'source' and 'filePath' are of the string type, so no need to specify 'InvariantCulture'.
+ string key = string.IsNullOrEmpty(source) ? filePath : $"{source}@{filePath}";
+ return AssemblyCache.TryGetValue(key, out assembly);
+ }
+
+ private static Assembly LoadAssembly(string name, string filePath, out Exception error)
{
// First we try to load the assembly based on the filename
Assembly loadedAssembly = null;
error = null;
- if (!string.IsNullOrEmpty(filename))
+ if (!string.IsNullOrEmpty(filePath))
{
try
{
- loadedAssembly = Assembly.LoadFrom(filename);
+ loadedAssembly = Assembly.LoadFrom(filePath);
return loadedAssembly;
}
catch (FileNotFoundException fileNotFound)
@@ -1562,7 +1657,7 @@ namespace System.Management.Automation
EngineHostInterface = hostInterface as InternalHost ?? new InternalHost(hostInterface, this);
// Hook up the assembly cache
- AssemblyCache = new Dictionary();
+ AssemblyCache = new Dictionary(StringComparer.OrdinalIgnoreCase);
// Initialize the fixed toplevel session state and the current session state
TopLevelSessionState = EngineSessionState = new SessionStateInternal(this);
diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs
index bc40e903fa..c0d4c2cb7c 100644
--- a/src/System.Management.Automation/engine/InitialSessionState.cs
+++ b/src/System.Management.Automation/engine/InitialSessionState.cs
@@ -410,7 +410,7 @@ namespace System.Management.Automation.Runspaces
/// The cloned object.
public override InitialSessionStateEntry Clone()
{
- SessionStateAssemblyEntry entry = new SessionStateAssemblyEntry(Name, FileName);
+ var entry = new SessionStateAssemblyEntry(Name, FileName);
entry.SetPSSnapIn(this.PSSnapIn);
entry.SetModule(this.Module);
return entry;
@@ -2405,9 +2405,14 @@ namespace System.Management.Automation.Runspaces
// Load the assemblies and initialize the assembly cache...
foreach (SessionStateAssemblyEntry ssae in Assemblies)
{
- if (etwEnabled) RunspaceEventSource.Log.LoadAssemblyStart(ssae.Name, ssae.FileName);
- Exception error = null;
- Assembly asm = context.AddAssembly(ssae.Name, ssae.FileName, out error);
+ if (etwEnabled)
+ {
+ RunspaceEventSource.Log.LoadAssemblyStart(ssae.Name, ssae.FileName);
+ }
+
+ // Specify the source only if this is for module loading.
+ // The source is used for porper cleaning of the assembly cache when a module is unloaded.
+ Assembly asm = context.AddAssembly(ssae.Module?.Name, ssae.Name, ssae.FileName, out Exception error);
if (asm == null || error != null)
{
@@ -3374,105 +3379,6 @@ namespace System.Management.Automation.Runspaces
context.EngineSessionState.SetVariableAtScope(qv, "global", true, CommandOrigin.Internal);
}
- ///
- /// Remove anything that would have been bound by this ISS instance.
- /// At this point, it removes assemblies and cmdlet entries at the top level.
- /// It also removes types and formats.
- /// The other entry types - functions, variables, aliases
- /// are not removed by this function.
- ///
- ///
- internal void Unbind(ExecutionContext context)
- {
- lock (_syncObject)
- {
- SessionStateInternal ss = context.EngineSessionState;
-
- // Remove the assemblies from the assembly cache...
- foreach (SessionStateAssemblyEntry ssae in Assemblies)
- {
- context.RemoveAssembly(ssae.Name);
- }
-
- // Remove all of the commands from the top-level session state.
- foreach (SessionStateCommandEntry cmd in Commands)
- {
- SessionStateCmdletEntry ssce = cmd as SessionStateCmdletEntry;
- if (ssce != null)
- {
- List matches;
- if (context.TopLevelSessionState.GetCmdletTable().TryGetValue(ssce.Name, out matches))
- {
- // Remove the name from the list...
- for (int i = matches.Count - 1; i >= 0; i--)
- {
- if (matches[i].ModuleName.Equals(cmd.PSSnapIn.Name))
- {
- string name = matches[i].Name;
- matches.RemoveAt(i);
- context.TopLevelSessionState.RemoveCmdlet(name, i, /*force*/ true);
- }
- }
- // And remove the entry if the list is now empty...
- if (matches.Count == 0)
- {
- context.TopLevelSessionState.RemoveCmdletEntry(ssce.Name, true);
- }
- }
-
- continue;
- }
- }
-
- // Remove all of the providers from the top-level provider table.
- if (_providers != null && _providers.Count > 0)
- {
- Dictionary> providerTable = context.TopLevelSessionState.Providers;
-
- foreach (SessionStateProviderEntry sspe in _providers)
- {
- List pl;
- if (providerTable.TryGetValue(sspe.Name, out pl))
- {
- Diagnostics.Assert(pl != null, "There should never be a null list of entries in the provider table");
- // For each provider with the same name...
- for (int i = pl.Count - 1; i >= 0; i--)
- {
- ProviderInfo pi = pl[i];
-
- // If it was implemented by this entry, remove it
- if (pi.ImplementingType == sspe.ImplementingType)
- {
- RemoveAllDrivesForProvider(pi, context.TopLevelSessionState);
- pl.RemoveAt(i);
- }
- }
-
- // If there are no providers left with this name, remove the key.
- if (pl.Count == 0)
- {
- providerTable.Remove(sspe.Name);
- }
- }
- }
- }
-
- List formatFilesToRemove = new List();
- if (this.Formats != null)
- {
- formatFilesToRemove.AddRange(this.Formats.Select(static f => f.FileName));
- }
-
- List typeFilesToRemove = new List();
- if (this.Types != null)
- {
- typeFilesToRemove.AddRange(this.Types.Select(static t => t.FileName));
- }
-
- RemoveTypesAndFormats(context, formatFilesToRemove, typeFilesToRemove);
- }
- }
-
internal static void RemoveTypesAndFormats(ExecutionContext context, IList formatFilesToRemove, IList typeFilesToRemove)
{
// The formats and types tables are implemented in such a way that
@@ -3904,11 +3810,9 @@ namespace System.Management.Automation.Runspaces
this.Formats.Add(formatEntry);
}
- SessionStateAssemblyEntry assemblyEntry = new SessionStateAssemblyEntry(psSnapInInfo.AssemblyName, psSnapInInfo.AbsoluteModulePath);
-
+ var assemblyEntry = new SessionStateAssemblyEntry(psSnapInInfo.AssemblyName, psSnapInInfo.AbsoluteModulePath);
assemblyEntry.SetPSSnapIn(psSnapInInfo);
-
- this.Assemblies.Add(assemblyEntry);
+ Assemblies.Add(assemblyEntry);
// entry from types.ps1xml references a type (Microsoft.PowerShell.Commands.SecurityDescriptorCommandsBase) in this assembly
if (psSnapInInfo.Name.Equals(CoreSnapin, StringComparison.OrdinalIgnoreCase))
@@ -4005,20 +3909,16 @@ namespace System.Management.Automation.Runspaces
throw e;
}
- Dictionary cmdlets = null;
- Dictionary> aliases = null;
- Dictionary providers = null;
-
string assemblyPath = assembly.Location;
- PSSnapInHelpers.AnalyzePSSnapInAssembly(assembly, assemblyPath, psSnapInInfo: null, module, out cmdlets, out aliases, out providers, helpFile: out _);
-
- // If this is an in-memory assembly, don't added it to the list of AssemblyEntries
- // since it can't be loaded by path or name
- if (!string.IsNullOrEmpty(assembly.Location))
- {
- SessionStateAssemblyEntry assemblyEntry = new SessionStateAssemblyEntry(assembly.FullName, assemblyPath);
- this.Assemblies.Add(assemblyEntry);
- }
+ PSSnapInHelpers.AnalyzePSSnapInAssembly(
+ assembly,
+ assemblyPath,
+ psSnapInInfo: null,
+ module,
+ out Dictionary cmdlets,
+ out Dictionary> aliases,
+ out Dictionary providers,
+ helpFile: out _);
if (cmdlets != null)
{
diff --git a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs
index 45011edb90..8f503cbe46 100644
--- a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs
+++ b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs
@@ -548,32 +548,15 @@ namespace Microsoft.PowerShell.Commands
private void ImportModule_ViaAssembly(ImportModuleOptions importModuleOptions, Assembly suppliedAssembly)
{
bool moduleLoaded = false;
+ string moduleName = "dynamic_code_module_" + suppliedAssembly.FullName;
+
// Loop through Module Cache to ensure that the module is not already imported.
- if (suppliedAssembly != null && Context.Modules.ModuleTable != null)
+ foreach (KeyValuePair pair in Context.Modules.ModuleTable)
{
- foreach (KeyValuePair pair in Context.Modules.ModuleTable)
+ if (pair.Value.Path == string.Empty)
{
- // if the module in the moduleTable is an assembly module without path, the moduleName is the key.
- string moduleName = "dynamic_code_module_" + suppliedAssembly;
- if (pair.Value.Path == string.Empty)
- {
- if (pair.Key.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
- {
- moduleLoaded = true;
- if (BasePassThru)
- {
- WriteObject(pair.Value);
- }
-
- break;
- }
- else
- {
- continue;
- }
- }
-
- if (pair.Value.Path.Equals(suppliedAssembly.Location, StringComparison.OrdinalIgnoreCase))
+ // If the module in the moduleTable is an assembly module without path, the moduleName is the key.
+ if (pair.Key.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
{
moduleLoaded = true;
if (BasePassThru)
@@ -583,12 +566,26 @@ namespace Microsoft.PowerShell.Commands
break;
}
+
+ continue;
+ }
+
+ if (pair.Value.Path.Equals(suppliedAssembly.Location, StringComparison.OrdinalIgnoreCase))
+ {
+ moduleLoaded = true;
+ if (BasePassThru)
+ {
+ WriteObject(pair.Value);
+ }
+
+ break;
}
}
if (!moduleLoaded)
{
PSModuleInfo module = LoadBinaryModule(
+ parentModule: null,
moduleName: null,
fileName: null,
suppliedAssembly,
@@ -596,15 +593,13 @@ namespace Microsoft.PowerShell.Commands
ss: null,
importModuleOptions,
ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors | ManifestProcessingFlags.NullOnFirstError,
- this.BasePrefix,
- loadTypes: false,
- loadFormats: false,
+ BasePrefix,
out bool found);
- if (found && module != null)
+ if (found && module is not null)
{
// Add it to all module tables ...
- AddModuleToModuleTables(this.Context, this.TargetSessionState.Internal, module);
+ AddModuleToModuleTables(Context, TargetSessionState.Internal, module);
if (BasePassThru)
{
WriteObject(module);
@@ -1380,14 +1375,25 @@ namespace Microsoft.PowerShell.Commands
private bool IsPs1xmlFileHelper_IsPresentInEntries(RemoteDiscoveryHelper.CimModuleFile cimModuleFile, IEnumerable manifestEntries)
{
- if (manifestEntries.Any(s => s.EndsWith(cimModuleFile.FileName, StringComparison.OrdinalIgnoreCase)))
+ const string ps1xmlExt = ".ps1xml";
+ string fileName = cimModuleFile.FileName;
+
+ foreach (string entry in manifestEntries)
{
- return true;
+ if (entry.EndsWith(fileName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
}
- if (manifestEntries.Any(s => FixupFileName(string.Empty, s, ".ps1xml", isImportingModule: true).EndsWith(cimModuleFile.FileName, StringComparison.OrdinalIgnoreCase)))
+ foreach (string entry in manifestEntries)
{
- return true;
+ string tempName = entry.EndsWith(ps1xmlExt, StringComparison.OrdinalIgnoreCase) ? entry : entry + ps1xmlExt;
+ string resolvedPath = ResolveRootedFilePath(tempName, Context);
+ if (resolvedPath is not null && resolvedPath.EndsWith(fileName, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
}
return false;
@@ -1894,13 +1900,10 @@ namespace Microsoft.PowerShell.Commands
else if (this.ParameterSetName.Equals(ParameterSet_Assembly, StringComparison.OrdinalIgnoreCase))
{
// Now load all of the supplied assemblies...
- if (Assembly != null)
+ foreach (Assembly suppliedAssembly in Assembly)
{
- foreach (Assembly suppliedAssembly in Assembly)
- {
- ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name);
- ImportModule_ViaAssembly(importModuleOptions, suppliedAssembly);
- }
+ ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name);
+ ImportModule_ViaAssembly(importModuleOptions, suppliedAssembly);
}
}
else if (this.ParameterSetName.Equals(ParameterSet_Name, StringComparison.OrdinalIgnoreCase))
diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
index 53dc8d058c..1c94d557a8 100644
--- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
+++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
@@ -682,9 +682,19 @@ namespace Microsoft.PowerShell.Commands
return result;
}
- private PSModuleInfo LoadModuleNamedInManifest(PSModuleInfo parentModule, ModuleSpecification moduleSpecification, string moduleBase, bool searchModulePath,
- string prefix, SessionState ss, ImportModuleOptions options, ManifestProcessingFlags manifestProcessingFlags, bool loadTypes,
- bool loadFormats, object privateData, out bool found, string shortModuleName, PSLanguageMode? manifestLanguageMode)
+ private PSModuleInfo LoadModuleNamedInManifest(
+ PSModuleInfo parentModule,
+ ModuleSpecification moduleSpecification,
+ string moduleBase,
+ bool searchModulePath,
+ string prefix,
+ SessionState ss,
+ ImportModuleOptions options,
+ ManifestProcessingFlags manifestProcessingFlags,
+ object privateData,
+ out bool found,
+ string shortModuleName,
+ PSLanguageMode? manifestLanguageMode)
{
PSModuleInfo module = null;
PSModuleInfo tempModuleInfoFromVerification = null;
@@ -698,11 +708,16 @@ namespace Microsoft.PowerShell.Commands
var importingModule = manifestProcessingFlags.HasFlag(ManifestProcessingFlags.LoadElements);
string extension = Path.GetExtension(moduleSpecification.Name);
+
// First check for fully-qualified paths - either absolute or relative
string rootedPath = ResolveRootedFilePath(moduleSpecification.Name, this.Context);
if (string.IsNullOrEmpty(rootedPath))
{
- rootedPath = FixupFileName(moduleBase, moduleSpecification.Name, extension, importingModule);
+ // Use the name of the parent module if it's specified, otherwise, use the current module name.
+ // - If the current module is a nested module, then the parent module will be specifeid.
+ // - If the current module is a root module, then the parent module will not be specified.
+ string moduleName = parentModule?.Name ?? ModuleIntrinsics.GetModuleName(moduleSpecification.Name);
+ rootedPath = FixFileName(moduleName, moduleBase, moduleSpecification.Name, extension: null, canLoadAssembly: importingModule);
}
else
{
@@ -925,8 +940,6 @@ namespace Microsoft.PowerShell.Commands
options,
manifestProcessingFlags,
prefix,
- loadTypes,
- loadFormats,
out found,
shortModuleName,
disableFormatUpdates: false);
@@ -1524,6 +1537,7 @@ namespace Microsoft.PowerShell.Commands
Dbg.Assert(moduleManifestPath != null, "moduleManifestPath for module (.psd1) can't be null");
string moduleBase = Path.GetDirectoryName(moduleManifestPath);
+ string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath);
if ((manifestProcessingFlags &
(ManifestProcessingFlags.LoadElements | ManifestProcessingFlags.WriteErrors |
@@ -1624,24 +1638,32 @@ namespace Microsoft.PowerShell.Commands
invalidOperation.SetErrorId("Modules_WildCardNotAllowedInModuleToProcessAndInNestedModules");
throw invalidOperation;
}
+
// See if this module is already loaded. Since the manifest entry may not
// have an extension and the module table is indexed by full names, we
// may have search through all the extensions.
PSModuleInfo loadedModule = null;
- string rootedPath = this.FixupFileName(moduleBase, actualRootModule, extension: null, importingModule);
- string mtpExtension = Path.GetExtension(rootedPath);
- if (!string.IsNullOrEmpty(mtpExtension) && ModuleIntrinsics.IsPowerShellModuleExtension(mtpExtension))
+ string rootedPath = null;
+
+ // For a root module, we use its own module name instead of the manifest module name when calling 'FixFileName'.
+ // This is because when actually loading the root module later, it won't have access to the parent manifest module,
+ // and we will use its own name to query for already loaded assemblies from 'Context.AssemblyCache'.
+ string rootModuleName = ModuleIntrinsics.GetModuleName(actualRootModule);
+ string extension = Path.GetExtension(actualRootModule);
+ if (!string.IsNullOrEmpty(extension) && ModuleIntrinsics.IsPowerShellModuleExtension(extension))
{
+ rootedPath = FixFileName(rootModuleName, moduleBase, actualRootModule, extension: null, canLoadAssembly: importingModule);
TryGetFromModuleTable(rootedPath, out loadedModule);
}
else
{
foreach (string extensionToTry in ModuleIntrinsics.PSModuleExtensions)
{
- rootedPath = this.FixupFileName(moduleBase, actualRootModule, extensionToTry, importingModule);
- TryGetFromModuleTable(rootedPath, out loadedModule);
- if (loadedModule != null)
+ rootedPath = FixFileName(rootModuleName, moduleBase, actualRootModule, extensionToTry, canLoadAssembly: importingModule);
+ if (TryGetFromModuleTable(rootedPath, out loadedModule))
+ {
break;
+ }
}
}
@@ -2049,7 +2071,6 @@ namespace Microsoft.PowerShell.Commands
{
bool nameMissingOrEmpty = false;
var invalidNames = new List();
- string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath);
expFeatureList = new List(features.Length);
foreach (Hashtable feature in features)
@@ -2162,61 +2183,72 @@ namespace Microsoft.PowerShell.Commands
// Indicates the ISS.Bind() should be called...
bool doBind = false;
- // Set up to load any required assemblies that have been specified...
- List tmpAssemblyList;
- List assemblyList = new List();
- List fixedUpAssemblyPathList = new List();
-
- if (
- !GetListOfStringsFromData(data, moduleManifestPath, "RequiredAssemblies", manifestProcessingFlags,
- out tmpAssemblyList))
+ if (!GetListOfStringsFromData(
+ data,
+ moduleManifestPath,
+ "RequiredAssemblies",
+ manifestProcessingFlags,
+ out List assemblyList))
{
containedErrors = true;
- if (bailOnFirstError) return null;
- }
- else
- {
- if (tmpAssemblyList != null && tmpAssemblyList.Count > 0)
+ if (bailOnFirstError)
{
- foreach (string assembly in tmpAssemblyList)
- {
- assemblyList.Add(assembly);
- }
+ return null;
}
-
- if ((assemblyList != null) && importingModule)
+ }
+ else if (assemblyList != null && importingModule)
+ {
+ foreach (string assembly in assemblyList)
{
- foreach (string assembly in assemblyList)
+ if (WildcardPattern.ContainsWildcardCharacters(assembly))
{
- if (WildcardPattern.ContainsWildcardCharacters(assembly))
+ PSInvalidOperationException invalidOperation = PSTraceSource.NewInvalidOperationException(
+ Modules.WildCardNotAllowedInRequiredAssemblies,
+ moduleManifestPath);
+ invalidOperation.SetErrorId("Modules_WildCardNotAllowedInRequiredAssemblies");
+ throw invalidOperation;
+ }
+ else
+ {
+ string fileName = null;
+ string ext = Path.GetExtension(assembly);
+
+ // Note that we don't need to load the required assemblies eagerly because they will be loaded before
+ // processing type and format data. So, when calling 'FixupFileName', we only attempt to resolve the
+ // path, and avoid triggering the loading of the assembly.
+ if (ModuleIntrinsics.ProcessableAssemblyExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase))
{
- PSInvalidOperationException invalidOperation = PSTraceSource.NewInvalidOperationException(
- Modules.WildCardNotAllowedInRequiredAssemblies,
- moduleManifestPath);
- invalidOperation.SetErrorId("Modules_WildCardNotAllowedInRequiredAssemblies");
- throw invalidOperation;
+ fileName = FixFileNameWithoutLoadingAssembly(moduleBase, assembly, extension: null);
}
else
{
- string fileName = FixupFileName(moduleBase, assembly, StringLiterals.PowerShellNgenAssemblyExtension, importingModule, out bool pathIsResolved);
- if (!pathIsResolved)
+ bool isPathResolved = false;
+ foreach (string extToTry in ModuleIntrinsics.ProcessableAssemblyExtensions)
{
- fileName = FixupFileName(moduleBase, assembly, StringLiterals.PowerShellILAssemblyExtension, importingModule);
+ fileName = FixFileNameWithoutLoadingAssembly(moduleBase, assembly, extToTry, out isPathResolved);
+ if (isPathResolved)
+ {
+ break;
+ }
}
- string loadMessage = StringUtil.Format(Modules.LoadingFile, "Assembly", fileName);
- WriteVerbose(loadMessage);
- iss.Assemblies.Add(new SessionStateAssemblyEntry(assembly, fileName));
- fixedUpAssemblyPathList.Add(fileName);
-
- fileName = FixupFileName(moduleBase, assembly, StringLiterals.PowerShellILExecutableExtension, importingModule);
- loadMessage = StringUtil.Format(Modules.LoadingFile, "Executable", fileName);
- WriteVerbose(loadMessage);
- iss.Assemblies.Add(new SessionStateAssemblyEntry(assembly, fileName));
- fixedUpAssemblyPathList.Add(fileName);
-
- doBind = true;
+ if (!isPathResolved)
+ {
+ // We didn't resolve the assembly path, so remove the '.exe' extension that was added in the
+ // last iteration of the above loop.
+ int index = fileName.LastIndexOf('.');
+ fileName = fileName.Substring(0, index);
+ }
}
+
+ WriteVerbose(StringUtil.Format(Modules.LoadingFile, "Assembly", fileName));
+
+ // Set a fake PSModuleInfo object to indicate the module it comes from.
+ var assemblyEntry = new SessionStateAssemblyEntry(assembly, fileName);
+ assemblyEntry.SetModule(new PSModuleInfo(moduleName, path: null, context: null, sessionState: null));
+
+ iss.Assemblies.Add(assemblyEntry);
+ doBind = true;
}
}
}
@@ -2253,8 +2285,7 @@ namespace Microsoft.PowerShell.Commands
continue;
}
- string resolvedEntryFileName = ResolveRootedFilePath(entry.FileName, Context) ??
- entry.FileName;
+ string resolvedEntryFileName = ResolveRootedFilePath(entry.FileName, Context) ?? entry.FileName;
if (resolvedEntryFileName.Equals(resolvedFileName, StringComparison.OrdinalIgnoreCase))
{
isAlreadyLoaded = true;
@@ -2377,7 +2408,7 @@ namespace Microsoft.PowerShell.Commands
key: "FileList",
manifestProcessingFlags,
moduleBase,
- extension: string.Empty,
+ extension: null,
// Don't check file existence - don't want to change current behavior without feature team discussion.
verifyFilesExist: false,
out List fileList))
@@ -2517,10 +2548,18 @@ namespace Microsoft.PowerShell.Commands
// If there is a session state, set up to import/export commands and variables
if (ss != null)
{
- ss.Internal.SetVariable(SpecialVariables.PSScriptRootVarPath, Path.GetDirectoryName(moduleManifestPath),
- true, CommandOrigin.Internal);
- ss.Internal.SetVariable(SpecialVariables.PSCommandPathVarPath, moduleManifestPath, true,
+ ss.Internal.SetVariable(
+ SpecialVariables.PSScriptRootVarPath,
+ moduleBase,
+ asValue: true,
CommandOrigin.Internal);
+
+ ss.Internal.SetVariable(
+ SpecialVariables.PSCommandPathVarPath,
+ moduleManifestPath,
+ asValue: true,
+ CommandOrigin.Internal);
+
ss.Internal.Module = manifestInfo;
// without ModuleToProcess a manifest will export everything by default
@@ -2974,8 +3013,6 @@ namespace Microsoft.PowerShell.Commands
ss: null,
options: nestedModuleOptions,
manifestProcessingFlags: manifestProcessingFlags,
- loadTypes: true,
- loadFormats: true,
privateData: privateData,
found: out found,
shortModuleName: null,
@@ -3077,8 +3114,6 @@ namespace Microsoft.PowerShell.Commands
ss: ss,
options: options,
manifestProcessingFlags: manifestProcessingFlags,
- loadTypes: (exportedTypeFiles == null || exportedTypeFiles.Count == 0), // If types files already loaded, don't load snapin files
- loadFormats: (exportedFormatFiles == null || exportedFormatFiles.Count == 0), // if format files already loaded, don't load snapin files
privateData: privateData,
found: out found,
shortModuleName: null,
@@ -4403,8 +4438,6 @@ namespace Microsoft.PowerShell.Commands
out List list)
{
list = null;
-
- bool importingModule = manifestProcessingFlags.HasFlag(ManifestProcessingFlags.LoadElements);
if (!GetListOfStringsFromData(data, moduleManifestPath, key, manifestProcessingFlags, out List listOfStrings))
{
return false;
@@ -4426,7 +4459,7 @@ namespace Microsoft.PowerShell.Commands
{
try
{
- string fixedFileName = FixupFileName(moduleBase, s, extension, importingModule, skipLoading: true);
+ string fixedFileName = FixFileNameWithoutLoadingAssembly(moduleBase, s, extension);
var dir = Path.GetDirectoryName(fixedFileName);
if (string.Equals(psHome, dir, StringComparison.OrdinalIgnoreCase) ||
@@ -4581,12 +4614,22 @@ namespace Microsoft.PowerShell.Commands
}
}
+ private string FixFileNameWithoutLoadingAssembly(string moduleBase, string fileName, string extension)
+ {
+ return FixFileName(moduleName: null, moduleBase, fileName, extension, canLoadAssembly: false, pathIsResolved: out _);
+ }
+
+ private string FixFileNameWithoutLoadingAssembly(string moduleBase, string fileName, string extension, out bool pathIsResolved)
+ {
+ return FixFileName(moduleName: null, moduleBase, fileName, extension, canLoadAssembly: false, out pathIsResolved);
+ }
+
///
/// A utility routine to fix up a file name so it's rooted and has an extension.
///
- internal string FixupFileName(string moduleBase, string name, string extension, bool isImportingModule, bool skipLoading = false)
+ private string FixFileName(string moduleName, string moduleBase, string fileName, string extension, bool canLoadAssembly)
{
- return FixupFileName(moduleBase, name, extension, isImportingModule, pathIsResolved: out _, skipLoading);
+ return FixFileName(moduleName, moduleBase, fileName, extension, canLoadAssembly, pathIsResolved: out _);
}
///
@@ -4596,24 +4639,33 @@ namespace Microsoft.PowerShell.Commands
/// When fixing up an assembly file, this method loads the resovled assembly if it's in the process of actually loading a module.
/// Read the comments in the method for the detailed information.
///
+ /// Name of the module that we are processing, used for caching purpose when we need to load an assembly.
/// The base path to use if the file is not rooted.
- /// The file name to resolve.
- /// The extension to use in case the given name has no extension.
- /// Indicate if we are loading a module.
+ /// The file name to resolve.
+ /// The extension to use for the look up.
+ /// Indicate if we can load assembly for the resolution.
/// Indicate if the returned path is fully resolved.
- /// Indicate if the resolved module should be loaded.
///
- /// The resolved file path. Or, the combined path of and when the file path cannot be resolved.
+ /// The resolved file path. Or, the combined path of and when the file path cannot be resolved.
///
- internal string FixupFileName(string moduleBase, string name, string extension, bool isImportingModule, out bool pathIsResolved, bool skipLoading = false)
+ private string FixFileName(string moduleName, string moduleBase, string fileName, string extension, bool canLoadAssembly, out bool pathIsResolved)
{
pathIsResolved = false;
- string originalName = name;
- string originalExt = Path.GetExtension(name);
- if (string.IsNullOrEmpty(originalExt))
+ string originalName = fileName;
+ string originalExt = Path.GetExtension(fileName);
+
+ if (string.IsNullOrEmpty(extension))
{
- name += extension;
+ // When 'extension' is not explicitly specified, we honor the original extension.
+ extension = originalExt;
+ }
+ else if (!extension.Equals(originalExt, StringComparison.OrdinalIgnoreCase))
+ {
+ // When 'extension' is explicitly specified, append it if the original extension is different.
+ // Note: the original extension could actually be part of the file name. For example, the name
+ // is `Microsoft.PowerShell.Command.Utility`, in which case the extension is `.Utility`.
+ fileName += extension;
}
// Try to get the resolved fully qualified path to the file.
@@ -4625,24 +4677,24 @@ namespace Microsoft.PowerShell.Commands
// Check for combinedPath in this case will get us the normalized rooted path 'C:\Windows\System32\WindowsPowerShell\v1.0\WSMan.format.ps1xml'.
// The 'Microsoft.WSMan.Management' module in PowerShell was updated to not use the relative path for 'FormatsToProcess' entry,
// but it's safer to keep the original behavior to avoid unexpected breaking changes.
- string combinedPath = Path.Combine(moduleBase, name);
- string resolvedPath = IsRooted(name)
- ? ResolveRootedFilePath(name, Context) ?? ResolveRootedFilePath(combinedPath, Context)
+ string combinedPath = Path.Combine(moduleBase, fileName);
+ string resolvedPath = IsRooted(fileName)
+ ? ResolveRootedFilePath(fileName, Context) ?? ResolveRootedFilePath(combinedPath, Context)
: ResolveRootedFilePath(combinedPath, Context);
// Return the path if successfully resolved.
- if (resolvedPath != null)
+ if (resolvedPath is not null)
{
- if (isImportingModule && resolvedPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !skipLoading)
+ if (canLoadAssembly && resolvedPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
// If we are fixing up an assembly file path and we are actually loading the module, then we load the resolved assembly file here.
- // This is because we process type/format ps1xml files before 'RootModule' and 'NestedModules' entries during the module loading.
- // A types.ps1xml file could refer to a type defined in the assembly that is specified in the 'RootModule' or 'NestedModule', and
- // in that case, processing the types.ps1xml file would fail because it happens before processing the 'RootModule', which loads
- // the assembly. We cannot move the processing of types.ps1xml file after processing 'RootModule' either, because the 'RootModule'
- // might refer to members defined in the types.ps1xml file. In order to make it work for this paradox, we have to load the resolved
- // assembly when we are actually loading the module. However, when it's module analysis, there is no need to load the assembly.
- ExecutionContext.LoadAssembly(name: null, filename: resolvedPath, error: out _);
+ // This is because we process type/format ps1xml files before 'RootModule' during the module loading. A types.ps1xml file could
+ // refer to a type defined in the assembly that is specified in the 'RootModule', and in that case, processing the types.ps1xml file
+ // would fail because it happens before processing the 'RootModule', which loads the assembly.
+ // We cannot move the processing of types.ps1xml file after processing 'RootModule' either, because the 'RootModule' might refer to
+ // members defined in the types.ps1xml file. In order to make it work for this paradox, we have to load the resolved assembly when
+ // we are actually loading the module. However, when it's module analysis, there is no need to load the assembly.
+ Context.AddAssembly(source: moduleName, assemblyName: null, filePath: resolvedPath, error: out _);
}
pathIsResolved = true;
@@ -4655,12 +4707,12 @@ namespace Microsoft.PowerShell.Commands
// For dlls, we cannot get the path from the provider.
// We need to load the assembly and then get the path.
// If the module is already loaded, this is not expensive since the assembly is already loaded in the AppDomain
- if (!string.IsNullOrEmpty(extension) &&
+ if (canLoadAssembly && !string.IsNullOrEmpty(extension) &&
(extension.Equals(StringLiterals.PowerShellILAssemblyExtension, StringComparison.OrdinalIgnoreCase) ||
- extension.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase)))
+ extension.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase)))
{
- Assembly assembly = ExecutionContext.LoadAssembly(name: originalName, filename: null, error: out _);
- if (assembly != null)
+ Assembly assembly = Context.AddAssembly(source: moduleName, assemblyName: originalName, filePath: null, error: out _);
+ if (assembly is not null)
{
pathIsResolved = true;
result = assembly.Location;
@@ -4974,8 +5026,6 @@ namespace Microsoft.PowerShell.Commands
/// Module name specified in the cmdlet.
internal void RemoveModule(PSModuleInfo module, string moduleNameInRemoveModuleCmdlet)
{
- bool isTopLevelModule = false;
-
// if the module path is empty string, means it is a dynamically generated assembly.
// We have set the module path to be module name as key to make it unique, we need update here as well in case the module can be removed.
if (module.Path == string.Empty)
@@ -4983,7 +5033,7 @@ namespace Microsoft.PowerShell.Commands
module.Path = module.Name;
}
- bool shouldModuleBeRemoved = ShouldModuleBeRemoved(module, moduleNameInRemoveModuleCmdlet, out isTopLevelModule);
+ bool shouldModuleBeRemoved = ShouldModuleBeRemoved(module, moduleNameInRemoveModuleCmdlet, out bool isTopLevelModule);
if (shouldModuleBeRemoved)
{
@@ -5214,6 +5264,15 @@ namespace Microsoft.PowerShell.Commands
// And the appdomain level module path cache.
PSModuleInfo.RemoveFromAppDomainLevelCache(module.Name);
+
+ // And remove the module assembly entries that may have been added from the assembly cache.
+ Context.RemoveFromAssemblyCache(source: module.Name);
+ if (module.ModuleType == ModuleType.Binary && !string.IsNullOrEmpty(module.RootModule))
+ {
+ // We also need to clean up the cache entries that are possibly referenced by the root module in this case.
+ string rootModuleName = ModuleIntrinsics.GetModuleName(module.RootModule);
+ Context.RemoveFromAssemblyCache(source: rootModuleName);
+ }
}
}
}
@@ -5903,6 +5962,7 @@ namespace Microsoft.PowerShell.Commands
ext.Equals(StringLiterals.PowerShellILExecutableExtension, StringComparison.OrdinalIgnoreCase))
{
module = LoadBinaryModule(
+ parentModule,
ModuleIntrinsics.GetModuleName(fileName),
fileName,
assemblyToLoad: null,
@@ -5911,8 +5971,6 @@ namespace Microsoft.PowerShell.Commands
options,
manifestProcessingFlags,
prefix,
- loadTypes: true,
- loadFormats: true,
out found);
if (found && module != null)
@@ -6384,6 +6442,7 @@ namespace Microsoft.PowerShell.Commands
///
/// Load a binary module. A binary module is an assembly that should contain cmdlets.
///
+ /// The parent module for which this module is a nested module.
/// The name of the snapin or assembly to load.
/// The path to the assembly to load.
/// The assembly to load so no lookup need be done.
@@ -6395,12 +6454,11 @@ namespace Microsoft.PowerShell.Commands
///
/// The set of options that are used while importing a module.
/// The manifest processing flags to use when processing the module.
- /// Load the types files mentioned in the snapin registration.
- /// Load the formst files mentioned in the snapin registration.
/// Command name prefix.
/// Sets this to true if an assembly was found.
/// THe module info object that was created...
internal PSModuleInfo LoadBinaryModule(
+ PSModuleInfo parentModule,
string moduleName,
string fileName,
Assembly assemblyToLoad,
@@ -6409,12 +6467,10 @@ namespace Microsoft.PowerShell.Commands
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
string prefix,
- bool loadTypes,
- bool loadFormats,
out bool found)
{
return LoadBinaryModule(
- parentModule: null,
+ parentModule,
moduleName,
fileName,
assemblyToLoad,
@@ -6423,8 +6479,6 @@ namespace Microsoft.PowerShell.Commands
options,
manifestProcessingFlags,
prefix,
- loadTypes,
- loadFormats,
out found,
shortModuleName: null,
disableFormatUpdates: false);
@@ -6446,8 +6500,6 @@ namespace Microsoft.PowerShell.Commands
/// The set of options that are used while importing a module.
/// The manifest processing flags to use when processing the module.
/// Command name prefix.
- /// Load the types files mentioned in the snapin registration.
- /// Load the formst files mentioned in the snapin registration.
/// Sets this to true if an assembly was found.
/// Short name for module.
///
@@ -6462,44 +6514,36 @@ namespace Microsoft.PowerShell.Commands
ImportModuleOptions options,
ManifestProcessingFlags manifestProcessingFlags,
string prefix,
- bool loadTypes,
- bool loadFormats,
out bool found,
string shortModuleName,
bool disableFormatUpdates)
{
- PSModuleInfo module = null;
-
if (string.IsNullOrEmpty(moduleName) && string.IsNullOrEmpty(fileName) && assemblyToLoad == null)
+ {
throw PSTraceSource.NewArgumentNullException("moduleName,fileName,assemblyToLoad");
+ }
+
+ bool isParentEngineModule = parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name);
// Load the dll and process any cmdlets it might contain...
InitialSessionState iss = InitialSessionState.Create();
List detectedCmdlets = null;
List> detectedAliases = null;
Assembly assembly = null;
- Exception error = null;
string modulePath = string.Empty;
Version assemblyVersion = new Version(0, 0, 0, 0);
- var importingModule = (manifestProcessingFlags & ManifestProcessingFlags.LoadElements) != 0;
+ bool importingModule = (manifestProcessingFlags & ManifestProcessingFlags.LoadElements) != 0;
// See if we're loading a straight assembly...
if (assemblyToLoad != null)
{
// Figure out what to use for a module path...
- if (!string.IsNullOrEmpty(fileName))
- {
- modulePath = fileName;
- }
- else
- {
- modulePath = assemblyToLoad.Location;
- }
+ modulePath = string.IsNullOrEmpty(fileName) ? assemblyToLoad.Location : fileName;
// And what to use for a module name...
if (string.IsNullOrEmpty(moduleName))
{
- moduleName = "dynamic_code_module_" + assemblyToLoad.GetName();
+ moduleName = "dynamic_code_module_" + assemblyToLoad.FullName;
}
if (importingModule)
@@ -6507,59 +6551,41 @@ namespace Microsoft.PowerShell.Commands
// Passing module as a parameter here so that the providers can have the module property populated.
// For engine providers, the module should point to top-level module name
// For FileSystem, the module is Microsoft.PowerShell.Core and not System.Management.Automation
- if (parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name))
- {
- iss.ImportCmdletsFromAssembly(assemblyToLoad, parentModule);
- }
- else
- {
- iss.ImportCmdletsFromAssembly(assemblyToLoad, null);
- }
+ iss.ImportCmdletsFromAssembly(assemblyToLoad, isParentEngineModule ? parentModule : null);
}
assemblyVersion = GetAssemblyVersionNumber(assemblyToLoad);
assembly = assemblyToLoad;
- // If this is an in-memory only assembly, add it directly to the assembly cache if
- // it isn't already there.
- if (string.IsNullOrEmpty(assembly.Location))
- {
- if (!Context.AssemblyCache.ContainsKey(assembly.FullName))
- {
- Context.AssemblyCache.Add(assembly.FullName, assembly);
- }
- }
+
+ // Use the parent module name for caching if there is one.
+ string source = parentModule?.Name ?? moduleName;
+ // Add it to the assembly cache if it isn't already there.
+ Context.AddToAssemblyCache(source, assembly);
}
else if (importingModule)
{
- assembly = Context.AddAssembly(moduleName, fileName, out error);
+ // Use the parent module name for caching if there is one.
+ string source = parentModule?.Name ?? moduleName;
+ assembly = Context.AddAssembly(source, moduleName, fileName, out Exception error);
if (assembly == null)
{
if (error != null)
+ {
throw error;
+ }
found = false;
return null;
}
assemblyVersion = GetAssemblyVersionNumber(assembly);
-
- if (string.IsNullOrEmpty(fileName))
- modulePath = assembly.Location;
- else
- modulePath = fileName;
+ modulePath = string.IsNullOrEmpty(fileName) ? assembly.Location : fileName;
// Passing module as a parameter here so that the providers can have the module property populated.
// For engine providers, the module should point to top-level module name
// For FileSystem, the module is Microsoft.PowerShell.Core and not System.Management.Automation
- if (parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name))
- {
- iss.ImportCmdletsFromAssembly(assembly, parentModule);
- }
- else
- {
- iss.ImportCmdletsFromAssembly(assembly, null);
- }
+ iss.ImportCmdletsFromAssembly(assembly, isParentEngineModule ? parentModule : null);
}
else
{
@@ -6576,58 +6602,25 @@ namespace Microsoft.PowerShell.Commands
}
found = true;
- if (string.IsNullOrEmpty(shortModuleName))
- module = new PSModuleInfo(moduleName, modulePath, Context, ss);
- else
- module = new PSModuleInfo(shortModuleName, modulePath, Context, ss);
+ string nameToUse = string.IsNullOrEmpty(shortModuleName) ? moduleName : shortModuleName;
+ PSModuleInfo module = new PSModuleInfo(nameToUse, modulePath, Context, ss);
module.SetModuleType(ModuleType.Binary);
module.SetModuleBase(moduleBase);
module.SetVersion(assemblyVersion);
- module.ImplementingAssembly = assemblyToLoad ?? assembly;
+ module.ImplementingAssembly = assembly;
if (importingModule)
{
SetModuleLoggingInformation(module);
}
- // Add the types table entries
- List typesFileNames = new List();
- foreach (SessionStateTypeEntry sste in iss.Types)
- {
- typesFileNames.Add(sste.FileName);
- }
-
- if (typesFileNames.Count > 0)
- {
- module.SetExportedTypeFiles(new ReadOnlyCollection(typesFileNames));
- }
-
- // Add the format file entries
- List formatsFileNames = new List();
- foreach (SessionStateFormatEntry ssfe in iss.Formats)
- {
- formatsFileNames.Add(ssfe.FileName);
- }
-
- if (formatsFileNames.Count > 0)
- {
- module.SetExportedFormatFiles(new ReadOnlyCollection(formatsFileNames));
- }
-
// Add the module info the providers...
foreach (SessionStateProviderEntry sspe in iss.Providers)
{
// For engine providers, the module should point to top-level module name
// For FileSystem, the module is Microsoft.PowerShell.Core and not System.Management.Automation
- if (parentModule != null && InitialSessionState.IsEngineModule(parentModule.Name))
- {
- sspe.SetModule(parentModule);
- }
- else
- {
- sspe.SetModule(module);
- }
+ sspe.SetModule(isParentEngineModule ? parentModule : module);
}
// Add all of the exported cmdlets to the module object...
@@ -6741,16 +6734,7 @@ namespace Microsoft.PowerShell.Commands
iss.Bind(Context, updateOnly: true, module, options.NoClobber, options.Local, setLocation: false);
// Scan all of the types in the assembly to register JobSourceAdapters.
- IEnumerable allTypes = Array.Empty();
- if (assembly != null)
- {
- allTypes = assembly.ExportedTypes;
- }
- else if (assemblyToLoad != null)
- {
- allTypes = assemblyToLoad.ExportedTypes;
- }
-
+ IEnumerable allTypes = assembly?.ExportedTypes ?? Array.Empty();
foreach (Type type in allTypes)
{
// If it derives from JobSourceAdapter and it's not already registered, register it...
diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
index ada03fb491..d845a1d2fb 100644
--- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
+++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
@@ -885,25 +885,35 @@ namespace System.Management.Automation
}
// The extensions of all of the files that can be processed with Import-Module, put the ni.dll in front of .dll to have higher priority to be loaded.
- internal static readonly string[] PSModuleProcessableExtensions = new string[] {
- StringLiterals.PowerShellDataFileExtension,
- StringLiterals.PowerShellScriptFileExtension,
- StringLiterals.PowerShellModuleFileExtension,
- StringLiterals.PowerShellCmdletizationFileExtension,
- StringLiterals.PowerShellNgenAssemblyExtension,
- StringLiterals.PowerShellILAssemblyExtension,
- StringLiterals.PowerShellILExecutableExtension,
- };
+ internal static readonly string[] PSModuleProcessableExtensions = new string[]
+ {
+ StringLiterals.PowerShellDataFileExtension,
+ StringLiterals.PowerShellScriptFileExtension,
+ StringLiterals.PowerShellModuleFileExtension,
+ StringLiterals.PowerShellCmdletizationFileExtension,
+ StringLiterals.PowerShellNgenAssemblyExtension,
+ StringLiterals.PowerShellILAssemblyExtension,
+ StringLiterals.PowerShellILExecutableExtension,
+ };
// A list of the extensions to check for implicit module loading and discovery, put the ni.dll in front of .dll to have higher priority to be loaded.
- internal static readonly string[] PSModuleExtensions = new string[] {
- StringLiterals.PowerShellDataFileExtension,
- StringLiterals.PowerShellModuleFileExtension,
- StringLiterals.PowerShellCmdletizationFileExtension,
- StringLiterals.PowerShellNgenAssemblyExtension,
- StringLiterals.PowerShellILAssemblyExtension,
- StringLiterals.PowerShellILExecutableExtension,
- };
+ internal static readonly string[] PSModuleExtensions = new string[]
+ {
+ StringLiterals.PowerShellDataFileExtension,
+ StringLiterals.PowerShellModuleFileExtension,
+ StringLiterals.PowerShellCmdletizationFileExtension,
+ StringLiterals.PowerShellNgenAssemblyExtension,
+ StringLiterals.PowerShellILAssemblyExtension,
+ StringLiterals.PowerShellILExecutableExtension,
+ };
+
+ // A list of the extensions to check for required assemblies.
+ internal static readonly string[] ProcessableAssemblyExtensions = new string[]
+ {
+ StringLiterals.PowerShellNgenAssemblyExtension,
+ StringLiterals.PowerShellILAssemblyExtension,
+ StringLiterals.PowerShellILExecutableExtension
+ };
///
/// Returns true if the extension is one of the module extensions...
@@ -915,7 +925,9 @@ namespace System.Management.Automation
foreach (string ext in PSModuleProcessableExtensions)
{
if (extension.Equals(ext, StringComparison.OrdinalIgnoreCase))
+ {
return true;
+ }
}
return false;
diff --git a/test/powershell/engine/Module/IsolatedModule.Tests.ps1 b/test/powershell/engine/Module/IsolatedModule.Tests.ps1
new file mode 100644
index 0000000000..114b1d1610
--- /dev/null
+++ b/test/powershell/engine/Module/IsolatedModule.Tests.ps1
@@ -0,0 +1,74 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+Describe "Isolated module scenario - load the whole module in custom ALC" {
+ It "Loading 'IsolatedModule' should work as expected" {
+ ## The 'IsolatedModule' module can be found at '\test\tools\Modules'.
+ ## The module assemblies are created and deployed by '\test\tools\TestAlc'.
+ ## The module defines its own custom ALC and has its module structure organized in a special way that allows the module to be loaded in that custom ALC.
+ ## The file structure of this module is as follows:
+ ## │ IsolatedModule.psd1
+ ## │ Test.Isolated.Init.dll (contains the custom ALC and code to setup 'Resolving' handler)
+ ## │
+ ## └───Dependencies
+ ## Newtonsoft.Json.dll (version 10.0.0.0 dependency)
+ ## Test.Isolated.Nested.dll (nested binary module)
+ ## Test.Isolated.Root.dll (root binary module)
+ $module = Import-Module IsolatedModule -PassThru
+ $nestedCmd = Get-Command Test-NestedCommand
+ $rootCmd = Get-Command Test-RootCommand
+
+ $module.ModuleType | Should -Be "Binary"
+ $module.RootModule | Should -Not -BeNullOrEmpty
+
+ ## The type 'Test.Isolated.Nested.Foo' from the nested module can be resolved and should be from the same load context.
+ $context1 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext($nestedCmd.ImplementingType.Assembly)
+ $context2 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext([Test.Isolated.Nested.Foo].Assembly)
+ $context1.Name | Should -BeExactly "MyCustomALC"
+ $context1 | Should -Be $context2
+
+ ## Test-NestedCommand depends on NewtonSoft.Json 10.0.0.0 while PowerShell depends on 13.0.0.0 or higher.
+ ## The exact version of NewtonSoft.Json should be loaded to the custom ALC.
+ $foo = [Test.Isolated.Nested.Foo]::new("Hello", "World")
+ Test-NestedCommand -Param $foo | Should -BeExactly "Hello-World-Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed"
+
+ ## The type 'Test.Isolated.Root.Red' from the root module can be resolved and should be from the same load context.
+ $context3 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext($rootCmd.ImplementingType.Assembly)
+ $context4 = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext([Test.Isolated.Root.Red].Assembly)
+ $context3.Name | Should -BeExactly "MyCustomALC"
+ $context3 | Should -Be $context4
+ $context3 | Should -Be $context1
+
+ ## No type identity issue in parameter binding because they are from the same assembly instance.
+ $red = [Test.Isolated.Root.Red]::new("RED!")
+ Test-RootCommand -Param $red | Should -BeExactly "RED!"
+
+ ## Removing the module should have its assemblies removed from 'Context.AssemblyCache' and results in the 'TypeNotFound'
+ ## error when trying to resolve the following 2 types:
+ ## - 'Test.Isolated.Nested.Bar', from 'Test.Isolated.Nested.dll'
+ ## - 'Test.Isolated.Root.Yellow', from 'Test.Isolated.Root.dll'
+ ## The types cannot be found because:
+ ## 1. they are from a load context that is not visible to PowerShell,
+ ## 2. those two assemblies have been removed from the cache when unloading the module.
+ ## [Test.Isolated.Nested.Foo] and [Test.Isolated.Root.Red] can still be found because they were added to type cache when
+ ## successfully resolved above.
+ Remove-Module IsolatedModule
+ { [Test.Isolated.Nested.Bar] } | Should -Throw -ErrorId "TypeNotFound"
+ { [Test.Isolated.Root.Yellow] } | Should -Throw -ErrorId "TypeNotFound"
+ }
+
+ It "WSMan and Certificate providers should reference the manifest module instead of the nested module" -Skip:(!$IsWindows) {
+ $wsManModule = Import-Module Microsoft.WSMan.Management -PassThru
+ $securityModule = Import-Module Microsoft.PowerShell.Security -PassThru
+
+ $wsManModule.ModuleType | Should -Be "Manifest"
+ $securityModule.ModuleType | Should -Be "Manifest"
+
+ ## For engine providers, the 'Module' property should point to top-level module, instead of the nested module.
+ $wsManProvider = Get-PSProvider WSMan
+ $certificateProvider = Get-PSProvider Certificate
+
+ $wsManProvider.Module | Should -Be $wsManModule
+ $certificateProvider.Module | Should -Be $securityModule
+ }
+}
diff --git a/test/powershell/engine/Module/TestModuleManifest.Tests.ps1 b/test/powershell/engine/Module/TestModuleManifest.Tests.ps1
index 2b84e75f90..091b968c27 100644
--- a/test/powershell/engine/Module/TestModuleManifest.Tests.ps1
+++ b/test/powershell/engine/Module/TestModuleManifest.Tests.ps1
@@ -18,12 +18,21 @@ Describe "Test-ModuleManifest tests" -tags "CI" {
New-Item -ItemType Directory -Path testdrive:/module/foo > $null
New-Item -ItemType Directory -Path testdrive:/module/bar > $null
+
New-Item -ItemType File -Path testdrive:/module/foo/bar.psm1 > $null
+ New-Item -ItemType File -Path testdrive:/module/foo/bar.ps1 > $null
+ New-Item -ItemType File -Path testdrive:/module/foo/bar.ps1xml > $null
+
New-Item -ItemType File -Path testdrive:/module/bar/foo.psm1 > $null
+ New-Item -ItemType File -Path testdrive:/module/bar/foo.ps1 > $null
+ New-Item -ItemType File -Path testdrive:/module/bar/foo.ps1xml > $null
+
$testModulePath = "testdrive:/module/test.psd1"
$fileList = "foo\bar.psm1","bar/foo.psm1"
+ $scripts = "foo\/bar.ps1","bar/\foo.ps1"
+ $ps1xml = "foo//bar.ps1xml","bar\\foo.ps1xml"
- New-ModuleManifest -NestedModules $fileList -RootModule foo\bar.psm1 -RequiredAssemblies $fileList -Path $testModulePath -TypesToProcess $fileList -FormatsToProcess $fileList -ScriptsToProcess $fileList -FileList $fileList -ModuleList $fileList
+ New-ModuleManifest -NestedModules $fileList -RootModule foo\bar.psm1 -RequiredAssemblies $fileList -Path $testModulePath -TypesToProcess $ps1xml -FormatsToProcess $ps1xml -ScriptsToProcess $scripts -FileList $fileList -ModuleList $fileList
Test-Path $testModulePath | Should -BeTrue
diff --git a/test/tools/Modules/IsolatedModule/IsolatedModule.psd1 b/test/tools/Modules/IsolatedModule/IsolatedModule.psd1
new file mode 100644
index 0000000000..2bbe183c24
--- /dev/null
+++ b/test/tools/Modules/IsolatedModule/IsolatedModule.psd1
@@ -0,0 +1,16 @@
+#
+# Module manifest for module 'IsolatedModule'
+#
+
+@{
+ ModuleVersion = '0.0.1'
+ GUID = '20d4742b-b17d-4ce8-b8da-29b25433cd18'
+ Author = 'Microsoft Corporation'
+
+ RootModule = 'Test.Isolated.Root.dll'
+ NestedModules = @('Test.Isolated.Init.dll', 'Test.Isolated.Nested.dll')
+ FunctionsToExport = @()
+ CmdletsToExport = @('Test-NestedCommand', 'Test-RootCommand')
+ VariablesToExport = '*'
+ AliasesToExport = @()
+}
diff --git a/test/tools/TestAlc/TestAlc.sln b/test/tools/TestAlc/TestAlc.sln
new file mode 100644
index 0000000000..c97dde9eb9
--- /dev/null
+++ b/test/tools/TestAlc/TestAlc.sln
@@ -0,0 +1,34 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30114.105
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Isolated.Init", "init\Test.Isolated.Init.csproj", "{87AFE044-D21A-4ECE-A8F7-9A31678811D9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Isolated.Nested", "nested\Test.Isolated.Nested.csproj", "{702DAF12-2A79-4756-8E2A-E18B46639AD3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Isolated.Root", "root\Test.Isolated.Root.csproj", "{9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87AFE044-D21A-4ECE-A8F7-9A31678811D9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {702DAF12-2A79-4756-8E2A-E18B46639AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {702DAF12-2A79-4756-8E2A-E18B46639AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {702DAF12-2A79-4756-8E2A-E18B46639AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {702DAF12-2A79-4756-8E2A-E18B46639AD3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D0B1FAD-52F7-436A-AB9D-5A53E370C72B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/test/tools/TestAlc/init/Init.cs b/test/tools/TestAlc/init/Init.cs
new file mode 100644
index 0000000000..4241e56fa4
--- /dev/null
+++ b/test/tools/TestAlc/init/Init.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Management.Automation;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Test.Isolated.Init
+{
+ internal class CustomLoadContext : AssemblyLoadContext
+ {
+ private readonly string _dependencyDirPath;
+
+ public CustomLoadContext(string dependencyDirPath)
+ : base("MyCustomALC", isCollectible: false)
+ {
+ _dependencyDirPath = dependencyDirPath;
+ }
+
+ protected override Assembly Load(AssemblyName assemblyName)
+ {
+ // We do the simple logic here of looking for an assembly of the given name
+ // in the configured dependency directory.
+ string assemblyPath = Path.Combine(_dependencyDirPath, $"{assemblyName.Name}.dll");
+
+ if (File.Exists(assemblyPath))
+ {
+ // The ALC must use inherited methods to load assemblies.
+ // Assembly.Load*() won't work here.
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ // For other assemblies, return null to allow other resolutions to continue.
+ return null;
+ }
+ }
+
+ public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
+ {
+ private static readonly CustomLoadContext s_context;
+ private static readonly HashSet s_moduleAssemblies;
+
+ static Init()
+ {
+ string dependencyDirPath = Path.Combine(Path.GetDirectoryName(typeof(Init).Assembly.Location), "Dependencies");
+ s_context = new CustomLoadContext(dependencyDirPath);
+ s_moduleAssemblies = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Test.Isolated.Nested",
+ "Test.Isolated.Root"
+ };
+ }
+
+ public void OnImport()
+ {
+ // Add the Resolving event handler here.
+ AssemblyLoadContext.Default.Resolving += ResolveAlcEngine;
+ }
+
+ public void OnRemove(PSModuleInfo psModuleInfo)
+ {
+ // Remove the Resolving event handler here.
+ AssemblyLoadContext.Default.Resolving -= ResolveAlcEngine;
+ }
+
+ private static Assembly ResolveAlcEngine(AssemblyLoadContext defaultAlc, AssemblyName assemblyToResolve)
+ {
+ // We only want to resolve our module assemblies here.
+ if (s_moduleAssemblies.Contains(assemblyToResolve.Name))
+ {
+ // This is where the nested module 'Test.Isolated.Nested.dll' and the root module 'Test.Isolated.Root.dll'
+ // gets loaded into our custom ALC and then passed through into PowerShell's ALC.
+ return s_context.LoadFromAssemblyName(assemblyToResolve);
+ }
+
+ // Let the resolution chain continue for other assemblies.
+ return null;
+ }
+ }
+}
diff --git a/test/tools/TestAlc/init/Test.Isolated.Init.csproj b/test/tools/TestAlc/init/Test.Isolated.Init.csproj
new file mode 100644
index 0000000000..c1a291fa55
--- /dev/null
+++ b/test/tools/TestAlc/init/Test.Isolated.Init.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ false
+ None
+
+
+ false
+
+
+ ..\..\Modules\IsolatedModule
+
+
+
+
+
+
+
diff --git a/test/tools/TestAlc/nested/NestedCommand.cs b/test/tools/TestAlc/nested/NestedCommand.cs
new file mode 100644
index 0000000000..8882292013
--- /dev/null
+++ b/test/tools/TestAlc/nested/NestedCommand.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Management.Automation;
+using Newtonsoft.Json;
+
+namespace Test.Isolated.Nested
+{
+ [Cmdlet("Test", "NestedCommand")]
+ public class TestNestedCommand : PSCmdlet
+ {
+ [Parameter(Mandatory = true)]
+ public Foo Param { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ WriteObject($"{Param.Name}-{Param.Path}-{typeof(StringEscapeHandling).Assembly.FullName}");
+ }
+ }
+
+ public class Foo
+ {
+ public string Name { get; }
+
+ public string Path { get; }
+
+ public Foo(string name, string path)
+ {
+ Name = name;
+ Path = path;
+ }
+ }
+
+ public class Bar
+ {
+ public string Id { get; }
+
+ public Bar(string id)
+ {
+ Id = id;
+ }
+ }
+}
diff --git a/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj b/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj
new file mode 100644
index 0000000000..2f4a64332b
--- /dev/null
+++ b/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ false
+ None
+
+
+ false
+
+
+ ..\..\Modules\IsolatedModule\Dependencies
+
+
+
+
+
+
+
+
diff --git a/test/tools/TestAlc/root/RootCommand.cs b/test/tools/TestAlc/root/RootCommand.cs
new file mode 100644
index 0000000000..74aa40006f
--- /dev/null
+++ b/test/tools/TestAlc/root/RootCommand.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Management.Automation;
+
+namespace Test.Isolated.Root
+{
+ [Cmdlet("Test", "RootCommand")]
+ public class TestRootCommand : PSCmdlet
+ {
+ [Parameter(Mandatory = true)]
+ public Red Param { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ WriteObject(Param.Name);
+ }
+ }
+
+ public class Red
+ {
+ public string Name { get; }
+
+ public Red(string name)
+ {
+ Name = name;
+ }
+ }
+
+ public class Yellow
+ {
+ public string Id { get; }
+
+ public Yellow(string id)
+ {
+ Id = id;
+ }
+ }
+}
diff --git a/test/tools/TestAlc/root/Test.Isolated.Root.csproj b/test/tools/TestAlc/root/Test.Isolated.Root.csproj
new file mode 100644
index 0000000000..ab333e0668
--- /dev/null
+++ b/test/tools/TestAlc/root/Test.Isolated.Root.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ false
+ None
+
+
+ false
+
+
+ ..\..\Modules\IsolatedModule\Dependencies
+
+
+
+
+
+
+