Bagaimana cara memuat Majelis ke AppDomain dengan semua referensi secara rekursif?

113

Saya ingin memuat ke AppDomainbeberapa rakitan baru yang memiliki pohon referensi kompleks (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

Sejauh yang saya mengerti, ketika sebuah assembly sedang dimuat AppDomain, referensinya tidak akan dimuat secara otomatis, dan saya harus memuatnya secara manual. Jadi ketika saya melakukannya:

string dir = @"SomePath"; // different from AppDomain.CurrentDomain.BaseDirectory
string path = System.IO.Path.Combine(dir, "MyDll.dll");

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
setup.ApplicationBase = dir;
AppDomain domain = AppDomain.CreateDomain("SomeAppDomain", null, setup);

domain.Load(AssemblyName.GetAssemblyName(path));

dan mendapatkan FileNotFoundException:

Tidak dapat memuat file atau assembly 'MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' atau salah satu dependensinya. Sistem tidak dapat menemukan berkas yang dicari.

Saya pikir bagian kuncinya adalah salah satu ketergantungannya .

Oke, saya lakukan selanjutnya sebelumnya domain.Load(AssemblyName.GetAssemblyName(path));

foreach (AssemblyName refAsmName in Assembly.ReflectionOnlyLoadFrom(path).GetReferencedAssemblies())
{
    domain.Load(refAsmName);
}

Tapi mendapat FileNotFoundExceptionlagi, di majelis (direferensikan) lain.

Bagaimana cara memuat semua referensi secara rekursif?

Apakah saya harus membuat pohon referensi sebelum memuat perakitan root? Bagaimana cara mendapatkan referensi majelis tanpa memuatnya?

abatishchev
sumber
1
Saya telah memuat rakitan seperti ini berkali-kali sebelumnya, saya tidak pernah harus memuat semua referensinya secara manual. Saya tidak yakin premis pertanyaan ini benar.
Mick

Jawaban:

68

Anda perlu memanggil CreateInstanceAndUnwrapsebelum objek proxy Anda akan dieksekusi di domain aplikasi asing.

 class Program
{
    static void Main(string[] args)
    {
        AppDomainSetup domaininfo = new AppDomainSetup();
        domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
        Evidence adevidence = AppDomain.CurrentDomain.Evidence;
        AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);

        Type type = typeof(Proxy);
        var value = (Proxy)domain.CreateInstanceAndUnwrap(
            type.Assembly.FullName,
            type.FullName);

        var assembly = value.GetAssembly(args[0]);
        // AppDomain.Unload(domain);
    }
}

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFile(assemblyPath);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }
}

Juga, perhatikan bahwa jika Anda menggunakan LoadFromAnda kemungkinan besar akan mendapatkan FileNotFoundpengecualian karena resolver Majelis akan mencoba menemukan rakitan yang Anda muat di GAC atau folder bin aplikasi saat ini. Gunakan LoadFileuntuk memuat file assembly arbitrary sebagai gantinya - tetapi perhatikan bahwa jika Anda melakukan ini, Anda harus memuat sendiri dependensi apa pun.

Jduv
sumber
20
Lihat kode yang saya tulis untuk menyelesaikan masalah ini: github.com/jduv/AppDomainToolkit . Secara khusus, lihat metode LoadAssemblyWithReferences di kelas ini: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv
3
Saya telah menemukan bahwa meskipun ini berfungsi sebagian besar waktu, dalam beberapa kasus Anda sebenarnya masih perlu melampirkan penangan ke AppDomain.CurrentDomain.AssemblyResolveacara seperti yang dijelaskan dalam jawaban MSDN ini . Dalam kasus saya, saya mencoba untuk menghubungkan ke penerapan SpecRun yang berjalan di bawah MSTest, tetapi saya pikir itu berlaku untuk banyak situasi di mana kode Anda mungkin tidak berjalan dari AppDomain "utama" - ekstensi VS, MSTest, dll.
Aaronaught
Ah menarik. Saya akan memeriksanya dan melihat apakah saya dapat membuatnya sedikit lebih mudah untuk dikerjakan melalui ADT. Maaf kode itu sudah agak mati untuk sementara waktu sekarang - kita semua memiliki pekerjaan harian :).
Jduv
@Jduv Akan memberi suara positif pada komentar Anda sekitar 100 kali jika saya bisa. Perpustakaan Anda membantu saya memecahkan masalah yang tampaknya tidak terpecahkan yang saya alami dengan pemuatan perakitan dinamis di bawah MSBuild. Anda harus mempromosikannya menjadi jawaban!
Philip Daniels
2
@Jduv apakah Anda yakin bahwa assemblyvariabel akan mereferensikan perakitan dari "MyDomain"? Saya pikir oleh var assembly = value.GetAssembly(args[0]);Anda akan memuat Anda args[0]ke kedua domain dan assemblyvariabel akan merujuk salinan dari domain aplikasi utama
Igor Bendrup
14

http://support.microsoft.com/kb/837908/en-us

Versi C #:

Buat kelas moderator dan wariskan dari MarshalByRefObject:

class ProxyDomain : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFrom(assemblyPath);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }
}

panggilan dari situs klien

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
rockvista
sumber
6
Bagaimana solusi ini dimasukkan ke dalam konteks membuat AppDomain baru, dapatkah seseorang menjelaskannya?
Tri Q Tran
2
A MarshalByRefObjectdapat diteruskan ke appdomains. Jadi saya akan menebak bahwa Assembly.LoadFrommencoba memuat rakitan di appdomain baru, apa yang hanya mungkin, jika objek pemanggil dapat dilewatkan di antara appdomains tersebut. Ini juga disebut remoting seperti yang dijelaskan di sini: msdn.microsoft.com/en-us/library/…
Christoph Meißner
32
Ini tidak berhasil. Jika Anda menjalankan kode dan memeriksa AppDomain.CurrentDomain.GetAssemblies () Anda akan melihat bahwa rakitan target yang Anda coba muat dimuat ke dalam domain aplikasi saat ini dan bukan yang proxy.
Jduv
41
Ini benar-benar tidak masuk akal. Mewarisi dari MarshalByRefObjecttidak secara ajaib membuatnya dimuat di satu sama lain AppDomain, itu hanya memberi tahu kerangka .NET untuk membuat proxy jarak jauh transparan alih-alih menggunakan serialisasi ketika Anda membuka referensi dari satu AppDomainsama lain AppDomain(cara yang khas adalah CreateInstanceAndUnwrapmetode). Tidak percaya jawaban ini memiliki lebih dari 30 suara positif; kode di sini hanyalah cara menelepon yang berputar-putar Assembly.LoadFrom.
Aaronaught
1
Ya, ini sepertinya benar-benar tidak masuk akal, namun ia memiliki 28 suara dan ditandai sebagai jawabannya. Tautan yang diberikan bahkan tidak menyebutkan MarshalByRefObject. Cukup aneh. Jika ini benar-benar melakukan sesuatu, saya ingin seseorang menjelaskan caranya
Mick
12

Setelah Anda meneruskan instance assembly kembali ke domain pemanggil, domain pemanggil akan mencoba memuatnya! Inilah mengapa Anda mendapatkan pengecualian. Ini terjadi di baris terakhir kode Anda:

domain.Load(AssemblyName.GetAssemblyName(path));

Jadi, apa pun yang ingin Anda lakukan dengan perakitan, harus dilakukan di kelas proxy - kelas yang mewarisi MarshalByRefObject .

Perhatikan bahwa domain pemanggil dan domain yang baru dibuat harus memiliki akses ke rakitan kelas proxy. Jika masalah Anda tidak terlalu rumit, pertimbangkan untuk membiarkan folder ApplicationBase tidak berubah, sehingga akan sama dengan folder domain pemanggil (domain baru hanya akan memuat Assemblies yang dibutuhkannya).

Dalam kode sederhana:

public void DoStuffInOtherDomain()
{
    const string assemblyPath = @"[AsmPath]";
    var newDomain = AppDomain.CreateDomain("newDomain");
    var asmLoaderProxy = (ProxyDomain)newDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ProxyDomain).FullName);

    asmLoaderProxy.GetAssembly(assemblyPath);
}

class ProxyDomain : MarshalByRefObject
{
    public void GetAssembly(string AssemblyPath)
    {
        try
        {
            Assembly.LoadFrom(AssemblyPath);
            //If you want to do anything further to that assembly, you need to do it here.
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
}

Jika Anda perlu memuat rakitan dari folder yang berbeda dari folder domain aplikasi Anda saat ini, buat domain aplikasi baru dengan folder jalur pencarian dll tertentu.

Misalnya, baris pembuatan domain aplikasi dari kode di atas harus diganti dengan:

var dllsSearchPath = @"[dlls search path for new app domain]";
AppDomain newDomain = AppDomain.CreateDomain("newDomain", new Evidence(), dllsSearchPath, "", true);

Dengan cara ini, semua dll secara otomatis akan diselesaikan dari dllsSearchPath.

Nir
sumber
Mengapa saya harus memuat rakitan dengan menggunakan kelas proxy? Apa perbedaannya dibandingkan memuatnya menggunakan Assembly.LoadFrom (string). Saya tertarik dengan detail teknis, dari perspektif CLR. Saya akan sangat berterima kasih jika Anda dapat memberikan jawaban.
Dennis Kassel
Anda menggunakan kelas proxy untuk menghindari rakitan baru dimuat ke domain pemanggil Anda. Jika Anda akan menggunakan Assembly.LoadFrom (string), domain pemanggil akan mencoba memuat referensi assembly baru dan tidak akan menemukannya karena tidak mencari rakitan di "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir
11

Di AppDomain baru Anda, coba tetapkan penangan peristiwa AssemblyResolve . Peristiwa itu dipanggil saat dependensi hilang.

David
sumber
Tidak. Sebenarnya, Anda mendapatkan pengecualian pada baris Anda mendaftarkan acara ini di AppDomain baru. Anda harus mendaftarkan acara ini di AppDomain saat ini.
pengguna1004959
Itu dilakukan jika kelas diwarisi dari MarshalByRefObject. Tidak jika kelas hanya ditandai dengan atribut [Serializable].
pengguna2126375
5

Anda perlu menangani peristiwa AppDomain.AssemblyResolve atau AppDomain.ReflectionOnlyAssemblyResolve (tergantung pada pemuatan yang Anda lakukan) jika rakitan yang direferensikan tidak ada di GAC atau di jalur probing CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Dustin Campbell
sumber
Jadi saya harus menunjukkan perakitan yang diminta secara manual? Bahkan di AppDomain's AppBase baru? Apakah ada cara untuk tidak melakukan itu?
abatishchev
5

Butuh beberapa saat bagi saya untuk memahami jawaban @ user1996230, jadi saya memutuskan untuk memberikan contoh yang lebih eksplisit. Dalam contoh di bawah ini saya membuat proxy untuk objek yang dimuat di AppDomain lain dan memanggil metode pada objek itu dari domain lain.

class ProxyObject : MarshalByRefObject
{
    private Type _type;
    private Object _object;

    public void InstantiateObject(string AssemblyPath, string typeName, object[] args)
    {
        assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + AssemblyPath); //LoadFrom loads dependent DLLs (assuming they are in the app domain's base directory
        _type = assembly.GetType(typeName);
        _object = Activator.CreateInstance(_type, args); ;
    }

    public void InvokeMethod(string methodName, object[] args)
    {
        var methodinfo = _type.GetMethod(methodName);
        methodinfo.Invoke(_object, args);
    }
}

static void Main(string[] args)
{
    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationBase = @"SomePathWithDLLs";
    AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
    ProxyObject proxyObject = (ProxyObject)domain.CreateInstanceFromAndUnwrap(typeof(ProxyObject).Assembly.Location,"ProxyObject");
    proxyObject.InstantiateObject("SomeDLL","SomeType", new object[] { "someArgs});
    proxyObject.InvokeMethod("foo",new object[] { "bar"});
}
grouma
sumber
Beberapa kesalahan ketik kecil dalam kode, dan saya harus mengakui bahwa saya tidak percaya ini akan berhasil, tetapi ini adalah penyelamat bagi saya. Terima kasih banyak.
Owen Ivory
4

Kuncinya adalah event AssemblyResolve yang dimunculkan oleh AppDomain.

[STAThread]
static void Main(string[] args)
{
    fileDialog.ShowDialog();
    string fileName = fileDialog.FileName;
    if (string.IsNullOrEmpty(fileName) == false)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        if (Directory.Exists(@"c:\Provisioning\") == false)
            Directory.CreateDirectory(@"c:\Provisioning\");

        assemblyDirectory = Path.GetDirectoryName(fileName);
        Assembly loadedAssembly = Assembly.LoadFile(fileName);

        List<Type> assemblyTypes = loadedAssembly.GetTypes().ToList<Type>();

        foreach (var type in assemblyTypes)
        {
            if (type.IsInterface == false)
            {
                StreamWriter jsonFile = File.CreateText(string.Format(@"c:\Provisioning\{0}.json", type.Name));
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                jsonFile.WriteLine(serializer.Serialize(Activator.CreateInstance(type)));
                jsonFile.Close();
            }
        }
    }
}

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string[] tokens = args.Name.Split(",".ToCharArray());
    System.Diagnostics.Debug.WriteLine("Resolving : " + args.Name);
    return Assembly.LoadFile(Path.Combine(new string[]{assemblyDirectory,tokens[0]+ ".dll"}));
}
Leslie Marshall
sumber
0

Saya harus melakukan ini beberapa kali dan telah meneliti banyak solusi berbeda.

Solusi yang saya temukan paling elegan dan mudah dicapai dapat diterapkan seperti itu.

1. Buat proyek yang Anda dapat membuat antarmuka sederhana

antarmuka akan berisi tanda tangan dari setiap anggota yang ingin Anda panggil.

public interface IExampleProxy
{
    string HelloWorld( string name );
}

Sangat penting untuk menjaga proyek ini tetap bersih dan ringan. Ini adalah proyek yang AppDomaindapat menjadi referensi dan memungkinkan kita untuk tidak merujuk Assemblypada domain yang ingin kita muat dalam domain terpisah dari perakitan klien kita.

2. Sekarang buat proyek yang memiliki kode yang ingin Anda muat secara terpisah AppDomain.

Proyek ini seperti proyek klien akan mereferensikan proj proxy dan Anda akan mengimplementasikan antarmuka.

public interface Example : MarshalByRefObject, IExampleProxy
{
    public string HelloWorld( string name )
    {
        return $"Hello '{ name }'";
    }
}

3. Selanjutnya, dalam proyek klien, muat kode lain AppDomain.

Jadi, sekarang kita buat yang baru AppDomain. Dapat menentukan lokasi dasar untuk referensi perakitan. Probing akan memeriksa rakitan dependen di GAC dan di direktori saat ini dan lokasi AppDomainbasis.

// set up domain and create
AppDomainSetup domaininfo = new AppDomainSetup
{
    ApplicationBase = System.Environment.CurrentDirectory
};

Evidence adevidence = AppDomain.CurrentDomain.Evidence;

AppDomain exampleDomain = AppDomain.CreateDomain("Example", adevidence, domaininfo);

// assembly ant data names
var assemblyName = "<AssemblyName>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null|<keyIfSigned>";
var exampleTypeName = "Example";

// Optional - get a reflection only assembly type reference
var @type = Assembly.ReflectionOnlyLoad( assemblyName ).GetType( exampleTypeName ); 

// create a instance of the `Example` and assign to proxy type variable
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( assemblyName, exampleTypeName );

// Optional - if you got a type ref
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( @type.Assembly.Name, @type.Name );    

// call any members you wish
var stringFromOtherAd = proxy.HelloWorld( "Tommy" );

// unload the `AppDomain`
AppDomain.Unload( exampleDomain );

jika perlu, ada banyak cara berbeda untuk memuat rakitan. Anda dapat menggunakan cara berbeda dengan solusi ini. Jika Anda memiliki nama yang memenuhi syarat rakitan maka saya suka menggunakan CreateInstanceAndUnwrapkarena memuat byte rakitan dan kemudian memberi contoh jenis Anda untuk Anda dan mengembalikan objectyang Anda dapat dengan mudah dilemparkan ke jenis proxy Anda atau jika Anda tidak memasukkannya ke dalam kode yang diketik dengan kuat Anda bisa gunakan runtime bahasa dinamis dan tetapkan objek yang dikembalikan ke dynamicvariabel yang diketik, lalu panggil anggotanya secara langsung.

Itu dia.

Hal ini memungkinkan untuk memuat rakitan yang proyek klien Anda tidak memiliki referensi secara terpisah AppDomain dan memanggil anggota di dalamnya dari klien.

Untuk menguji, saya suka menggunakan jendela Modul di Visual Studio. Ini akan menunjukkan domain rakitan klien Anda dan semua modul yang dimuat di domain itu serta domain aplikasi baru Anda dan rakitan atau modul apa yang dimuat di domain itu.

Kuncinya adalah memastikan kode Anda diturunkan MarshalByRefObjectatau dapat diserialkan.

`MarshalByRefObject akan memungkinkan Anda untuk mengkonfigurasi masa pakai domain yang masuk. Contoh, katakanlah Anda ingin domain dihancurkan jika proxy belum dipanggil dalam 20 menit.

Saya harap ini membantu.

SimperT
sumber
Hai, jika saya ingat dengan benar, masalah intinya adalah bagaimana memuat semua dependensi secara rekursif, maka pertanyaannya. Silakan uji kode Anda dengan mengubah HelloWorld untuk mengembalikan kelas tipe Foo, FooAssemblyyang memiliki properti tipe Bar, BarAssembly, yaitu total 3 majelis. Apakah itu akan terus bekerja?
abatishchev
Ya, perlu direktori yang tepat disebutkan dalam tahap probing assembly. AppDomain memiliki ApplicationBase, namun saya tidak mengujinya. Juga file konfigurasi Anda dapat menentukan direktori probing assembly seperti app.config yang dapat digunakan dll juga hanya untuk disalin di properti. Selain itu, jika Anda memiliki kontrol atas pembuatan rakitan yang ingin memuat dalam domain aplikasi terpisah, referensi bisa mendapatkan HintPath yang menentukan untuk mencarinya. Jika semua itu gagal, saya akan mengakibatkan berlangganan ke acara AppDomains AssemblyResolve baru dan secara manual memuat rakitan. Contoh contoh untuk itu.
SimperT