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

发布于 2024-10-02 17:26:28 字数 2179 浏览 2 评论 0原文

我有一个 .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
{
}

客户端assembly 创建将在其中托管服务器程序集的 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()

I have a .NET application in which assemblies in separate AppDomains must share serialized objects that are passed by value.

Both assemblies reference a shared assembly that defines the base class for the server class and also defines the base class for the entiy type that will be passed between domains:

public abstract class ServerBase : MarshalByRefObject
{
    public abstract EntityBase GetEntity();
}

[Serializable]
public abstract class EntityBase
{
}

The server assembly defines the server class and a concrete implemetation of the entity type:

public class Server : ServerBase
{
    public override EntityBase GetEntity()
    {
        return new EntityItem();
    }
}

[Serializable]
public class EntityItem : EntityBase
{
}

The client assembly creates the AppDomain in which the server assembly will be hosted and uses an instance of the server class to request a concrete instance of the entity type:

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

Unfortnately, this approach fails with a SerializationException because the client assembly has no direct knowledge of the concrete type that is being returned.

I have read that .NET remoting supports unknown types when using binary serialization, but I am not sure whether this applies to my setup or how to configure it.

Alternatively, is there any other way of passing an unknown concrete type from the server to the client, given that the client only needs to access it via its known base class interface.

Thanks for your advice,

Tim

EDIT:

As requested by Hans, here is the exception message and stack trace.

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

°如果伤别离去 2024-10-09 17:26:28

这会失败,因为 CLR 根本不可能找到该程序集,您将其放在了无法找到的位置。通过添加对程序集的引用并将其 Copy Local 属性设置为 True 以便将 server.dll 复制到构建目录中,可以简单地解决此问题。如果您想将其保留在原来的位置,那么您必须实现 AppDomain.AssemblyResolve 来帮助 CLR 找到它。

This fails because the CLR just has no hope of being able to find the assembly, you put it in an unfindable location. Trivially solve this by adding a reference to the assembly and setting its Copy Local property to True so that server.dll gets copied into your build directory. If you want to keep it where it is at then you'll have to implement AppDomain.AssemblyResolve to help the CLR finding it.

酸甜透明夹心 2024-10-09 17:26:28

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

你会说 .Net远程处理依赖于紧耦合?

I asked a related question a while back:

Would you say .Net remoting relies on tight coupling?

待天淡蓝洁白时 2024-10-09 17:26:28

我想我有一个解决方案,感谢当前的帖子,以及这个及其接受的答案: AppDomain.Load () 失败并出现 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 中声明的每种类型必须是可序列化的(例如:带有[Serializing]属性)

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

  /// <summary>
/// Source: https://stackoverflow.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://stackoverflow.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://stackoverflow.com/questions/4185816/how-to-pass-an-unknown-type-between-two-net-appdomains"/>
public class PlugInLoader
{       

    /// <summary>
    /// https://stackoverflow.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 中实例化我们的具体类型的自定义代理,并使用它们,就好像它们在主应用程序中可用一样。

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

I think I have a solution thanks to current post, and this one and its accepted answer : AppDomain.Load() fails with FileNotFoundException

First thing, I think you should use an interface in place of a base class to be your handler. The interface should be declared on base class, and then you only use it.

Solution : create a concrete type in the shared assembly, which inherits from MarshalByRefObject, and implements your server interface. This concrete type is a proxy that can be serialized/deserialized between AppDomains because your main application knows its definition. You no longer need to inherit from MarshalByRefObject in your class ServerBase.

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

}

Note: in order to send and receive data, each type declared in IServer must be serializable (for example: with [Serializable] attribute)

Then, you can use the method found in previous link "Loader class".
Here is my modified Loader class which instanciate concrete type in shared assembly, and returns a Proxy for each Plugin:

  /// <summary>
/// Source: https://stackoverflow.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://stackoverflow.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());
    }

}

Then, in the master application, I also use the code of "dedpichto" and "James Thurley" to create AppDomain, instanciate and invoke Loader class. I am then able to use my Proxy as it was my plugin, because .NET creates a "transparent proxy" due to MarshalByRefObject :

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

    /// <summary>
    /// https://stackoverflow.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);
        }
  }
}

It's really hard to find out a working solution, but we finally can keep instances of custom proxies of our concrete types instanciated in another AppDomain and use them as if they was available in main application.

Hope this (huge answer) helps !

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文