1

I have a need to use JsonConverter class in System.Text.Json in a module.

I would like the module to work in both Windows PowerShell 5.1 and PowerShell 7.1.4.

I have created a minimal cmdlet using the psmodule template and .NET CLI.

This is what is inside of my csproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
    <AssemblyName>sopost</AssemblyName>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.0">
      <PrivateAssets>All</PrivateAssets>
    </PackageReference>
    <PackageReference Include="System.Text.Json" Version="5.0.0" />
  </ItemGroup>
</Project>

Steps to reproduce:

dotnet new -i Microsoft.PowerShell.Standard.Module.Template
dotnet new psmodule
dotnet add package System.Text.Json --version 5.0.0
 
Update targetframeworks to "netcoreapp2.0;net461"
Replace template cs file with contents from link below

dotnet build
dotnet publish -f netcoreapp2.0
dotnet publish -f net461

I can then import the module using PowerShell 7.1.4 from either of the publish directories and the module functions correctly.

PS C:\Dev\sopost\bin\Debug> Test-SampleCmdlet -FavoriteNumber 42 -FavoritePet Horse { "FavoritePet": "Horse", "FavoriteNumber": 42 }

I can import the module using Windows PowerShell 5.1 from the 461\publish directory, but the cmdlet will generate an exception when it actually hits the code that deals with System.Text.Json.

Test-SampleCmdlet : Could not load file or assembly 'System.Runtime.CompilerServices.Unsafe, Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.

Here is a link to the single cs file that is the source for this cmdlet: cmdlet source

This is a very simple example of using System.Text.Json, but contains enough code to repro my issue.

Is there any easy way to accomplish what I'm trying to do? Is it possible to have a single module that can be used in both Windows PowerShell 5.1 and PowerShell 7.1.4 ? If yes, what is the directory layout of that module?

Is it possible to have a Windows PowerShell 5.1 module that uses System.Text.Json ?

Any advice / pointers would be greatly appreciated!

Ash
  • 3,030
  • 3
  • 15
  • 33
PshMike
  • 11
  • 3
  • Looks similar to https://stackoverflow.com/a/60610905/7411885 ? Check if the bindings are getting generated correctly in `appName.dll.config`. Another direction is to make sure `CompilerServices` is registered into GAC for powershell to be able to import it: https://stackoverflow.com/a/62770487/7411885 – Cpt.Whale Sep 30 '21 at 19:21

1 Answers1

1

The binding redirection file is not used by PowerShell as far as I know. I would try adding the required DLLs to the GAC as suggested in the comments.

One thing worth noting is that the minimum version of .NET Framework supported with System.Text.Json is 4.7.2.

If this is an issue or you have issues then you can dynamically bind the required assemblies when loading the module, but it is not ideal as the load might succeed in future imports if the required version is in the GAC, loaded by PowerShell. or already in the AppDomain and cause type conflicts because of version incompatibility.

using System;
using System.IO;
using System.Management.Automation;
using System.Reflection;

namespace YourNamespace
{
#if NET472
    public class UnsafeAssemblyHandler : IModuleAssemblyInitializer, IModuleAssemblyCleanup
    {
        private static readonly string s_asmLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        public void OnImport()
        {
            AppDomain.CurrentDomain.AssemblyResolve += HandleAssemblyResolve;
        }

        public void OnRemove(PSModuleInfo psModuleInfo)
        {
            AppDomain.CurrentDomain.AssemblyResolve -= HandleAssemblyResolve;
        }

        private static Assembly HandleAssemblyResolve(object sender, ResolveEventArgs args)
        {
            var requiredAssembly = new AssemblyName(args.Name);

            string possibleAssembly = Path.Combine(s_asmLocation, $"{requiredAssembly.Name}.dll");

            AssemblyName bundledAssembly = null;
            try
            {
                bundledAssembly = AssemblyName.GetAssemblyName(possibleAssembly);
            }
            catch
            {
                return null;
            }

            if (bundledAssembly.Version < requiredAssembly.Version)
            {
                return null;
            }

            return Assembly.LoadFrom(possibleAssembly);
        }
    }
#endif
}

Sample .csproj file where I use this.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net472;net5.0</TargetFrameworks>
    <LangVersion>latest</LangVersion>
    <RootNamespace>MyProject</RootNamespace>
    <AssemblyName>MyProject</AssemblyName>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="PowerShellStandard.Library" Version="5.1.0"/>
    <PackageReference Include="System.Text.Json" Version="5.0.2"/>
  </ItemGroup>
</Project>

I think this may help as a more robust solution that avoids any conflicts on .NET Core/5/6 and .NET Framework.

Assembly resolve handler for side-by-side loading with Assembly.LoadFile()

I am not sure there is a perfect answer here, but there are solutions that work and there are on-going conversations about dealing with dependency resolution in PowerShell.

Hope this helps you find what works for your module.

Ash
  • 3,030
  • 3
  • 15
  • 33