In a Blazor app, I want to load an external assembly and execute a method. For this, I have created a new ASP.Net Core webapplication using the Blazor template.
Then, in a Razor Page (which will be compiled and executed by browser/wasm) I use reflection to load the assembly and run the method (based on code found here)
// download external assembly from server
HttpClient client = new HttpClient();
var bytes = await client.GetByteArrayAsync("http://localhost:62633/_framework/MyCustomLib.dll");
//load assembly
var assembly = System.Reflection.Assembly.Load(bytes);
// get type/method info
var type = assembly.GetType("MyCustomLib.MyCustomClass");
var method = type.GetMethod("WriteSomething");
// instantiate object and run method
object classInstance = Activator.CreateInstance(type, null);
method.Invoke(classInstance, null);
The method WriteSomething contains a single Console.WriteLine() which prints something in the browser's console, thanks to blazor/mono.wasm goodness. The complete code in this library is:
namespace MyCustomLib
{
public class MyCustomClass
{
public void WriteSomething()
{
System.Console.WriteLine("This is printed from a loaded dll 3 !");
}
}
}
Result:
As you can see, this works great when MyCustomLib.dll is built as a .NET Framework Class Library. However, I want to use a .NET Standard Class Library.
When I build a MyCustomLib.dll as a .NET Standard 2.0 library, and execute the same blazor app, I get the following error in the browser's console:
Could not load file or assembly 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies.
I would expect mono.wasm would have loaded the necessary dependencies to support .NET Standard assemblies.
Loading the assembly into the AppDomain yields the same result.
var assembly = AppDomain.CurrentDomain.Load(bytes);
Switching down to netstandard 1.6 gives me a similar error, this time about System.Runtime (because mono.wasm expects Mono.Runtime I assume).
Maybe there's a way to perform LoadAssembly on the assemblies referenced by the netstandard2.0 package, but I wouldn't know how.
How can I load a .NET Standard 2.0 into the browser environment using Blazor?
After doing some further investigation, I've concluded that my problem is that my external library is not properly linked to the mono.net dependencies. This is why, when you build a Blazor app, it is compiled a second time to /dist/_framework/_bin.
I've found three possible solutions to this problem:
1. Turn the external class library into a Blazor Web app
This way, your app will automatically be converted to a mono-compatible assembly when built. A simple peek in to a Blazor .csproj shows the dependencies needed to achieve this. For it to work, I had to change the .csproj of my external assembly:
from a default netstandard library:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
into a web app:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RunCommand>dotnet</RunCommand>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="0.7.0" PrivateAssets="all" />
</ItemGroup>
</Project>
These are the only dependencies needed. On build, the compatible assembly will be found in the /dist/_framework/_bin folder. It can then be loaded using the methods described in the question.
This works, but feels a bit hacky because the only reason we're turning the library into a web app is so that it can compile itself into a properly linked assembly.
2. Load the netstandard2.0 mono facade
Another solution is to unzip the Nuget Package from Microsoft.AspNetCore.Blazor.Build and grab the netstandard.dll. It's found in the tools\mono\bcl\Facades folder. Now, when doing the following in the main Blazor app:
var netstandard = await client.GetByteArrayAsync("http://localhost:62633/_framework/netstandard.dll");
var externallib = await client.GetByteArrayAsync("http://localhost:62633/_framework/MyCustomLib.dll");
AppDomain.CurrentDomain.Load(netstandard);
var assembly = AppDomain.CurrentDomain.Load(externallib);
then the unmodified netstandard 2.0 library MyCustomLib will be loaded without errors.
No need to change it to a web app
This works, but it feels even hackier than the first solution, unsure whether this will fail later along the way...
3. Use the Blazor Build tools
The Blazor Build tools, currently found here, they have a ResolveRuntimeDependenciesCommand command for the CLI which seems to do exactly what a blazor web app is doing when it spits output to /_framework/_bin.
I'm still looking at how this could be used to convert a "non blazor-webapp" assembly into a mono-compatible one.
Feel free to comment or answer with additional information. I'm leaving this question open until a "cleaner" solution is found.
This is caused by the blazor linker stripping out the dependency to netstandard since it's not explicitly used in the main WebAssemblyHost project.
When dynamically loading a DLL that uses netstandard, it will assume that it was already loaded and crash.
If you plan to load assemblies dynamically, I recommend using
<BlazorWebAssemblyEnableLinking>false</BlazorWebAssemblyEnableLinking>
to ensure non of the base dependencies is stripped out.
Related
I wrapped into a single .netstandard 2.0 project a logger which uses Serilog dlls. One of them is Serilog.Settings.AppSettings. We could call this project Standard.Logger. I had to specify these lines into the project to put the nuget dlls into the correct place.
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
Whithout them in bin/Debug there aren't the nuget dlls. This project has the init of the logger Serilog
public static void Init(string context)
{
Serilog.Log.Logger = new LoggerConfiguration().
WriteTo.Async(l => l.File($"C:\\log\\serilog.{context}.{Process.GetCurrentProcess().Id}.log",
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] ({Properties}) {Message}{NewLine}{Exception}",
fileSizeLimitBytes: 20971520, // 20Megabytes
retainedFileCountLimit: 5,
rollOnFileSizeLimit: true,
buffered: false)
).
ReadFrom.AppSettings(). // PROBLEM IF I CALL IT
CreateLogger();
}
The call of the init of the logger is into another .netstandard 2.0 project. We could call this project Standard.Base. This project has also installed System.Configuration.ConfigurationManager 6.0 to read the configuration from app.config. (I didn't find another nuget package that works).
Everything works fine, also other referenced .NET Framework projects that uses Standard.Base with the init of Standard.Log.
The problem comes from a COM+ library, when it starts he ask for another version of System.Configuration.Manager that obviously there isn't. In Event Viewer there are this errors:
Could not load file or assembly 'System.Configuration.ConfigurationManager, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. The system cannot find the file specified.
and the following (I must obscure sensibly company data)
Got it, this COM+ in .Net Framework try to load a different version of Serilog.Settings.AppSettings (2.0.0.0 instead of 2.2.2) and System.Configuration.ConfigurationManager (4.0.0.0 instead of 4.4.1 neeeded by Serilog.Settings.AppSettings). But why?
I tried to install into Standard.Log the System.Configuration.ConfigurationManager 6.0 and I would expect the every other project can understand it. All the projects but not COM+ project.
I can't find a solution for this DLL hell, I read this https://nickcraver.com/blog/2020/02/11/binding-redirects/ I got it's the binding redirect but it doesn't solve my problems
Any other suggestions?
I found the solution it works.
Binding redirect it isn't easy with COM+ and dllhost, but in the COM+ project we can add this
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
This can force to load the assembly and there aren't problems about version.
I have found an interesting case which I can't fully understand.
My runtime - PowerShell 7.2.4
I have two PowerShell Core modules: Net6PowerShellModule and NetstandardPowerShellModule.
Net6PowerShellModule.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<None Remove="Net6PowerShellModule.psd1" />
</ItemGroup>
<ItemGroup>
<Content Include="Net6PowerShellModule.psd1">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="System.Management.Automation" Version="7.2.4" />
</ItemGroup>
</Project>
Test-Net6Cmdlet.cs
using System.Management.Automation;
namespace Net6PowerShellModule
{
[Cmdlet("Test", "Net6Cmdlet")]
public class TestNet6Cmdlet : PSCmdlet
{
protected override void ProcessRecord()
{
var type = typeof(Microsoft.Extensions.DependencyInjection.IServiceCollection);
WriteObject("Net6 Cmdlet Run.");
}
}
}
Net6PowerShellModule.psd1
#{
RootModule = 'Net6PowerShellModule.dll'
ModuleVersion = '0.0.1'
GUID = '8c1bd929-32bd-44c3-af6b-d9dd261e34f3'
CmdletsToExport = 'Test-Net6Cmdlet'
}
NetstandardPowerShellModule.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<None Remove="NetstandardPowerShellModule.psd1" />
</ItemGroup>
<ItemGroup>
<Content Include="NetstandardPowerShellModule.psd1">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
<PackageReference Include="System.Management.Automation" Version="6.1.2" />
</ItemGroup>
</Project>
Test-NetstandardCmdlet.cs
using System.Management.Automation;
namespace NetstandardPowerShellModule
{
[Cmdlet("Test", "NetstandardCmdlet")]
public class TestNetstandardCmdlet : PSCmdlet
{
protected override void ProcessRecord()
{
var type = typeof(Microsoft.Extensions.DependencyInjection.IServiceCollection);
WriteObject("Netstandard 2.0 Cmdlet Run.");
}
}
}
NetstandardPowerShellModule.psd1
#{
RootModule = 'NetstandardPowerShellModule.dll'
ModuleVersion = '0.0.1'
GUID = 'eef615d0-aecf-4e89-8f4c-53174f8c99d6'
CmdletsToExport = 'Test-NetstandardCmdlet'
}
Next, I have two possible use cases for those modules:
Import Net6PowerShellModule
Import NetstandardPowerShellModule
Run Test-Net6Cmdlet
Run Test-NetstandardCmdlet
Result - everything runs correctly. The only loaded assembly is Microsoft.Extensions.DependencyInjection.Abstractions version 6.0.0.
Import Net6PowerShellModule
Import NetstandardPowerShellModule
Run Test-NetstandardCmdlet
Run Test-Net6Cmdlet.
Result - error: Test-Net6Cmdlet: Could not load file or assembly 'Microsoft.Extensions.DependencyInjection.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'. Could not find or load a specific file. (0x80131621)
Based on this question, this is not an expected behavior:
Subsequent attempts to load a different version of the assembly:
cause a statement-terminating error in Powershell (Core) 7+
Assembly with same name is already loaded.
Why does the first scenario work then?
If I add the RequiredAssemblies = #('Microsoft.Extensions.DependencyInjection.Abstractions.dll') line in both module manifests, I got an expected behavior: regardless of which module is loaded first, I still got an error: Assembly with same name is already loaded.
Update 1
Articles that I've already looked at:
https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts
PowerShell: why am I able to load multiple versions of the same .NET assembly in one session and also "bypass Dependency hell"?
https://devblogs.microsoft.com/powershell/resolving-powershell-module-assembly-dependency-conflicts/ - this article explains a lot about Assembly Load Contexts, but I still cannot understand the difference between having RequiredAssemblies defined in .psd1 files or using the dependencies in code.
tl;dr
When authoring a module, it is best to use a module manifest (.psd1 file) with a RequiredAssemblies key to declare dependent (helper) assemblies, because it makes loading of such assemblies predictable, because it happens at the time of module import.
In Windows PowerShell, attempts to load a different version of an already-loaded assembly are quietly ignored and the newly imported module is made to use the already-loaded assembly too, which may or may not work.
In PowerShell (Core) v6+, attempts to load a different version of an already-loaded assembly are categorically prevented.
If you use implicit loading of dependent assemblies, based on .NET's mechanisms, the timing of when these dependencies are loaded isn't guaranteed, and, in PowerShell Core, you'll bypass the check that prevents attempts to load a different version of an already-loaded assembly - which may or may not work and is only allowed if the later load attempt requests a lower version than the one already loaded.
Non-trivial workarounds are needed to reliably load different versions of a given assembly side by side.
Background information
One of your links, Resolving PowerShell module assembly dependency conflicts, contains all relevant information, but it's a lengthy read with complex, in-depth information, and it doesn't clarify the way in which PowerShell (Core) (v6+) overrides the behavior of the underlying .NET (Core) / .NET 5+ framework.
Terminology: For simplicity, I'll use the following terms:
Windows PowerShell is the legacy, ships-with-Windows Windows-only edition of PowerShell whose latest and final version is 5.1.x.
It is built on top of .NET Framework, the legacy, Windows-only edition of .NET, whose latest and last version is v4.8.x
PowerShell Core (officially now just PowerShell) is the modern, cross-platform, install-on-demand PowerShell edition, whose first (but now obsolete) version was v6 and whose current version as of this writing is v7.2.6.
It is built on top of .NET Core (officially since v5 just .NET), the modern, cross-platform, install-on-demand edition of .NET.
The underlying problem:
At the .NET framework / CLR (Common Language Runtime) level, the fundamental problem is the inability to load different versions of a given assembly side by side into the same application domain (.NET Framework) / ALC (assembly-load context, .NET Core): the version that is loaded first into a session quietly wins, with .NET Core imposing the additional restriction that only attempts to load lower versions succeed (see below).
PowerShell uses only a single application domain / ALC, including for modules, necessitating workarounds to enable side-by-side loading of different versions of a (dependent) assembly. The linked article details various workarounds; a robust, generic, in-process workaround requires PowerShell Core and is limited to binary (compiled) modules and is nontrivial; limited workarounds, some out-of-process, are available for Windows PowerShell and cross-PowerShell-edition code.
Behavior without a workaround:
Windows PowerShell and .NET Framework:
.NET Framework quietly ignores subsequent attempts to load a different version of an already-loaded assembly, and makes the caller use the already-loaded version, irrespective of whether the requested version is higher or lower than the already loaded one.
Curiously, when the caller uses reflection with typeof to inspect the assembly and its types it just loaded (whether implicitly or explicitly), the requested assembly version is reported - even though it is the already loaded assembly and its types that are actually used.
Windows PowerShell does not overlay this behavior with custom functionality, although whether or not you use a module manifest (.psd1 file) with a RequiredAssemblies entry can make a difference with respect to when dependent assemblies are loaded (see below).
Upshot:
Attempts to load a different version of an already-loaded assembly never fail and quietly make the caller use the already-loaded version.
This may or may not work, and you'll only notice at the time the types from the assembly in question are actually used (construction of an instance, property access, method calls, ...).
Notably, it is likely to fail if a later caller attempts to load a higher version of an assembly, as it may rely on features that the already-loaded lower version doesn't have, but even the inverse scenario isn't guaranteed to succeed, given that a higher version may have introduced breaking changes.
PowerShell Core and .NET Core:
.NET Core:
Quietly ignores subsequent attempts to load a lower version of an already-loaded assembly, and makes the caller use the already-loaded version
Throws an exception for attempts to load a higher version: Could not find or load a specific file. (0x80131621)
The rationale appears to be: an already-loaded higher version is at least likely to be backward-compatible with the requested lower version - although, in the absence of semantic versioning for .NET assemblies - this is not guaranteed.
When no exception occurs, and the caller uses reflection with typeof to inspect the assembly and its types it just loaded (whether implicitly or explicitly), it does see the actual, already-loaded assembly version - unlike in .NET Framework.
PowerShell Core does overlay this behavior with custom functionality, unlike Windows PowerShell:
It defines a custom dependency handler for the plugin-oriented [System.Reflection.Assembly]::LoadFrom() .NET Core API, which it uses behind the scenes in the following contexts:
When loading a module with a module manifest (.psd1) that declares dependent assemblies via its RequiredAssemblies entry.
Calls to Add-Type with -Path / -LiteralPath.
in using assembly statements.
This custom dependency resolution handler categorically prevents attempts to load a different version of an already-loaded version, irrespective of which version number is higher.
A statement-terminating Assembly with same name is already loaded error occurs (in the case of using assembly, loading a script breaks in the parsing stage with Cannot load assembly '...').
Upshot:
In PowerShell Core, attempts to load a different version of an already-loaded assembly always fail if you let PowerShell make the load attempt.
Only if you use .NET Core's implicit dependency loading is there a chance that the load attempt succeeds, namely if an attempt is made to load a lower version later. Implicit dependency loading happens automatically, via the [System.Reflection.Assembly]::Load() .NET API, which is based on supplying a dependent assembly's (location-independent) full name and default probing paths for looking for the file hosting it, which notably includes the directory of the calling assembly itself.
This may or may not work, given that versioning of .NET assemblies doesn't use semantic versioning, so a higher version of an assembly isn't guaranteed to be backward-compatible.
Also, the timing of implicit loading of dependencies isn't predictable, and usually doesn't happen until the types from the assembly in question are actually used (construction of an instance, property access, method calls, ...). Thus, if you rely on your modules to use implicit dependency loading, there is no guaranteed relationship between when a module is imported vs. when actual dependency loading is attempted, so it may fail if a dependency with a lower version happens to be loaded first in a given session (but, as stated, even if the higher version is loaded first, you may see a later error when the types are actually used).
I am attempting to call Kentico Kontent using their DeliveryClient in a Kentico Portal project that doesn't have dependency injection. This project would like to migrate to Kontent but would like a new feature in the extisting project to be implemented with Kontent before the transition.
After installing the Kontent Delivery SDK, here's my code
var clientTest = DeliveryClientBuilder.WithProjectId("MyProjectId").Build();
I get a runtime error
System.IO.FileNotFoundException: 'Could not load file or assembly 'Microsoft.Extensions.DependencyInjection, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60' or one of its dependencies. The system cannot find the file specified.'
Obviously, this project doesn't have dependency injection and I hear setting it up in a portal project is difficult if not impossible. So I decide to new up my own delivery client as outlined in Kentico's blog post
var test = new DeliveryClient(options);
But DeliveryClient is marked internal: 'DeliveryClient' is inaccessible due to its protection level
How do I proceed?
Details
Kentico.Kontent.Delivery 12.3.0
Kentico version 12.0.22
.Net Framework 4.6.1
Although the Kentico.Kontent.Delivery 12.* targets netstandard2.0 which means it should be compatible with .NET Framework 4.6.1, there is evidence that this setup can cause issues. There are a few things that I would recommend before trying anything else:
Upgrade to .NET 4.8 (or at least to 4.7.2)
Enable Automatic Binding Redirection in your project by adding:
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
No matter how you instantiate/register the DeliveryClient (DeliveryClientBuilder services.AddDeliveryClient()), the SDK always uses Microsoft.Extensions.DependencyInjection internally, so there is no workaround. You will need to address the assembly binding problem.
The actual issue was much dumber. I had installed the Kontent Delivery Client nuget sdk in another project and only referenced it in this one. Installing it for this project solved the problem. I am able to call the builder without any errors now.
I am creating a c# .net framework library (.dll) that will act as a plugin for another piece of software. I want to use the Newtonsoft.json library but when the I call the json library I get the classic "FileNotFoundException":
Could not load file or assembly 'Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed' or one of its dependencies. The system cannot find the file specified.
The .dll is definitely being copied to the directory I am trying to run the plugin from. A lot of others have had this issue and I have tried everything I've seen (updating, reinstalling, etc.) but no luck yet. Because this is a class library I do not have an "app.config" file to play with binding redirects, so I haven't been able to try that.
I have very basic code right now, this package is the only external reference I am using. I've only been working on this today, only ever trying with Newtonsoft.Json version 12.0.2. It is weird to me that it appears VS is looking for 12.0.0.0, however. This is not an option to download. Similarly, I've tried downgrading to version 11.0.2, and then it gives me the same error saying it can't find version 11.0.0.0.
Even though your project may not have an .app config, you can still view the package references by unloading your project and viewing the .csproj file.
To try and force a direct reference to Newtonsoft.Json 12.0.2, you can add this snippet
<ItemGroup>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.2</Version>
</PackageReference>
</ItemGroup>
I would recommend removing any other references in your .csproj for Newtonsoft.Json as well
I am trying to code an application in C#.NET Core that can be extended using MEF. Currently, I am able to do that without any issues with libraries, that have no dependencies or have the same dependencies as the host app (so dependencies are already loaded). But, if I want to use a library with a NuGet reference, that is not used by the main app, the loading of this library fails on that reference.
How can I force the main app to load the missing NuGet dependency, if it tries to load an assembly with such reference? It seems to me as a pretty common use case, but I am lost here and cannot find a way out. Thanks.
For reference, I am posting the portion of the code.
[ImportMany]
private IEnumerable<Lazy<IService, IServiceMetadata>> _asrServices;
...
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(Path.Combine(Directory.GetCurrentDirectory(), "Services")));
CompositionContainer _container = new CompositionContainer(catalog);
...
foreach (Lazy<IService, IServiceMetadata> _service in _asrServices)
{
var _serviceInstance = _service.Value // here the loading fails
}
Jiri
.NET currently has two build "systems". One is the original project files, that import Microsoft.Common.props and Microsoft.CSharp.targets (assuming it's a c# project) and lots of XML in between, that has been around ever since .NET was first released, apparently in 2002. Since .NET Core was made generally available in 2016 there has been a new project system generally called SDK projects because the way a *proj file references the build system is though an Sdk element or attribute in the msbuild xml. Although it's off-topic, because there's a common bad assumption, I want to point out that although SDK projects were created for .NET Core, you can target the .NET Framework from SDK projects.
With the original project files, when you build, all the projects references get copied to the output directory. However, with SDK projects, only the project's assembly is copied to output (I'm not sure, but I think even content set to copy to output doesn't actually get copied on build). In order to get everything in a single directory, you should use the dotnet cli's publish command.
So, if you have a build script that builds your project and copies all the plugins somewhere, you should add a dotnet publish step to the script for each plugin using the SDK style project file.