How to pass an unknown type between two .NET AppDomains?
Asked Answered
L

3

5

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()
Lather answered 15/11, 2010 at 15:21 Comment(0)
C
1

I asked a related question a while back:

Would you say .Net remoting relies on tight coupling?

Confer answered 15/11, 2010 at 17:25 Comment(0)
R
2

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.

Rolandorolandson answered 15/11, 2010 at 15:40 Comment(5)
Thanks Hans. Your suggestion is very reasonable, but I need to be sure that it doesn't introduce an additonal problem. What I have described is part of a sandboxing scenario, so I don't want the CLR to load the unknown assembly into the primary AppDomain (which has broader permissions) if it is likely to compromise security. Do you have a view on this? Thanks again.Lather
Use an interface, declared in its own assembly and referenced by both.Rolandorolandson
OK, I changed the EntityBase class to be an interface and it resides, as before, in the shared assembly, but the exception is still thrown (and presumably for the reason that you have already stated - that the object passed is unknown by the client).Lather
We still haven't see a good exception message + stack trace so it is still guessing at the real problem. If you haven't done anything about the assembly probing problem then, sure, you've probably got the same problem. Its worse now because both assemblies have to load the exact same assembly.Rolandorolandson
I have posted the exception message and stack trace. I am happy to implement AppDomain.AssemblyResolve, but I was awaiting your comments about the security implications in my scenario.Lather
C
1

I asked a related question a while back:

Would you say .Net remoting relies on tight coupling?

Confer answered 15/11, 2010 at 17:25 Comment(0)
R
0

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://mcmap.net/q/375529/-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://mcmap.net/q/371786/-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://mcmap.net/q/371786/-how-to-pass-an-unknown-type-between-two-net-appdomains"/>
public class PlugInLoader
{       

    /// <summary>
    /// https://mcmap.net/q/375529/-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 !

Roberson answered 22/2, 2018 at 15:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.