Làm thế nào để tải một Assembly vào AppDomain với tất cả các tham chiếu một cách đệ quy?


113

Tôi muốn tải một AppDomainsố lắp ráp mới có cây tham chiếu phức tạp (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

Theo như tôi hiểu, khi một assembly đang được tải đến AppDomain, các tham chiếu của nó sẽ không được tải tự động và tôi phải tải chúng theo cách thủ công. Vì vậy, khi tôi làm:

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));

và có FileNotFoundException:

Không thể tải tệp hoặc lắp ráp 'MyDll, Phiên bản = 1.0.0.0, Văn hóa = trung lập, PublicKeyToken = null' hoặc một trong các phụ thuộc của nó. Hệ thống không thể tìm thấy các tập tin được chỉ định.

Tôi nghĩ rằng phần quan trọng là một trong những phụ thuộc của nó .

Ok, tôi làm tiếp theo trước domain.Load(AssemblyName.GetAssemblyName(path));

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

Nhưng có FileNotFoundExceptionmột lần nữa, trên một hội đồng (được tham chiếu) khác.

Làm thế nào để tải tất cả các tham chiếu một cách đệ quy?

Tôi có phải tạo cây tham chiếu trước khi tải root assembly không? Làm thế nào để lấy tham chiếu của một assembly mà không cần tải nó?


1
Tôi đã tải các hội đồng như thế này nhiều lần trước đây, tôi chưa bao giờ phải tải thủ công tất cả các tham chiếu của nó. Tôi không chắc tiền đề của câu hỏi này là đúng.
Mick

Câu trả lời:


68

Bạn cần gọi CreateInstanceAndUnwraptrước khi đối tượng proxy của bạn thực thi trong miền ứng dụng nước ngoài.

 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);
        }
    }
}

Ngoài ra, hãy lưu ý rằng nếu bạn sử dụng, LoadFrombạn sẽ có thể gặp một FileNotFoundngoại lệ vì trình phân giải Assembly sẽ cố gắng tìm tập hợp bạn đang tải trong GAC hoặc thư mục bin của ứng dụng hiện tại. Sử dụng LoadFileđể tải một tệp lắp ráp tùy ý thay thế - nhưng lưu ý rằng nếu bạn làm điều này, bạn sẽ cần tự tải bất kỳ phần phụ thuộc nào.


20
Kiểm tra mã tôi đã viết để giải quyết vấn đề này: github.com/jduv/AppDomainToolkit . Cụ thể, hãy xem phương thức LoadAssemblyWithRefutions trong lớp này: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv Ngày

3
Tôi đã tìm thấy rằng mặc dù công trình này hầu hết thời gian, trong một số trường hợp, bạn thực sự vẫn cần phải đính kèm một handler cho sự AppDomain.CurrentDomain.AssemblyResolvekiện như mô tả trong câu trả lời MSDN này . Trong trường hợp của tôi, tôi đang cố gắng kết nối với triển khai SpecRun chạy trong MSTest, nhưng tôi nghĩ nó áp dụng cho nhiều trường hợp trong đó mã của bạn có thể không chạy từ phần mở rộng AppDomain "chính" - VS, MSTest, v.v.
Aaronaught

Ah thú vị. Tôi sẽ xem xét vấn đề đó và xem liệu tôi có thể làm việc đó dễ dàng hơn một chút thông qua ADT hay không. Xin lỗi vì mã này đã hơi chết trong một thời gian - tất cả chúng ta đều có công việc hàng ngày :).
Jduv

@Jduv Sẽ ủng hộ bình luận của bạn khoảng 100 lần nếu tôi có thể. Thư viện của bạn đã giúp tôi giải quyết một vấn đề dường như không thể giải quyết được mà tôi đang gặp phải khi tải lắp ráp động trong MSBuild. Bạn nên quảng bá nó thành một câu trả lời!
Philip Daniels

2
@Jduv Bạn có chắc rằng assemblybiến đó sẽ tham chiếu đến assembly từ "MyDomain" không? Tôi nghĩ rằng var assembly = value.GetAssembly(args[0]);bạn sẽ tải của bạn args[0]vào cả hai miền và assemblybiến sẽ tham chiếu bản sao từ miền ứng dụng chính
Igor Bendrup

14

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

Phiên bản C #:

Tạo một lớp người kiểm duyệt và kế thừa nó từ MarshalByRefObject:

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

cuộc gọi từ trang web của khách hàng

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);

6
Giải pháp này được đặt trong bối cảnh tạo một AppDomain mới như thế nào, ai đó có thể giải thích?
Tri Q Tran,

2
A MarshalByRefObjectcó thể được chuyển xung quanh tên miền ứng dụng. Vì vậy, tôi sẽ đoán rằng Assembly.LoadFromcố gắng tải assembly trong một miền ứng dụng mới, điều chỉ có thể xảy ra, nếu đối tượng gọi có thể được chuyển giữa các miền ứng dụng đó. Điều này còn được gọi là quá trình xóa như được mô tả ở đây: msdn.microsoft.com/en-us/library/…
Christoph Meißner

32
Điều này không hiệu quả. Nếu bạn thực thi mã và kiểm tra AppDomain.CurrentDomain.GetAssemblies (), bạn sẽ thấy rằng lắp ráp đích mà bạn đang cố gắng tải được tải vào miền ứng dụng hiện tại chứ không phải proxy.
Jduv

41
Đây là điều hoàn toàn vô nghĩa. Kế thừa từ MarshalByRefObjectkhông làm cho nó tải mọi thứ một cách kỳ diệu AppDomain, nó chỉ ra lệnh cho .NET framework tạo một proxy loại bỏ minh bạch thay vì sử dụng tuần tự hóa khi bạn mở tham chiếu từ một AppDomaintrong khác AppDomain(cách thông thường là CreateInstanceAndUnwrapphương pháp). Không thể tin được câu trả lời này có hơn 30 lượt ủng hộ; mã ở đây chỉ là một cách gọi vòng vo vô nghĩa Assembly.LoadFrom.
Aaronaught

1
Vâng, nó trông giống như hoàn toàn vô nghĩa, nhưng nó đã có 28 phiếu bầu và được đánh dấu là câu trả lời. Liên kết được cung cấp thậm chí không đề cập đến MarshalByRefObject. Khá kỳ quái. Nếu điều này thực sự làm bất cứ điều gì tôi muốn yêu một ai đó để giải thích như thế nào
Mick

12

Khi bạn chuyển phiên bản hợp ngữ trở lại miền người gọi, miền người gọi sẽ cố gắng tải nó! Đây là lý do tại sao bạn nhận được ngoại lệ. Điều này xảy ra trong dòng mã cuối cùng của bạn:

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

Vì vậy, bất cứ điều gì bạn muốn làm với assembly, nên được thực hiện trong một lớp proxy - một lớp kế thừa MarshalByRefObject .

Hãy tính rằng miền của người gọi và miền được tạo mới đều phải có quyền truy cập vào hội đồng lớp proxy. Nếu vấn đề của bạn không quá phức tạp, hãy cân nhắc giữ nguyên thư mục ApplicationBase, vì vậy nó sẽ giống như thư mục miền người gọi (miền mới sẽ chỉ tải các Assemblies mà nó cần).

Trong mã đơn giản:

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);
        }
    }
}

Nếu bạn cần tải các tập hợp từ một thư mục khác với thư mục miền ứng dụng hiện tại của bạn, hãy tạo miền ứng dụng mới với thư mục đường dẫn tìm kiếm dlls cụ thể.

Ví dụ: dòng tạo miền ứng dụng từ mã trên phải được thay thế bằng:

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

Bằng cách này, tất cả các dlls sẽ tự động được giải quyết từ dllsSearchPath.


Tại sao tôi phải tải lắp ráp bằng cách sử dụng một lớp proxy? Sự khác biệt so với tải nó bằng Assembly.LoadFrom (string) là gì. Tôi quan tâm đến các chi tiết kỹ thuật, từ quan điểm của CLR. Tôi sẽ rất biết ơn nếu bạn có thể cung cấp một câu trả lời.
Dennis Kassel

Bạn sử dụng lớp proxy để tránh lắp ráp mới được tải vào miền người gọi của bạn. Nếu bạn sẽ sử dụng Assembly.LoadFrom (string), miền người gọi sẽ cố gắng tải các tham chiếu lắp ráp mới và sẽ không tìm thấy chúng vì nó không tìm kiếm các hợp ngữ trong "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir

11

Trên AppDomain mới của bạn, hãy thử thiết lập trình xử lý sự kiện AssemblyResolve . Sự kiện đó được gọi khi thiếu phụ thuộc.


Nó không. Trên thực tế, bạn nhận được một ngoại lệ trên dòng bạn đang đăng ký sự kiện này trên AppDomain mới. Bạn phải đăng ký sự kiện này trên AppDomain hiện tại.
user1004959

Nó thực hiện nếu lớp được kế thừa từ MarshalByRefObject. Sẽ không xảy ra nếu lớp chỉ được đánh dấu bằng thuộc tính [Serializable].
user2126375 23/02

5

Bạn cần xử lý các sự kiện AppDomain.AssemblyResolve hoặc AppDomain.ReflectionOnlyAssemblyResolve (tùy thuộc vào tải bạn đang thực hiện) trong trường hợp cụm được tham chiếu không nằm trong GAC hoặc trên đường dẫn thăm dò của CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve


Vì vậy, tôi phải chỉ ra yêu cầu lắp ráp theo cách thủ công? Ngay cả khi nó có trong AppBase của AppDomain mới? Có cách nào để không làm điều đó không?
abatishchev

5

Tôi đã mất một lúc để hiểu câu trả lời của @ user1996230 vì vậy tôi quyết định cung cấp một ví dụ rõ ràng hơn. Trong ví dụ dưới đây, tôi tạo proxy cho một đối tượng được tải trong một AppDomain khác và gọi một phương thức trên đối tượng đó từ một miền khác.

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"});
}

Một số lỗi chính tả nhỏ trong mã, và tôi phải thừa nhận rằng tôi không tin rằng nó sẽ hoạt động, nhưng đây là một cứu cánh cho tôi. Cảm ơn rất nhiều.
Owen Ivory

4

Chìa khóa là sự kiện AssemblyResolve do AppDomain đưa ra.

[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"}));
}

0

Tôi đã phải làm điều này vài lần và đã nghiên cứu nhiều giải pháp khác nhau.

Giải pháp mà tôi thấy thanh lịch và dễ thực hiện nhất có thể được thực hiện như vậy.

1. Tạo một dự án mà bạn có thể tạo một giao diện đơn giản

giao diện sẽ chứa chữ ký của bất kỳ thành viên nào bạn muốn gọi.

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

Điều quan trọng là giữ cho dự án này sạch sẽ và gọn gàng. Đây là một dự án mà cả hai đều AppDomaincó thể tham chiếu và sẽ cho phép chúng tôi không tham chiếu đến miền Assemblymà chúng tôi muốn tải trong miền riêng từ hội đồng khách hàng của chúng tôi.

2. Bây giờ tạo dự án có mã bạn muốn tải riêng biệt AppDomain.

Dự án này cũng như với proj máy khách sẽ tham chiếu đến proxy proj và bạn sẽ triển khai giao diện.

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

3. Tiếp theo, trong dự án máy khách, tải mã trong một dự án khác AppDomain.

Vì vậy, bây giờ chúng tôi tạo một cái mới AppDomain. Có thể chỉ định vị trí cơ sở cho các tham chiếu lắp ráp. Việc dò tìm sẽ kiểm tra các tập hợp phụ thuộc trong GAC và trong thư mục hiện tại và AppDomainloc cơ sở.

// 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 );

nếu bạn cần, có rất nhiều cách khác nhau để tải một tổ hợp. Bạn có thể sử dụng một cách khác với giải pháp này. Nếu bạn có tên đủ điều kiện lắp ráp thì tôi muốn sử dụng CreateInstanceAndUnwrapvì nó tải các byte lắp ráp và sau đó khởi tạo loại của bạn cho bạn và trả về một loại objectmà bạn có thể truyền đơn giản đến loại proxy của mình hoặc nếu bạn không có tên đó thành mã được nhập mạnh, bạn có thể sử dụng thời gian chạy ngôn ngữ động và gán đối tượng trả về cho một dynamicbiến đã định kiểu, sau đó chỉ cần gọi trực tiếp các thành viên trên đó.

Đây là bạn có nó.

Điều này cho phép tải một assembly mà chương trình khách hàng của bạn không có tham chiếu đến trong một vùng riêng biệt AppDomainvà gọi các thành viên trên đó từ ứng dụng khách.

Để kiểm tra, tôi muốn sử dụng cửa sổ Mô-đun trong Visual Studio. Nó sẽ hiển thị cho bạn miền lắp ráp máy khách của bạn và tất cả các mô-đun được tải trong miền đó cũng như miền ứng dụng mới của bạn và những cụm hoặc mô-đun nào được tải trong miền đó.

Điều quan trọng là phải đảm bảo mã của bạn có nguồn gốc MarshalByRefObjecthoặc có thể tuần tự hóa.

`MarshalByRefObject sẽ cho phép bạn định cấu hình thời gian tồn tại của miền. Ví dụ: giả sử bạn muốn miền bị hủy nếu proxy không được gọi trong 20 phút.

Tôi hi vọng cái này giúp được.


Xin chào, nếu tôi nhớ không nhầm, vấn đề cốt lõi là làm thế nào để tải tất cả các phụ thuộc một cách đệ quy, do đó câu hỏi. Vui lòng kiểm tra mã của bạn bằng cách thay đổi HelloWorld để trả về một lớp kiểu Foo, FooAssemblycó thuộc tính kiểu Bar, BarAssembly, tức là tổng số 3 cụm. Nó có tiếp tục hoạt động không?
abatishchev

Có, cần liệt kê thư mục thích hợp trong giai đoạn thăm dò lắp ráp. AppDomain có ApplicationBase, tuy nhiên, tôi đã không thử nghiệm nó. Ngoài ra các tệp cấu hình, bạn có thể chỉ định các thư mục thăm dò lắp ráp chẳng hạn như app.config mà một dll có thể sử dụng cũng như chỉ cần đặt để sao chép trong thuộc tính. Ngoài ra, nếu bạn có quyền kiểm soát việc xây dựng hội đồng muốn tải trong miền ứng dụng riêng biệt, các tham chiếu có thể nhận được HintPath chỉ định để tìm kiếm nó. Nếu tất cả những điều đó không thành công, tôi sẽ đăng ký sự kiện AppDomains AssemblyResolve mới và tải các hợp ngữ theo cách thủ công. Ví dụ cho điều đó.
SimperT
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.