Cách đúng để tải Assembly, Tìm lớp và gọi phương thức Run ()


81

Chương trình giao diện điều khiển mẫu.

class Program
{
    static void Main(string[] args)
    {
        // ... code to build dll ... not written yet ...
        Assembly assembly = Assembly.LoadFile(@"C:\dyn.dll");
        // don't know what or how to cast here
        // looking for a better way to do next 3 lines
        IRunnable r = assembly.CreateInstance("TestRunner");
        if (r == null) throw new Exception("broke");
        r.Run();

    }
}

Tôi muốn tự động xây dựng một assembly (.dll), rồi tải assembly, khởi tạo một lớp và gọi phương thức Run () của lớp đó. Tôi có nên thử truyền lớp TestRunner đến một thứ gì đó không? Không chắc chắn cách các kiểu trong một hội đồng (mã động) sẽ biết về các kiểu trong (ứng dụng lắp ráp / shell tĩnh) của tôi như thế nào. Có tốt hơn không nếu chỉ sử dụng một vài dòng mã phản chiếu để gọi Run () chỉ trên một đối tượng? Mã đó sẽ trông như thế nào?

CẬP NHẬT: William Edmondson - xem bình luận


Nói về tương lai ... bạn đã làm việc với MEF chưa? Hãy là bạn exportimportcác lớp học trong hội đồng riêng biệt bắt nguồn từ một giao diện nổi tiếng
RJB

Câu trả lời:


78

Sử dụng AppDomain

Sẽ an toàn hơn và linh hoạt hơn nếu tải cụm lắp ráp vào AppDomainđầu tiên của nó .

Vì vậy, thay vì câu trả lời được đưa ra trước đây :

var asm = Assembly.LoadFile(@"C:\myDll.dll");
var type = asm.GetType("TestRunner");
var runnable = Activator.CreateInstance(type) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();

Tôi sẽ đề xuất những điều sau (phỏng theo câu trả lời này cho một câu hỏi liên quan ):

var domain = AppDomain.CreateDomain("NewDomainName");
var t = typeof(TypeIWantToLoad);
var runnable = domain.CreateInstanceFromAndUnwrap(@"C:\myDll.dll", t.Name) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();

Bây giờ bạn có thể dỡ bỏ lắp ráp và có các cài đặt bảo mật khác nhau.

Nếu bạn muốn linh hoạt hơn và mạnh mẽ hơn cho việc tải và dỡ các tổ hợp động, bạn nên xem Khung bổ trợ được quản lý (tức là System.AddInkhông gian tên). Để biết thêm thông tin, hãy xem bài viết này về Phần bổ trợ và Khả năng mở rộng trên MSDN .


1
Điều gì xảy ra nếu TypeIWantToLoad là một chuỗi? Bạn có giải pháp thay thế cho asm.GetType của câu trả lời trước ("type string") không?
paz

2
Tôi nghĩ CreateInstanceFromAndUnwrapyêu cầu AssemblyName hơn là một đường dẫn; bạn đã có nghĩa là CreateFrom(path, fullname).Unwrap()? Ngoài ra, tôi bị đốt cháy bởi MarshalByRefObjectyêu cầu
drzaus

1
Có lẽ CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)?
fadden

1
Xin chào các bạn, tôi tin rằng bạn đang nhầm lẫn giữa CreateInstanceAndUnwrap với CreateInstanceFromAndUnwrap.
cdiggins

48

Nếu bạn không có quyền truy cập vào TestRunnerthông tin kiểu trong hợp ngữ gọi (nghe có vẻ như không), bạn có thể gọi phương thức như sau:

Assembly assembly = Assembly.LoadFile(@"C:\dyn.dll");
Type     type     = assembly.GetType("TestRunner");
var      obj      = Activator.CreateInstance(type);

// Alternately you could get the MethodInfo for the TestRunner.Run method
type.InvokeMember("Run", 
                  BindingFlags.Default | BindingFlags.InvokeMethod, 
                  null,
                  obj,
                  null);

Nếu bạn có quyền truy cập vào IRunnablekiểu giao diện, bạn có thể truyền phiên bản của mình sang kiểu đó (chứ không phải TestRunnerkiểu được triển khai trong hợp ngữ được tạo động hoặc được tải, phải không?):

  Assembly assembly  = Assembly.LoadFile(@"C:\dyn.dll");
  Type     type      = assembly.GetType("TestRunner");
  IRunnable runnable = Activator.CreateInstance(type) as IRunnable;
  if (runnable == null) throw new Exception("broke");
  runnable.Run();

+1 Nó hoạt động bằng cách sử dụng dòng type.invokeMember. Tôi nên sử dụng phương pháp đó hay tiếp tục cố gắng làm điều gì đó với giao diện? Tôi thậm chí không cần phải lo lắng về việc đưa điều đó vào mã được xây dựng động.
BuddyJoe

Hmm, không phải khối mã thứ hai phù hợp với bạn? Cụm gọi của bạn có quyền truy cập vào kiểu IRunnable không?
Jeff Sternal

Khối thứ hai không hoạt động. Gọi assembly không thực sự biết về IRunnable. Vì vậy, tôi đoán tôi sẽ gắn bó với phương pháp thứ hai. Theo dõi nhẹ. Khi tôi lấy lại mã và sau đó làm lại dyn.dll, tôi dường như không thể thay thế nó vì nó đang được sử dụng. Bất cứ điều gì như Assembly.UnloadType hoặc thứ gì đó để cho phép tôi thay thế .dll? Hay tôi nên làm điều đó "trong bộ nhớ"? suy nghĩ? thanks
BuddyJoe

Đoán rằng tôi không biết cách thích hợp để làm điều "trong bộ nhớ" nếu đó là giải pháp tốt nhất.
BuddyJoe

Tôi không nhớ lại các chi tiết cụ thể (và tôi sẽ rời khỏi máy tính của mình một lúc), nhưng tôi tin rằng một Assembly chỉ có thể được tải một lần cho mỗi AppDomain - vì vậy bạn sẽ phải tạo AppDomains mới cho mỗi phiên bản Assembly ( và Tải các cụm từ vào đó) hoặc bạn sẽ phải khởi động lại ứng dụng của mình trước khi có thể biên dịch phiên bản mới của Assembly.
Jeff Sternal

12

Tôi đang làm chính xác những gì bạn đang tìm kiếm trong công cụ quy tắc của tôi, công cụ này sử dụng CS-Script để biên dịch động, tải và chạy C #. Nó sẽ dễ dàng được dịch sang những gì bạn đang tìm kiếm và tôi sẽ đưa ra một ví dụ. Đầu tiên, mã (rút gọn):

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CSScriptLibrary;

namespace RulesEngine
{
    /// <summary>
    /// Make sure <typeparamref name="T"/> is an interface, not just any type of class.
    /// 
    /// Should be enforced by the compiler, but just in case it's not, here's your warning.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class RulesEngine<T> where T : class
    {
        public RulesEngine(string rulesScriptFileName, string classToInstantiate)
            : this()
        {
            if (rulesScriptFileName == null) throw new ArgumentNullException("rulesScriptFileName");
            if (classToInstantiate == null) throw new ArgumentNullException("classToInstantiate");

            if (!File.Exists(rulesScriptFileName))
            {
                throw new FileNotFoundException("Unable to find rules script", rulesScriptFileName);
            }

            RulesScriptFileName = rulesScriptFileName;
            ClassToInstantiate = classToInstantiate;

            LoadRules();
        }

        public T @Interface;

        public string RulesScriptFileName { get; private set; }
        public string ClassToInstantiate { get; private set; }
        public DateTime RulesLastModified { get; private set; }

        private RulesEngine()
        {
            @Interface = null;
        }

        private void LoadRules()
        {
            if (!File.Exists(RulesScriptFileName))
            {
                throw new FileNotFoundException("Unable to find rules script", RulesScriptFileName);
            }

            FileInfo file = new FileInfo(RulesScriptFileName);

            DateTime lastModified = file.LastWriteTime;

            if (lastModified == RulesLastModified)
            {
                // No need to load the same rules twice.
                return;
            }

            string rulesScript = File.ReadAllText(RulesScriptFileName);

            Assembly compiledAssembly = CSScript.LoadCode(rulesScript, null, true);

            @Interface = compiledAssembly.CreateInstance(ClassToInstantiate).AlignToInterface<T>();

            RulesLastModified = lastModified;
        }
    }
}

Thao tác này sẽ đưa một giao diện kiểu T, biên dịch tệp .cs thành một hợp ngữ, khởi tạo một lớp thuộc một kiểu nhất định và căn chỉnh lớp đã khởi tạo đó với giao diện T. Về cơ bản, bạn chỉ cần đảm bảo rằng lớp khởi tạo triển khai giao diện đó. Tôi sử dụng thuộc tính để thiết lập và truy cập mọi thứ, như vậy:

private RulesEngine<IRulesEngine> rulesEngine;

public RulesEngine<IRulesEngine> RulesEngine
{
    get
    {
        if (null == rulesEngine)
        {
            string rulesPath = Path.Combine(Application.StartupPath, "Rules.cs");

            rulesEngine = new RulesEngine<IRulesEngine>(rulesPath, typeof(Rules).FullName);
        }

        return rulesEngine;
    }
}

public IRulesEngine RulesEngineInterface
{
    get { return RulesEngine.Interface; }
}

Ví dụ của bạn, bạn muốn gọi Run (), vì vậy tôi sẽ tạo một giao diện xác định phương thức Run (), như sau:

public interface ITestRunner
{
    void Run();
}

Sau đó, tạo một lớp triển khai nó, như thế này:

public class TestRunner : ITestRunner
{
    public void Run()
    {
        // implementation goes here
    }
}

Thay đổi tên của RulesEngine thành một cái gì đó giống như TestHarness và đặt thuộc tính của bạn:

private TestHarness<ITestRunner> testHarness;

public TestHarness<ITestRunner> TestHarness
{
    get
    {
        if (null == testHarness)
        {
            string sourcePath = Path.Combine(Application.StartupPath, "TestRunner.cs");

            testHarness = new TestHarness<ITestRunner>(sourcePath , typeof(TestRunner).FullName);
        }

        return testHarness;
    }
}

public ITestRunner TestHarnessInterface
{
    get { return TestHarness.Interface; }
}

Sau đó, bất cứ nơi nào bạn muốn gọi nó, bạn chỉ cần chạy:

ITestRunner testRunner = TestHarnessInterface;

if (null != testRunner)
{
    testRunner.Run();
}

Nó có thể sẽ hoạt động tốt cho một hệ thống plugin, nhưng mã của tôi bị giới hạn ở việc tải và chạy một tệp, vì tất cả các quy tắc của chúng tôi đều nằm trong một tệp nguồn C #. Tuy nhiên, tôi nghĩ rằng sẽ khá dễ dàng để sửa đổi nó để chỉ chuyển vào tệp loại / nguồn cho mỗi tệp bạn muốn chạy. Bạn chỉ cần chuyển mã từ getter vào một phương thức lấy hai tham số đó.

Ngoài ra, hãy sử dụng IRunnable của bạn thay cho ITestRunner.


@Interface là gì? ý tưởng rất hay ở đây. cần phải tiêu hóa đầy đủ điều này. 1
BuddyJoe

rất thú vị, tôi đã không nhận ra trình phân tích cú pháp C # phải xem một ký tự vượt qua ký tự @ để xem nó là một phần của tên biến hay chuỗi @ "".
BuddyJoe,

Cảm ơn. Dấu @ trước tên biến được sử dụng khi tên biến là một từ khóa. Bạn không thể đặt tên biến "lớp", "giao diện", "mới", v.v ... Nhưng bạn có thể đặt tên nếu bạn thêm ký tự @. Có lẽ không thành vấn đề trong trường hợp của tôi với chữ "I" viết hoa, nhưng ban đầu nó là một biến nội bộ với getter và setter trước khi tôi chuyển nó thành thuộc tính tự động.
Chris Doggett,

Đúng rồi. Tôi quên mất điều @. Làm thế nào bạn sẽ xử lý câu hỏi tôi đã hỏi Jeff Sternal về "điều trong ký ức"? Tôi đoán vấn đề lớn của tôi bây giờ là tôi có thể tạo .dll động và tải nó, nhưng tôi chỉ có thể làm điều đó một lần. Không biết làm thế nào để "dỡ bỏ" lắp ráp. Có thể tạo một AppDomain khác tải lắp ráp trong không gian đó, sử dụng nó và sau đó gỡ bỏ AppDomain thứ hai này. Rửa sạch. Nói lại.?
BuddyJoe

1
Không có cách nào để dỡ lắp ráp trừ khi bạn sử dụng AppDomain thứ hai. Tôi không chắc CS-Script thực hiện nó như thế nào trong nội bộ, nhưng một phần của công cụ quy tắc của tôi mà tôi đã loại bỏ là FileSystemWatcher tự động chạy lại LoadRules () bất cứ khi nào tệp thay đổi. Chúng tôi chỉnh sửa các quy tắc, đẩy chúng ra cho người dùng, khách hàng của họ ghi đè tệp đó, FileSystemWatcher nhận thấy các thay đổi, biên dịch lại và tải lại DLL bằng cách ghi một tệp khác vào thư mục tạm thời. Khi máy khách khởi động, nó sẽ xóa thư mục đó trước khi biên dịch động đầu tiên, vì vậy chúng tôi không còn thừa.
Chris Doggett,

6

Bạn sẽ cần phải sử dụng phản chiếu để có loại "TestRunner". Sử dụng phương thức Assembly.GetType.

class Program
{
    static void Main(string[] args)
    {
        Assembly assembly = Assembly.LoadFile(@"C:\dyn.dll");
        Type type = assembly.GetType("TestRunner");
        var obj = (TestRunner)Activator.CreateInstance(type);
        obj.Run();
    }
}

Đây không phải là thiếu một bước, trong đó bạn có được thích hợp MethodInfotừ loại và cuộc gọi Invoke? (Tôi hiểu câu hỏi ban đầu như xác định người gọi không biết bất cứ điều gì về Gõ vào câu hỏi.)
Jeff xương ức

Bạn đang thiếu một thứ. Bạn phải chuyển đối tượng để nhập TestRunner. var obj = (TestRunner) Activator.CreateInstance (loại);
Miễn phí

Có vẻ như Tyndall thực sự đang xây dựng dll này trong một bước trước đó. Việc triển khai này giả định rằng anh ta biết rằng phương thức Run () đã tồn tại và biết rằng nó không có tham số. Nếu đây là những thực sự không rõ sau đó ông sẽ cần phải làm phản ánh sâu hơn một chút
William Edmondson

hmmm. TestRunner là một lớp bên trong mã được viết động của tôi. Vì vậy, mã tĩnh này trong ví dụ của bạn không thể giải quyết TestRunner. Nó không biết nó là gì.
BuddyJoe

@WilliamEdmondson làm thế nào bạn có thể sử dụng "(TestRunner)" trong mã vì nó không được tham chiếu ở đây?
Antoops

2

Khi bạn xây dựng lắp ráp của mình, bạn có thể gọi AssemblyBuilder.SetEntryPoint, sau đó lấy lại nó từ thuộc Assembly.EntryPointtính để gọi nó.

Hãy nhớ rằng bạn sẽ muốn sử dụng chữ ký này và lưu ý rằng nó không cần phải được đặt tên Main:

static void Run(string[] args)

AssemblyBuilder là gì? Tôi đã cố gắng CodeDomProvider và sau đó "provider.CompileAssemblyFromSource"
BuddyJoe
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.