AppDomain await async Task prevent SerializationEx

2019-04-29 22:08发布

问题:

I have a windows service, which loads assembly in another AppDomain at runtime. Then it executes them and finally unloads the AppDomain. The problem is the execute method from the plugins are async tasks and I get the SerializationException because Task does not inherit from MarshalByRefObject.

I wrapped the plugin in a proxy which inherits from MarshalByRefObject, but I dont know how to get rid of the SerializationException?

public interface IPlugin : IDisposable
{
    Guid GUID { get; }
    string Name { get; }
    string Description { get; }
    Task Execute(PluginPanel panel, string user);
}

The proxy:

[Serializable()]
public class PluginProxy : MarshalByRefObject, IPlugin
{
    private IPlugin m_Plugin;

    public bool Init(string file)
    {
        Assembly ass = Assembly.Load(AssemblyName.GetAssemblyName(file));
        if (ass == null || ass.GetTypes() == null || ass.GetTypes().Length == 0)
            return false;
        foreach (Type type in ass.GetTypes())
        {
            if (type.IsInterface || type.IsAbstract)
                continue;
            if (type.GetInterface(typeof(IPlugin).FullName) != null)
            {
                m_Plugin = (IPlugin)Activator.CreateInstance(type);
                return true;
            }
        }
        return false;
    }


    public Guid GUID { get { return m_Plugin.GUID; } }
    public string Name { get { return m_Plugin.Name; } }
    public string Description { get { return m_Plugin.Description; } }
    // I debugged and found out the error happens AFTER m_Plugin.Execute
    // so the method runs well, but the return back to the pProxy.Execute is throwing the SerializationException       
    public async Task Execute(PluginPanel panel, string user) { await m_Plugin.Execute(panel, user); }
}

And the Method which loads the Assembly and gets the SerializationException:

        AppDomainSetup setup = new AppDomainSetup();
        // some setup stuff

        AppDomain dom = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, setup);
        PluginProxy pProxy = (PluginProxy)dom.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().CodeBase, typeof(PluginProxy).FullName);
        pProxy.Init(app.Apppath);
        // I await the task later in code, because the user can cancel the execution
        try { tExe = pProxy.Execute(panel, user.Username); }
           catch (System.Runtime.Serialization.SerializationException e)
           {
               // runs always in this catch, even if no Exception from the plugin was thrown
           }
           catch (Exception e) { AddToErrorLog(panel.PanelName, e); }
           finally
           {
               pProxy.Dispose();
               AppDomain.Unload(dom);
           }

Maybe my whole concept of loading Plugins is wrong?

回答1:

Thanks to Hamlet Hakobyan and the post from Stephen Toub, I think I was able to solve the problem.

I replaced the line from the caller

 try { tExe = pProxy.Execute(panel, user.Username); }

with

 tExe = DoWorkInOtherDomain(pProxy, panel, user.Username);

and the method DoWorkInOtherDomain:

private Task DoWorkInOtherDomain(PluginProxy pProxy, PluginPanel panel, string user)
    {
        var ch = new MarshaledResultSetter<string>();
        pProxy.Execute(panel, user, ch);
        return ch.Task;
    }

and finally the proxy class:

 Task.Run(() =>
        {
            try
            {
                m_Plugin.Execute(panel, user).Wait();
            }
            catch (AggregateException e)
            {
                if (e.InnerExceptions != null)
                    foreach (Exception ein in e.InnerExceptions)
                        AddToErrorLog(panel.PanelName, ein);
            }
            catch (Exception e) { AddToErrorLog(panel.PanelName, e); }
            finally { ch.SetResult(AppDomain.CurrentDomain.FriendlyName); }
        });

I need to call Wait() in

m_Plugin.Execute(panel, user).Wait();

it catches the Exceptions from the plugin so everything is doing fine. The Wait() call should only blocking the Task.Run and not the other Tasks.

Can anyone tell me if this is a good solution or should I change something? I dont need a result so I just do:

 ch.SetResult(AppDomain.CurrentDomain.FriendlyName);

because I dont know how I should do it without a result.