In .NET Framework 4.8, i could use AssemblyBuilder to create a new Assembly based on an existing Assembly, add some further modifications (such as encryption) and save it as a new .dll with AssemblyBuilder.Save().
In .NET 6, they removed the functionality to save an Assembly to file, along with other methods i used.
As an alternative i looked into Roslyn and how to generate Assemblies. The thing is, that i don't have the source code i want to compile into a new Assembly, but only an already compiled Assembly i want to modify.
My use case would look like this:
private void Protect(FileInfo dllSourceFile, FileInfo targetFile) {
Assembly assembly = Assembly.LoadFrom(dllSourceFile.FullName);
CSharpCompilation compilation = CSharpCompilation.Create(assembly);
//...modify compilation, encrypt files
var result = compilation.Emit(targetFile.FullName);
}
Is there any functionality in Roslyn (or .Net 6) that helps me here? Even better if it directly uses AssemblyBuilder instead of Assembly.
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.
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.
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.
I have a console application, which references some of my other projects in the solution. On startup, I need to do some reflection work and get the assemblies by name. Apparently, this does not work as expected.
I have a project (assembly) called MyApplication.Domain which I from my .NET MVC project can get by calling GetAssembly("MyApplication.Domain") using this method:
public static Assembly GetAssembly(string name)
{
return AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.GetName().Name == name);
}
But running this on my console application, just returns null. The assembly is not even in the AppDomain.CurrentDomain.GetAssemblies() list.
Then if I try calling this method, using a class in the MyApplication.Domain assembly, I get the assembly back just like I wish to do using the other method?
public static Assembly[] GetAssembliesOf<T>() where T : class
{
var assemblies = from assembly in AppDomain.CurrentDomain.GetAssemblies()
from type in assembly.GetTypes()
where typeof (T).IsAssignableFrom(type)
where !type.IsAbstract
select assembly;
return assemblies.ToArray();
}
// Usage: GetAssemblyOf<MyClassInDomainAssembly>();
Things I have checked:
The project or assembly is referenced
It is set to Copy Local
The build is running with Any CPU like the assembly
The target framework the same for all projects and assemblies (4.5.1)
Is there some funky stuff I need to know about loading assemblies in console application?
As a side note the console application is actually a Self-Host Web API 2.
Your observations are correct and expected. .Net is lazy and the collection in question will be filled as your assemblies get loaded and they are loaded only when they are used. The fact that you can observe this in console application and not in mvc is just because your console application did less before you executed that line.
More over when you have reference to an assembly finding it by name is just pointless as you can do typeof(AnyTypeFromThatAssembly).Assembly. And if you really want to load an assembly by name you can executed Assembly.Load but this requires fully qualified assembly name which you can get again by typeof(AnyTypeFromThatAssembly).Assembly.FullName.ToString().