如何在两个 .NET AppDomain 之间传递未知类型?

Posted

技术标签:

【中文标题】如何在两个 .NET AppDomain 之间传递未知类型?【英文标题】:How to pass an unknown type between two .NET AppDomains? 【发布时间】:2010-11-15 15:21:25 【问题描述】:

我有一个 .NET 应用程序,其中不同 AppDomain 中的程序集必须共享按值传递的序列化对象。

两个程序集都引用了一个共享程序集,该程序集定义了服务器类的基类,还定义了将在域之间传递的实体类型的基类:

public abstract class ServerBase : MarshalByRefObject

    public abstract EntityBase GetEntity();


[Serializable]
public abstract class EntityBase


服务器程序集定义了服务器类和实体类型的具体实现:

public class Server : ServerBase

    public override EntityBase GetEntity()
    
        return new EntityItem();
    


[Serializable]
public class EntityItem : EntityBase


客户端程序集创建将托管服务器程序集的AppDomain,并使用服务器类的实例来请求实体类型的具体实例:

class Program

    static void Main()
    
        var domain = AppDomain.CreateDomain("Server");

        var server = (ServerBase)Activator.CreateInstanceFrom(
            domain,
            @"..\..\..\Server\bin\Debug\Server.dll",
            "Server.Server").Unwrap();

        var entity = server.GetEntity();
    

不幸的是,此方法失败并返回 SerializationException,因为客户端程序集不直接知道返回的具体类型。

我已阅读 .NET 远程处理在使用二进制序列化时支持未知类型,但我不确定这是否适用于我的设置或如何配置它。

或者,如果客户端只需要通过其已知的基类接口访问它,是否有任何其他方法可以将未知的具体类型从服务器传递到客户端。

感谢您的建议,

提姆

编辑:

按照汉斯的要求,这里是异常消息和堆栈跟踪。

SerializationException
Type is not resolved for member 'Server.EntityItem,Server, Version=1.0.0.0,Culture=neutral, PublicKeyToken=null'.

at Interop.ServerBase.GetEntity()
at Client.Program.Main() in C:\Users\Tim\Visual Studio .Net\Solutions\MEF Testbed\Client\Program.cs:line 12
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()

【问题讨论】:

【参考方案1】:

这失败了,因为 CLR 只是没有希望能够找到程序集,你把它放在一个找不到的位置。通过添加对程序集的引用并将其 Copy Local 属性设置为 True 来轻松解决此问题,以便将 server.dll 复制到您的构建目录中。如果您想将其保留在原处,则必须实现 AppDomain.AssemblyResolve 以帮助 CLR 找到它。

【讨论】:

谢谢汉斯。您的建议非常合理,但我需要确保它不会引入额外的问题。我所描述的是沙盒场景的一部分,因此如果 CLR 可能会危及安全性,我不希望 CLR 将未知程序集加载到主 AppDomain(具有更广泛的权限)中。你对此有看法吗?再次感谢。 使用一个接口,在它自己的程序集中声明并被两者引用。 好的,我将 EntityBase 类更改为一个接口,并且它像以前一样驻留在共享程序集中,但仍然抛出异常(并且可能是因为您已经说过 - 那传递的对象是客户端未知的)。 我们仍然没有看到好的异常消息+堆栈跟踪,所以它仍然在猜测真正的问题。如果您还没有对装配探测问题做任何事情,那么,当然,您可能遇到了同样的问题。现在更糟了,因为两个程序集都必须加载 exact 相同的程序集。 我已经发布了异常消息和堆栈跟踪。我很高兴实施 AppDomain.AssemblyResolve,但我正在等待您的 cmets 了解我的场景中的安全隐患。【参考方案2】:

不久前我问了一个相关问题:

Would you say .Net remoting relies on tight coupling?

【讨论】:

【参考方案3】:

感谢当前的帖子,我想我有一个解决方案,这个帖子及其接受的答案:AppDomain.Load() fails with FileNotFoundException

首先,我认为您应该使用接口代替基类作为您的处理程序。接口应该在基类上声明,然后你只能使用它。

解决方案:在共享程序集中创建一个具体类型,它继承自MarshalByRefObject,并实现您的服务器接口。这个具体类型是一个代理,可以在 AppDomain 之间进行序列化/反序列化,因为您的主应用程序知道它的定义。您不再需要从类ServerBase 中的MarshalByRefObject 继承。

  // - MUST be serializable, and MUSNT'T use unknown types for main App
  [Serializable]
  public class Query 
  
     ...
  

  public interface IServerBase
     
       string Execute(Query q);
   

  public abstract class ServerBase : IServerBase
  
       public abstract string Execute(Query q);
  

// Our CUSTOM PROXY: the concrete type which will be known from main App
[Serializable]
public class ServerBaseProxy : MarshalByRefObject, IServerBase

    private IServerBase _hostedServer;

    /// <summary>
    /// cstor with no parameters for deserialization
    /// </summary>
    public ServerBaseProxy ()
    

    

    /// <summary>
    /// Internal constructor to use when you write "new ServerBaseProxy"
    /// </summary>
    /// <param name="name"></param>
    public ServerBaseProxy(IServerBase hostedServer)
    
        _hostedServer = hostedServer;
          

    public string Execute(Query q)
    
        return(_hostedServer.Execute(q));
    


注意:为了发送和接收数据,IServer中声明的每个类型必须是可序列化的(例如:带有[Serializable]属性)

然后,您可以使用上一个链接“Loader class”中找到的方法。 这是我修改后的 Loader 类,它在共享程序集中实例化具体类型,并为每个插件返回一个代理:

  /// <summary>
/// Source: https://***.com/questions/16367032/appdomain-load-fails-with-filenotfoundexception
/// </summary>
public class Loader : MarshalByRefObject


    /// <summary>
    /// Load plugins
    /// </summary>
    /// <param name="assemblyName"></param>
    /// <returns></returns>
    public IPlugin[] LoadPlugins(string assemblyPath)
    
        List<PluginProxy> proxyList = new List<PluginProxy>(); // a proxy could be transfered outsite AppDomain, but not the plugin itself ! https://***.com/questions/4185816/how-to-pass-an-unknown-type-between-two-net-appdomains

        var assemb = Assembly.LoadFrom(assemblyPath); // use Assembly.Load if you want to use an Assembly name and not a path

        var types = from type in assemb.GetTypes()
                    where typeof(IPlugin).IsAssignableFrom(type)
                    select type;

        var instances = types.Select(
            v => (IPlugin)Activator.CreateInstance(v)).ToArray();

        foreach (IPlugin instance in instances)
        
            proxyList.Add(new PluginProxy(instance));
        
        return (proxyList.ToArray());
    


然后,在主应用程序中,我也使用“dedpichto”和“James Thurley”的代码来创建AppDomain,实例化和调用Loader类。然后我可以使用我的代理,因为它是我的插件,因为 .NET 由于MarshalByRefObject 创建了一个“透明代理”:

   /// <see cref="https://***.com/questions/4185816/how-to-pass-an-unknown-type-between-two-net-appdomains"/>
public class PlugInLoader
       

    /// <summary>
    /// https://***.com/questions/16367032/appdomain-load-fails-with-filenotfoundexception
    /// </summary>
    public void LoadPlugins(string pluginsDir)
    
        // List all directories where plugins could be
        var privatePath = "";
        var paths = new List<string>();
        List<DirectoryInfo> dirs = new DirectoryInfo(pluginsDir).GetDirectories().ToList();
        dirs.Add(new DirectoryInfo(pluginsDir));
        foreach (DirectoryInfo d in dirs)
            privatePath += d.FullName + ";";
        if (privatePath.Length > 1) privatePath = privatePath.Substring(0, privatePath.Length - 1);

        // Create AppDomain !
        AppDomainSetup appDomainSetup = AppDomain.CurrentDomain.SetupInformation;
        appDomainSetup.PrivateBinPath = privatePath; 

        Evidence evidence = AppDomain.CurrentDomain.Evidence;
        AppDomain sandbox = AppDomain.CreateDomain("sandbox_" + Guid.NewGuid(), evidence, appDomainSetup);

        try
        
            // Create an instance of "Loader" class of the shared assembly, that is referenced in current main App
            sandbox.Load(typeof(Loader).Assembly.FullName);

            Loader loader = (Loader)Activator.CreateInstance(
                sandbox,
                typeof(Loader).Assembly.FullName,
                typeof(Loader).FullName,
                false,
                BindingFlags.Public | BindingFlags.Instance,
                null,
                null,
                null,
                null).Unwrap();

            // Invoke loader in shared assembly to instanciate concrete types. As long as concrete types are unknown from here, they CANNOT be received by Serialization, so we use the concrete Proxy type.

            foreach (var d in dirs)
            
                var files = d.GetFiles("*.dll");
                foreach (var f in files)
                
                    // This array does not contains concrete real types, but concrete types of "my custom Proxy" which implements IPlugin. And here, we are outside their AppDomain, so "my custom Proxy" is under the form of a .NET "transparent proxy" (we can see in debug mode) generated my MarshalByRefObject.
                    IPlugin[] plugins = loader.LoadPlugins(f.FullName);
                    foreach (IPlugin plugin in plugins)
                    
                        // The custom proxy methods can be invoked ! 
                        string n = plugin.Name.ToString();
                        PluginResult result = plugin.Execute(new PluginParameters(), new PluginQuery()  Arguments = "", Command = "ENUMERATE", QueryType = PluginQueryTypeEnum.Enumerate_Capabilities );
                        Debug.WriteLine(n);
                                        
                
            
        
        finally
        
            AppDomain.Unload(sandbox);
        
  

确实很难找到可行的解决方案,但我们终于可以将具体类型的自定义代理实例保存在另一个 AppDomain 中,并像在主应用程序中一样使用它们。

希望这(巨大的答案)有所帮助!

【讨论】:

以上是关于如何在两个 .NET AppDomain 之间传递未知类型?的主要内容,如果未能解决你的问题,请参考以下文章

将 log4net 配置文件传递给新的 AppDomain

多个 .NET AppDomain 的代码示例

将套接字传递给新的 AppDomain

如何跨 AppDomain 将引用作为方法参数传递?

.NET 进程和 AppDomain 在啥情况下会共享内存中加载的程序集?

替换 Net Core 中的 AppDomain