Load unreferenced Dll - c#

I'm trying to load System.Data.SqlClient dynamically.
The System.Data.SqlClient Nuget is installed but there is no reference to it in the project.
And I don't know the right path to the nuget directory.
Is there a way to do this dynamically?
Here is my code
internal static Type GetFastType(this string typeName, string assembly)
{
if (string.IsNullOrEmpty(assembly))
throw new Exception("AssemblyName cannot be empty");
if (!assembly.ToLower().EndsWith(".dll"))
assembly += ".dll";
var key = typeName + assembly;
if (CachedStringTypes.ContainsKey(key))
return CachedStringTypes.Get(key);
// Assembly.LoadFrom(assembly) // throw exception as the dll is not found
if (!CachedAssembly.ContainsKey(assembly))
CachedAssembly.Add(assembly, Assembly.LoadFrom(assembly));
return CachedStringTypes.GetOrAdd(key, CachedAssembly.Get(assembly).GetType(typeName, true, true));
}
And here is how I run it
var type ="System.Data.SqlClient.SqlConnection".GetFastType("System.Data.SqlClient");

Required reading:
Read this MSDN article: Best Practices for Assembly Loading
In short:
It looks like you're assuming the System.Data.SqlClient.SqlConnection class always exists inside System.Data.SqlClient.dll.
This is an incorrect assumption:
A NuGet package is not a .NET assembly.
A NuGet package does not map 1:1 with a .NET assembly nor namespaces.
A NuGet package can contain multiple assemblies.
A NuGet package can contain zero assemblies.
A NuGet package can contain assemblies that don't have any types defined in them at all!
They could be assemblies that only contain Resources or other embedded items
They could be assemblies that use Type-Forwarding to redirect types that previously existed in this assembly other assemblies. Only the JIT uses this feature, however, not reflection.
And those "forwarded-to" assemblies don't have to exist in NuGet packages either: they can be "in-box" assemblies built-in to the runtime like mscorlib.dll and System.Data.dll).
They could be stub assemblies that don't provide any types when those types are already provided by the Base Class Library - the NuGet package only exists to provide those types for other platforms.
This is the situation you're dealing with.
A NuGet package can have very different effects based on the project's target (.NET Framework, .NET Standard, .NET Core, etc)
Your code cannot assume that a specific class is located in a specific assembly file - this breaks .NET's notion of backwards-compatibility through type-forwarding.
In your case...
In your case, your code assumes System.Data.SqlClient.SqlConnection exists inside an assembly file named System.Data.SqlClient. This assumption is false in many cases, but true in some cases.
Here is the top-level directory structure of the System.Data.SqlClient NuGet package:
Observe how inside the package there are subdirectories for each supported target (in this case, MonoAndroid10, MonoTouch10, net46, net451, net461, netcoreapp2.1, netstandard1.2, etc). For each of these targets the package provides different assemblies:
When targeting .NET Framework 4.5.1, .NET Framework 4.6 or .NET Framework 4.6.1 the files from the net451, net46 and net461 directories (respectively) will be used. These folders contain a single file named System.Data.SqlClient.dll which does not contain any classes. This is because when you target the .NET Framework 4.x, the System.Data.SqlClient (namespace) types are already provided by the Base Class Library inside System.Data.dll, so there is no need for any additional types. (So if you're building only for .NET Framework 4.x then you don't need the System.Data.SqlClient NuGet package at all.
Here's a screenshot of the insides of that assembly file using the .NET Reflector tool (a tool which lets you see inside and decompile .NET assemblies) if you don't believe me:
When targeting other platforms via .NET Standard (i.e. where System.Data.dll isn't included by default, or when System.Data.dll does not include SqlClient) then the NuGet package will use the netstandard1.2, netstandard1.3, netstandard2.0 directories, which does contain a System.Data.SqlClient.dll that does contain the System.Data.SqlClient namespace with the types that you're after. Here's a screenshot of that assembly:
And other platforms like MonoAndroid, MonoTouch, xamarinios, xamarintvos, etc also have their own specific version of the assembly file (or files!).
But even if you know your program will only run on a single specific platform where a specific NuGet package contains an assembly DLL that contains a specific type - it's still "wrong" because of type-forwarding: https://learn.microsoft.com/en-us/dotnet/framework/app-domains/type-forwarding-in-the-common-language-runtime
While Type-Forwarding means that most programs that reference types in certain assemblies will continue to work fine, it does not apply to reflection-based assembly-loading and type-loading, which is what your code does. Consider this scenario:
A new version of the System.Data.SqlClient NuGet package comes out that now has two assemblies:
System.Data.SqlClient.dll (which is the same as before, except SqlConnection is removed but has a [TypeForwardedTo] attribute set that cites System.Data.SqlClient.SqlConnection.dll).
System.Data.SqlClient.SqlConnection.dll (the SqlConnection class now lives in this assembly).
Your code will now break because it explicitly loads only System.Data.SqlClient.dll and not System.Data.SqlClient.SqlConnection.dll and enumerates those types.
Here be dragons...
Now, assuming you're prepared to disregard all of that advice and still write programs that assume a specific type exists in a specific assembly, then the process is straightforward:
// Persistent state:
Dictionary<String,Assembly> loadedAssemblies = new Dictionary<String,Assembly>();
Dictionary<(String assembly, String typeName),Type> typesByAssemblyAndName = new Dictionary<(String assembly, String typeName),Type>();
// The function:
static Type GetExpectedTypeFromAssemblyFile( String assemblyFileName, String typeName )
{
var t = ( assemblyFileName, typeName );
if( !typesByName.TryGetValue( t, out Type type ) )
{
if( !loadedAssemblies.TryGetValue( assemblyFileName, out Assembly assembly ) )
{
assembly = Assembly.LoadFrom( assemblyFileName );
loadedAssemblies[ assemblyFileName ] = assembly;
}
type = assembly.GetType( typeName ); // throws if the type doesn't exist
typesByName[ t ] = type;
}
return type;
}
// Usage:
static IDbConnection CreateSqlConnection()
{
const String typeName = "System.Data.SqlClient.SqlConnection";
const String assemblyFileName = "System.Data.SqlClient.dll";
Type sqlConnectionType = GetExpectedTypeFromAssemblyFile( assemblyFileName, typeName );
Object sqlConnectionInstance = Activator.CreateInstance( sqlConnectionType ); // Creates an instance of the specified type using that type's default constructor.
return (IDbConnection)sqlConnectionInstance;
}

For anyone that may have hade the same problem, i found the solution
Here is how you could load the right type in the right way
var type = Type.GetType($"{typeName}, {assembly}");
eg.
var type =Type.GetType("System.Data.SqlClient.SqlConnection, System.Data.SqlClient");
This way, it should load the dll dynamicly.

I think you have to provide the full path to LoadFrom(...). You should be aware of the probing path of the application, so just concat that path to the name of the assembly. I don't think is straighforward to load from a path that is not in the probing path unless doing some thricks with the app domain.

Related

How to get all the referenced assemblies using reflection ? (.net4)

I've made a piece of code that gives me the referenced assemblies of one assembly (in .net5) and it was working greatly, it gave me for exemple if I use the File.WriteAllText method the assembly "System.IO.FileSystem" (this is by using the Assembly.GetReferencedAssemblies method).
But now I need to get this code to work on .NET4 (for Unity Engine). But i've seen that the myAssembly.GetReferencedAssemblies do not give the same output as in .NET5.
It now gives me only: myAssembly.dll and mscorlib.dll
And I cannot make it work to give me like previously all the referenced assemblies (for exemple System or System.IO ...)
Here's a simple example:
using System.IO;
public class Plugin {
static Plugin() {
// Just use the File class to keep the System.IO assembly reference
File.WriteAllText("test", string.Empty);
}
}
public static void Test() {
string myPluginPath = "myAssembly.dll";
// Load the assembly
Assembly assembly = Assembly.LoadFrom(myPluginPath);
// Get all the referenced assemblies
foreach (AssemblyName name in assembly.GetReferencedAssemblies()) {
Console.WriteLine(name.Name);
// Different outputs:
//
// .NET 5
// - System.IO.FileSystem
// - ....
// .NET 4
// - mscorlib
// - Plugin
}
}
Any idea how to make the myAssembly.GetReferencedAssemblies work on .net4 ? Thanks!
Any idea how to make the myAssembly.GetReferencedAssemblies work on .net4?
System.IO.File resides in mscorlib.dll in .NET Framework so both versions work correctly. Why do you need the actual assembly names? You cannot rely on them across platforms.
On the other hand, if you need this to resolve assembly qualified type names, then you can do it by using the legacy assembly identities so Type.GetType("System.IO.File, mscorlib, Version=4.0.0.0") works also in .NET 5.
It's because even on the newer platforms there is an mscorlib.dll, which contains nothing but a bunch of [assembly:TypeForwardedTo(...)] attributes that provide a redirect to the new location.
But it will not work the other way around so do not expect that you can resolve the type File from a System.IO.FileSystem.dll in .NET Framework 4.0
Update after the comment:
In .NET Framework the best way for handling potentially harmful plugins is creating separate AppDomains for them with restricted permissions. Here is an example from my unit tests to create such a sandbox domain and here is an example usage. The AppDomains can even be unloaded along with their referenced assemblies.
The bad news is that this will not work in .NET 5 because starting with .NET Core you cannot create AppDomains anymore (not quite a problem if you already have a solution for .NET 5). But for the sake of completeness: starting with .NET Core 3.0 the AssemblyLoadContext type is the recommended way for handling plugins. Though it does not create a restricted environment the same way as AppDomain, you can use AssemblyDependencyResolver to control the assembly loading requests of the plugins.

IsAssignableFrom does not work when versions do not match

I have interface A defined in A.dll version 3.0
I have B.dll which references A.dll 3.0 and implements interface A in class B.
I now have the same A.dll, identical in every way except it is version 3.1.
I try to load B.dll referencing version 3.0 of A.dll. But 3.1 is loaded in memory.
var assembly = Assembly.LoadFile(dllPath);
var types = assembly
.GetTypes();
var extensionClass = types
.First(x => typeof(T).IsAssignableFrom(x) && x.IsClass);
_logger.Debug.Log($"Creating instance for {dllPath} of type {extensionClass.FullName}");
var instance = (T)Activator.CreateInstance(extensionClass);
return instance;
But that does not work.
I get sequence contains no matching elements.
I cannot use an App.config to do an assembly redirect as the code loading B.dll is running inside a host process out of my control.
What can I do aside from keeping A.dll at the same version forever?

Types loaded from an assembly file are not equal to the same type when referenced in .NET Core

I take a Type, query its assembly location, and then load the assembly from the same address, and find the same type from the loaded assembly. The resulting type is not equal to the original type.
Here's the test case:
[TestMethod]
public void TestTypeLoadingWithFilePath()
{
var originalType = typeof(SomeClass);
var assemblyAddress = originalType.Assembly.Location;
var loadedAssembly = Assembly.LoadFile(assemblyAddress);
Assert.IsNotNull(loadedAssembly);
var loadedType = loadedAssembly.GetType(originalType.FullName);
Assert.IsNotNull(loadedType);
Assert.AreEqual(originalType, loadedType);
}
The test fails on the last assertion.
This only happens on .NET Core on Windows. (I'm testing against latest version, 2.1.4). But this was not the case with .NET Framework.
My questions are:
Is this by design, or a bug?
If it's by design, why?
Again, if it's by design, doesn't this mean different behavior between two implementations of .NET Standard? (.NET Core vs. .NET Framework)
This is a normal behavior. Using Assembly.LoadFile will load the assembly and create a new "instance" of it. To fix this, simply use Assembly.LoadFrom instead. This will first look in the current context if the requested assembly is already loaded, and take this one if it is. Comparing types like you're doing will then work.
Edit: I don't know if it's intended, but this method works in both .NetFramework and .NetCore.

.NET Core assembly search order

In .NET Framework, the loader searches dependencies in current folder and then in GAC.
But in .NET Core there is no GAC, so does .NET Core assembly loader only search assemblies in current folder or in global nuget cache(someuser/.nuget/packages folder) also?
Also I found a /usr/local/share/dotnet/shared folder in my Mac(C:\Program Files\dotnet\shared in Windows) where all base libraries are located, like System.Runtime.dll, System.Collections.dll.
Does assembly loader looks there too?
Also I found that these base libraries are duplicated also in in global nuget cache.
Why?
The directories used by the PackageCompilationAssemblyResolver will be largely dependent on your environment. However, they may include:
C:\Program Files\dotnet\store\x64\netcoreapp2.0 (or equivalent path for your machine)
C:\Users\{user}\.nuget\packages
C:\Program Files\dotnet\sdk\NuGetFallbackFolder
Note that this is just for packages (i.e NuGet). Other assembly resolvers exist for references and application assemblies...
Note that if you are using an assembly resolver directly in your code, you can specify the paths used for resolution as follows:
ICompilationAssemblyResolver assemblyResolver = new CompositeCompilationAssemblyResolver
(new ICompilationAssemblyResolver[]
{
new AppBaseCompilationAssemblyResolver(basePath), //e.g. project path
new ReferenceAssemblyPathResolver(defaultReferenceAssembliesPath, fallbackSearchPaths),
new PackageCompilationAssemblyResolver(nugetPackageDirectory) //e.g. C:\Users\\{user}\\.nuget\packages
});
For more details on managing the resolution of assemblies within you code, see this article - Resolving Assemblies in .NET Core
.Net core is based on Nuget packages being cross platform. NET Core applications rely heavily on NuGet to resolve their dependencies, which simplifies development.
You can check link Is there any GAC equivalent for .NET Core?

Reference to two assemblies, each one with reference to the another assembly but with different version

In a ASP.Net application I have a class.
This class need reference to two assemblies that I wrote before:
FMDriver.dll
MyWorldObj.dll
Each one of those assemblies use MySql.Data.dll but with different versions.
If I add references from my new project (with Copy Local = true) to those two assemblies I get errors because MYWorld.dll can't find MySql.Data.dll. Maybe that's because it uses an older version af the MySql library? If I remove reference to FMDrvier.dll MyWorldObj work properly.
I have used Binding Redirects before to get all referenced assemblies to use one version. Not sure if this solution will work for you.
You essentially provide an old version and a new version:
<bindingRedirect
oldVersion="old assembly version"
newVersion="new assembly version"/>
Then make sure the new version .dll is in the bin if your project.
Otherwise it looks like time to upgrade the assembly to use the same version.

Categories

Resources