12

I've created a class called "Application" and loaded it in my main script with:

Import-Module -NAME "C:\PowerShell_Scripts\Class\Application.ps1" -GLOBAL -FORCE;

However if I ONLY make changes to the class file and run the code in PowerShell ISE none of the changes are applied. It's almost as if the class is still in memory even though I've used -FORCE.

I've also tried to remove the module before loading it and the same issue happens:

Remove-Module "Application" -ErrorAction Ignore -FORCE;
Import-Module -NAME "C:\PowerShell_Scripts\Class\Application.ps1" -GLOBAL -FORCE;

If I make a single character change in my main script then it reloads the class! But I shouldn't have to modify the main script to force PowerShell to reload the class, that just seems silly.

Is there a way to remove the Application class from memory if it exists?

NOTE: Files with just functions in them work file. This only applies to Class imports.

Addition: In the console, if I run the Remove-Module command it runs successfully but I can STILL create new objects with:

$appDetails = [Application]::new($applicationID);

Doesn't make sense to me...

MAIN SCRIPT:

# Application Details
# -----------------
  #ID
  $applicationID     = 1;

############################################
#
# Load Supporting Scripts
#
############################################

try
{
    Remove-Module "Application" -ErrorAction Ignore -FORCE;
    Remove-Module "Common" -ErrorAction Ignore -FORCE;
    Remove-Module "ServerData" -ErrorAction Ignore -FORCE;

    Import-Module -NAME "C:\PowerShell_Scripts\Common.ps1" -GLOBAL -FORCE;
    Import-Module -NAME "C:\PowerShell_Scripts\ServerData.ps1" -GLOBAL -FORCE;
    Import-Module -NAME "C:\PowerShell_Scripts\Class\Application.ps1" -GLOBAL -FORCE;
}
catch
{
    Write-Host "`nError: Cannot load required PowerShell scripts. Ensure C:\PowerShell_Scripts\ exists and has the required files." -ForegroundColor Red;

    EXIT;
}

############################################
#
# Load the SharePoint Snapin Module.
#
############################################

LoadSharePointModule;
############################################
#
# Display component details to user.
#
############################################

#Create object of "Application" to get app details based on the ID.
$appDetails = [Application]::new($applicationID);

Write-Host "Ending ......";

APPLICATION CLASS FILE

Class Application
{
    #Class Properties
    [STRING] $appName;

    [INT32] $appID;
    [INT32] $versionMajor;
    [INT32] $versionOS;
    [INT32] $versionCentraAdmin;
    [INT32] $versionMain;
    [INT32] $versionGUI;
    [INT32] $versionWorkflow;
    [INT32] $versionForm;
    [INT32] $versionVS;
    [INT32] $versionOther;
    [INT32] $versionFull;

    [OBJECT] $spDevSite;
    [OBJECT] $versionList;

    #Constructor: Setup class properties.
    Application ([INT32] $appID)
    {
        Write-Host "`nGathering application details ..." -ForegroundColor Yellow;

        try
        {
            #Get the SharePoint Developer site Object.
            $this.spDevSite = Get-SPWeb -ErrorAction Stop $GLOBAL:spDevURL;
        }
        catch
        {
            Write-Host "`nUnable to connect to SharePoint Developer site!: $($GLOBAL:spDevURL)";

            #EXIT;
        }

        #Assign class property.
        $this.appID = $appID;
    }

}

I have deliberately set the URL for $GLOBAL:spDevURL; so that the Constructor fails for this test. It fails normally and displays

Write-Host "`nUnable to connect to SharePoint Developer site!: $($GLOBAL:spDevURL)";

But if I make a change to this line and run the script, the change is not applied.

Vidarious
  • 746
  • 2
  • 10
  • 22
  • Does the same thing happen if you run your script containing the `Import` command twice? I notice the same behaviour as you on the first run, but after running the script again it appears to update – Bassie Mar 16 '17 at 15:35
  • Nope, if I make a change to the Application class file then run the main script 100 times it will never show the change. However if I had a single character anywhere in the main script (like a comment) then it refreshes and loads the new class properly. – Vidarious Mar 16 '17 at 15:40
  • Are you able to update your question with both the script and the class, or are they massive files? It may also help if you post the output of `Get-Module` – Bassie Mar 16 '17 at 15:42
  • I've tried Remove-Variable, Remove-Item on [Application]. Still .. if I type [Application] in the console it still shows me it present with a base type of "System.Object". Even after I remove the module as well. – Vidarious Mar 16 '17 at 15:43
  • Added Scripts for more details. – Vidarious Mar 16 '17 at 15:49
  • Is `[Application]` still available after you close the ISE or PowerShell window, re-open it and try again (after removing the module)? This might be an issue with powershell sessions. – Bassie Mar 16 '17 at 16:06
  • 1
    Possible duplicate of [PowerShell 5 and classes - Cannot convert the "X" value of type "X" to type "X"](http://stackoverflow.com/questions/36804102/powershell-5-and-classes-cannot-convert-the-x-value-of-type-x-to-type-x) – TechSpud Mar 16 '17 at 16:17
  • 1
    Can't re-load a class, once in a session - see http://stackoverflow.com/a/36812564/1368849 – TechSpud Mar 16 '17 at 16:18
  • 1
    Yep, looks right. Changing to $appDetails = New-Object -ErrorAction Stop Application($applicationID) works well. – Vidarious Mar 16 '17 at 17:29
  • @Vidarious That is great - you could even add that as an answer and accept it as it may help other users. Not sure if this really counts as a duplicate as the question asked is different, so it probably helps to have it up here – Bassie Mar 16 '17 at 18:07

1 Answers1

20

The Known Issue

There is a known issue in PowerShell 5.0 and 5.1 that explains this behavior. The issue was acknowledged by DongBo Wang on the PowerShell 6 team in November 2016. He wrote the following:

"The module analysis result is stored in a cache with the module file path as the key and the PSModuleInfo object as the value. The cache entries are not properly invalidated based on the LastWriteTime of the module file, and thus same cached value got reused."

In other words, PowerShell 5.0, 5.1, and 6.0 keeps (and uses) old copies of classes in memory when it shouldn't.

Implications

This issue causes considerable problems for development using PowerShell classes if you do not compensate for it. I wrote a test that covers about 100 of the scenarios where class reloading is important. Vaguely speaking, in about 17 of those scenarios PowerShell 5.0 and 5.1 doesn't reload the class when it should. This means using the same session across edits creates a real likelihood the interpreter will have cached duplicate copies of the same or similar classes. That makes behavior unpredictable and causes strange results that cannot be troubleshot.

Workaround

I have found that you can still be productive developing using PowerShell classes. You just need to perform each test run in a fresh PowerShell session when a project involves PowerShell classes whose source the PowerShell interpreter may consider to have changed. The customary way to do this is to invoke your test command from your PowerShell console by invoking powershell.exe:

powershell.exe -Command { Invoke-Pester }

That's not a terribly inefficient test-edit-test cycle if you've got tight unit tests. If you need to step through code, you'll need to launch a fresh copy of ISE each time you make an edit.

With this workaround, I have found the productivity impact of this bug to be manageable. I developed this and this entirely using this workaround. Each of those projects involve a significant amount of code involving PowerShell classes.

alx9r
  • 3,675
  • 4
  • 26
  • 55